Skip to content

Instantly share code, notes, and snippets.

@stravant
Created February 28, 2021 04:10
Show Gist options
  • Save stravant/d0d3e4218b2c4f79b302036485282de3 to your computer and use it in GitHub Desktop.
Save stravant/d0d3e4218b2c4f79b302036485282de3 to your computer and use it in GitHub Desktop.
ldraw -> obj converter for Roblox importing
import sys
from pathlib import Path
from math import sqrt
part_name = sys.argv[1]
part_file = part_name + ".dat"
ENTRY_TYPE_COMMENT = '0'
ENTRY_TYPE_SUBMODEL = '1'
ENTRY_TYPE_LINE = '2'
ENTRY_TYPE_TRI = '3'
ENTRY_TYPE_QUAD = '4'
ENTRY_TYPE_OPT_LINE = '5'
MODEL_COUNT = 0
GLOBAL_SCALE = 20
def debugprint(*args):
#print(*args)
pass
###########################################################
## Math code
def cframe_new():
return [[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]]
def scalevec(v, scale):
return [component * scale for component in v]
def addvec(*vs):
result = [0, 0, 0]
for v in vs:
for i in range(3):
result[i] += v[i]
return result
def subvec(a, b):
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
def vectoworld(cf, v):
pos = [0, 0, 0]
for i in range(3):
pos = addvec(pos, scalevec(cf[i + 1], v[i]))
return pos
def pointtoworld(cf, v):
return addvec(cf[0], vectoworld(cf, v))
def mul_cframe(cf1, cf2):
newpos = pointtoworld(cf1, cf2[0])
ux = vectoworld(cf1, cf2[1])
uy = vectoworld(cf1, cf2[2])
uz = vectoworld(cf1, cf2[3])
return [newpos, ux, uy, uz]
def vec_cross(a, b):
return [a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2], a[0]*b[1] - a[1]*b[0]]
def vec_dot(a, b):
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
def vec_len(v):
return sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
#####################################################
## Output code
vert_to_index = {}
next_vert_index = 1
output_tris = []
smooth_edge_normal = {}
smooth_edge_normal_count = {}
next_edge_normal_index = 1
edge_to_normal_index = {}
vert_to_normal_index = {}
smooth_edges = []
def hash_vert(v):
return hash(str(v))
def hash_edge(a, b):
return hash_vert(a) + hash_vert(b)
def result_add_triangle(a, b, c):
output_tris.append([a, b, c])
def result_add_smooth(a, b):
smooth_edges.append([a, b])
smooth_edge_normal_count[hash_edge(a, b)] = 0
smooth_edge_normal[hash_edge(a, b)] = [0, 0, 0]
def preprocess_add_normal(normal, a, b):
edge_hash = hash_edge(a, b)
if edge_hash in smooth_edge_normal:
smooth_edge_normal_count[edge_hash] += 1
smooth_edge_normal[edge_hash] = \
addvec(smooth_edge_normal[edge_hash], normal)
def get_normal(tri):
return vec_cross(subvec(tri[1], tri[0]), subvec(tri[2], tri[1]))
def preprocess_edge_normals():
for tri in output_tris:
normal = get_normal(tri)
preprocess_add_normal(normal, tri[0], tri[1])
preprocess_add_normal(normal, tri[1], tri[2])
preprocess_add_normal(normal, tri[2], tri[0])
non_2_facecount = 0
no_normals_count = 0
count = 0
for id in smooth_edge_normal:
if smooth_edge_normal_count[id] != 2:
non_2_facecount += 1
count += 1
normal = smooth_edge_normal[id]
if normal[0] == 0 and normal[1] == 0 and normal[2] == 0:
no_normals_count += 1
else:
normal = scalevec(normal, 1 / vec_len(normal))
smooth_edge_normal[id] = normal
if non_2_facecount > 0:
debugprint(" >> Adjacent face count is not 2! (x%d) <<" % non_2_facecount)
if no_normals_count > 0:
debugprint(" >> No normals for face! (x%d) <<" % no_normals_count)
debugprint(" Recorded %d edges to be smoothed" % count)
def write_to_file(file):
debugprint("Writing to file...")
preprocess_edge_normals()
file.write("g\n")
for tri in output_tris:
for v in tri:
id = hash_vert(v)
if not id in vert_to_index:
file.write("v %f %f %f\n" % \
tuple(scalevec(v, 1/GLOBAL_SCALE)))
global next_vert_index
vert_to_index[id] = next_vert_index
next_vert_index += 1
global next_edge_normal_index
for i in range(len(output_tris)):
normal = get_normal(output_tris[i])
file.write("vn %f %f %f\n" % tuple(normal))
next_edge_normal_index += 1
for edge in smooth_edges:
id = hash_edge(edge[0], edge[1])
normal = smooth_edge_normal[id]
edge_to_normal_index[id] = next_edge_normal_index
vert_to_normal_index[hash_vert(edge[0])] = next_edge_normal_index
vert_to_normal_index[hash_vert(edge[1])] = next_edge_normal_index
next_edge_normal_index += 1
file.write("vn %f %f %f\n" % tuple(normal))
normal_face_count = 0
smooth_face_count = 0
for i in range(len(output_tris)):
tri = output_tris[i]
h1 = hash_vert(tri[0])
h2 = hash_vert(tri[1])
h3 = hash_vert(tri[2])
v1 = vert_to_index[h1]
v2 = vert_to_index[h2]
v3 = vert_to_index[h3]
n1 = i + 1
n2 = i + 1
n3 = i + 1
id1 = hash_edge(tri[0], tri[1])
id2 = hash_edge(tri[1], tri[2])
id3 = hash_edge(tri[2], tri[0])
if id1 in edge_to_normal_index or id2 in edge_to_normal_index or id3 in edge_to_normal_index:
# is a rounded face
smooth_face_count += 1
if h1 in vert_to_normal_index:
n1 = vert_to_normal_index[h1]
if h2 in vert_to_normal_index:
n2 = vert_to_normal_index[h2]
if h3 in vert_to_normal_index:
n3 = vert_to_normal_index[h3]
else:
normal_face_count += 1
s1 = "%d//%d" % (v1, n1)
s2 = "%d//%d" % (v2, n2)
s3 = "%d//%d" % (v3, n3)
file.write("f %s %s %s\n" % (s1, s2, s3))
# file.write("g DebugLines\n")
# for edge in smooth_edges:
# v1 = vert_to_index[hash_vert(edge[0])]
# v2 = vert_to_index[hash_vert(edge[1])]
# file.write("l %d %d" % (v1, v2))
debugprint(" Wrote faces, %d normal / %d smooth" % \
(normal_face_count, smooth_face_count))
#####################################################
## Parsing code
INVERT_NEXT = False
def add_submodel(lvl, cframe, inverted, color, pos, ux, uy, uz, submodel):
global MODEL_COUNT
MODEL_COUNT = MODEL_COUNT + 1
if MODEL_COUNT > 1000:
debugprint("Overflow")
exit(0)
#print("Submodel `%s` at:" % submodel, pos, ux, uy, uz)
nx = [ux[0], uy[0], uz[0]]
ny = [ux[1], uy[1], uz[1]]
nz = [ux[2], uy[2], uz[2]]
cframe = mul_cframe(cframe, [pos, nx, ny, nz])
global INVERT_NEXT
if INVERT_NEXT:
inverted = not inverted
INVERT_NEXT = False
add_part(lvl + 1, cframe, inverted, submodel)
def handle_bfc(line_parts):
#print("Handle BFC %s", str(line_parts))
if line_parts[0] == 'INVERTNEXT':
global INVERT_NEXT
INVERT_NEXT = True
def add_comment(line_parts):
#print("Comment:", line_parts)
if line_parts and line_parts[0] == 'BFC':
handle_bfc(line_parts[1:])
def is_cframe_inverted(cframe):
return vec_dot(vec_cross(cframe[1], cframe[2]), cframe[3]) > 0
def add_quad(cframe, inverted, color, a, b, c, d):
a = pointtoworld(cframe, a)
b = pointtoworld(cframe, b)
c = pointtoworld(cframe, c)
d = pointtoworld(cframe, d)
if inverted == is_cframe_inverted(cframe):
result_add_triangle(c, b, a)
result_add_triangle(d, c, a)
else:
result_add_triangle(a, b, c)
result_add_triangle(a, c, d)
def add_tri(cframe, inverted, color, a, b, c):
a = pointtoworld(cframe, a)
b = pointtoworld(cframe, b)
c = pointtoworld(cframe, c)
if inverted == is_cframe_inverted(cframe):
result_add_triangle(c, b, a)
else:
result_add_triangle(a, b, c)
def add_line(cframe, color, a, b):
a = pointtoworld(cframe, a)
b = pointtoworld(cframe, b)
# unused
def add_smooth_line(cframe, a, b):
a = pointtoworld(cframe, a)
b = pointtoworld(cframe, b)
result_add_smooth(a, b)
def fvec(v):
return [float(component) for component in v]
def parse_line(lvl, cframe, inverted, line_parts, file):
if len(line_parts) == 0:
pass # empty line
elif line_parts[0] == ENTRY_TYPE_COMMENT:
add_comment(line_parts[1:])
elif line_parts[0] == ENTRY_TYPE_SUBMODEL:
add_submodel(lvl, cframe, inverted, int(line_parts[1]), \
fvec(line_parts[2:5]), fvec(line_parts[5:8]), \
fvec(line_parts[8:11]), fvec(line_parts[11:14]), \
line_parts[14])
elif line_parts[0] == ENTRY_TYPE_LINE:
add_line(cframe, int(line_parts[1]), \
fvec(line_parts[2:5]), fvec(line_parts[5:8]))
elif line_parts[0] == ENTRY_TYPE_TRI:
add_tri(cframe, inverted, int(line_parts[1]), \
fvec(line_parts[2:5]), fvec(line_parts[5:8]), \
fvec(line_parts[8:11]))
elif line_parts[0] == ENTRY_TYPE_QUAD:
add_quad(cframe, inverted, int(line_parts[1]), \
fvec(line_parts[2:5]), fvec(line_parts[5:8]), \
fvec(line_parts[8:11]), fvec(line_parts[11:14]))
elif line_parts[0] == ENTRY_TYPE_OPT_LINE:
add_smooth_line(cframe, \
fvec(line_parts[2:5]), fvec(line_parts[5:8]))
else:
debugprint("Line:", line_parts)
def add_part(lvl, cframe, inverted, file_name):
debugprint("%sPart %s %s" % (" " * lvl, file_name, \
"(inverted)" if inverted else ""))
file_path = Path("ldraw") / "parts" / file_name
if not file_path.exists():
file_path = Path("ldraw") / "p" / file_name
if file_path.exists():
with file_path.open() as content:
for line in content.readlines():
parse_line(lvl, cframe, inverted, line.split(), file_name)
else:
debugprint(" NOT FOUND %s" % file_name)
invert_y_cframe = cframe_new()
invert_y_cframe[2] = [0, -1, 0]
add_part(0, invert_y_cframe, False, part_file)
with open(part_name + ".obj", "w") as output_file:
write_to_file(output_file)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment