diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index 589a274..b92fe76 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -27,17 +27,24 @@ class ModelTransform: translation: Vector = field(default_factory=Vector) rotation: Quaternion = field(default_factory=Quaternion) +@dataclass +class VertexWeight: + """ Class representing a vertex weight in a .msh file. """ + + weight: float = 1.0 + bone: int = 0 + @dataclass class GeometrySegment: """ Class representing a 'SEGM' section in a .msh file. """ - material_name: str = "" + material_name: str = field(default_factory=str) positions: List[Vector] = field(default_factory=list) normals: List[Vector] = field(default_factory=list) colors: List[List[float]] = None texcoords: List[Vector] = field(default_factory=list) - # TODO: Skin support. + weights: List[List[VertexWeight]] = None polygons: List[List[int]] = field(default_factory=list) triangles: List[List[int]] = field(default_factory=list) @@ -63,5 +70,7 @@ class Model: transform: ModelTransform = field(default_factory=ModelTransform) + bone_map: List[str] = None + geometry: List[GeometrySegment] = None collisionprimitive: CollisionPrimitive = None diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index a55a5ef..946303b 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -34,6 +34,9 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: check_for_bad_lod_suffix(obj) + if obj.type == "ARMATURE": + models_list += expand_armature(obj) + local_translation, local_rotation, _ = obj.matrix_local.decompose() model = Model() @@ -48,7 +51,7 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: if obj.type in MESH_OBJECT_TYPES: mesh = obj.to_mesh() - model.geometry = create_mesh_geometry(mesh) + model.geometry = create_mesh_geometry(mesh, obj.vertex_groups) obj.to_mesh_clear() _, _, world_scale = obj.matrix_world.decompose() @@ -64,6 +67,9 @@ def gather_models(apply_modifiers: bool, export_target: str) -> List[Model]: if get_is_collision_primitive(obj): model.collisionprimitive = get_collision_primitive(obj) + if obj.vertex_groups: + model.bone_map = [group.name for group in obj.vertex_groups] + models_list.append(model) return models_list @@ -80,7 +86,7 @@ def create_parents_set() -> Set[str]: return parents -def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: +def create_mesh_geometry(mesh: bpy.types.Mesh, has_weights: bool) -> List[GeometrySegment]: """ Creates a list of GeometrySegment objects from a Blender mesh. Does NOT create triangle strips in the GeometrySegment however. """ @@ -93,7 +99,7 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: material_count = max(len(mesh.materials), 1) segments: List[GeometrySegment] = [GeometrySegment() for i in range(material_count)] - vertex_cache: List[Dict[Tuple[float], int]] = [dict() for i in range(material_count)] + vertex_cache = [dict() for i in range(material_count)] vertex_remap: List[Dict[Tuple[int, int], int]] = [dict() for i in range(material_count)] polygons: List[Set[int]] = [set() for i in range(material_count)] @@ -101,6 +107,10 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: for segment in segments: segment.colors = [] + if has_weights: + for segment in segments: + segment.weights = [] + for segment, material in zip(segments, mesh.materials): segment.material_name = material.name @@ -139,6 +149,11 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: for v in mesh.vertex_colors.active.data[loop_index].color: yield v + if segment.weights is not None: + for v in mesh.vertices[vertex_index].groups: + yield v.group + yield v.weight + vertex_cache_entry = tuple(get_cache_vertex()) cached_vertex_index = cache.get(vertex_cache_entry, vertex_cache_miss_index) @@ -162,6 +177,11 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: if segment.colors is not None: segment.colors.append(list(mesh.vertex_colors.active.data[loop_index].color)) + if segment.weights is not None: + groups = mesh.vertices[vertex_index].groups + + segment.weights.append([VertexWeight(v.weight, v.group) for v in groups]) + return new_index for tri in mesh.loop_triangles: @@ -181,10 +201,12 @@ def create_mesh_geometry(mesh: bpy.types.Mesh) -> List[GeometrySegment]: def get_model_type(obj: bpy.types.Object) -> ModelType: """ Get the ModelType for a Blender object. """ - # TODO: Skinning support, etc if obj.type in MESH_OBJECT_TYPES: - return ModelType.STATIC + if obj.vertex_groups: + return ModelType.SKIN + else: + return ModelType.STATIC return ModelType.NULL @@ -320,6 +342,31 @@ def select_objects(export_target: str) -> List[bpy.types.Object]: return objects + parents +def expand_armature(obj: bpy.types.Object) -> List[Model]: + bones: List[Model] = [] + + for bone in obj.data.bones: + model = Model() + + transform = bone.matrix_local + + if bone.parent: + transform = bone.parent.matrix_local.inverted() @ transform + model.parent = bone.parent.name + else: + model.parent = obj.name + + local_translation, local_rotation, _ = transform.decompose() + + model.model_type = ModelType.BONE + model.name = bone.name + model.transform.rotation = convert_rotation_space(local_rotation) + model.transform.translation = convert_vector_space(local_translation) + + bones.append(model) + + return bones + def convert_vector_space(vec: Vector) -> Vector: return Vector((-vec.x, vec.z, vec.y)) diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index 71914a0..b70f766 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -17,6 +17,7 @@ def save_scene(output_file, scene: Scene): with msh2.create_child("SINF") as sinf: _write_sinf(sinf, scene) + model_index: Dict[str, int] = {model.name:i for i, model in enumerate(scene.models)} material_index: Dict[str, int] = {} with msh2.create_child("MATL") as matl: @@ -24,7 +25,7 @@ def save_scene(output_file, scene: Scene): for index, model in enumerate(scene.models): with msh2.create_child("MODL") as modl: - _write_modl(modl, model, index, material_index) + _write_modl(modl, model, index, material_index, model_index) with hedr.create_child("CL1L"): pass @@ -97,7 +98,7 @@ def _write_matd(matd: Writer, material_name: str, material: Material): with matd.create_child("TX3D") as tx3d: tx3d.write_string(material.texture3) -def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str, int]): +def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str, int], model_index: Dict[str, int]): with modl.create_child("MTYP") as mtyp: mtyp.write_u32(model.model_type.value) @@ -124,6 +125,10 @@ def _write_modl(modl: Writer, model: Model, index: int, material_index: Dict[str with geom.create_child("SEGM") as segm: _write_segm(segm, segment, material_index) + if model.bone_map: + with geom.create_child("ENVL") as envl: + _write_envl(envl, model, model_index) + if model.collisionprimitive is not None: with modl.create_child("SWCI") as swci: swci.write_u32(model.collisionprimitive.shape.value) @@ -147,6 +152,10 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str for position in segment.positions: posl.write_f32(position.x, position.y, position.z) + if segment.weights: + with segm.create_child("WGHT") as wght: + _write_wght(wght, segment.weights) + with segm.create_child("NRML") as nrml: nrml.write_u32(len(segment.normals)) @@ -189,3 +198,23 @@ def _write_segm(segm: Writer, segment: GeometrySegment, material_index: Dict[str for index in islice(strip, 2, len(strip)): strp.write_u16(index) + +def _write_wght(wght: Writer, weights: List[List[VertexWeight]]): + wght.write_u32(len(weights)) + + for weight_list in weights: + weight_list += [VertexWeight(0.0, 0)] * 4 + weight_list = sorted(weight_list, key=lambda w: w.weight, reverse=True) + weight_list = weight_list[:4] + + total_weight = max(sum(map(lambda w: w.weight, weight_list)), 1e-5) + + for weight in weight_list: + wght.write_i32(weight.bone) + wght.write_f32(weight.weight / total_weight) + +def _write_envl(envl: Writer, model: Model, model_index: Dict[str, int]): + envl.write_u32(len(model.bone_map)) + + for bone_name in model.bone_map: + envl.write_u32(model_index[bone_name])