From 45dc30e741b23adddd3af9ae4b4355bee80db9c7 Mon Sep 17 00:00:00 2001 From: Armando Arredondo Soto <32519217+arredondos@users.noreply.github.com> Date: Sat, 16 May 2020 03:00:33 +0200 Subject: [PATCH] add two scripts to extract and embed 3d models c2obj.py extracts the 3d models from the hard-coded C code found in the `model.in.c` files and generates a standard .obj file obj2c.py converts back the geometry in an .obj file to compatible C code --- c2obj.py | 141 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ obj2c.py | 107 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 c2obj.py create mode 100644 obj2c.py diff --git a/c2obj.py b/c2obj.py new file mode 100644 index 00000000..ca4faaa9 --- /dev/null +++ b/c2obj.py @@ -0,0 +1,141 @@ +""" +This module attempts to parse the ``model.inc.c`` files and extract the +3D models within as standard Wavefront OBJ files. + +Example: + Specify the path to the ``.inc.c`` file and a directory where to save + the extracted ``.obj`` files. + + $ python c2obj.py ./actors/mario/model.inc.c ./actors/mario/obj/ + +This is a work in progress and it currently has some serious limitations: + * It only extracts geometry information, so no textures or any other info + * It makes assumptions about the layout of the code in the C source + * It hasn't been properly tested. + +""" + +def parse(filename, output_directory): + from os import path, mkdir + + if not path.isdir(output_directory): + try: + mkdir(output_directory) + except OSError: + print(f'Could not use output directory {output_directory}.') + + vtx_def = 'static const Vtx ' + vtx_data = {} + reading_vtx = False + current_vtx_name = '' + current_vtx_data = [] + current_vtx_vertices = 0 + + gfx_def = 'const Gfx ' + reading_gfx = False + current_gfx_vertices = 0 + current_gfx_faces = 0 + insert_vert_call = 'gsSPVertex(' + insert_1tri_call = 'gsSP1Triangle(' + insert_2tri_call = 'gsSP2Triangles(' + gfx_count = 0 + + end_of_block = '};' + + with open(filename, 'r') as f: + for line in f: + line = line.strip() + + if line.startswith(vtx_def): + vtx_name = line.split(' ')[3][:-2] + current_vtx_name = vtx_name + current_vtx_data = [] + reading_vtx = True + continue + + if line.startswith(gfx_def): + from datetime import datetime + + current_gfx_name = line.split(' ')[2][:-2] + current_gfx_file = open(path.join(output_directory, current_gfx_name + '.obj'), 'w') + current_gfx_file.write("# Armando Arredondo's SM64 Wavefront OBJ Geometry Converter\n") + current_gfx_file.write('# File Created: {}\n\n'.format(datetime.now())) + reading_gfx = True + continue + + if line == end_of_block: + if reading_vtx: + vtx_data[current_vtx_name] = current_vtx_data + reading_vtx = False + + elif reading_gfx: + current_gfx_file.write(f'# {current_gfx_faces} faces\n\n') + current_gfx_file.close() + current_gfx_vertices = 0 + reading_gfx = False + gfx_count += 1 + + continue + + if reading_vtx: + line = line.replace('{', '[').replace('}', ']') + tri = eval(line[:-1])[0] + current_vtx_data.append(tri) + continue + + if reading_gfx: + if line.startswith(insert_vert_call): + args = line[len(insert_vert_call):].split(',') + current_vtx_name = args[0] + + if current_gfx_vertices > 0: + current_gfx_file.write(f'# {current_gfx_faces} faces\n\n') + + current_gfx_faces = 0 + current_vtx_vertices = len(vtx_data[current_vtx_name]) + current_gfx_vertices += current_vtx_vertices + + current_gfx_file.write(f'#\n# object {current_vtx_name}\n#\n\n') + current_vtx_data = vtx_data[current_vtx_name] + for tri in current_vtx_data: + v = tri[0] + current_gfx_file.write('v {:.3f} {:.3f} {:.3f}\n'.format(*v)) + current_gfx_file.write(f'# {current_vtx_vertices} vertices\n\n') + + for tri in current_vtx_data: + n = [_decode_normal(u) for u in tri[3][:3]] + current_gfx_file.write('vn {:.3f} {:.3f} {:.3f}\n'.format(*n)) + current_gfx_file.write(f'# {current_vtx_vertices} vertex normals\n\n') + + current_gfx_file.write(f'g {current_vtx_name}\n\n') + + elif line.startswith(insert_2tri_call): + args = line[len(insert_2tri_call):].split(',') + correction = current_gfx_vertices - current_vtx_vertices + 1 + indexes = [eval(args[i]) + correction for i in [0, 1, 2, 4, 5, 6]] + current_gfx_file.write('f {0}//{0} {1}//{1} {2}//{2}\n'.format(*indexes[:3])) + current_gfx_file.write('f {0}//{0} {1}//{1} {2}//{2}\n'.format(*indexes[3:])) + current_gfx_faces += 2 + + elif line.startswith(insert_1tri_call): + args = line[len(insert_1tri_call):].split(',') + correction = current_gfx_vertices - current_vtx_vertices + 1 + indexes = [eval(args[i]) + correction for i in [0, 1, 2]] + current_gfx_file.write('f {0}//{0} {1}//{1} {2}//{2}\n'.format(*indexes)) + current_gfx_faces += 1 + + continue + + print(f'{gfx_count} models extracted.') + +def _decode_normal(x): + y = x if x <= 127 else x - 255 + return y / 127 + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser() + parser.add_argument('filename', help = 'filename of the .inc.c source file') + parser.add_argument('output_directory', help = 'directory where to put the extracted .obj files') + args = parser.parse_args() + parse(args.filename, args.output_directory) \ No newline at end of file diff --git a/obj2c.py b/obj2c.py new file mode 100644 index 00000000..8e84c806 --- /dev/null +++ b/obj2c.py @@ -0,0 +1,107 @@ +""" +This module generates a fragment of C code, in the style of that found in +the ``model.inc.c`` files, that encodes the geometry of the model specified +by the Wavefront OBJ file. + +Example: + Specify the path to the ``.obj`` file and pipe the output of the script + into the desired destination ``.c`` file. + + $ python obj2c.py left_hand_closed.obj > left_hand_closed.inc.c + +This is a work in progress and it currently has some serious limitations: + * It only encodes the geometry information of the OBJ file, so no + texture mapping or any other info. + * The generated fragment of C code has to be manually pasted into the + desired source file. Make sure that the name of the Gfx structure + you're pasting matches the one you're replacing. + * It hasn't been properly tested. + +""" + +def parse(filename): + from os.path import basename, splitext + from re import sub + + # WARNIGN! + # `gfx_name` is just a guess. You have to manually check that the name + # of the Gfx structure you're pasting matches the one you're replacing. + clean = lambda fn: sub('\W|^(?=\d)','_', fn) + gfx_name = clean(splitext(basename(filename))[0]) + gfx_vertices = [] + gfx_normals = [] + vertex_to_normal = {} + gfx_v_count = 0 + + vtx_name = '' + vtx_faces = [] + vtx_v_count = 0 + + output_upper = [] + output_lower = [f'const Gfx {gfx_name}[] = {{'] + + with open(filename, 'r') as obj: + for line in obj: + line = line.strip() + + if line.startswith('v '): + coordinates = [eval(x) for x in line.split()[1:4]] + gfx_vertices.append(coordinates) + vtx_v_count += 1 + gfx_v_count += 1 + + if line.startswith('vn '): + coordinates = [eval(x) for x in line.split()[1:4]] + gfx_normals.append([_encode_normal(x) for x in coordinates]) + + if line.startswith('g '): + vtx_name = line.split()[1] + + if line.startswith('f '): + pairs = [pair.split('//') for pair in line.split()[1:4]] + vtx_faces.append([int(pair[0]) for pair in pairs]) + for (x, y) in pairs: + vertex_to_normal[int(x) - 1] = int(y) - 1 + + if line.startswith('# ') and line.endswith('faces'): + output_upper.append(f'static const Vtx {vtx_name}[] = {{') + for i in range(gfx_v_count - vtx_v_count, gfx_v_count): + v_string = '[{}, {}, {}]'.format(*gfx_vertices[i]) + n_string = '[{}, {}, {}, 0x00]'.format(*gfx_normals[vertex_to_normal[i]]) + combined = f' [[{v_string}, 0, [0, 0], {n_string}]],' + output_upper.append(combined.replace('[', '{').replace(']', '}')) + + output_upper.append('};\n') + output_lower.append(f' gsSPVertex({vtx_name}, {vtx_v_count}, 0),') + + n = len(vtx_faces) + correction = vtx_v_count - gfx_v_count - 1 + for i in range(int(n / 2)): + f1 = [vtx_faces[2 * i][j] + correction for j in range(3)] + f2 = [vtx_faces[2 * i + 1][j] + correction for j in range(3)] + output_lower.append(' gsSP2Triangles({}, {}, {}, 0x0, {}, {}, {}, 0x0),'.format(*f1, *f2)) + + if n % 2 != 0: + f3 = [vtx_faces[-1][j] + correction for j in range(3)] + output_lower.append(' gsSP1Triangle({}, {}, {}, 0x0),'.format(*f3)) + + vtx_v_count = 0 + vtx_faces = [] + + output_lower.append(' gsSPEndDisplayList(),') + output_lower.append('};') + + for line in output_upper + output_lower: + print(line) + +def _encode_normal(x): + x *= 127 + if x <= 0: x += 255 + return hex(int(x)) + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser() + parser.add_argument('filename', help = 'filename of the .obj file to parse') + args = parser.parse_args() + parse(args.filename) \ No newline at end of file