Created
February 28, 2021 04:10
-
-
Save stravant/d0d3e4218b2c4f79b302036485282de3 to your computer and use it in GitHub Desktop.
ldraw -> obj converter for Roblox importing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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