From 7d4e4c0b34a613dd3c0220475ae4e448197522c1 Mon Sep 17 00:00:00 2001 From: Treeki Date: Sat, 12 Mar 2011 23:17:12 +0100 Subject: initial commit. now I can start playing with stuff! --- 3dlib/obj2tmdl.py | 492 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100755 3dlib/obj2tmdl.py (limited to '3dlib/obj2tmdl.py') diff --git a/3dlib/obj2tmdl.py b/3dlib/obj2tmdl.py new file mode 100755 index 0000000..a000ca1 --- /dev/null +++ b/3dlib/obj2tmdl.py @@ -0,0 +1,492 @@ +#!/usr/bin/env python2 +# Obj2tmdl +# Converter for Wavefront OBJ => Treeki Model + +import os +import struct +import sys +u32 = struct.Struct('>I') +u16 = struct.Struct('>H') +u8 = struct.Struct('>B') +f32 = struct.Struct('>f') + +sys.path.append('/home/Treeki/Wii.py/Wii.py') +sys.path.append('/home/me/Packages/Wii.py') +import Wii + +class MaterialLib: + class Material: + pass + + + def __init__(self, filename): + self.current_mat = None + self.materials = {} + + for line in open(filename, 'r'): + self.parse_line(line) + + + def parse_line(self, line): + line = line.strip() + if line == '' or line[0] == '#': + return + + line = line.split() + cmd = line[0] + + if cmd == 'newmtl': + self.current_mat = MaterialLib.Material() + self.materials[line[1]] = self.current_mat + + elif cmd == 'map_Ka': + self.current_mat.texture = ' '.join(line[1:]) + + elif cmd == 'Kd': + r = int(float(line[1]) * 255) + g = int(float(line[2]) * 255) + b = int(float(line[3]) * 255) + self.current_mat.colour = (r,g,b,255) + + + def prepare_textures(self): + # get a list of every texture file used + self.texture_id = {} + textures = [] + id = 0 + + for mat in self.materials.values(): + if hasattr(mat, 'texture'): + if mat.texture not in textures: + textures.append(mat.texture) + self.texture_id[mat.texture] = id + id += 1 + + + if len(textures) == 0: + self.tpl = None + return + + # create a gxtexconv script file and an ID mapping + #script = [] + #self.texture_id = {} + # + #for tex, id in zip(textures, range(len(textures))): + # script.append('' % (tex.replace('\\','/'),id)) + # self.texture_id[id] = tex + # + #print script + # + #open('texture_script_temp_0991.scf', 'w').write('\n'.join(script)) + # + #os.system('gxtexconv -s texture_script_temp_0991.scf -o generated_texture_0991.tpl') + + # fix this later + texture_files = [tex.replace('\\','/').replace(' ','\\ ').replace('.jpg','.png') for tex in textures] + print 'zetsubou wpng RGB5A3 %s generated_texture_0991.tpl' % ' '.join(texture_files) + os.system('zetsubou wpng RGB5A3 %s generated_texture_0991.tpl' % ' '.join(texture_files)) + self.tpl = open('generated_texture_0991.tpl', 'rb').read() + + #os.remove('texture_script_temp_0991.scf') + os.remove('generated_texture_0991.tpl') + + + +class ObjReader: + class ObjShape: + def __init__(self, name): + self.face_groups = [ \ + ObjReader.ObjFaceGroup('quads'), \ + ObjReader.ObjFaceGroup('triangles')] + + self.quad_group, self.tri_group = self.face_groups + + self.shape_name = name + self.material_name = None + + + class ObjFace: + __slots__ = ('vertices', 'texcoords', 'normals') + + def __init__(self, vertices, texcoords, normals): + self.vertices = vertices + self.texcoords = texcoords + self.normals = normals + + + class ObjFaceGroup: + def __init__(self, type): + self.type = type + self.faces = [] + + + def __init__(self): + self.material_lib = None + + self.vertex_lists = [] + self.texcoord_lists = [] + self.normal_lists = [] + self.shapes = [] + + self.current_shape = None + + + def parse_line(self, line): + line = line.strip() + if line == '' or line[0] == '#': + return + + line = line.split() + cmd = line[0] + + if cmd == 'v': + self.vertex_lists.append(map(float, line[1:])) + + elif cmd == 'vt': + tc = map(float, line[1:])[:2] # chop off third coord + tc[1] = 1.0 - tc[1] + self.texcoord_lists.append(tc) + + elif cmd == 'vn': + self.normal_lists.append(map(float, line[1:])) + + elif cmd == 'f': + v, t, n = [], [], [] + + if len(line) == 4: + # triangle + group = self.current_shape.tri_group + elif len(line) == 5: + # quad + group = self.current_shape.quad_group + + for entry in line[1:]: + entry_split = map(lambda x:-1 if x == '' else int(x), entry.split('/')) + + v.append(self.vertex_lists[entry_split[0] - 1]) + + if entry_split[1] == -1: + t.append([0.0, 0.0]) + else: + t.append(self.texcoord_lists[entry_split[1] - 1]) + + if entry_split[2] == -1: + n.append([0.0, 0.0, 0.0]) + else: + n.append(self.normal_lists[entry_split[2] - 1]) + + group.faces.append(ObjReader.ObjFace(v, t, n)) + + #elif cmd == 'g': + # self.current_shape = ObjReader.ObjShape(line[1]) + # self.shapes.append(self.current_shape) + + elif cmd == 'mtllib': + self.material_lib = MaterialLib(os.path.join(self.path, ' '.join(line[1:]))) + + elif cmd == 'usemtl': + self.current_shape = ObjReader.ObjShape(line[1]) + self.shapes.append(self.current_shape) + self.current_shape.material_name = line[1] + + + +# === Treeki Model Format === +# Header: +# +# struct tmdl_header { +# u32 magic; // always TMDL +# u32 version; // currently 2 +# u32 shape_count; +# }; +# [[Followed by an array of u32s for each shape offset]] +# +# struct tmdl_shape { +# u32 dl_offset; // must be aligned to 0x20 +# u32 dl_size; // must be aligned to 0x20 +# u32 mat_offset; +# u32 pos_arr_offset; // must be aligned to 0x20 +# u32 pos_arr_size; // must be aligned to 0x20 +# u32 nrm_arr_offset; // must be aligned to 0x20 +# u32 nrm_arr_size; // must be aligned to 0x20 +# u32 uv_arr_offset; // must be aligned to 0x20 +# u32 uv_arr_size; // must be aligned to 0x20 +# }; +# +# struct tmdl_material { +# u32 texture_id; // once the model is bound, points to id within tpl +# }; +# +# dl_offset points to a GX display list which is used for +# rendering the model in question. + + +class TmdlWriter: + def __init__(self): + self.shapes = [] + + + def build_with_obj(self, obj): + self.shapes = obj.shapes + self.material_lib = obj.material_lib + self.materials = obj.material_lib.materials + + self.material_lib.prepare_textures() + + + def pack(self): + offset = 0 + + header = struct.pack('>4sI I', 'TMDL', 2, len(self.shapes)) + offset += len(header) + + # reserve space for the shape offsets, we'll add them later + offset += (4 * len(self.shapes)) + + # create the materials + material_offs = {} + material_data = '' + + for name,material in self.materials.iteritems(): + material_offs[name] = offset + material_struct = self.build_material_struct(material) + material_data += material_struct + offset += len(material_struct) + + + # create the shapes -- first, we'll assemble the GX arrays and DLs + shape_arrays = [] + packed_shape_arrays = [] + shape_display_lists = [] + + print 'Building shapes... [%d]' % len(self.shapes) + + count = 0 + for shape in self.shapes: + pos = self.build_array_from_shape_attr(shape, 'vertices') + nrm = self.build_array_from_shape_attr(shape, 'normals') + uv = self.build_array_from_shape_attr(shape, 'texcoords') + shape_arrays.append((pos,nrm,uv)) + + ppos = self.pack_data_array(pos) + pnrm = self.pack_data_array(nrm) + puv = self.pack_data_array(uv) + packed_shape_arrays.append((ppos,pnrm,puv)) + + dl = self.build_display_list_with_shape(shape, pos, nrm, uv) + shape_display_lists.append(dl) + count += 1 + + print '%d done' % count + + print 'Shapes built' + + + # calculate offsets to every display list + # shape struct is currently 36 bytes, change this if the size changes + # also, make sure they're aligned to an offset of 0x20 + temp_offset = offset + (len(self.shapes) * 36) + + if temp_offset & 0x1f != 0: + aligned_offset = (temp_offset + 0x20) & ~0x1f + dl_start_padding = '\0' * (aligned_offset - temp_offset) + temp_offset = aligned_offset + else: + dl_start_padding = '' + + shape_display_list_offsets = [] + for dl in shape_display_lists: + shape_display_list_offsets.append(temp_offset) + temp_offset += len(dl) + + # calculate offsets to every GX array + pos_array_offsets = [] + nrm_array_offsets = [] + uv_array_offsets = [] + + for pos,nrm,uv in packed_shape_arrays: + pos_array_offsets.append(temp_offset) + temp_offset += len(pos) + nrm_array_offsets.append(temp_offset) + temp_offset += len(nrm) + uv_array_offsets.append(temp_offset) + temp_offset += len(uv) + + + # and now, create the shape structs themselves + shape_offsets = [] + shape_data = '' + + for shape, dl, dl_offset, pos_offs, nrm_offs, uv_offs, (pos, nrm, uv) in \ + zip(self.shapes, shape_display_lists, shape_display_list_offsets, \ + pos_array_offsets, nrm_array_offsets, uv_array_offsets, \ + packed_shape_arrays): + + # first off, store the offset + shape_offsets.append(offset) + + # now build the struct + shape_struct = struct.pack('>IIIIIIIII', \ + dl_offset, len(dl), material_offs[shape.material_name], \ + pos_offs, len(pos), \ + nrm_offs, len(nrm), \ + uv_offs, len(uv)) + + shape_data += shape_struct + offset += len(shape_struct) + + + # almost there! + print 'Packing model...' + + tmdl_bits = [header] + tmdl_add = tmdl_bits.append + + for offs in shape_offsets: + tmdl_add(u32.pack(offs)) + + tmdl_add(material_data) + tmdl_add(shape_data) + tmdl_add(dl_start_padding) + + tmdl_bits += shape_display_lists + + for pos,nrm,uv in packed_shape_arrays: + tmdl_bits.append(pos) + tmdl_bits.append(nrm) + tmdl_bits.append(uv) + + return ''.join(tmdl_bits) + + + def build_material_struct(self, material): + texture_id = 0xFFFFFFFF + if hasattr(material, 'texture'): + texture_id = self.material_lib.texture_id[material.texture] + + cR, cG, cB, cA = material.colour + return struct.pack('>IBBBB', texture_id, cR, cG, cB, cA) + + + def build_array_from_shape_attr(self, shape, attr): + assemble = [] + + for g in shape.face_groups: + for f in g.faces: + assemble += f.__dict__[attr] + + final = [] + for i in assemble: + if i not in final: + final.append(i) + + if len(final) >= 0xFFFE: + print '=== WARNING! List too big! ===' + + return final + + + def pack_data_array(self, data): + out = [] + + for piece in data: + for bit in piece: + out.append(f32.pack(bit)) + + packed = ''.join(out) + + if len(packed) % 0x20 != 0: + aligned = (len(packed) + 0x1f) & ~0x1fe + packed += '\0' * (aligned - len(packed)) + + return packed + + + def build_display_list_with_shape(self, shape, vtx_list, nrm_list, uv_list): + # http://hitmen.c02.at/files/yagcd/yagcd/chap5.html + # assume vertex descriptor: [data is specified in this order] + # GXClearVtxDesc() + # GXSetVtxDesc(GX_VA_POS, GX_DIRECT) + # GXSetVtxDesc(GX_VA_NRM, GX_DIRECT) + # GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT) + + dl_bits = [] + _dl_add = dl_bits.append + + _vtx_index = vtx_list.index + _nrm_index = nrm_list.index + _uv_index = uv_list.index + + _u16_pack = u16.pack + + for face_group in shape.face_groups: + if len(face_group.faces) == 0: + continue + + vtx_num = reduce(lambda x,y:x+len(y.vertices), face_group.faces, 0) + + # commands: GX_QUADS = 0x80, GX_TRIANGLES = 0x90 + if face_group.type == 'quads': + _dl_add('\x80') # opcode + elif face_group.type == 'triangles': + _dl_add('\x90') # opcode + + _dl_add(_u16_pack(vtx_num)) + + # now send over the data + for face in face_group.faces: + for v, t, n in zip(face.vertices, face.texcoords, face.normals): + _dl_add(_u16_pack(_vtx_index(v))) + _dl_add(_u16_pack(_nrm_index(n))) + _dl_add(_u16_pack(_uv_index(t))) + + # done! + return self.build_display_list(dl_bits) + + + def build_display_list(self, dl_bits): + # assemble it + dl = ''.join(dl_bits) + + # pad the display list to 0x20 bytes with GX_NOP (0x00) + if len(dl) % 0x20 != 0: + pad_count = ((len(dl) + 0x20) & ~0x1F) - len(dl) + dl += '\x00' * pad_count + + return dl + + +#file = r'H:\ISOs\NSMBWii\testmush3' +#file = 'testmdl2' +#file = 'simple2' +file = sys.argv[1] +shortfn = file[file.rfind('/')+1:] + +if 'tex-only' in sys.argv: + mtl = MaterialLib(file+'.mtl') + mtl.prepare_textures() + open('tex_%s.tpl' % shortfn, 'wb').write(mtl.tpl) + sys.exit() + + + +obj = ObjReader() +if '/' in file: + obj.path = file[:file.rfind('/')] +else: + obj.path = '.' + +for line in open(file+'.obj'): + obj.parse_line(line) + +tmdl = TmdlWriter() +tmdl.build_with_obj(obj) + +tmdl_file = tmdl.pack() +tpl_file = tmdl.material_lib.tpl + +arc = Wii.U8() +arc['t3d'] = None +arc['t3d/mdl_%s.tmdl' % shortfn] = tmdl_file +if tpl_file: arc['t3d/tex_%s.tpl' % shortfn] = tpl_file +arc.dumpFile('%s.arc' % file) -- cgit v1.2.3