Technically got the armature-attachments working for animations as well; lots of clean up and testing needed though

This commit is contained in:
Colin Basnett
2026-01-12 16:55:51 -08:00
parent bc7ea6472e
commit e50a964dd3
5 changed files with 222 additions and 190 deletions

View File

@@ -4,7 +4,7 @@ from psk_psa_py.psa.data import Psa
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
from mathutils import Matrix, Quaternion, Vector from mathutils import Matrix, Quaternion, Vector
from ..shared.helpers import PsxBoneCollection, create_psx_bones, get_coordinate_system_transform from ..shared.helpers import PsxBoneCollection, convert_bpy_quaternion_to_psx_quaternion, convert_vector_to_vector3, create_psx_bones, get_coordinate_system_transform
class PsaBuildSequence: class PsaBuildSequence:
@@ -47,9 +47,54 @@ class PsaBuildOptions:
return 'DATA' if self.sequence_source == 'ACTIVE_ACTION' else 'OBJECT' return 'DATA' if self.sequence_source == 'ACTIVE_ACTION' else 'OBJECT'
class PsaExportBone:
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
@property
def is_armature_root_bone(self) -> bool:
return self.pose_bone is not None and self.pose_bone.parent is None
@property
def is_attached_to_armature(self) -> bool:
return self.get_attached_armature() is not None
def get_attached_armature(self) -> tuple[Object, PoseBone] | None:
if not self.is_armature_root_bone:
return None
assert self.armature_object is not None
match self.armature_object.parent_type:
case 'BONE':
parent_bone_name = self.armature_object.parent_bone
assert self.armature_object.parent is not None
parent_armature_object = self.armature_object.parent
assert parent_armature_object.pose is not None
parent_pose_bone = parent_armature_object.pose.bones.get(parent_bone_name)
if parent_pose_bone is None:
return None
return (parent_armature_object, parent_pose_bone)
case _:
return None
def get_attached_armature_transform(self) -> Matrix:
attached_armature, attached_pose_bone = self.get_attached_armature() or (None, None)
if attached_armature is None or attached_pose_bone is None:
return Matrix.Identity(4)
if attached_pose_bone.parent is not None:
attached_bone_matrix = attached_pose_bone.parent.matrix.inverted() @ attached_pose_bone.matrix
else:
attached_bone_matrix = attached_armature.matrix_world @ attached_pose_bone.matrix
return attached_bone_matrix
def _get_pose_bone_location_and_rotation( def _get_pose_bone_location_and_rotation(
pose_bone: PoseBone, pose_bone: PoseBone,
armature_object: Object, armature_object: Object,
export_bone: PsaExportBone,
export_space: str, export_space: str,
scale: Vector, scale: Vector,
coordinate_system_transform: Matrix, coordinate_system_transform: Matrix,
@@ -61,6 +106,20 @@ def _get_pose_bone_location_and_rotation(
# Get the bone's pose matrix and transform it into the 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. # 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. # Otherwise, it will be the identity matrix.
if export_bone.is_attached_to_armature:
# Get the world space matrix of both this bone and the bone that we're attached to,
# then calculate a matrix relative to the attached bone.
world_matrix = armature_object.matrix_world @ pose_bone.matrix
assert export_bone.armature_object
my_parent = export_bone.armature_object.parent
assert my_parent
my_parent_bone = export_bone.armature_object.parent_bone
assert my_parent.pose
parent_pose_bone = my_parent.pose.bones[my_parent_bone]
parent_world_matrix = my_parent.matrix_world @ parent_pose_bone.matrix
pose_bone_matrix = parent_world_matrix.inverted() @ world_matrix
else:
match export_space: match export_space:
case 'ARMATURE': case 'ARMATURE':
pose_bone_matrix = pose_bone.matrix pose_bone_matrix = pose_bone.matrix
@@ -70,7 +129,6 @@ def _get_pose_bone_location_and_rotation(
pose_bone_matrix = Matrix.Identity(4) pose_bone_matrix = Matrix.Identity(4)
case _: case _:
assert False, f'Invalid export space: {export_space}' 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 # 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. # other bones are relative to their parent bones.
pose_bone_matrix = coordinate_system_transform @ pose_bone_matrix pose_bone_matrix = coordinate_system_transform @ pose_bone_matrix
@@ -116,7 +174,7 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
# Build list of PSA bones. # Build list of PSA bones.
# Note that the PSA bones are just here to validate the hierarchy. # Note that the PSA bones are just here to validate the hierarchy.
# The bind pose information is not used by the engine. # The bind pose information is not used by the engine.
psa.bones = [psx_bone for psx_bone, _ in psx_bone_create_result.bones] psa.bones = [bone.psx_bone for bone in psx_bone_create_result.bones]
# No bones are going to be exported. # No bones are going to be exported.
if len(psa.bones) == 0: if len(psa.bones) == 0:
@@ -189,22 +247,11 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
def add_key(location: Vector, rotation: Quaternion): def add_key(location: Vector, rotation: Quaternion):
key = Psa.Key() key = Psa.Key()
key.location.x = location.x key.location = convert_vector_to_vector3(location)
key.location.y = location.y key.rotation = convert_bpy_quaternion_to_psx_quaternion(rotation)
key.location.z = location.z
key.rotation.x = rotation.x
key.rotation.y = rotation.y
key.rotation.z = rotation.z
key.rotation.w = rotation.w
key.time = 1.0 / psa_sequence.fps key.time = 1.0 / psa_sequence.fps
psa.keys.append(key) psa.keys.append(key)
class PsaExportBone:
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. # Extract the scale from the world matrix of the evaluated armature object.
@@ -219,15 +266,16 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
# locations. # locations.
export_bones: list[PsaExportBone] = [] export_bones: list[PsaExportBone] = []
for psx_bone, armature_object in psx_bone_create_result.bones: for bone in psx_bone_create_result.bones:
if armature_object is None: if bone.armature_object is None:
export_bones.append(PsaExportBone(None, None, Vector((1.0, 1.0, 1.0)))) export_bone = PsaExportBone(None, None, Vector((1.0, 1.0, 1.0)))
export_bones.append(export_bone)
continue continue
assert armature_object.pose assert bone.armature_object.pose
pose_bone = armature_object.pose.bones[psx_bone.name.decode('windows-1252')] pose_bone = bone.armature_object.pose.bones[bone.psx_bone.name.decode('windows-1252')]
export_bones.append(PsaExportBone(pose_bone, armature_object, armature_scales[armature_object])) export_bones.append(PsaExportBone(pose_bone, bone.armature_object, armature_scales[bone.armature_object]))
match options.sampling_mode: match options.sampling_mode:
case 'INTERPOLATED': case 'INTERPOLATED':
@@ -255,6 +303,7 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
location, rotation = _get_pose_bone_location_and_rotation( location, rotation = _get_pose_bone_location_and_rotation(
export_bone.pose_bone, export_bone.pose_bone,
export_bone.armature_object, export_bone.armature_object,
export_bone,
options.export_space, options.export_space,
export_bone.scale, export_bone.scale,
coordinate_system_transform=coordinate_system_transform coordinate_system_transform=coordinate_system_transform
@@ -278,6 +327,7 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
location, rotation = _get_pose_bone_location_and_rotation( location, rotation = _get_pose_bone_location_and_rotation(
pose_bone=export_bone.pose_bone, pose_bone=export_bone.pose_bone,
armature_object=export_bone.armature_object, armature_object=export_bone.armature_object,
export_bone=export_bone,
export_space=options.export_space, export_space=options.export_space,
scale=export_bone.scale, scale=export_bone.scale,
coordinate_system_transform=coordinate_system_transform, coordinate_system_transform=coordinate_system_transform,
@@ -305,6 +355,7 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
location, rotation = _get_pose_bone_location_and_rotation( location, rotation = _get_pose_bone_location_and_rotation(
pose_bone=export_bone.pose_bone, pose_bone=export_bone.pose_bone,
armature_object=export_bone.armature_object, armature_object=export_bone.armature_object,
export_bone=export_bone,
export_space=options.export_space, export_space=options.export_space,
scale=export_bone.scale, scale=export_bone.scale,
coordinate_system_transform=coordinate_system_transform, coordinate_system_transform=coordinate_system_transform,

View File

@@ -847,7 +847,12 @@ class PSA_OT_export_collection_populate_sequences(Operator):
if collection is None: if collection is None:
self.report({'ERROR'}, 'No collection found in context') self.report({'ERROR'}, 'No collection found in context')
return {'CANCELLED'} return {'CANCELLED'}
try:
input_objects = get_psk_input_objects_for_collection(collection) input_objects = get_psk_input_objects_for_collection(collection)
except RuntimeError as e:
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
return {'CANCELLED'}
# Keep track of what sequences were selected, then restore the selected status after we have updated the lists. # Keep track of what sequences were selected, then restore the selected status after we have updated the lists.
def store_is_selected_for_sequence_list(sequences: Iterable[PsaExportSequenceMixin]) -> dict[int, bool]: def store_is_selected_for_sequence_list(sequences: Iterable[PsaExportSequenceMixin]) -> dict[int, bool]:

View File

@@ -106,7 +106,7 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
bone_collection_indices=options.bone_collection_indices bone_collection_indices=options.bone_collection_indices
) )
psk.bones = [psx_bone for psx_bone, _ in psx_bone_create_result.bones] psk.bones = [bone.psx_bone for bone in psx_bone_create_result.bones]
# Materials # Materials
match options.material_order_mode: match options.material_order_mode:

View File

@@ -3,7 +3,7 @@ from collections import Counter
from typing import List, Iterable, Optional, Dict, Tuple, cast as typing_cast from typing import List, Iterable, Optional, Dict, Tuple, cast as typing_cast
from bpy.types import Armature, AnimData, Collection, Context, Object, ArmatureModifier, SpaceProperties, PropertyGroup from bpy.types import Armature, AnimData, Collection, Context, Object, ArmatureModifier, SpaceProperties, PropertyGroup
from mathutils import Matrix, Vector, Quaternion as BpyQuaternion from mathutils import Matrix, Vector, Quaternion as BpyQuaternion
from psk_psa_py.shared.data import PsxBone, Quaternion from psk_psa_py.shared.data import PsxBone, Quaternion, Vector3
from ..shared.types import BpyCollectionProperty, PSX_PG_bone_collection_list_item from ..shared.types import BpyCollectionProperty, PSX_PG_bone_collection_list_item
@@ -69,30 +69,33 @@ def populate_bone_collection_list(
unique_armature_data = set() unique_armature_data = set()
for armature_object in armature_objects: for armature_object in armature_objects:
armature = typing_cast(Armature, armature_object.data) armature_data = typing_cast(Armature, armature_object.data)
if armature is None: if armature_data is None:
continue continue
if primary_key == 'DATA' and armature_object.data in unique_armature_data: if primary_key == 'DATA':
if armature_data in unique_armature_data:
# Skip this armature since we have already added an entry for it and we are using the data as the key. # Skip this armature since we have already added an entry for it and we are using the data as the key.
continue continue
unique_armature_data.add(armature_data)
unique_armature_data.add(armature_object.data) unassigned_bone_count = sum(map(lambda bone: 1 if len(bone.collections) == 0 else 0, armature_data.bones))
if unassigned_bone_count > 0:
item = bone_collection_list.add() item = bone_collection_list.add()
item.armature_object_name = armature_object.name item.armature_object_name = armature_object.name
item.armature_data_name = armature_object.data.name if armature_object.data else '' item.armature_data_name = armature_data.name if armature_data else ''
item.name = 'Unassigned' # TODO: localize item.name = 'Unassigned'
item.index = -1 item.index = -1
# Count the number of bones without an assigned bone collection # Count the number of bones without an assigned bone collection
item.count = sum(map(lambda bone: 1 if len(bone.collections) == 0 else 0, armature.bones)) item.count = unassigned_bone_count
item.is_selected = unassigned_collection_is_selected item.is_selected = unassigned_collection_is_selected
for bone_collection_index, bone_collection in enumerate(armature.collections_all): for bone_collection_index, bone_collection in enumerate(armature_data.collections_all):
item = bone_collection_list.add() item = bone_collection_list.add()
item.armature_object_name = armature_object.name item.armature_object_name = armature_object.name
item.armature_data_name = armature_object.data.name if armature_object.data else '' item.armature_data_name = armature_data.name if armature_data else ''
item.name = bone_collection.name item.name = bone_collection.name
item.index = bone_collection_index item.index = bone_collection_index
item.count = len(bone_collection.bones) item.count = len(bone_collection.bones)
@@ -168,7 +171,6 @@ def convert_string_to_cp1252_bytes(string: str) -> bytes:
raise RuntimeError(f'The string "{string}" contains characters that cannot be encoded in the Windows-1252 codepage') from e raise RuntimeError(f'The string "{string}" contains characters that cannot be encoded in the Windows-1252 codepage') from e
# TODO: Perhaps export space should just be a transform matrix, since the below is not actually used unless we're using WORLD space.
def create_psx_bones_from_blender_bones( def create_psx_bones_from_blender_bones(
bones: List[bpy.types.Bone], bones: List[bpy.types.Bone],
armature_object_matrix_world: Matrix, armature_object_matrix_world: Matrix,
@@ -212,23 +214,23 @@ def create_psx_bones_from_blender_bones(
location.z *= armature_object_scale.z location.z *= armature_object_scale.z
# Copy the calculated location and rotation to the bone. # Copy the calculated location and rotation to the bone.
psx_bone.location.x = location.x psx_bone.location = convert_vector_to_vector3(location)
psx_bone.location.y = location.y psx_bone.rotation = convert_bpy_quaternion_to_psx_quaternion(rotation)
psx_bone.location.z = location.z
psx_bone.rotation.w = rotation.w
psx_bone.rotation.x = rotation.x
psx_bone.rotation.y = rotation.y
psx_bone.rotation.z = rotation.z
psx_bones.append(psx_bone) psx_bones.append(psx_bone)
return psx_bones return psx_bones
class PsxBoneResult:
def __init__(self, psx_bone: PsxBone, armature_object: Object | None) -> None:
self.psx_bone: PsxBone = psx_bone
self.armature_object: Object | None = armature_object
class PsxBoneCreateResult: class PsxBoneCreateResult:
def __init__(self, def __init__(self,
bones: list[tuple[PsxBone, Object | None]], # List of tuples of (psx_bone, armature_object) bones: list[PsxBoneResult], # List of tuples of (psx_bone, armature_object)
armature_object_root_bone_indices: dict[Object, int], armature_object_root_bone_indices: dict[Object, int],
armature_object_bone_names: dict[Object, list[str]], armature_object_bone_names: dict[Object, list[str]],
): ):
@@ -238,16 +240,30 @@ class PsxBoneCreateResult:
@property @property
def has_false_root_bone(self) -> bool: def has_false_root_bone(self) -> bool:
return len(self.bones) > 0 and self.bones[0][1] is None return len(self.bones) > 0 and self.bones[0].armature_object is None
def convert_bpy_quaternion_to_psx_quaternion(other: BpyQuaternion) -> Quaternion: def convert_vector_to_vector3(vector: Vector) -> Vector3:
quaternion = Quaternion() """
quaternion.x = other.x Convert a Blender mathutils.Vector to a psk_psa_py Vector3.
quaternion.y = other.y """
quaternion.z = other.z vector3 = Vector3()
quaternion.w = other.w vector3.x = vector.x
return quaternion vector3.y = vector.y
vector3.z = vector.z
return vector3
def convert_bpy_quaternion_to_psx_quaternion(quaternion: BpyQuaternion) -> Quaternion:
"""
Convert a Blender mathutils.Quaternion to a psk_psa_py Quaternion.
"""
psx_quaternion = Quaternion()
psx_quaternion.x = quaternion.x
psx_quaternion.y = quaternion.y
psx_quaternion.z = quaternion.z
psx_quaternion.w = quaternion.w
return psx_quaternion
class PsxBoneCollection: class PsxBoneCollection:
@@ -366,27 +382,7 @@ def create_psx_bones(
# Store the index of the root bone for each armature object. # Store the index of the root bone for each armature object.
# We will need this later to correctly assign vertex weights. # We will need this later to correctly assign vertex weights.
armature_object_root_bone_indices: dict[Object | None, int] = dict() armature_object_root_bone_indices: dict[Object | None, int] = dict()
bones: list[tuple[PsxBone, Object | None]] = [] bones: list[PsxBoneResult] = []
if len(armature_objects) == 0 or total_bone_count == 0:
# If the mesh has no armature object or no bones, simply assign it a dummy bone at the root to satisfy the
# 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 = Quaternion(0.0, 0.0, 0.0, 1.0)
bones.append((psx_bone, None))
armature_object_root_bone_indices[None] = 0
else:
# If we have multiple root armature objects, create a root bone at the world origin.
if len(armature_tree.root_nodes) > 1:
psx_bone = PsxBone()
psx_bone.name = convert_string_to_cp1252_bytes(root_bone_name)
psx_bone.children_count = total_bone_count
psx_bone.rotation = Quaternion(0.0, 0.0, 0.0, 1.0)
bones.append((psx_bone, None))
armature_object_root_bone_indices[None] = 0
# Iterate through all the armature objects. # Iterate through all the armature objects.
for armature_object in armature_objects: for armature_object in armature_objects:
@@ -423,16 +419,11 @@ def create_psx_bones(
root_bone = armature_psx_bones[0] root_bone = armature_psx_bones[0]
root_bone_location = Vector((root_bone.location.x, root_bone.location.y, root_bone.location.z)) root_bone_location = Vector((root_bone.location.x, root_bone.location.y, root_bone.location.z))
relative_location = parent_bone_world_rotation.inverted() @ (root_bone_location - parent_bone_world_location) relative_location = parent_bone_world_rotation.inverted() @ (root_bone_location - parent_bone_world_location)
root_bone.location.x = relative_location.x root_bone.location = convert_vector_to_vector3(relative_location)
root_bone.location.y = relative_location.y
root_bone.location.z = relative_location.z
# Convert the root bone rotation to be relative to the parent bone. # Convert the root bone rotation to be relative to the parent bone.
root_bone_rotation = BpyQuaternion((root_bone.rotation.w, root_bone.rotation.x, root_bone.rotation.y, root_bone.rotation.z)) root_bone_rotation = BpyQuaternion((root_bone.rotation.w, root_bone.rotation.x, root_bone.rotation.y, root_bone.rotation.z))
relative_rotation = parent_bone_world_rotation.inverted() @ root_bone_rotation relative_rotation = parent_bone_world_rotation.inverted() @ root_bone_rotation
root_bone.rotation.w = relative_rotation.w root_bone.rotation = convert_bpy_quaternion_to_psx_quaternion(relative_rotation)
root_bone.rotation.x = relative_rotation.x
root_bone.rotation.y = relative_rotation.y
root_bone.rotation.z = relative_rotation.z
case 'OBJECT': case 'OBJECT':
raise NotImplementedError('Parenting armature objects to other armature objects is not yet implemented.') raise NotImplementedError('Parenting armature objects to other armature objects is not yet implemented.')
@@ -450,7 +441,7 @@ def create_psx_bones(
armature_object_root_bone_indices[armature_object] = len(bones) armature_object_root_bone_indices[armature_object] = len(bones)
bones.extend((psx_bone, armature_object) for psx_bone in armature_psx_bones) bones.extend(PsxBoneResult(psx_bone, armature_object) for psx_bone in armature_psx_bones)
# Check if any of the armatures are parented to one another. # Check if any of the armatures are parented to one another.
# If so, adjust the hierarchy as though they are part of the same armature object. # If so, adjust the hierarchy as though they are part of the same armature object.
@@ -466,17 +457,17 @@ def create_psx_bones(
match armature_object.parent_type: match armature_object.parent_type:
case 'OBJECT': case 'OBJECT':
# Parent this armature's root bone to the root bone of the parent object. # Parent this armature's root bone to the root bone of the parent object.
bones[root_bone_index][0].parent_index = parent_root_bone_index bones[root_bone_index].psx_bone.parent_index = parent_root_bone_index
case 'BONE': case 'BONE':
# Parent this armature's root bone to the specified bone in the parent. # Parent this armature's root bone to the specified bone in the parent.
new_parent_index = None new_parent_index = None
for bone_index, (bone, bone_armature_object) in enumerate(bones): for bone_index, bone in enumerate(bones):
if bone.name == convert_string_to_cp1252_bytes(armature_object.parent_bone) and bone_armature_object == armature_object.parent: if bone.psx_bone.name == convert_string_to_cp1252_bytes(armature_object.parent_bone) and bone.armature_object == armature_object.parent:
new_parent_index = bone_index new_parent_index = bone_index
break break
if new_parent_index == None: if new_parent_index == None:
raise RuntimeError(f'Bone \'{armature_object.parent_bone}\' could not be found in armature \'{armature_object.parent.name}\'.') raise RuntimeError(f'Bone \'{armature_object.parent_bone}\' could not be found in armature \'{armature_object.parent.name}\'.')
bones[root_bone_index][0].parent_index = new_parent_index bones[root_bone_index].psx_bone.parent_index = new_parent_index
case _: case _:
raise RuntimeError(f'Unhandled parent type ({armature_object.parent_type}) for object {armature_object.name}.\n' raise RuntimeError(f'Unhandled parent type ({armature_object.parent_type}) for object {armature_object.name}.\n'
f'Parent type must be \'Object\' or \'Bone\'.' f'Parent type must be \'Object\' or \'Bone\'.'
@@ -489,35 +480,25 @@ def create_psx_bones(
case 'ARMATURE': case 'ARMATURE':
# The bone is in world-space. We need to convert it to armature (object) space. # The bone is in world-space. We need to convert it to armature (object) space.
# Get this from matrix_local. # Get this from matrix_local.
root_bone, root_bone_armature_object = bones[0] root_bone, root_bone_armature_object = bones[0].psx_bone, bones[0].armature_object
if root_bone_armature_object is None: if root_bone_armature_object is None:
raise RuntimeError('Cannot export to Armature space when multiple armatures are being exported.') raise RuntimeError('Cannot export to Armature space when multiple armatures are being exported.')
armature_data = typing_cast(Armature, root_bone_armature_object.data) armature_data = typing_cast(Armature, root_bone_armature_object.data)
matrix_local = armature_data.bones[root_bone.name.decode('windows-1252')].matrix_local matrix_local = armature_data.bones[root_bone.name.decode('windows-1252')].matrix_local
location, rotation, _ = matrix_local.decompose() location, rotation, _ = matrix_local.decompose()
root_bone.location.x = location.x root_bone.location = convert_vector_to_vector3(location)
root_bone.location.y = location.y root_bone.rotation = convert_bpy_quaternion_to_psx_quaternion(rotation)
root_bone.location.z = location.z
root_bone.rotation.w = rotation.w
root_bone.rotation.x = rotation.x
root_bone.rotation.y = rotation.y
root_bone.rotation.z = rotation.z
case 'ROOT': case 'ROOT':
# Zero out the root bone transforms. # Zero out the root bone transforms.
root_bone = bones[0][0] root_bone = bones[0].psx_bone
root_bone.location.x = 0.0 root_bone.location = Vector3.zero()
root_bone.location.y = 0.0 root_bone.rotation = Quaternion.identity()
root_bone.location.z = 0.0
root_bone.rotation.w = 1.0
root_bone.rotation.x = 0.0
root_bone.rotation.y = 0.0
root_bone.rotation.z = 0.0
case _: case _:
assert False, f'Invalid export space: {export_space}' assert False, f'Invalid export space: {export_space}'
# Check if there are bone name conflicts between armatures. # Check if there are bone name conflicts between armatures.
bone_name_counts = Counter(bone[0].name.decode('windows-1252').upper() for bone in bones) bone_name_counts = Counter(bone.psx_bone.name.decode('windows-1252').upper() for bone in bones)
for bone_name, count in bone_name_counts.items(): for bone_name, count in bone_name_counts.items():
if count > 1: if count > 1:
error_message = f'Found {count} bones with the name "{bone_name}". ' error_message = f'Found {count} bones with the name "{bone_name}". '
@@ -529,16 +510,16 @@ def create_psx_bones(
raise RuntimeError(error_message) raise RuntimeError(error_message)
# Apply the scale to the bone locations. # Apply the scale to the bone locations.
for psx_bone, _ in bones: for bone in bones:
psx_bone.location.x *= scale bone.psx_bone.location.x *= scale
psx_bone.location.y *= scale bone.psx_bone.location.y *= scale
psx_bone.location.z *= scale bone.psx_bone.location.z *= scale
coordinate_system_matrix = get_coordinate_system_transform(forward_axis, up_axis) coordinate_system_matrix = get_coordinate_system_transform(forward_axis, up_axis)
coordinate_system_default_rotation = coordinate_system_matrix.to_quaternion() coordinate_system_default_rotation = coordinate_system_matrix.to_quaternion()
# Apply the coordinate system transform to the root bone. # Apply the coordinate system transform to the root bone.
root_psx_bone = bones[0][0] root_psx_bone = bones[0].psx_bone
# Get transform matrix from root bone location and rotation. # 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_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_rotation = BpyQuaternion((root_psx_bone.rotation.w, root_psx_bone.rotation.x, root_psx_bone.rotation.y, root_psx_bone.rotation.z))
@@ -548,13 +529,8 @@ def create_psx_bones(
) )
root_bone_matrix = coordinate_system_default_rotation.inverted().to_matrix().to_4x4() @ root_bone_matrix root_bone_matrix = coordinate_system_default_rotation.inverted().to_matrix().to_4x4() @ root_bone_matrix
location, rotation, _ = root_bone_matrix.decompose() location, rotation, _ = root_bone_matrix.decompose()
root_psx_bone.location.x = location.x root_psx_bone.location = convert_vector_to_vector3(location)
root_psx_bone.location.y = location.y root_psx_bone.rotation = convert_bpy_quaternion_to_psx_quaternion(rotation)
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) convert_bpy_quaternion_to_psx_quaternion(coordinate_system_default_rotation)
@@ -678,7 +654,7 @@ def get_armature_for_mesh_object(mesh_object: Object) -> Optional[Object]:
def _get_psk_input_objects(mesh_dfs_objects: Iterable[DfsObject]) -> PskInputObjects: def _get_psk_input_objects(mesh_dfs_objects: Iterable[DfsObject]) -> PskInputObjects:
mesh_dfs_objects = list(mesh_dfs_objects) mesh_dfs_objects = list(mesh_dfs_objects)
if len(mesh_dfs_objects) == 0: if len(mesh_dfs_objects) == 0:
raise RuntimeError('At least one mesh must be selected') raise RuntimeError('No mesh objects were found to export.')
input_objects = PskInputObjects() input_objects = PskInputObjects()
input_objects.mesh_dfs_objects = mesh_dfs_objects input_objects.mesh_dfs_objects = mesh_dfs_objects
# Get the armature objects used on all the meshes being exported. # Get the armature objects used on all the meshes being exported.

View File

@@ -40,7 +40,7 @@ class PSK_OT_bone_collection_list_populate(Operator):
populate_bone_collection_list(export_operator.bone_collection_list, input_objects.armature_objects) populate_bone_collection_list(export_operator.bone_collection_list, input_objects.armature_objects)
for bone_collection in export_operator.bone_collection_list: for bone_collection in export_operator.bone_collection_list:
bone_collection.is_selected = selected_status[hash(bone_collection)] bone_collection.is_selected = selected_status.get(hash(bone_collection), False)
return {'FINISHED'} return {'FINISHED'}