diff options
Diffstat (limited to '')
-rwxr-xr-x | 3dlib/obj2tmdl.py | 492 |
1 files changed, 492 insertions, 0 deletions
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('<filepath=\"%s\" id=\"tex%d\" colfmt=5 />' % (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)
|