From bcc9cf513195aa15731e9384be328ff81fef9b93 Mon Sep 17 00:00:00 2001 From: Jake Date: Thu, 21 Jan 2021 21:03:54 -0800 Subject: [PATCH 01/16] Basic LTB rigid mesh support added --- src/abc.py | 32 ++++ src/importer.py | 28 ++- src/reader_ltb_pc.py | 396 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 454 insertions(+), 2 deletions(-) create mode 100644 src/reader_ltb_pc.py diff --git a/src/abc.py b/src/abc.py index d892822..92a1f5e 100644 --- a/src/abc.py +++ b/src/abc.py @@ -51,6 +51,9 @@ def __init__(self): self.location = Vector() self.normal = Vector() + # LTB specific + self.colour = 0 + class FaceVertex(object): def __init__(self): @@ -58,6 +61,11 @@ def __init__(self): self.vertex_index = 0 self.reversed = False + # LTB specific + + # Supports up to 4 UVs, so let's add some more! + self.extra_texcoords = [Vector(), Vector(), Vector()] + class Face(object): def __init__(self): @@ -69,6 +77,22 @@ def __init__(self): self.faces = [] self.vertices = [] + # LTB specific + self.texture_count = 0 + self.render_style = 0 + self.render_priority = 0 + self.textures = [] # Order? + self.type = 7 # Null + + self.max_bones_per_face = 0 + self.max_bones_per_vert = 0 + self.vert_count = 0 + self.face_count = 0 + + # Basis Vector??? + self.s = Vector() + self.t = Vector() + def get_face_vertices(self, face_index): return [self.vertices[vertex.vertex_index] for vertex in self.faces[face_index].vertices] @@ -86,6 +110,11 @@ def __init__(self): self.lod_weight = 1.0 self.name = '' self.lods = [] + + # LTB specific + self.lod_min = 0.0 + self.lod_max = 0.0 + self.lod_distances = [] class Node(object): @@ -212,6 +241,9 @@ def __init__(self): # Flip animation keyframes self.flip_anim = True + + # LTB specific + @property def keyframe_count(self): diff --git a/src/importer.py b/src/importer.py index 23f8f09..c2f937e 100644 --- a/src/importer.py +++ b/src/importer.py @@ -12,6 +12,7 @@ # Format imports from .reader_abc_v6_pc import ABCV6ModelReader from .reader_abc_pc import ABCModelReader +from .reader_ltb_pc import PCLTBModelReader from .reader_ltb_ps2 import PS2LTBModelReader from . import utils @@ -77,9 +78,25 @@ def import_model(model, options): if bone.parent is not None: bone.use_connect = bone.parent.tail == bone.head # End If - # End For + # FIXME: Make bones touch their parents. + # This however breaks the animations. + # + # I think I need to offset this in the animation processing, + # and animations might be a touch broken right now but it's not noticable...just ugly in blender. + # ---------------------------- + # for node in model.nodes: + # bone = armature.edit_bones[node.name] + + # if len(bone.children) == 0: + # continue + # # End If + + # for child in bone.children: + # bone.tail = child.head + # # End For + Ops.object.mode_set(mode='OBJECT') ''' Add sockets as empties with a child-of constraint to the appropriate bone. ''' @@ -616,9 +633,16 @@ def draw(self, context): box.row().prop(self, 'should_clear_scene') def execute(self, context): + + # Load the model + try: + model = PCLTBModelReader().from_file(self.filepath) + except Exception as e: + model = PS2LTBModelReader().from_file(self.filepath) + # Load the model #try: - model = PS2LTBModelReader().from_file(self.filepath) + #model = PS2LTBModelReader().from_file(self.filepath) #except Exception as e: # show_message_box(str(e), "Read Error", 'ERROR') # return {'CANCELLED'} diff --git a/src/reader_ltb_pc.py b/src/reader_ltb_pc.py new file mode 100644 index 0000000..2f97374 --- /dev/null +++ b/src/reader_ltb_pc.py @@ -0,0 +1,396 @@ +import os +from .abc import * +from .io import unpack +from mathutils import Vector, Matrix, Quaternion + +# LTB Mesh Types +LTB_Type_Rigid_Mesh = 4 +LTB_Type_Skeletal_Mesh = 5 +LTB_Type_Vertex_Animated_Mesh = 6 +LTB_Type_Null_Mesh = 7 + +# Data stream flags +VTX_Position = 0x0001 +VTX_Normal = 0x0002 +VTX_Colour = 0x0004 +VTX_UV_Sets_1 = 0x0010 +VTX_UV_Sets_2 = 0x0020 +VTX_UV_Sets_3 = 0x0040 +VTX_UV_Sets_4 = 0x0080 +VTX_BasisVector = 0x0100 + +# Animation Compression Types +CMP_None = 0 +CMP_Relevant = 1 +CMP_Relevant_16 = 2 +CMP_Relevant_Rot16 = 3 + +# +# Supports LTB v23 +# +class PCLTBModelReader(object): + def __init__(self): + + self._version = 0 + self._node_count = 0 + self._lod_count = 0 + + def _read_matrix(self, f): + data = unpack('16f', f) + rows = [data[0:4], data[4:8], data[8:12], data[12:16]] + return Matrix(rows) + + def _read_vector(self, f): + return Vector(unpack('3f', f)) + + def _read_quaternion(self, f): + x, y, z, w = unpack('4f', f) + return Quaternion((w, x, y, z)) + + def _read_string(self, f): + return f.read(unpack('H', f)[0]).decode('ascii') + + def _read_weight(self, f): + weight = Weight() + weight.node_index = unpack('I', f)[0] + weight.location = self._read_vector(f) + weight.bias = unpack('f', f)[0] + return weight + + def _read_vertex(self, f): + vertex = Vertex() + weight_count = unpack('H', f)[0] + vertex.sublod_vertex_index = unpack('H', f)[0] + vertex.weights = [self._read_weight(f) for _ in range(weight_count)] + vertex.location = self._read_vector(f) + vertex.normal = self._read_vector(f) + return vertex + + def _read_face_vertex(self, f): + face_vertex = FaceVertex() + face_vertex.texcoord.xy = unpack('2f', f) + face_vertex.vertex_index = unpack('H', f)[0] + return face_vertex + + def _read_face(self, f): + face = Face() + face.vertices = [self._read_face_vertex(f) for _ in range(3)] + return face + + def _read_null_mesh(self, lod, f): + # No data here but a filler int! + f.seek(4, 1) + return lod + + def _read_rigid_mesh(self, lod, f): + data_type = unpack('4I', f) + bone = unpack('I', f)[0] + + # We need face vertex data alongside vertices! + face_vertex_list = [] + + for mask in data_type: + for _ in range(lod.vert_count): + vertex = Vertex() + face_vertex = FaceVertex() + + # Dirty flags + is_vertex_used = False + is_face_vertex_used = False + + + if mask & VTX_Position: + vertex.location = self._read_vector(f) + is_vertex_used = True + if mask & VTX_Normal: + vertex.normal = self._read_vector(f) + is_vertex_used = True + if mask & VTX_Colour: + vertex.colour = unpack('i', f)[0] + is_vertex_used = True + if mask & VTX_UV_Sets_1: + face_vertex.texcoord.xy = unpack('2f', f) + is_face_vertex_used = True + if mask & VTX_UV_Sets_2: + face_vertex.extra_texcoords[0].xy = unpack('2f', f) + is_face_vertex_used = True + if mask & VTX_UV_Sets_3: + face_vertex.extra_texcoords[1].xy = unpack('2f', f) + is_face_vertex_used = True + if mask & VTX_UV_Sets_4: + face_vertex.extra_texcoords[2].xy = unpack('2f', f) + is_face_vertex_used = True + if mask & VTX_BasisVector: + vertex.s = self._read_vector(f) + vertex.t = self._read_vector(f) + is_vertex_used = True + # End If + + if is_vertex_used: + lod.vertices.append(vertex) + + if is_face_vertex_used: + face_vertex_list.append(face_vertex) + + # End For + # End For + + # Make sure our stuff is good!! + print ("Vert Count Check: %d/%d" % (lod.vert_count, len(lod.vertices))) + assert(lod.vert_count == len(lod.vertices)) + + # We need a "global" face, we'll fill it and re-use it. + face = Face() + for _ in range(lod.face_count * 3): + vertex_index = unpack('H', f)[0] + + face_vertex = face_vertex_list[vertex_index] + face_vertex.vertex_index = vertex_index + + # If we have room, append! + if len(face.vertices) < 3: + face.vertices.append(face_vertex) + + # If we're now over, then flush! + if len(face.vertices) >= 3: + lod.faces.append(face) + # Make a new face, and append our face vertex + face = Face() + #face.vertices.append(face_vertex) + + # Make sure our stuff is good!! + print ("Face Count Check: %d/%d" % (lod.face_count, len(lod.faces))) + assert(lod.face_count == len(lod.faces)) + + return lod + + def _read_lod(self, f): + lod = LOD() + + lod.texture_count = unpack('I', f)[0] + lod.textures = unpack('4I', f) + lod.render_style = unpack('I', f)[0] + lod.render_priority = unpack('b', f)[0] + + lod.type = unpack('I', f)[0] + + # NULL Type + if lod.type == LTB_Type_Null_Mesh: + # Early return here, because there's no more data... + return self._read_null_mesh(lod, f) + + # Some common data + obj_size = unpack('I', f)[0] + lod.vert_count = unpack('I', f)[0] + lod.face_count = unpack('I', f)[0] + lod.max_bones_per_face = unpack('I', f)[0] + lod.max_bones_per_vert = unpack('I', f)[0] + + if lod.type == LTB_Type_Rigid_Mesh: + lod = self._read_rigid_mesh(lod, f) + + return lod + + def _read_piece(self, f): + piece = Piece() + + piece.name = self._read_string(f) + lod_count = unpack('I', f)[0] + piece.lod_distances = [unpack('f', f)[0] for _ in range(lod_count)] + piece.lod_min = unpack('I', f)[0] + piece.lod_max = unpack('I', f)[0] + piece.lods = [self._read_lod(f) for _ in range(lod_count)] + + return piece + + def _read_node(self, f): + node = Node() + node.name = self._read_string(f) + node.index = unpack('H', f)[0] + node.flags = unpack('b', f)[0] + node.bind_matrix = self._read_matrix(f) + node.inverse_bind_matrix = node.bind_matrix.inverted() + node.child_count = unpack('I', f)[0] + return node + + def _read_transform(self, f): + transform = Animation.Keyframe.Transform() + transform.location = self._read_vector(f) + transform.rotation = self._read_quaternion(f) + + # Two unknown floats! + if self._version == 13: + f.seek(8, 1) + + return transform + + def _read_child_model(self, f): + child_model = ChildModel() + child_model.name = self._read_string(f) + child_model.build_number = unpack('I', f)[0] + child_model.transforms = [self._read_transform(f) for _ in range(self._node_count)] + return child_model + + def _read_keyframe(self, f): + keyframe = Animation.Keyframe() + keyframe.time = unpack('I', f)[0] + keyframe.string = self._read_string(f) + return keyframe + + def _read_animation(self, f): + animation = Animation() + animation.extents = self._read_vector(f) + animation.name = self._read_string(f) + animation.unknown1 = unpack('i', f)[0] + animation.interpolation_time = unpack('I', f)[0] if self._version >= 12 else 200 + animation.keyframe_count = unpack('I', f)[0] + animation.keyframes = [self._read_keyframe(f) for _ in range(animation.keyframe_count)] + animation.node_keyframe_transforms = [] + for _ in range(self._node_count): + + # Skip past -1 + if self._version == 13: + f.seek(4, 1) + + animation.node_keyframe_transforms.append( + [self._read_transform(f) for _ in range(animation.keyframe_count)]) + return animation + + def _read_socket(self, f): + socket = Socket() + socket.node_index = unpack('I', f)[0] + socket.name = self._read_string(f) + socket.rotation = self._read_quaternion(f) + socket.location = self._read_vector(f) + return socket + + def _read_anim_binding(self, f): + anim_binding = AnimBinding() + anim_binding.name = self._read_string(f) + anim_binding.extents = self._read_vector(f) + anim_binding.origin = self._read_vector(f) + return anim_binding + + def _read_weight_set(self, f): + weight_set = WeightSet() + weight_set.name = self._read_string(f) + node_count = unpack('I', f)[0] + weight_set.node_weights = [unpack('f', f)[0] for _ in range(node_count)] + return weight_set + + def from_file(self, path): + model = Model() + model.name = os.path.splitext(os.path.basename(path))[0] + with open(path, 'rb') as f: + + # + # HEADER + # + file_type = unpack('H', f)[0] + file_version = unpack('H', f)[0] + + if file_type is not 1: + raise Exception('Unsupported File Type! Only mesh LTB files are supported.') + # End If + + if file_version is not 9: + raise Exception('Unsupported File Version! Importer currently only supports v9.') + # End If + + # Skip 4 ints + f.seek(4 * 4, 1) + + self.version = unpack('i', f)[0] + + # Hope to support at least up to v25 someday! + if self.version not in [23]: + raise Exception('Unsupported file version ({}).'.format(self._version)) + # End If + + model.version = self.version + + keyframe_count = unpack('i', f)[0] + animation_count = unpack('i', f)[0] + node_count = unpack('i', f)[0] + piece_count = unpack('i', f)[0] + child_model_count = unpack('i', f)[0] + face_count = unpack('i', f)[0] + vertex_count = unpack('i', f)[0] + vertex_weight_count = unpack('i', f)[0] + lod_count = unpack('i', f)[0] + socket_count = unpack('i', f)[0] + weight_set_count = unpack('i', f)[0] + string_count = unpack('i', f)[0] + string_length = unpack('i', f)[0] + vertex_animation_data_size = unpack('i', f)[0] + animation_data_size = unpack('i', f)[0] + + model.command_string = self._read_string(f) + + model.internal_radius = unpack('f', f)[0] + + # + # OBB Information + # + obb_count = unpack('i', f)[0] + + # TODO: Figure out OBB + + # + # Pieces + # + + # Yep again! + piece_count = unpack('i', f)[0] + model.pieces = [self._read_piece(f) for _ in range(piece_count)] + + return model + + # OLD - Reference + next_section_offset = 0 + while next_section_offset != -1: + f.seek(next_section_offset) + section_name = self._read_string(f) + next_section_offset = unpack('i', f)[0] + if section_name == 'Header': + self._version = unpack('I', f)[0] + if self._version not in [9, 10, 11, 12, 13]: + raise Exception('Unsupported file version ({}).'.format(self._version)) + model.version = self._version + f.seek(8, 1) + self._node_count = unpack('I', f)[0] + f.seek(20, 1) + self._lod_count = unpack('I', f)[0] + f.seek(4, 1) + self._weight_set_count = unpack('I', f)[0] + f.seek(8, 1) + + # Unknown new value + if self._version >= 13: + f.seek(4,1) + + model.command_string = self._read_string(f) + model.internal_radius = unpack('f', f)[0] + f.seek(64, 1) + model.lod_distances = [unpack('f', f)[0] for _ in range(self._lod_count)] + elif section_name == 'Pieces': + weight_count, pieces_count = unpack('2I', f) + model.pieces = [self._read_piece(f) for _ in range(pieces_count)] + elif section_name == 'Nodes': + model.nodes = [self._read_node(f) for _ in range(self._node_count)] + build_undirected_tree(model.nodes) + weight_set_count = unpack('I', f)[0] + model.weight_sets = [self._read_weight_set(f) for _ in range(weight_set_count)] + elif section_name == 'ChildModels': + child_model_count = unpack('H', f)[0] + model.child_models = [self._read_child_model(f) for _ in range(child_model_count)] + elif section_name == 'Animation': + animation_count = unpack('I', f)[0] + model.animations = [self._read_animation(f) for _ in range(animation_count)] + elif section_name == 'Sockets': + socket_count = unpack('I', f)[0] + model.sockets = [self._read_socket(f) for _ in range(socket_count)] + elif section_name == 'AnimBindings': + anim_binding_count = unpack('I', f)[0] + model.anim_bindings = [self._read_anim_binding(f) for _ in range(anim_binding_count)] + return model From 25063855e770f49a417b0ca6587944bae106a86e Mon Sep 17 00:00:00 2001 From: Jake Date: Thu, 21 Jan 2021 22:08:43 -0800 Subject: [PATCH 02/16] Read in LTB non-matrix palette skeletal meshes --- src/reader_ltb_pc.py | 165 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 164 insertions(+), 1 deletion(-) diff --git a/src/reader_ltb_pc.py b/src/reader_ltb_pc.py index 2f97374..468584f 100644 --- a/src/reader_ltb_pc.py +++ b/src/reader_ltb_pc.py @@ -25,6 +25,8 @@ CMP_Relevant_16 = 2 CMP_Relevant_Rot16 = 3 +Invalid_Bone = 255 + # # Supports LTB v23 # @@ -98,10 +100,131 @@ def _read_rigid_mesh(self, lod, f): is_vertex_used = False is_face_vertex_used = False + if mask & VTX_Position: + vertex.location = self._read_vector(f) + + # One bone per vertex + weight = Weight() + weight.node_index = bone + weight.bias = 1.0 + + #vertex.weights.append(weight) + + is_vertex_used = True + if mask & VTX_Normal: + vertex.normal = self._read_vector(f) + is_vertex_used = True + if mask & VTX_Colour: + vertex.colour = unpack('i', f)[0] + is_vertex_used = True + if mask & VTX_UV_Sets_1: + face_vertex.texcoord.xy = unpack('2f', f) + is_face_vertex_used = True + if mask & VTX_UV_Sets_2: + face_vertex.extra_texcoords[0].xy = unpack('2f', f) + is_face_vertex_used = True + if mask & VTX_UV_Sets_3: + face_vertex.extra_texcoords[1].xy = unpack('2f', f) + is_face_vertex_used = True + if mask & VTX_UV_Sets_4: + face_vertex.extra_texcoords[2].xy = unpack('2f', f) + is_face_vertex_used = True + if mask & VTX_BasisVector: + vertex.s = self._read_vector(f) + vertex.t = self._read_vector(f) + is_vertex_used = True + # End If + + if is_vertex_used: + lod.vertices.append(vertex) + + if is_face_vertex_used: + face_vertex_list.append(face_vertex) + + # End For + # End For + + # Make sure our stuff is good!! + print ("Vert Count Check: %d/%d" % (lod.vert_count, len(lod.vertices))) + assert(lod.vert_count == len(lod.vertices)) + + # We need a "global" face, we'll fill it and re-use it. + face = Face() + for _ in range(lod.face_count * 3): + vertex_index = unpack('H', f)[0] + + face_vertex = face_vertex_list[vertex_index] + face_vertex.vertex_index = vertex_index + + # If we have room, append! + if len(face.vertices) < 3: + face.vertices.append(face_vertex) + # End If + + # If we're now over, then flush! + if len(face.vertices) >= 3: + lod.faces.append(face) + # Make a new face, and append our face vertex + face = Face() + # End If + # End For + + # Make sure our stuff is good!! + print ("Face Count Check: %d/%d" % (lod.face_count, len(lod.faces))) + assert(lod.face_count == len(lod.faces)) + + return lod + + def _read_skeletal_mesh(self, lod, f): + reindexed_bone = unpack('B', f)[0] + data_type = unpack('4I', f) + + matrix_palette = unpack('B', f)[0] + + print("Matrix Palette? %d" % matrix_palette) + + # We need face vertex data alongside vertices! + face_vertex_list = [] + + for mask in data_type: + for _ in range(lod.vert_count): + vertex = Vertex() + face_vertex = FaceVertex() + + # Dirty flags + is_vertex_used = False + is_face_vertex_used = False if mask & VTX_Position: vertex.location = self._read_vector(f) is_vertex_used = True + + weights = [] + + weight = Weight() + weight.bias = 1.0 + + weights.append(weight) + + for i in range(lod.max_bones_per_face): + # Skip the first one + if i == 0: + continue + # End If + + # There's 3 additional blends, + # If ... max_bones_per_face >= 2,3,4 + if lod.max_bones_per_face >= (i+1): + blend = unpack('f', f)[0] + weight.bias -= blend + + blend_weight = Weight() + blend_weight.bias = blend + weights.append(blend_weight) + # End If + # End For + + vertex.weights = []#weights if mask & VTX_Normal: vertex.normal = self._read_vector(f) is_vertex_used = True @@ -150,18 +273,53 @@ def _read_rigid_mesh(self, lod, f): # If we have room, append! if len(face.vertices) < 3: face.vertices.append(face_vertex) + # End If # If we're now over, then flush! if len(face.vertices) >= 3: lod.faces.append(face) # Make a new face, and append our face vertex face = Face() - #face.vertices.append(face_vertex) + # End If + # End For # Make sure our stuff is good!! print ("Face Count Check: %d/%d" % (lod.face_count, len(lod.faces))) assert(lod.face_count == len(lod.faces)) + bone_set_count = unpack('I', f)[0] + + for _ in range(bone_set_count): + index_start = unpack('H', f)[0] + index_count = unpack('H', f)[0] + + bone_list = unpack('4B', f) + + # ??? + index_buffer_index = unpack('I', f)[0] + + # Okay, now we can fill up our node indexes! + # for vertex_index in range(index_start, index_count): + # vertex = lod.vertices[vertex_index] + + # # We need to re-build the weight list for our vertex + # weights = [] + + # for (index, bone_index) in enumerate(bone_list): + # # If we've got an invalid bone (255) then ignore it + # if bone_index == Invalid_Bone: + # continue + # # End If + + # vertex.weights[index].node_index = bone_index + # # Keep this one! + # weights.append(vertex.weights[index]) + # # End For + + # vertex.weights = weights + # End For + # End For + return lod def _read_lod(self, f): @@ -188,6 +346,11 @@ def _read_lod(self, f): if lod.type == LTB_Type_Rigid_Mesh: lod = self._read_rigid_mesh(lod, f) + elif lod.type == LTB_Type_Skeletal_Mesh: + lod = self._read_skeletal_mesh(lod, f) + + nodes_used_count = unpack('B', f)[0] + nodes_used = [unpack('B', f)[0] for _ in range(nodes_used_count)] return lod From 0716825f1b004b91b2932c4749748f7ca84a7ac5 Mon Sep 17 00:00:00 2001 From: Jake Date: Thu, 21 Jan 2021 22:28:11 -0800 Subject: [PATCH 03/16] Junk commit to test github actions artifact upload (Sorry!) --- src/reader_ltb_pc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reader_ltb_pc.py b/src/reader_ltb_pc.py index 468584f..020dcfa 100644 --- a/src/reader_ltb_pc.py +++ b/src/reader_ltb_pc.py @@ -140,7 +140,7 @@ def _read_rigid_mesh(self, lod, f): if is_face_vertex_used: face_vertex_list.append(face_vertex) - + # End For # End For From 4167cf1656ccb474469402719918463ea212d775 Mon Sep 17 00:00:00 2001 From: Jake Date: Thu, 21 Jan 2021 23:12:43 -0800 Subject: [PATCH 04/16] Added mesh skinning. So you can do all those cool poses. --- src/reader_ltb_pc.py | 54 ++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/src/reader_ltb_pc.py b/src/reader_ltb_pc.py index 020dcfa..9c26f50 100644 --- a/src/reader_ltb_pc.py +++ b/src/reader_ltb_pc.py @@ -108,7 +108,7 @@ def _read_rigid_mesh(self, lod, f): weight.node_index = bone weight.bias = 1.0 - #vertex.weights.append(weight) + vertex.weights.append(weight) is_vertex_used = True if mask & VTX_Normal: @@ -202,9 +202,7 @@ def _read_skeletal_mesh(self, lod, f): weights = [] weight = Weight() - weight.bias = 1.0 - - weights.append(weight) + weight.bias = 1.0 for i in range(lod.max_bones_per_face): # Skip the first one @@ -224,7 +222,9 @@ def _read_skeletal_mesh(self, lod, f): # End If # End For - vertex.weights = []#weights + weights.append(weight) + + vertex.weights = weights if mask & VTX_Normal: vertex.normal = self._read_vector(f) is_vertex_used = True @@ -299,25 +299,31 @@ def _read_skeletal_mesh(self, lod, f): index_buffer_index = unpack('I', f)[0] # Okay, now we can fill up our node indexes! - # for vertex_index in range(index_start, index_count): - # vertex = lod.vertices[vertex_index] + for vertex_index in range(index_start, index_start + index_count): + vertex = lod.vertices[vertex_index] - # # We need to re-build the weight list for our vertex - # weights = [] + # We need to re-build the weight list for our vertex + weights = [] - # for (index, bone_index) in enumerate(bone_list): - # # If we've got an invalid bone (255) then ignore it - # if bone_index == Invalid_Bone: - # continue - # # End If + for (index, bone_index) in enumerate(bone_list): + # If we've got an invalid bone (255) then ignore it + if bone_index == Invalid_Bone: + continue + # End If - # vertex.weights[index].node_index = bone_index - # # Keep this one! - # weights.append(vertex.weights[index]) - # # End For + vertex.weights[index].node_index = bone_index + # Keep this one! + weights.append(vertex.weights[index]) + # End For - # vertex.weights = weights - # End For + total = 0.0 + for weight in weights: + total += weight.bias + + assert(total != 0.0) + + vertex.weights = weights + #End For # End For return lod @@ -507,6 +513,14 @@ def from_file(self, path): piece_count = unpack('i', f)[0] model.pieces = [self._read_piece(f) for _ in range(piece_count)] + # + # Nodes + # + model.nodes = [self._read_node(f) for _ in range(node_count)] + build_undirected_tree(model.nodes) + weight_set_count = unpack('I', f)[0] + model.weight_sets = [self._read_weight_set(f) for _ in range(weight_set_count)] + return model # OLD - Reference From ff3f275ecde71581664c7f5cbe3cbb5c3e8d9efc Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 22 Jan 2021 21:29:21 -0800 Subject: [PATCH 05/16] Add animation support (including compressed animations!) --- src/abc.py | 4 ++ src/importer.py | 46 ++++++++++++----- src/reader_ltb_pc.py | 115 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 132 insertions(+), 33 deletions(-) diff --git a/src/abc.py b/src/abc.py index 92a1f5e..be17fbc 100644 --- a/src/abc.py +++ b/src/abc.py @@ -202,6 +202,10 @@ def __init__(self): # Scaled by the animation bounding box self.transformed_vertex_deformations = [] + # LTB specific + self.compression_type = 0 + self.is_vetex_animation = 0 + class AnimBinding(object): def __init__(self): diff --git a/src/importer.py b/src/importer.py index c2f937e..61bd502 100644 --- a/src/importer.py +++ b/src/importer.py @@ -20,6 +20,7 @@ class ModelImportOptions(object): def __init__(self): + self.should_merge_duplicate_verts = False self.should_import_animations = False self.should_import_sockets = False self.bone_length_min = 0.1 @@ -221,7 +222,10 @@ def import_model(model, options): vertex_offset += len(lod.vertices) face_offset += len(lod.faces) + #bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)#, bm.verts, 0.0001) + bm.to_mesh(mesh) + bm.free() ''' Assign texture coordinates. @@ -275,6 +279,15 @@ def import_model(model, options): vertex_group.add([vertex_offset + vertex_index], weight.bias, 'REPLACE') vertex_offset += len(lod.vertices) + # Work-around for PC LTB meshes having overlapping but not connected vertices... + if options.should_merge_duplicate_verts: + # Merge duplicates + bm = bmesh.new() + bm.from_mesh(mesh) + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) + bm.to_mesh(mesh) + bm.free() + ''' Parent the mesh to the armature. ''' mesh_object.parent = armature_object @@ -338,9 +351,9 @@ def recursively_apply_transform(nodes, node_index, pose_bones, parent_matrix): # If we have a parent, make sure to apply their matrix with ours to get position relative to our parent # otherwise just use our matrix if parent_matrix != None: - pose_bone.matrix = parent_matrix @ matrix - else: - pose_bone.matrix = matrix + matrix = parent_matrix @ matrix + + pose_bone.matrix = matrix for _ in range(0, node.child_count): node_index = node_index + 1 @@ -577,9 +590,9 @@ class ImportOperatorLTB(bpy.types.Operator, bpy_extras.io_utils.ImportHelper): ) should_import_animations: BoolProperty( - name="Import Animations (not yet working)", + name="Import Animations", description="When checked, animations will be imported as actions.", - default=False, + default=True, ) should_import_sockets: BoolProperty( @@ -606,6 +619,12 @@ class ImportOperatorLTB(bpy.types.Operator, bpy_extras.io_utils.ImportHelper): default=True, ) + should_merge_duplicate_verts: BoolProperty( + name="Merge Duplicate Vertices", + description="When checked, any overlapping but non-connected vertices will be merged. (Recommended for PC LTB files.)", + default=True, + ) + def draw(self, context): layout = self.layout @@ -614,8 +633,9 @@ def draw(self, context): # box.row().prop(self, 'bone_length_min') # box.row().prop(self, 'should_import_sockets') - # box = layout.box() - # box.label(text='Meshes') + box = layout.box() + box.label(text='Meshes') + box.row().prop(self, 'should_merge_duplicate_verts') # box.row().prop(self, 'should_import_lods') # box.row().prop(self, 'should_merge_pieces') @@ -665,12 +685,14 @@ def execute(self, context): options.should_import_sockets = self.should_import_sockets options.should_merge_pieces = self.should_merge_pieces options.should_clear_scene = self.should_clear_scene + options.should_merge_duplicate_verts = self.should_merge_duplicate_verts options.image = image - try: - import_model(model, options) - except Exception as e: - show_message_box(str(e), "Import Error", 'ERROR') - return {'CANCELLED'} + #try: + # import_model(model, options) + #except Exception as e: + # show_message_box(str(e), "Import Error", 'ERROR') + # return {'CANCELLED'} + import_model(model, options) return {'FINISHED'} diff --git a/src/reader_ltb_pc.py b/src/reader_ltb_pc.py index 9c26f50..3112b9e 100644 --- a/src/reader_ltb_pc.py +++ b/src/reader_ltb_pc.py @@ -33,9 +33,9 @@ class PCLTBModelReader(object): def __init__(self): - self._version = 0 - self._node_count = 0 - self._lod_count = 0 + self.version = 0 + self.node_count = 0 + self.lod_count = 0 def _read_matrix(self, f): data = unpack('16f', f) @@ -382,22 +382,76 @@ def _read_node(self, f): node.child_count = unpack('I', f)[0] return node - def _read_transform(self, f): + def _read_uncompressed_transform(self, f): transform = Animation.Keyframe.Transform() + transform.location = self._read_vector(f) transform.rotation = self._read_quaternion(f) - # Two unknown floats! - if self._version == 13: - f.seek(8, 1) - return transform + def _process_compressed_vector(self, compressed_vector): + return Vector( (compressed_vector[0] / 16.0, compressed_vector[1] / 16.0, compressed_vector[2] / 16.0) ) + + def _process_compressed_quat(self, compressed_quat): + return Quaternion( (compressed_quat[3] / 0x7FFF, compressed_quat[0] / 0x7FFF, compressed_quat[1] / 0x7FFF, compressed_quat[2] / 0x7FFF) ) + + def _read_compressed_transform(self, compression_type, keyframe_count, f): + + node_transforms = [] + + for _ in range(self.node_count): + # RLE encoding + key_position_count = unpack('I', f)[0] + + compressed_positions = [] + if compression_type == CMP_Relevant or compression_type == CMP_Relevant_Rot16: + compressed_positions = [self._read_vector(f) for _ in range(key_position_count)] + elif compression_type == CMP_Relevant_16: + compressed_positions = [self._process_compressed_vector(unpack('3h', f)) for _ in range(key_position_count)] + # End If + + key_rotation_count = unpack('I', f)[0] + + compressed_rotations = [] + if compression_type == CMP_Relevant: + compressed_rotations = [self._read_quaternion(f) for _ in range(key_rotation_count)] + elif compression_type == CMP_Relevant_16 or compression_type == CMP_Relevant_Rot16: + compressed_rotations = [self._process_compressed_quat(unpack('4h', f)) for _ in range(key_rotation_count)] + # End If + + transforms = []#[Animation.Keyframe.Transform() for _ in range(keyframe_count)] + + previous_position = Vector( (0, 0, 0) ) + previous_rotation = Quaternion( (1, 0, 0, 0) ) + + for i in range(keyframe_count): + transform = Animation.Keyframe.Transform() + + try: + transform.location = compressed_positions[i] + except IndexError: + transform.location = previous_position + + try: + transform.rotation = compressed_rotations[i] + except IndexError: + transform.rotation = previous_rotation + + previous_position = transform.location + previous_rotation = transform.rotation + + transforms.append(transform) + # End For + + node_transforms.append(transforms) + # End For + + return node_transforms + def _read_child_model(self, f): child_model = ChildModel() child_model.name = self._read_string(f) - child_model.build_number = unpack('I', f)[0] - child_model.transforms = [self._read_transform(f) for _ in range(self._node_count)] return child_model def _read_keyframe(self, f): @@ -410,19 +464,26 @@ def _read_animation(self, f): animation = Animation() animation.extents = self._read_vector(f) animation.name = self._read_string(f) - animation.unknown1 = unpack('i', f)[0] - animation.interpolation_time = unpack('I', f)[0] if self._version >= 12 else 200 + animation.compression_type = unpack('i', f)[0] + animation.interpolation_time = unpack('I', f)[0] animation.keyframe_count = unpack('I', f)[0] animation.keyframes = [self._read_keyframe(f) for _ in range(animation.keyframe_count)] animation.node_keyframe_transforms = [] - for _ in range(self._node_count): - # Skip past -1 - if self._version == 13: - f.seek(4, 1) + if animation.compression_type == CMP_None: + animation.is_vertex_animation = unpack('b', f)[0] + + # We don't support vertex animations yet, so alert if we accidentally load some! + assert(animation.is_vertex_animation == 0) + + for _ in range(self.node_count): + animation.node_keyframe_transforms.append( + [self._read_uncompressed_transform(f) for _ in range(animation.keyframe_count)]) + # End For + else: + animation.node_keyframe_transforms = self._read_compressed_transform(animation.compression_type, animation.keyframe_count, f) + # End If - animation.node_keyframe_transforms.append( - [self._read_transform(f) for _ in range(animation.keyframe_count)]) return animation def _read_socket(self, f): @@ -473,14 +534,14 @@ def from_file(self, path): # Hope to support at least up to v25 someday! if self.version not in [23]: - raise Exception('Unsupported file version ({}).'.format(self._version)) + raise Exception('Unsupported file version ({}).'.format(self.version)) # End If model.version = self.version keyframe_count = unpack('i', f)[0] animation_count = unpack('i', f)[0] - node_count = unpack('i', f)[0] + self.node_count = unpack('i', f)[0] piece_count = unpack('i', f)[0] child_model_count = unpack('i', f)[0] face_count = unpack('i', f)[0] @@ -516,11 +577,23 @@ def from_file(self, path): # # Nodes # - model.nodes = [self._read_node(f) for _ in range(node_count)] + model.nodes = [self._read_node(f) for _ in range(self.node_count)] build_undirected_tree(model.nodes) weight_set_count = unpack('I', f)[0] model.weight_sets = [self._read_weight_set(f) for _ in range(weight_set_count)] + # + # Child Models + # + child_model_count = unpack('I', f)[0] + model.child_models = [self._read_child_model(f) for _ in range(child_model_count - 1)] + + # + # Animations + # + animation_count = unpack('I', f)[0] + model.animations = [self._read_animation(f) for _ in range(animation_count)] + return model # OLD - Reference From e16a3abb9c80341e12facde709a2d7ea559ee323 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 22 Jan 2021 21:51:10 -0800 Subject: [PATCH 06/16] Finish off importing sockets and animation bindings --- src/reader_ltb_pc.py | 72 ++++++++++++++------------------------------ 1 file changed, 23 insertions(+), 49 deletions(-) diff --git a/src/reader_ltb_pc.py b/src/reader_ltb_pc.py index 3112b9e..d7487cb 100644 --- a/src/reader_ltb_pc.py +++ b/src/reader_ltb_pc.py @@ -492,6 +492,7 @@ def _read_socket(self, f): socket.name = self._read_string(f) socket.rotation = self._read_quaternion(f) socket.location = self._read_vector(f) + socket.scale = self._read_vector(f) return socket def _read_anim_binding(self, f): @@ -594,53 +595,26 @@ def from_file(self, path): animation_count = unpack('I', f)[0] model.animations = [self._read_animation(f) for _ in range(animation_count)] - return model + # + # Sockets + # + socket_count = unpack('I', f)[0] + model.sockets = [self._read_socket(f) for _ in range(socket_count)] + + # + # Animation Bindings + # + anim_binding_count = unpack('I', f)[0] + + #model.anim_bindings = [self._read_anim_binding(f) for _ in range(anim_binding_count)] - # OLD - Reference - next_section_offset = 0 - while next_section_offset != -1: - f.seek(next_section_offset) - section_name = self._read_string(f) - next_section_offset = unpack('i', f)[0] - if section_name == 'Header': - self._version = unpack('I', f)[0] - if self._version not in [9, 10, 11, 12, 13]: - raise Exception('Unsupported file version ({}).'.format(self._version)) - model.version = self._version - f.seek(8, 1) - self._node_count = unpack('I', f)[0] - f.seek(20, 1) - self._lod_count = unpack('I', f)[0] - f.seek(4, 1) - self._weight_set_count = unpack('I', f)[0] - f.seek(8, 1) - - # Unknown new value - if self._version >= 13: - f.seek(4,1) - - model.command_string = self._read_string(f) - model.internal_radius = unpack('f', f)[0] - f.seek(64, 1) - model.lod_distances = [unpack('f', f)[0] for _ in range(self._lod_count)] - elif section_name == 'Pieces': - weight_count, pieces_count = unpack('2I', f) - model.pieces = [self._read_piece(f) for _ in range(pieces_count)] - elif section_name == 'Nodes': - model.nodes = [self._read_node(f) for _ in range(self._node_count)] - build_undirected_tree(model.nodes) - weight_set_count = unpack('I', f)[0] - model.weight_sets = [self._read_weight_set(f) for _ in range(weight_set_count)] - elif section_name == 'ChildModels': - child_model_count = unpack('H', f)[0] - model.child_models = [self._read_child_model(f) for _ in range(child_model_count)] - elif section_name == 'Animation': - animation_count = unpack('I', f)[0] - model.animations = [self._read_animation(f) for _ in range(animation_count)] - elif section_name == 'Sockets': - socket_count = unpack('I', f)[0] - model.sockets = [self._read_socket(f) for _ in range(socket_count)] - elif section_name == 'AnimBindings': - anim_binding_count = unpack('I', f)[0] - model.anim_bindings = [self._read_anim_binding(f) for _ in range(anim_binding_count)] - return model + for _ in range(anim_binding_count): + # Some LTB animation binding information can be incorrect... + # Almost like the mesh was accidentally cut off, very odd! + try: + model.anim_bindings.append(self._read_anim_binding(f)) + except Exception: + pass + + return model + \ No newline at end of file From 4285c4424d9cd519534a8d5ce3c05e0678a59f86 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 22 Jan 2021 22:07:13 -0800 Subject: [PATCH 07/16] Skip over OBB info if it's there, and fix an issue with uncompressed animations --- src/reader_ltb_pc.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/reader_ltb_pc.py b/src/reader_ltb_pc.py index d7487cb..2dcaea3 100644 --- a/src/reader_ltb_pc.py +++ b/src/reader_ltb_pc.py @@ -471,12 +471,12 @@ def _read_animation(self, f): animation.node_keyframe_transforms = [] if animation.compression_type == CMP_None: - animation.is_vertex_animation = unpack('b', f)[0] + for _ in range(self.node_count): + animation.is_vertex_animation = unpack('b', f)[0] - # We don't support vertex animations yet, so alert if we accidentally load some! - assert(animation.is_vertex_animation == 0) + # We don't support vertex animations yet, so alert if we accidentally load some! + assert(animation.is_vertex_animation == 0) - for _ in range(self.node_count): animation.node_keyframe_transforms.append( [self._read_uncompressed_transform(f) for _ in range(animation.keyframe_count)]) # End For @@ -565,7 +565,9 @@ def from_file(self, path): # obb_count = unpack('i', f)[0] - # TODO: Figure out OBB + # OBB information is a matrix per each node + # We don't use it anywhere, so just skip it. + f.seek(64 * obb_count, 1) # # Pieces @@ -617,4 +619,3 @@ def from_file(self, path): pass return model - \ No newline at end of file From b35f551298f74e9c7f3cb2efad1080996837c4e8 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 22 Jan 2021 22:10:52 -0800 Subject: [PATCH 08/16] Helpful comments --- src/reader_ltb_pc.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/reader_ltb_pc.py b/src/reader_ltb_pc.py index 2dcaea3..69f3488 100644 --- a/src/reader_ltb_pc.py +++ b/src/reader_ltb_pc.py @@ -401,7 +401,7 @@ def _read_compressed_transform(self, compression_type, keyframe_count, f): node_transforms = [] for _ in range(self.node_count): - # RLE encoding + # RLE! key_position_count = unpack('I', f)[0] compressed_positions = [] @@ -420,11 +420,13 @@ def _read_compressed_transform(self, compression_type, keyframe_count, f): compressed_rotations = [self._process_compressed_quat(unpack('4h', f)) for _ in range(key_rotation_count)] # End If - transforms = []#[Animation.Keyframe.Transform() for _ in range(keyframe_count)] + transforms = [] previous_position = Vector( (0, 0, 0) ) previous_rotation = Quaternion( (1, 0, 0, 0) ) + # RLE animations, if it doesn't change in any additional keyframe, + # then it we can just use the last known pos/rot! for i in range(keyframe_count): transform = Animation.Keyframe.Transform() From 7f4343b153d85b2135d71db0fd1186c25b42190c Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 22 Jan 2021 22:21:52 -0800 Subject: [PATCH 09/16] Null meshes still need to read in nodes_used_count --- src/reader_ltb_pc.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/reader_ltb_pc.py b/src/reader_ltb_pc.py index 69f3488..6fcc8e0 100644 --- a/src/reader_ltb_pc.py +++ b/src/reader_ltb_pc.py @@ -338,22 +338,22 @@ def _read_lod(self, f): lod.type = unpack('I', f)[0] - # NULL Type + # Check if it's a null mesh, it skips a lot of the data... if lod.type == LTB_Type_Null_Mesh: # Early return here, because there's no more data... - return self._read_null_mesh(lod, f) - - # Some common data - obj_size = unpack('I', f)[0] - lod.vert_count = unpack('I', f)[0] - lod.face_count = unpack('I', f)[0] - lod.max_bones_per_face = unpack('I', f)[0] - lod.max_bones_per_vert = unpack('I', f)[0] - - if lod.type == LTB_Type_Rigid_Mesh: - lod = self._read_rigid_mesh(lod, f) - elif lod.type == LTB_Type_Skeletal_Mesh: - lod = self._read_skeletal_mesh(lod, f) + lod = self._read_null_mesh(lod, f) + else: + # Some common data + obj_size = unpack('I', f)[0] + lod.vert_count = unpack('I', f)[0] + lod.face_count = unpack('I', f)[0] + lod.max_bones_per_face = unpack('I', f)[0] + lod.max_bones_per_vert = unpack('I', f)[0] + + if lod.type == LTB_Type_Rigid_Mesh: + lod = self._read_rigid_mesh(lod, f) + elif lod.type == LTB_Type_Skeletal_Mesh: + lod = self._read_skeletal_mesh(lod, f) nodes_used_count = unpack('B', f)[0] nodes_used = [unpack('B', f)[0] for _ in range(nodes_used_count)] From 1c51bf5ceca741f819a987ce64cecaf6a3b3199a Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 22 Jan 2021 22:22:10 -0800 Subject: [PATCH 10/16] Fix some missing stuff in a previous commit --- src/abc.py | 3 +++ src/importer.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/abc.py b/src/abc.py index be17fbc..017cd3e 100644 --- a/src/abc.py +++ b/src/abc.py @@ -158,6 +158,9 @@ def __init__(self): self.rotation = Quaternion() self.location = Vector() + # LTB specific + self.scale = Vector() + class Animation(object): class Keyframe(object): diff --git a/src/importer.py b/src/importer.py index 61bd502..4439f9a 100644 --- a/src/importer.py +++ b/src/importer.py @@ -628,10 +628,10 @@ class ImportOperatorLTB(bpy.types.Operator, bpy_extras.io_utils.ImportHelper): def draw(self, context): layout = self.layout - # box = layout.box() - # box.label(text='Nodes') + box = layout.box() + box.label(text='Nodes') # box.row().prop(self, 'bone_length_min') - # box.row().prop(self, 'should_import_sockets') + box.row().prop(self, 'should_import_sockets') box = layout.box() box.label(text='Meshes') From 0a1dcd46a6986fbe1ea3a400b3c746c2acce3383 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 22 Jan 2021 22:22:26 -0800 Subject: [PATCH 11/16] Use the first lod's first texture for the piece's material index. --- src/reader_ltb_pc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/reader_ltb_pc.py b/src/reader_ltb_pc.py index 6fcc8e0..0f506ef 100644 --- a/src/reader_ltb_pc.py +++ b/src/reader_ltb_pc.py @@ -370,6 +370,10 @@ def _read_piece(self, f): piece.lod_max = unpack('I', f)[0] piece.lods = [self._read_lod(f) for _ in range(lod_count)] + # Just use the first LODs first texture + if lod_count > 0: + piece.material_index = piece.lods[0].textures[0] + return piece def _read_node(self, f): From b24c469c2640b146e69910cf0d58dce1bc7be318 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 22 Jan 2021 22:51:41 -0800 Subject: [PATCH 12/16] Small cleanup, and re-implemented the converters. --- src/__init__.py | 11 +++++++++- src/converter.py | 56 ++++++++++++++++++++++++++++++++++++++++++++---- src/importer.py | 4 +--- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index c62cfdd..eeb4d0a 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -49,7 +49,8 @@ importer.ImportOperatorLTB, exporter.ExportOperatorABC, exporter.ExportOperatorLTA, - converter.ConvertLTBToLTA, + converter.ConvertPCLTBToLTA, + converter.ConvertPS2LTBToLTA, ) def register(): @@ -64,6 +65,10 @@ def register(): bpy.types.TOPBAR_MT_file_export.append(exporter.ExportOperatorABC.menu_func_export) bpy.types.TOPBAR_MT_file_export.append(exporter.ExportOperatorLTA.menu_func_export) + # Converters + bpy.types.TOPBAR_MT_file_import.append(converter.ConvertPCLTBToLTA.menu_func_import) + bpy.types.TOPBAR_MT_file_import.append(converter.ConvertPS2LTBToLTA.menu_func_import) + def unregister(): for cls in reversed(classes): @@ -76,3 +81,7 @@ def unregister(): # Export options bpy.types.TOPBAR_MT_file_export.remove(exporter.ExportOperatorABC.menu_func_export) bpy.types.TOPBAR_MT_file_export.remove(exporter.ExportOperatorLTA.menu_func_export) + + # Converters + bpy.types.TOPBAR_MT_file_import.remove(converter.ConvertPCLTBToLTA.menu_func_import) + bpy.types.TOPBAR_MT_file_import.remove(converter.ConvertPS2LTBToLTA.menu_func_import) \ No newline at end of file diff --git a/src/converter.py b/src/converter.py index f506edd..8c964d1 100644 --- a/src/converter.py +++ b/src/converter.py @@ -15,12 +15,14 @@ from .abc import * from .reader_ltb_ps2 import PS2LTBModelReader +from .reader_ltb_pc import PCLTBModelReader + from .writer_lta_pc import LTAModelWriter -class ConvertLTBToLTA(bpy.types.Operator, bpy_extras.io_utils.ImportHelper): +class ConvertPS2LTBToLTA(bpy.types.Operator, bpy_extras.io_utils.ImportHelper): """This appears in the tooltip of the operator and in the generated docs""" - bl_idname = 'io_scene_lithtech.ltb_convert' # important since its how bpy.ops.import_test.some_data is constructed + bl_idname = 'io_scene_lithtech.ps2_ltb_convert' # important since its how bpy.ops.import_test.some_data is constructed bl_label = 'Convert Lithtech PS2 LTB to LTA' bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' @@ -45,16 +47,62 @@ def execute(self, context): ltb_path = self.filepath lta_path = self.filepath.replace('ltb', 'lta') + lta_path = lta_path.replace('LTB', 'lta') + + # Just in-case the file ext replace fails... + assert(ltb_path != lta_path) + + print ("Converting %s to %s" % (ltb_path, lta_path) ) + + LTAModelWriter().write(model, lta_path, 'lithtech-talon') + + return {'FINISHED'} + + @staticmethod + def menu_func_import(self, context): + self.layout.operator(ConvertPS2LTBToLTA.bl_idname, text='Convert PS2 LTB to LTA.') + +class ConvertPCLTBToLTA(bpy.types.Operator, bpy_extras.io_utils.ImportHelper): + """This appears in the tooltip of the operator and in the generated docs""" + bl_idname = 'io_scene_lithtech.pc_ltb_convert' # important since its how bpy.ops.import_test.some_data is constructed + bl_label = 'Convert Lithtech PC LTB to LTA' + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + + # ImportHelper mixin class uses this + filename_ext = ".ltb" + + filter_glob: StringProperty( + default="*.ltb", + options={'HIDDEN'}, + maxlen=255, # Max internal buffer length, longer would be clamped. + ) + + def execute(self, context): + # Import the model + model = PCLTBModelReader().from_file(self.filepath) + model.name = os.path.splitext(os.path.basename(self.filepath))[0] + + # Fill in some parts of the model we currently dont.. + #stubber = ModelStubber() + #model = stubber.execute(model) + + ltb_path = self.filepath + lta_path = self.filepath.replace('ltb', 'lta') + lta_path = lta_path.replace('LTB', 'lta') + + # Just in-case the file ext replace fails... + assert(ltb_path != lta_path) print ("Converting %s to %s" % (ltb_path, lta_path) ) - LTAModelWriter().write(model, lta_path) + LTAModelWriter().write(model, lta_path, 'lithtech-jupiter') return {'FINISHED'} @staticmethod def menu_func_import(self, context): - self.layout.operator(ConvertLTBToLTA.bl_idname, text='Convert PS2 LTB to LTA.') + self.layout.operator(ConvertPCLTBToLTA.bl_idname, text='Convert PC LTB to LTA.') class ModelStubber(object): def execute(self, model): diff --git a/src/importer.py b/src/importer.py index 4439f9a..b723d8f 100644 --- a/src/importer.py +++ b/src/importer.py @@ -222,8 +222,6 @@ def import_model(model, options): vertex_offset += len(lod.vertices) face_offset += len(lod.faces) - #bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)#, bm.verts, 0.0001) - bm.to_mesh(mesh) bm.free() @@ -598,7 +596,7 @@ class ImportOperatorLTB(bpy.types.Operator, bpy_extras.io_utils.ImportHelper): should_import_sockets: BoolProperty( name="Import Sockets", description="When checked, sockets will be imported as Empty objects.", - default=True, + default=False, ) should_merge_pieces: BoolProperty( From 1eeea4fdf3d8976870dd3a78ce67625f32237767 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 22 Jan 2021 23:08:30 -0800 Subject: [PATCH 13/16] Update readme with some common issues --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bcd6b27..8aee34a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Download or clone the repository, and zip up the `src` folder. Go to `Edit -> Pr To download the respository, click the green `Code -> Download ZIP` at the top of the main page. -I will eventually add releases. +...or grab a release zip if one is there! ## Supported Formats @@ -19,12 +19,19 @@ Format | Import | Export ABC | Rigid and Skeletal | Limited LTA | No | Rigid and Skeletal LTB (PS2) | Rigid and Skeletal | No -LTB (PC) | No | No +LTB (PC) | Rigid and Skeletal | No The ABC file format description can be found on our wiki [here](https://github.com/cmbasnett/io_scene_abc/wiki/ABC). Additional format information can be found in [here](https://github.com/haekb/io_scene_lithtech/tree/master/research) +## Known Issues + - In order to export you must have a mesh, a armature hooked up, and at least one animation action setup + - Socket locations are a tad off in Blender (They're fine in engine.) + - Imported skeletal meshes are mirrored on the X axis (They're flipped back on export!) + - Converters may not provide 1:1 source files + - Converters don't convert lods! + ![](https://raw.githubusercontent.com/haekb/io_scene_lithtech/master/doc/readme/example.png) ## Credits From 8119700683a7e16d270e43a01bab35cc31fd25e8 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 22 Jan 2021 23:08:48 -0800 Subject: [PATCH 14/16] Fix a child-model LTA writer crash for jupiter, and add anim-weightsets. --- src/writer_lta_pc.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/writer_lta_pc.py b/src/writer_lta_pc.py index add5912..d3cd167 100644 --- a/src/writer_lta_pc.py +++ b/src/writer_lta_pc.py @@ -290,18 +290,33 @@ def write(self, model, path, version): # So we need to ignore that... if len(model.child_models) > 1: cm_node = on_load_cmds_node.create_child('add-childmodels') + + cm_container = cm_node + + # Jupiter requires child models to be in a wrapper + if self._version == LTAVersion.JUPITER.value: + cm_container = cm_node.create_container() + for i, child_model in enumerate(model.child_models): if i == 0: continue - child_model_node = cm_node.create_child('child-model') + child_model_node = cm_container.create_child('child-model') child_model_node.create_child('filename', child_model.name) child_model_node.create_child('save-index', child_model.build_number) # End For # End If ''' Animation Weightsets ''' - # TODO: I think these are facial weights? I don't think we support them right now.. + if len(model.weight_sets) > 0: + aws_node = on_load_cmds_node.create_child('anim-weightsets') + weightsets_container = aws_node.create_container() + for weight_set in model.weight_sets: + weightset_node = weightsets_container.create_child('anim-weightset') + weightset_node.create_child('name', weight_set.name) + weightset_node.create_child('weights').create_property(weight_set.node_weights) + # End For + # End If ########################################################## # NODES From 42736e8d09e6a4fb8e69e46c6c1669128f6cb7c0 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 22 Jan 2021 23:13:58 -0800 Subject: [PATCH 15/16] Added some research 010 templates --- research/pc_abc.bt | 252 +++++++++++++++++++++++++++ research/pc_ltb.bt | 419 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 671 insertions(+) create mode 100644 research/pc_abc.bt create mode 100644 research/pc_ltb.bt diff --git a/research/pc_abc.bt b/research/pc_abc.bt new file mode 100644 index 0000000..0d0235e --- /dev/null +++ b/research/pc_abc.bt @@ -0,0 +1,252 @@ +//------------------------------------------------ +//--- 010 Editor v10.0.1 Binary Template +// +// File: +// Authors: +// Version: +// Purpose: +// Category: +// File Mask: +// ID Bytes: +// History: +//------------------------------------------------ +typedef unsigned char uint8; + +struct Section { + short stringLength; + char Type[stringLength] ; + uint32 NextSection; +}; + +struct LTString { + short stringLength; + char Type[stringLength] ; +}; + +struct LTTexCoord { + float u,v; +}; + +struct LTVector { + float x,y,z; +}; + +struct LTRotation { + float x,y,z,w; +}; + +struct LTMatrix { + LTRotation m[4]; +}; + + +struct Transform { + LTVector Location; + LTRotation Rotation; +}; + +// +// Header Data +// + +struct Header { + uint32 Version; + uint32 KeyframeCount; + uint32 AnimationCount; + uint32 NodeCount; + uint32 PieceCount; + uint32 ChildModelCount; + uint32 FaceCount; + uint32 VertexCount; + uint32 WeightCount; + uint32 LODCount; + uint32 SocketCount; + uint32 WeightSetCount; + uint32 StringCount; + uint32 StringLengthTotal; + LTString CommandString; + float InternalRadius; + uint32 LODDistanceCount; + char Padding[60]; + float LODDistances[LODDistanceCount] ; +}; + +// +// Piece Data +// + +struct FaceVertex { + LTTexCoord TexCoord; + uint16 VertexIndex; +}; + +struct Face { + FaceVertex Vertices[3]; +}; + +struct Weight { + uint32 NodeIndex; + LTVector Location; // This boy, right here! + float Bias; +}; + +struct Vertex { + uint16 WeightCount; + uint16 SubLODVertexIndex; + Weight Weights[WeightCount] ; + LTVector Location; + LTVector Normal; +}; + +struct LOD { + uint32 FaceCount; + Face Faces[FaceCount] ; + uint32 VertexCount; + Vertex Vertices[VertexCount] ; +}; + +struct Piece (uint32 LODCount) { + uint16 MaterialIndex; + float SpecularPower; + float SpecularScale; + //float LODWeight; + uint16 Unknown; + LTString Name; + LOD LODs[LODCount] ; +}; + +struct PieceHeader (uint32 LODCount) { + uint32 WeightCount; + uint32 PieceCount; + Piece Pieces(LODCount)[PieceCount] ; +}; + +// +// Node Data +// + + + +struct Node { + LTString Name; + uint16 Index; + uint8 Flags; + LTMatrix BindMatrix; + uint32 ChildCount; +}; + +// +// Weight Data +// + +struct WeightSet { + LTString Name; + uint32 NodeCount; + float NodeWeights[NodeCount] ; +}; + +struct WeightHeader { + uint32 WeightSetCount; + WeightSet WeightSets[WeightSetCount] ; +}; + +// +// Child Models +// + + +struct ChildModel (uint32 NodeCount) { + LTString Name; + uint32 BuildNumber; + Transform Transforms[NodeCount] ; +}; + +struct ChildModelHeader (uint32 NodeCount) { + uint16 ChildModelCount; + ChildModel ChildModels(NodeCount)[ChildModelCount] ; +}; + +// +// Animation Data +// + +struct AnimationHeader { + int AnimCount; +}; + +struct KeyFrame { + int Time; + LTString Command; +}; + +struct KeyFrameTransform (uint32 KeyFrameCount) { + Transform Transforms[KeyFrameCount]; +}; + +struct Animation (int NodeCount) { + LTVector Extents; + LTString Name; + int UnkInt; + int InterpolationTime; + int KeyFrameCount; + KeyFrame KeyFrames[KeyFrameCount] ; + KeyFrameTransform Transforms(KeyFrameCount)[NodeCount] ; +}; + +// +// Socket Data +// + +struct Socket { + uint32 NodeIndex; + LTString Name; + LTRotation Rotation; + LTVector Location; +}; + +struct SocketHeader { + uint32 SocketCount; + Socket Sockets[SocketCount] ; +}; + +// +// AnimBinding Data +// + +struct AnimBinding { + LTString Name; + LTVector Extents; + LTVector Origin; +}; + +struct AnimBindingHeader { + uint32 BindingCount; + AnimBinding AnimBindings[BindingCount]; +}; + +// +// GO! +// + +Section section; +Header hdr; + +Section pieceSection; +PieceHeader piece(hdr.LODCount); + +Section nodeSection; +Node nodes[hdr.NodeCount] ; +WeightHeader weightsets; + +Section childSection; +ChildModelHeader ChildModels(hdr.NodeCount); + +Section AnimSection; +AnimationHeader animHdr; +Animation anim(hdr.NodeCount)[animHdr.AnimCount] ; + +Section SocketSection; +SocketHeader Sockets; + +Section AnimBindingsSection; +AnimBindingHeader AnimBindings; \ No newline at end of file diff --git a/research/pc_ltb.bt b/research/pc_ltb.bt new file mode 100644 index 0000000..fb3b475 --- /dev/null +++ b/research/pc_ltb.bt @@ -0,0 +1,419 @@ +//------------------------------------------------ +//--- 010 Editor v11.0 Binary Template +// +// File: +// Authors: +// Version: +// Purpose: +// Category: +// File Mask: +// ID Bytes: +// History: +//------------------------------------------------ + +local int RigidMesh = 4; +local int SkeletalMesh = 5; +local int VertexAnimatedMesh = 6; +local int NullMesh = 7; + +// Stream Data Flags +local int VTX_Position = 0x0001; +local int VTX_Normal = 0x0002; +local int VTX_Colour = 0x0004; +local int VTX_UV_Sets_1 = 0x0010; +local int VTX_UV_Sets_2 = 0x0020; +local int VTX_UV_Sets_3 = 0x0040; +local int VTX_UV_Sets_4 = 0x0080; +local int VTX_BasisVector = 0x0100; + +// Animation Compression Types +local int CMP_None = 0; +local int CMP_Relevant = 1; +local int CMP_Relevant_16 = 2; +local int CMP_Relevant_Rot16 = 3; + +struct LTString { + short Length; + char Value[Length]; +}; + +struct LTUV { + float u,v; +}; + +// Divide by 16 +struct LTCompressedVector { + short x,y,z; +}; + +struct LTVector { + float x,y,z; +}; + +// Divide by 0x7FFF +struct LTCompressedQuat { + short x,y,z,w; +}; + +struct LTQuat { + float x,y,z,w; +}; + +struct LTMatrix { + LTQuat m[4]; +}; + +struct OBB { + LTMatrix Matrix; + //LTVector Position; + //LTQuat Orientation; + //LTVector Dims; +}; + +struct Header { + short FileType; + short FileVersion; + int Filler[4]; + int MeshVersion; + + int KeyFrames; + int ParentAnims; + int Nodes; + int Pieces; + int ChildModels; + int Faces; + int Vertices; + int VertexWeights; + int LODs; + int Sockets; + int WeightSets; + int StringCount; + int StringLength; + int VertAnimationDataSize; + int AnimationData; // ? + LTString CommandString; + float Radius; + int OBBCount; + OBB OBBData[OBBCount]; + int PieceCount; +}; + +struct BoneSet { + unsigned short BoneIndexStart; + unsigned short BoneIndexCount; + + unsigned char BoneList[4]; + unsigned int IndexBufferIndex; +}; + +struct Piece { + int PieceType; + + if (PieceType == NullMesh) { + int Filler; + return; + } + + // Common data + int ObjSize; + int VertCount; + int FaceCount; + int MaxBonesPerFace; + int MaxBonesPerVert; + + local int i = 0; + local int j = 0; + + if (PieceType == RigidMesh) { + int DataType[4]; + int Bone; + + // Per DataType + for (i = 0; i < 4; i++) { + + // Per Vert + for (j = 0; j < VertCount; j++) { + if (DataType[i] & VTX_Position) { + LTVector Position; + } + if (DataType[i] & VTX_Normal) { + LTVector Normal; + } + if (DataType[i] & VTX_Colour) { + int RGBA; + } + if (DataType[i] & VTX_UV_Sets_1) { + LTUV uv_1; + } + if (DataType[i] & VTX_UV_Sets_2) { + LTUV uv_2; + } + if (DataType[i] & VTX_UV_Sets_3) { + LTUV uv_3; + } + if (DataType[i] & VTX_UV_Sets_4) { + LTUV uv_4; + } + if (DataType[i] & VTX_BasisVector) { + LTVector S; + LTVector T; + } + } + } + + unsigned short IndexList[FaceCount*3]; + } + else if (PieceType == SkeletalMesh) { + char ReindexedBones; + int DataType[4]; + + char MatrixPalette; + + if (MatrixPalette == 0) { + + // Per DataType + for (i = 0; i < 4; i++) { + + // Per Vert + for (j = 0; j < VertCount; j++) { + if (DataType[i] & VTX_Position) { + LTVector Position; + + if (MaxBonesPerFace >= 2) { + float Blend1; + } + if (MaxBonesPerFace >= 3) { + float Blend2; + } + if (MaxBonesPerFace >= 4) { + float Blend3; + } + } + if (DataType[i] & VTX_Normal) { + LTVector Normal; + } + if (DataType[i] & VTX_Colour) { + int RGBA; + } + if (DataType[i] & VTX_UV_Sets_1) { + LTUV uv_1; + } + if (DataType[i] & VTX_UV_Sets_2) { + LTUV uv_2; + } + if (DataType[i] & VTX_UV_Sets_3) { + LTUV uv_3; + } + if (DataType[i] & VTX_UV_Sets_4) { + LTUV uv_4; + } + if (DataType[i] & VTX_BasisVector) { + LTVector S; + LTVector T; + } + } // End Per Vert + } // End Per DataType + + unsigned short IndexList[FaceCount*3]; + + int BoneSetCount; + BoneSet BoneSets[BoneSetCount]; + + } else { + int MinBone; + int MaxBone; + + if (ReindexedBones) { + int ReindexedBoneCount; + int ReindexedBoneList[ReindexedBoneCount]; + } + + // Per DataType + for (i = 0; i < 4; i++) { + + // Per Vert + for (j = 0; j < VertCount; j++) { + if (DataType[i] & VTX_Position) { + // No position data if MaxBonesPerVert == 1? + + if (MaxBonesPerVert >= 2) { + LTVector Position; + float Blend1; + } + if (MaxBonesPerVert >= 3) { + float Blend2; + } + if (MaxBonesPerVert >= 4) { + float Blend3; + } + + // Needs to be last + if (MaxBonesPerVert >= 2) { + int Index; + } + } + if (DataType[i] & VTX_Normal) { + LTVector Normal; + } + if (DataType[i] & VTX_Colour) { + int RGBA; + } + if (DataType[i] & VTX_UV_Sets_1) { + LTUV uv_1; + } + if (DataType[i] & VTX_UV_Sets_2) { + LTUV uv_2; + } + if (DataType[i] & VTX_UV_Sets_3) { + LTUV uv_3; + } + if (DataType[i] & VTX_UV_Sets_4) { + LTUV uv_4; + } + if (DataType[i] & VTX_BasisVector) { + LTVector S; + LTVector T; + } + } // End Per Vert + } // End Per DataType + + + unsigned short IndexList[FaceCount*3]; + + + } // Matrix Palette + + } + +}; + +struct LOD { + int TextureCount; + int Textures[4]; + int RenderStyle; + char RenderPriority; + Piece piece; + char NodesUsedCount; + char NodesUsed[NodesUsedCount]; +}; + +struct PieceHeader { + LTString Name; + int LODCount; + float LODDistances[LODCount]; + int LODMin; + int LODMax; + LOD lods[LODCount] ; +}; + +struct Node { + // Forward declaration + struct Node; + + LTString Name; + short Index; + char Flags; + LTMatrix Matrix; + int ChildrenCount; + Node Children[ChildrenCount] ; +}; + +struct WeightSet { + LTString Name; + int WeightCount; + float Weights[WeightCount]; +}; + +struct WeightSetHeader { + int WeightSetCount; + WeightSet WeightSets[WeightSetCount] ; +}; + +struct ChildModel { + int ChildModelCount; + LTString ChildModels[ChildModelCount - 1] ; +}; + +struct KeyFrame { + unsigned int Time; + LTString CommandString; +}; + +struct AnimTransform (int CompressionType, int KeyFrameCount) { + if (CompressionType == CMP_None) { + char IsVertexAnimation; + // TODO: Figure out vertex animations + if (IsVertexAnimation == 0) { + // Transform + LTVector Translations[KeyFrameCount]; + LTQuat Rotations[KeyFrameCount]; + } + } + else if (CompressionType == CMP_Relevant) { + int KeyPosCount; + LTVector Positions[KeyPosCount]; + int KeyRotCount; + LTQuat Rotations[KeyRotCount]; + } + else if (CompressionType == CMP_Relevant_16) { + int KeyPosCount; + LTCompressedVector Positions[KeyPosCount]; + int KeyRotCount; + LTCompressedQuat Rotations[KeyRotCount]; + } + else if (CompressionType == CMP_Relevant_Rot16) { + int KeyPosCount; + LTVector Positions[KeyPosCount]; + int KeyRotCount; + LTCompressedQuat Rotations[KeyRotCount]; + } +}; + +struct Animation (int NodeCount) { + LTVector Dims; + LTString Name; + int CompressionType; + int Interp; + int KeyFrameCount; + KeyFrame Keyframes[KeyFrameCount] ; + AnimTransform Transforms(CompressionType, KeyFrameCount)[NodeCount] ; + +}; + +struct AnimHeader (int NodeCount) { + int AnimCount; + Animation Anims(NodeCount)[AnimCount] ; +}; + +struct Socket { + int NodeIndex; + LTString Name; + LTQuat Rotation; + LTVector Position; + LTVector Scale; +}; + +struct SocketHeader { + int SocketCount; + Socket Sockets[SocketCount] ; +}; + +struct AnimBinding { + LTString Name; + LTVector Dims; + LTVector Translation; +}; + +struct AnimBindingHeader { + int BindingCount; + AnimBinding Binding[BindingCount] ; +}; + +Header hdr; +PieceHeader pieces[hdr.PieceCount] ; +Node nodes; +WeightSetHeader weights; +ChildModel childModels; +AnimHeader anims(hdr.Nodes); +SocketHeader sockets; +AnimBindingHeader animBindings[hdr.ChildModels]; \ No newline at end of file From 0a08a4a15c9cd153e9fad13f886795ff8fbdfcd7 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 22 Jan 2021 23:24:44 -0800 Subject: [PATCH 16/16] Fix OBB for ltb's higher than v23. --- src/reader_ltb_pc.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/reader_ltb_pc.py b/src/reader_ltb_pc.py index 0f506ef..693b7b2 100644 --- a/src/reader_ltb_pc.py +++ b/src/reader_ltb_pc.py @@ -28,7 +28,7 @@ Invalid_Bone = 255 # -# Supports LTB v23 +# Supports LTB v23 (and maybe 24, 25?) # class PCLTBModelReader(object): def __init__(self): @@ -539,8 +539,7 @@ def from_file(self, path): self.version = unpack('i', f)[0] - # Hope to support at least up to v25 someday! - if self.version not in [23]: + if self.version not in [23, 24, 25]: raise Exception('Unsupported file version ({}).'.format(self.version)) # End If @@ -571,9 +570,14 @@ def from_file(self, path): # obb_count = unpack('i', f)[0] + obb_size = 64 + + if self.version > 23: + obb_size += 4 + # OBB information is a matrix per each node # We don't use it anywhere, so just skip it. - f.seek(64 * obb_count, 1) + f.seek(obb_size * obb_count, 1) # # Pieces