diff --git a/io_scene_psk_psa/psa/builder.py b/io_scene_psk_psa/psa/builder.py index fa5154a..e3dc2fe 100644 --- a/io_scene_psk_psa/psa/builder.py +++ b/io_scene_psk_psa/psa/builder.py @@ -48,58 +48,40 @@ class PsaBuildOptions: def _get_pose_bone_location_and_rotation( - pose_bone: PoseBone | None, - armature_object: Object | None, + pose_bone: PoseBone, + armature_object: Object, export_space: str, scale: Vector, coordinate_system_transform: Matrix, - has_false_root_bone: bool, ) -> Tuple[Vector, Quaternion]: - is_false_root_bone = pose_bone is None and armature_object is None - - # TODO: this is such a disaster; the false root bone idea needs revising. - - if is_false_root_bone: - pose_bone_matrix = coordinate_system_transform - elif pose_bone is not None and pose_bone.parent is not None: - pose_bone_matrix = pose_bone.matrix - pose_bone_parent_matrix = pose_bone.parent.matrix - pose_bone_matrix = pose_bone_parent_matrix.inverted() @ pose_bone_matrix + if pose_bone.parent is not None: + pose_bone_matrix = pose_bone.parent.matrix.inverted() @ pose_bone.matrix else: # Root bone - if has_false_root_bone: - pose_bone_matrix = armature_object.matrix_world @ pose_bone.matrix - else: - # Get the bone's pose matrix and transform it into the export space. - # In the case of an 'ARMATURE' export space, this will be the inverse of armature object's world matrix. - # Otherwise, it will be the identity matrix. - match export_space: - case 'ARMATURE': - pose_bone_matrix = pose_bone.matrix - case 'WORLD': - pose_bone_matrix = armature_object.matrix_world @ pose_bone.matrix - case 'ROOT': - pose_bone_matrix = Matrix.Identity(4) - case _: - assert False, f'Invalid export space: {export_space}' + # Get the bone's pose matrix and transform it into the export space. + # In the case of an 'ARMATURE' export space, this will be the inverse of armature object's world matrix. + # Otherwise, it will be the identity matrix. + match export_space: + case 'ARMATURE': + pose_bone_matrix = pose_bone.matrix + case 'WORLD': + pose_bone_matrix = armature_object.matrix_world @ pose_bone.matrix + case 'ROOT': + pose_bone_matrix = Matrix.Identity(4) + case _: + assert False, f'Invalid export space: {export_space}' - # The root bone is the only bone that should be transformed by the coordinate system transform, since all - # other bones are relative to their parent bones. - pose_bone_matrix = coordinate_system_transform @ pose_bone_matrix + # The root bone is the only bone that should be transformed by the coordinate system transform, since all + # other bones are relative to their parent bones. + pose_bone_matrix = coordinate_system_transform @ pose_bone_matrix location = pose_bone_matrix.to_translation() rotation = pose_bone_matrix.to_quaternion().normalized() - # Don't apply scale to the root bone of armatures if we have a false root. - if not has_false_root_bone or (pose_bone is None or pose_bone.parent is not None): - location *= scale - - if has_false_root_bone: - is_child_bone = not is_false_root_bone - else: - is_child_bone = pose_bone.parent is not None - - if is_child_bone: + if pose_bone.parent is not None: + # Don't apply scale to the root bone of armatures if we have a false root. + # TODO: probably remove this? + location *= scale rotation.conjugate() return location, rotation @@ -218,12 +200,12 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa: psa.keys.append(key) class PsaExportBone: - def __init__(self, pose_bone: Optional[PoseBone], armature_object: Optional[Object], scale: Vector): + def __init__(self, pose_bone: PoseBone | None, armature_object: Object | None, scale: Vector): self.pose_bone = pose_bone self.armature_object = armature_object self.scale = scale - armature_scales: Dict[Object, Vector] = {} + armature_scales: dict[Object, Vector] = {} # Extract the scale from the world matrix of the evaluated armature object. for armature_object in options.armature_objects: @@ -235,7 +217,7 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa: # Create a list of export pose bones, in the same order as the bones as they appear in the armature. # The object contains the pose bone, the armature object, and a pre-calculated scaling value to apply to the # locations. - export_bones: List[PsaExportBone] = [] + export_bones: list[PsaExportBone] = [] for psx_bone, armature_object in psx_bone_create_result.bones: if armature_object is None: @@ -250,11 +232,11 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa: match options.sampling_mode: case 'INTERPOLATED': # Used as a store for the last frame's pose bone locations and rotations. - last_frame: Optional[int] = None - last_frame_bone_poses: List[Tuple[Vector, Quaternion]] = [] + last_frame: int | None = None + last_frame_bone_poses: list[tuple[Vector, Quaternion]] = [] - next_frame: Optional[int] = None - next_frame_bone_poses: List[Tuple[Vector, Quaternion]] = [] + next_frame: int | None = None + next_frame_bone_poses: list[tuple[Vector, Quaternion]] = [] for _ in range(frame_count): if last_frame is None or last_frame != int(frame): @@ -275,8 +257,8 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa: export_bone.armature_object, options.export_space, export_bone.scale, - coordinate_system_transform=coordinate_system_transform, - has_false_root_bone=psx_bone_create_result.has_false_root_bone, + coordinate_system_transform=coordinate_system_transform + # has_false_root_bone=psx_bone_create_result.has_false_root_bone, ) last_frame_bone_poses.append((location, rotation)) @@ -299,7 +281,7 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa: export_space=options.export_space, scale=export_bone.scale, coordinate_system_transform=coordinate_system_transform, - has_false_root_bone=psx_bone_create_result.has_false_root_bone, + # has_false_root_bone=psx_bone_create_result.has_false_root_bone, ) next_frame_bone_poses.append((location, rotation)) @@ -326,7 +308,7 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa: export_space=options.export_space, scale=export_bone.scale, coordinate_system_transform=coordinate_system_transform, - has_false_root_bone=psx_bone_create_result.has_false_root_bone, + # has_false_root_bone=psx_bone_create_result.has_false_root_bone, ) add_key(location, rotation) diff --git a/io_scene_psk_psa/psk/builder.py b/io_scene_psk_psa/psk/builder.py index a784794..e03e144 100644 --- a/io_scene_psk_psa/psk/builder.py +++ b/io_scene_psk_psa/psk/builder.py @@ -238,7 +238,7 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil case _: assert False, f'Invalid export space: {options.export_space}' - vertex_transform_matrix = scale_matrix @ coordinate_system_matrix @ mesh_export_space_matrix + vertex_transform_matrix = scale_matrix @ coordinate_system_matrix.inverted() @ mesh_export_space_matrix point_transform_matrix = vertex_transform_matrix @ mesh_object.matrix_world # Vertices diff --git a/io_scene_psk_psa/shared/helpers.py b/io_scene_psk_psa/shared/helpers.py index a94eec8..0360c1e 100644 --- a/io_scene_psk_psa/shared/helpers.py +++ b/io_scene_psk_psa/shared/helpers.py @@ -172,18 +172,12 @@ def convert_string_to_cp1252_bytes(string: str) -> bytes: def create_psx_bones_from_blender_bones( bones: List[bpy.types.Bone], armature_object_matrix_world: Matrix, - forward_axis: str = 'X', - up_axis: str = 'Z', ) -> List[PsxBone]: """ Creates PSX bones from the given Blender bones. The bones are in world space based on the armature object's world matrix. """ - - coordinate_system_transform = get_coordinate_system_transform(forward_axis, up_axis) - coordinate_system_default_rotation = coordinate_system_transform.to_quaternion() - # Apply the scale of the armature object to the bone location. _, _, armature_object_scale = armature_object_matrix_world.decompose() @@ -209,11 +203,9 @@ def create_psx_bones_from_blender_bones( location = (parent_tail - parent_head) + bone.head else: location = armature_object_matrix_world @ bone.head - location = coordinate_system_transform @ location bone_rotation = bone.matrix.to_quaternion().conjugated() rotation = bone_rotation @ armature_object_matrix_world.to_3x3().to_quaternion() rotation.conjugate() - rotation = coordinate_system_default_rotation @ rotation location.x *= armature_object_scale.x location.y *= armature_object_scale.y @@ -352,9 +344,6 @@ def create_psx_bones( raise RuntimeError(f'When exporting multiple armatures, the Export Space must be World.\n' \ f'The following armatures are attempting to be exported: {root_armature_names}') - coordinate_system_matrix = get_coordinate_system_transform(forward_axis, up_axis) - coordinate_system_default_rotation = coordinate_system_matrix.to_quaternion() - total_bone_count = 0 for armature_object in filter(lambda x: x.data is not None, armature_objects): armature_data = typing_cast(Armature, armature_object.data) @@ -384,7 +373,7 @@ def create_psx_bones( # requirement that a PSK file must have at least one bone. psx_bone = PsxBone() psx_bone.name = convert_string_to_cp1252_bytes(root_bone_name) - psx_bone.rotation = convert_bpy_quaternion_to_psx_quaternion(coordinate_system_default_rotation) + psx_bone.rotation = Quaternion(0.0, 0.0, 0.0, 1.0) bones.append((psx_bone, None)) armature_object_root_bone_indices[None] = 0 @@ -394,7 +383,7 @@ def create_psx_bones( psx_bone = PsxBone() psx_bone.name = convert_string_to_cp1252_bytes(root_bone_name) psx_bone.children_count = total_bone_count - psx_bone.rotation = convert_bpy_quaternion_to_psx_quaternion(coordinate_system_default_rotation) + psx_bone.rotation = Quaternion(0.0, 0.0, 0.0, 1.0) bones.append((psx_bone, None)) armature_object_root_bone_indices[None] = 0 @@ -408,8 +397,6 @@ def create_psx_bones( armature_psx_bones = create_psx_bones_from_blender_bones( bones=armature_bones, armature_object_matrix_world=armature_object.matrix_world, - forward_axis=forward_axis, - up_axis=up_axis, ) # We have the bones in world space. If we are attaching this armature to a parent bone, we need to convert @@ -540,6 +527,36 @@ def create_psx_bones( error_message += f' This is the name of the automatically generated root bone. Consider changing this ' f'' raise RuntimeError(error_message) + + # Apply the scale to the bone locations. + for psx_bone, _ in bones: + psx_bone.location.x *= scale + psx_bone.location.y *= scale + psx_bone.location.z *= scale + + coordinate_system_matrix = get_coordinate_system_transform(forward_axis, up_axis) + coordinate_system_default_rotation = coordinate_system_matrix.to_quaternion() + + # Apply the coordinate system transform to the root bone. + root_psx_bone = bones[0][0] + # Get transform matrix from root bone location and rotation. + root_bone_location = Vector((root_psx_bone.location.x, root_psx_bone.location.y, root_psx_bone.location.z)) + root_bone_rotation = BpyQuaternion((root_psx_bone.rotation.w, root_psx_bone.rotation.x, root_psx_bone.rotation.y, root_psx_bone.rotation.z)) + root_bone_matrix = ( + Matrix.Translation(root_bone_location) @ + root_bone_rotation.to_matrix().to_4x4() + ) + root_bone_matrix = coordinate_system_default_rotation.inverted().to_matrix().to_4x4() @ root_bone_matrix + location, rotation, _ = root_bone_matrix.decompose() + root_psx_bone.location.x = location.x + root_psx_bone.location.y = location.y + root_psx_bone.location.z = location.z + root_psx_bone.rotation.w = rotation.w + root_psx_bone.rotation.x = rotation.x + root_psx_bone.rotation.y = rotation.y + root_psx_bone.rotation.z = rotation.z + + convert_bpy_quaternion_to_psx_quaternion(coordinate_system_default_rotation) return PsxBoneCreateResult( bones=bones,