Refactoring and fixing issues with PSK exports with non-default forward & up axes

This commit is contained in:
Colin Basnett
2025-01-31 00:39:24 -08:00
parent bea245583f
commit d1bae944f1
8 changed files with 289 additions and 205 deletions

View File

@@ -1,7 +1,6 @@
from typing import Optional from typing import Optional
from bpy.types import Bone, Action, PoseBone from bpy.types import Bone, Action, PoseBone
from mathutils import Vector
from .data import * from .data import *
from ..shared.helpers import * from ..shared.helpers import *
@@ -35,25 +34,32 @@ class PsaBuildOptions:
self.root_motion: bool = False self.root_motion: bool = False
self.scale = 1.0 self.scale = 1.0
self.sampling_mode: str = 'INTERPOLATED' # One of ('INTERPOLATED', 'SUBFRAME') self.sampling_mode: str = 'INTERPOLATED' # One of ('INTERPOLATED', 'SUBFRAME')
self.export_space = 'WORLD'
self.forward_axis = 'X'
self.up_axis = 'Z'
def _get_pose_bone_location_and_rotation(pose_bone: PoseBone, armature_object: Object, options: PsaBuildOptions): def _get_pose_bone_location_and_rotation(pose_bone: PoseBone, armature_object: Object, root_motion: bool, scale: float, coordinate_system_transform: Matrix) -> Tuple[Vector, Quaternion]:
if pose_bone.parent is not None: if pose_bone.parent is not None:
pose_bone_matrix = pose_bone.matrix pose_bone_matrix = pose_bone.matrix
pose_bone_parent_matrix = pose_bone.parent.matrix pose_bone_parent_matrix = pose_bone.parent.matrix
pose_bone_matrix = pose_bone_parent_matrix.inverted() @ pose_bone_matrix pose_bone_matrix = pose_bone_parent_matrix.inverted() @ pose_bone_matrix
else: else:
if options.root_motion: if root_motion:
# Get the bone's pose matrix, taking the armature object's world matrix into account. # Get the bone's pose matrix, taking the armature object's world matrix into account.
pose_bone_matrix = armature_object.matrix_world @ pose_bone.matrix pose_bone_matrix = armature_object.matrix_world @ pose_bone.matrix
else: else:
# Use the bind pose matrix for the root bone. # Use the bind pose matrix for the root bone.
pose_bone_matrix = pose_bone.matrix pose_bone_matrix = 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() location = pose_bone_matrix.to_translation()
rotation = pose_bone_matrix.to_quaternion().normalized() rotation = pose_bone_matrix.to_quaternion().normalized()
location *= options.scale location *= scale
if pose_bone.parent is not None: if pose_bone.parent is not None:
rotation.conjugate() rotation.conjugate()
@@ -86,46 +92,18 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
if len(bones) == 0: if len(bones) == 0:
raise RuntimeError('No bones available for export') raise RuntimeError('No bones available for export')
# The bone building code should be shared between the PSK and PSA exporters, since they both need to build a nearly identical bone list.
# Build list of PSA bones. # Build list of PSA bones.
for bone in bones: psa.bones = convert_blender_bones_to_psx_bones(
psa_bone = Psa.Bone() bones=bones,
bone_class=Psa.Bone,
try: export_space=options.export_space,
psa_bone.name = bytes(bone.name, encoding='windows-1252') armature_object_matrix_world=armature_object.matrix_world,
except UnicodeEncodeError: scale=options.scale,
raise RuntimeError(f'Bone name "{bone.name}" contains characters that cannot be encoded in the Windows-1252 codepage') forward_axis=options.forward_axis,
up_axis=options.up_axis
try: )
parent_index = bones.index(bone.parent)
psa_bone.parent_index = parent_index
psa.bones[parent_index].children_count += 1
except ValueError:
psa_bone.parent_index = 0
if bone.parent is not None:
rotation = bone.matrix.to_quaternion().conjugated()
inverse_parent_rotation = bone.parent.matrix.to_quaternion().inverted()
parent_head = inverse_parent_rotation @ bone.parent.head
parent_tail = inverse_parent_rotation @ bone.parent.tail
location = (parent_tail - parent_head) + bone.head
else:
armature_local_matrix = armature_object.matrix_local
location = armature_local_matrix @ bone.head
bone_rotation = bone.matrix.to_quaternion().conjugated()
local_rotation = armature_local_matrix.to_3x3().to_quaternion().conjugated()
rotation = bone_rotation @ local_rotation
rotation.conjugate()
psa_bone.location.x = location.x
psa_bone.location.y = location.y
psa_bone.location.z = location.z
psa_bone.rotation.x = rotation.x
psa_bone.rotation.y = rotation.y
psa_bone.rotation.z = rotation.z
psa_bone.rotation.w = rotation.w
psa.bones.append(psa_bone)
# Add prefixes and suffices to the names of the export sequences and strip whitespace. # Add prefixes and suffices to the names of the export sequences and strip whitespace.
for export_sequence in options.sequences: for export_sequence in options.sequences:
@@ -142,6 +120,8 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
context.window_manager.progress_begin(0, len(options.sequences)) context.window_manager.progress_begin(0, len(options.sequences))
coordinate_system_transform = get_coordinate_system_transform(options.forward_axis, options.up_axis)
for export_sequence_index, export_sequence in enumerate(options.sequences): for export_sequence_index, export_sequence in enumerate(options.sequences):
# Look up the pose bones for the bones that are going to be exported. # Look up the pose bones for the bones that are going to be exported.
pose_bones = [(bone_names.index(bone.name), bone) for bone in export_sequence.armature_object.pose.bones] pose_bones = [(bone_names.index(bone.name), bone) for bone in export_sequence.armature_object.pose.bones]
@@ -221,9 +201,13 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
last_frame_bone_poses.clear() last_frame_bone_poses.clear()
context.scene.frame_set(frame=last_frame) context.scene.frame_set(frame=last_frame)
for pose_bone in pose_bones: for pose_bone in pose_bones:
location, rotation = _get_pose_bone_location_and_rotation(pose_bone, location, rotation = _get_pose_bone_location_and_rotation(
pose_bone,
export_sequence.armature_object, export_sequence.armature_object,
options) root_motion=options.root_motion,
scale=options.scale,
coordinate_system_transform=coordinate_system_transform
)
last_frame_bone_poses.append((location, rotation)) last_frame_bone_poses.append((location, rotation))
next_frame = None next_frame = None
@@ -239,7 +223,13 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
next_frame = last_frame + 1 next_frame = last_frame + 1
context.scene.frame_set(frame=next_frame) context.scene.frame_set(frame=next_frame)
for pose_bone in pose_bones: for pose_bone in pose_bones:
location, rotation = _get_pose_bone_location_and_rotation(pose_bone, export_sequence.armature_object, options) location, rotation = _get_pose_bone_location_and_rotation(
pose_bone,
export_sequence.armature_object,
root_motion=options.root_motion,
scale=options.scale,
coordinate_system_transform=coordinate_system_transform
)
next_frame_bone_poses.append((location, rotation)) next_frame_bone_poses.append((location, rotation))
factor = frame % 1.0 factor = frame % 1.0

View File

@@ -383,11 +383,14 @@ class PSA_OT_export(Operator, ExportHelper):
transform_header.label(text='Transform') transform_header.label(text='Transform')
if transform_panel: if transform_panel:
flow = transform_panel.grid_flow() flow = transform_panel.grid_flow(row_major=True)
flow.use_property_split = True flow.use_property_split = True
flow.use_property_decorate = False flow.use_property_decorate = False
flow.prop(pg, 'root_motion', text='Root Motion') flow.prop(pg, 'root_motion')
flow.prop(pg, 'scale', text='Scale') flow.prop(pg, 'export_space')
flow.prop(pg, 'scale')
flow.prop(pg, 'forward_axis')
flow.prop(pg, 'up_axis')
@classmethod @classmethod
def _check_context(cls, context): def _check_context(cls, context):
@@ -514,6 +517,9 @@ class PSA_OT_export(Operator, ExportHelper):
options.root_motion = pg.root_motion options.root_motion = pg.root_motion
options.scale = pg.scale options.scale = pg.scale
options.sampling_mode = pg.sampling_mode options.sampling_mode = pg.sampling_mode
options.export_space = pg.export_space
options.forward_axis = pg.forward_axis
options.up_axis = pg.up_axis
try: try:
psa = build_psa(context, options) psa = build_psa(context, options)

View File

@@ -7,7 +7,7 @@ from bpy.props import BoolProperty, PointerProperty, EnumProperty, FloatProperty
StringProperty StringProperty
from bpy.types import PropertyGroup, Object, Action, AnimData, Context from bpy.types import PropertyGroup, Object, Action, AnimData, Context
from ...shared.data import bone_filter_mode_items from ...shared.data import bone_filter_mode_items, ForwardUpAxisMixin, ExportSpaceMixin
from ...shared.types import PSX_PG_bone_collection_list_item from ...shared.types import PSX_PG_bone_collection_list_item
@@ -102,7 +102,7 @@ def animation_data_override_update_cb(self: 'PSA_PG_export', context: Context):
self.nla_track = '' self.nla_track = ''
class PSA_PG_export(PropertyGroup): class PSA_PG_export(PropertyGroup, ForwardUpAxisMixin, ExportSpaceMixin):
root_motion: BoolProperty( root_motion: BoolProperty(
name='Root Motion', name='Root Motion',
options=empty_set, options=empty_set,

View File

@@ -4,7 +4,6 @@ from typing import Optional
import bmesh import bmesh
import numpy as np import numpy as np
from bpy.types import Material, Collection, Context from bpy.types import Material, Collection, Context
from mathutils import Matrix, Vector
from .data import * from .data import *
from .properties import triangle_type_and_bit_flags_to_poly_flags from .properties import triangle_type_and_bit_flags_to_poly_flags
@@ -30,34 +29,6 @@ class PskBuildOptions(object):
self.up_axis = 'Z' self.up_axis = 'Z'
def get_vector_from_axis_identifier(axis_identifier: str) -> Vector:
match axis_identifier:
case 'X':
return Vector((1.0, 0.0, 0.0))
case 'Y':
return Vector((0.0, 1.0, 0.0))
case 'Z':
return Vector((0.0, 0.0, 1.0))
case '-X':
return Vector((-1.0, 0.0, 0.0))
case '-Y':
return Vector((0.0, -1.0, 0.0))
case '-Z':
return Vector((0.0, 0.0, -1.0))
def get_coordinate_system_transform(forward_axis: str = 'X', up_axis: str = 'Z') -> Matrix:
forward = get_vector_from_axis_identifier(forward_axis)
up = get_vector_from_axis_identifier(up_axis)
left = up.cross(forward)
return Matrix((
(forward.x, forward.y, forward.z, 0.0),
(left.x, left.y, left.z, 0.0),
(up.x, up.y, up.z, 0.0),
(0.0, 0.0, 0.0, 1.0)
)).inverted()
def get_mesh_objects_for_collection(collection: Collection) -> Iterable[DfsObject]: def get_mesh_objects_for_collection(collection: Collection) -> Iterable[DfsObject]:
return filter(lambda x: x.obj.type == 'MESH', dfs_collection_objects(collection)) return filter(lambda x: x.obj.type == 'MESH', dfs_collection_objects(collection))
@@ -143,7 +114,12 @@ def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions)
coordinate_system_default_rotation = coordinate_system_matrix.to_quaternion() coordinate_system_default_rotation = coordinate_system_matrix.to_quaternion()
export_space_matrix = get_export_space_matrix() # TODO: maybe neutralize the scale here? export_space_matrix = get_export_space_matrix() # TODO: maybe neutralize the scale here?
scale_matrix = coordinate_system_matrix @ Matrix.Scale(options.scale, 4) scale_matrix = Matrix.Scale(options.scale, 4)
# We effectively need 3 transforms, I think:
# 1. The transform for the mesh vertices.
# 2. The transform for the bone locations.
# 3. The transform for the bone rotations.
if armature_object is None or len(armature_object.data.bones) == 0: if armature_object is None or len(armature_object.data.bones) == 0:
# If the mesh has no armature object or no bones, simply assign it a dummy bone at the root to satisfy the # If the mesh has no armature object or no bones, simply assign it a dummy bone at the root to satisfy the
@@ -161,65 +137,14 @@ def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions)
armature_data = typing.cast(Armature, armature_object.data) armature_data = typing.cast(Armature, armature_object.data)
bones = [armature_data.bones[bone_name] for bone_name in bone_names] bones = [armature_data.bones[bone_name] for bone_name in bone_names]
for bone in bones: psk.bones = convert_blender_bones_to_psx_bones(
psk_bone = Psk.Bone() bones, Psk.Bone,
try: options.export_space,
psk_bone.name = bytes(bone.name, encoding='windows-1252') armature_object.matrix_world,
except UnicodeEncodeError: options.scale,
raise RuntimeError( options.forward_axis,
f'Bone name "{bone.name}" contains characters that cannot be encoded in the Windows-1252 codepage') options.up_axis
psk_bone.flags = 0 )
psk_bone.children_count = 0
try:
parent_index = bones.index(bone.parent)
psk_bone.parent_index = parent_index
psk.bones[parent_index].children_count += 1
except ValueError:
psk_bone.parent_index = 0
if bone.parent is not None:
rotation = bone.matrix.to_quaternion().conjugated()
inverse_parent_rotation = bone.parent.matrix.to_quaternion().inverted()
parent_head = inverse_parent_rotation @ bone.parent.head
parent_tail = inverse_parent_rotation @ bone.parent.tail
location = (parent_tail - parent_head) + bone.head
else:
def get_armature_local_matrix():
match options.export_space:
case 'WORLD':
return armature_object.matrix_world
case 'ARMATURE':
return Matrix.Identity(4)
case _:
raise ValueError(f'Invalid export space: {options.export_space}')
armature_local_matrix = get_armature_local_matrix()
location = armature_local_matrix @ bone.head
bone_rotation = bone.matrix.to_quaternion().conjugated()
local_rotation = armature_local_matrix.to_3x3().to_quaternion().conjugated()
rotation = bone_rotation @ local_rotation
rotation.conjugate()
rotation = coordinate_system_default_rotation @ rotation
location = scale_matrix @ location
# If the armature object has been scaled, we need to scale the bone's location to match.
_, _, armature_object_scale = armature_object.matrix_world.decompose()
location.x *= armature_object_scale.x
location.y *= armature_object_scale.y
location.z *= armature_object_scale.z
psk_bone.location.x = location.x
psk_bone.location.y = location.y
psk_bone.location.z = location.z
psk_bone.rotation.w = rotation.w
psk_bone.rotation.x = rotation.x
psk_bone.rotation.y = rotation.y
psk_bone.rotation.z = rotation.z
psk.bones.append(psk_bone)
# MATERIALS # MATERIALS
for material in options.materials: for material in options.materials:
@@ -248,6 +173,8 @@ def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions)
material_names = [m.name for m in options.materials] material_names = [m.name for m in options.materials]
vertex_transform_matrix = scale_matrix @ coordinate_system_matrix @ export_space_matrix
for object_index, input_mesh_object in enumerate(input_objects.mesh_objects): for object_index, input_mesh_object in enumerate(input_objects.mesh_objects):
obj, instance_objects, matrix_world = input_mesh_object.obj, input_mesh_object.instance_objects, input_mesh_object.matrix_world obj, instance_objects, matrix_world = input_mesh_object.obj, input_mesh_object.instance_objects, input_mesh_object.matrix_world
@@ -329,12 +256,12 @@ def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions)
raise ValueError(f'Invalid object evaluation state: {options.object_eval_state}') raise ValueError(f'Invalid object evaluation state: {options.object_eval_state}')
vertex_offset = len(psk.points) vertex_offset = len(psk.points)
matrix_world = scale_matrix @ export_space_matrix @ mesh_object.matrix_world point_transform_matrix = vertex_transform_matrix @ mesh_object.matrix_world
# VERTICES # VERTICES
for vertex in mesh_data.vertices: for vertex in mesh_data.vertices:
point = Vector3() point = Vector3()
v = matrix_world @ vertex.co v = point_transform_matrix @ vertex.co
point.x = v.x point.x = v.x
point.y = v.y point.y = v.y
point.z = v.z point.z = v.z

View File

@@ -99,6 +99,35 @@ class PSK_OT_populate_material_name_list(Operator):
return {'FINISHED'} return {'FINISHED'}
def material_list_names_search_cb(self, context: Context, edit_text: str):
for material in bpy.data.materials:
yield material.name
class PSK_OT_material_list_name_add(Operator):
bl_idname = 'psk.export_material_name_list_item_add'
bl_label = 'Add Material'
bl_description = 'Add a material to the material name list (useful if you want to add a material slot that is not actually used in the mesh)'
bl_options = {'INTERNAL'}
name: StringProperty(search=material_list_names_search_cb)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
export_operator = get_collection_export_operator_from_context(context)
if export_operator is None:
self.report({'ERROR_INVALID_CONTEXT'}, 'No valid export operator found in context')
return {'CANCELLED'}
m = export_operator.material_name_list.add()
m.material_name = self.name
m.index = len(export_operator.material_name_list) - 1
return {'FINISHED'}
class PSK_OT_material_list_move_up(Operator): class PSK_OT_material_list_move_up(Operator):
bl_idname = 'psk.export_material_list_item_move_up' bl_idname = 'psk.export_material_list_item_move_up'
bl_label = 'Move Up' bl_label = 'Move Up'
@@ -198,10 +227,7 @@ def get_sorted_materials_by_names(materials: Iterable[Material], material_names:
return materials_in_collection + materials_not_in_collection return materials_in_collection + materials_not_in_collection
def get_psk_build_options_from_property_group(mesh_objects: Iterable[Object], pg: 'PSK_PG_export', depsgraph: Optional[Depsgraph] = None) -> PskBuildOptions: def get_psk_build_options_from_property_group(pg: 'PSK_PG_export') -> PskBuildOptions:
if depsgraph is None:
depsgraph = bpy.context.evaluated_depsgraph_get()
options = PskBuildOptions() options = PskBuildOptions()
options.object_eval_state = pg.object_eval_state options.object_eval_state = pg.object_eval_state
options.export_space = pg.export_space options.export_space = pg.export_space
@@ -212,8 +238,12 @@ def get_psk_build_options_from_property_group(mesh_objects: Iterable[Object], p
options.up_axis = pg.up_axis options.up_axis = pg.up_axis
# TODO: perhaps move this into the build function and replace the materials list with a material names list. # TODO: perhaps move this into the build function and replace the materials list with a material names list.
materials = get_materials_for_mesh_objects(depsgraph, mesh_objects) # materials = get_materials_for_mesh_objects(depsgraph, mesh_objects)
options.materials = get_sorted_materials_by_names(materials, [m.material_name for m in pg.material_name_list])
# The material name list may contain materials that are not on the mesh objects.
# Therefore, we can perhaps take the material_name_list as gospel and simply use it as a lookup table.
# If a look-up fails, replace it with an empty material.
options.materials = [bpy.data.materials.get(x.material_name, None) for x in pg.material_name_list]
return options return options
@@ -241,7 +271,7 @@ class PSK_OT_export_collection(Operator, ExportHelper, PskExportMixin):
self.report({'ERROR_INVALID_CONTEXT'}, str(e)) self.report({'ERROR_INVALID_CONTEXT'}, str(e))
return {'CANCELLED'} return {'CANCELLED'}
options = get_psk_build_options_from_property_group([x.obj for x in input_objects.mesh_objects], self) options = get_psk_build_options_from_property_group(self)
try: try:
result = build_psk(context, input_objects, options) result = build_psk(context, input_objects, options)
@@ -297,6 +327,8 @@ class PSK_OT_export_collection(Operator, ExportHelper, PskExportMixin):
col = row.column(align=True) col = row.column(align=True)
col.operator(PSK_OT_material_list_name_move_up.bl_idname, text='', icon='TRIA_UP') col.operator(PSK_OT_material_list_name_move_up.bl_idname, text='', icon='TRIA_UP')
col.operator(PSK_OT_material_list_name_move_down.bl_idname, text='', icon='TRIA_DOWN') col.operator(PSK_OT_material_list_name_move_down.bl_idname, text='', icon='TRIA_DOWN')
col.separator()
col.operator(PSK_OT_material_list_name_add.bl_idname, text='', icon='ADD')
# TRANSFORM # TRANSFORM
transform_header, transform_panel = layout.panel('Transform', default_closed=False) transform_header, transform_panel = layout.panel('Transform', default_closed=False)
@@ -391,7 +423,7 @@ class PSK_OT_export(Operator, ExportHelper):
pg = getattr(context.scene, 'psk_export') pg = getattr(context.scene, 'psk_export')
input_objects = get_psk_input_objects_for_context(context) input_objects = get_psk_input_objects_for_context(context)
options = get_psk_build_options_from_property_group([x.obj for x in input_objects.mesh_objects], pg) options = get_psk_build_options_from_property_group(pg)
try: try:
result = build_psk(context, input_objects, options) result = build_psk(context, input_objects, options)
@@ -418,4 +450,5 @@ classes = (
PSK_OT_populate_material_name_list, PSK_OT_populate_material_name_list,
PSK_OT_material_list_name_move_up, PSK_OT_material_list_name_move_up,
PSK_OT_material_list_name_move_down, PSK_OT_material_list_name_move_down,
PSK_OT_material_list_name_add,
) )

View File

@@ -2,7 +2,7 @@ from bpy.props import EnumProperty, CollectionProperty, IntProperty, PointerProp
BoolProperty BoolProperty
from bpy.types import PropertyGroup, Material from bpy.types import PropertyGroup, Material
from ...shared.data import bone_filter_mode_items from ...shared.data import bone_filter_mode_items, ExportSpaceMixin, ForwardUpAxisMixin
from ...shared.types import PSX_PG_bone_collection_list_item from ...shared.types import PSX_PG_bone_collection_list_item
empty_set = set() empty_set = set()
@@ -17,26 +17,6 @@ export_space_items = [
('ARMATURE', 'Armature', 'Export in armature space'), ('ARMATURE', 'Armature', 'Export in armature space'),
] ]
axis_identifiers = ('X', 'Y', 'Z', '-X', '-Y', '-Z')
forward_items = (
('X', 'X Forward', ''),
('Y', 'Y Forward', ''),
('Z', 'Z Forward', ''),
('-X', '-X Forward', ''),
('-Y', '-Y Forward', ''),
('-Z', '-Z Forward', ''),
)
up_items = (
('X', 'X Up', ''),
('Y', 'Y Up', ''),
('Z', 'Z Up', ''),
('-X', '-X Up', ''),
('-Y', '-Y Up', ''),
('-Z', '-Z Up', ''),
)
class PSK_PG_material_list_item(PropertyGroup): class PSK_PG_material_list_item(PropertyGroup):
material: PointerProperty(type=Material) material: PointerProperty(type=Material)
index: IntProperty() index: IntProperty()
@@ -46,21 +26,7 @@ class PSK_PG_material_name_list_item(PropertyGroup):
index: IntProperty() index: IntProperty()
class PskExportMixin(ExportSpaceMixin, ForwardUpAxisMixin):
def forward_axis_update(self, _context):
if self.forward_axis == self.up_axis:
# Automatically set the up axis to the next available axis
self.up_axis = next((axis for axis in axis_identifiers if axis != self.forward_axis), 'Z')
def up_axis_update(self, _context):
if self.up_axis == self.forward_axis:
# Automatically set the forward axis to the next available axis
self.forward_axis = next((axis for axis in axis_identifiers if axis != self.up_axis), 'X')
class PskExportMixin:
object_eval_state: EnumProperty( object_eval_state: EnumProperty(
items=object_eval_state_items, items=object_eval_state_items,
name='Object Evaluation State', name='Object Evaluation State',
@@ -78,12 +44,6 @@ class PskExportMixin:
min=0.0001, min=0.0001,
soft_max=100.0 soft_max=100.0
) )
export_space: EnumProperty(
name='Export Space',
description='Space to export the mesh in',
items=export_space_items,
default='WORLD'
)
bone_filter_mode: EnumProperty( bone_filter_mode: EnumProperty(
name='Bone Filter', name='Bone Filter',
options=empty_set, options=empty_set,
@@ -92,18 +52,6 @@ class PskExportMixin:
) )
bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item) bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item)
bone_collection_list_index: IntProperty(default=0) bone_collection_list_index: IntProperty(default=0)
forward_axis: EnumProperty(
name='Forward',
items=forward_items,
default='X',
update=forward_axis_update
)
up_axis: EnumProperty(
name='Up',
items=up_items,
default='Z',
update=up_axis_update
)
material_name_list: CollectionProperty(type=PSK_PG_material_name_list_item) material_name_list: CollectionProperty(type=PSK_PG_material_name_list_item)
material_name_list_index: IntProperty(default=0) material_name_list_index: IntProperty(default=0)

View File

@@ -1,6 +1,9 @@
from ctypes import * from ctypes import *
from typing import Tuple from typing import Tuple
from bpy.props import EnumProperty
from mathutils import Vector, Matrix
class Color(Structure): class Color(Structure):
_fields_ = [ _fields_ = [
@@ -99,3 +102,90 @@ bone_filter_mode_items = (
('ALL', 'All', 'All bones will be exported'), ('ALL', 'All', 'All bones will be exported'),
('BONE_COLLECTIONS', 'Bone Collections', 'Only bones belonging to the selected bone collections and their ancestors will be exported') ('BONE_COLLECTIONS', 'Bone Collections', 'Only bones belonging to the selected bone collections and their ancestors will be exported')
) )
axis_identifiers = ('X', 'Y', 'Z', '-X', '-Y', '-Z')
forward_items = (
('X', 'X Forward', ''),
('Y', 'Y Forward', ''),
('Z', 'Z Forward', ''),
('-X', '-X Forward', ''),
('-Y', '-Y Forward', ''),
('-Z', '-Z Forward', ''),
)
up_items = (
('X', 'X Up', ''),
('Y', 'Y Up', ''),
('Z', 'Z Up', ''),
('-X', '-X Up', ''),
('-Y', '-Y Up', ''),
('-Z', '-Z Up', ''),
)
def forward_axis_update(self, _context):
if self.forward_axis == self.up_axis:
# Automatically set the up axis to the next available axis
self.up_axis = next((axis for axis in axis_identifiers if axis != self.forward_axis), 'Z')
def up_axis_update(self, _context):
if self.up_axis == self.forward_axis:
# Automatically set the forward axis to the next available axis
self.forward_axis = next((axis for axis in axis_identifiers if axis != self.up_axis), 'X')
class ForwardUpAxisMixin:
forward_axis: EnumProperty(
name='Forward',
items=forward_items,
default='X',
update=forward_axis_update
)
up_axis: EnumProperty(
name='Up',
items=up_items,
default='Z',
update=up_axis_update
)
export_space_items = [
('WORLD', 'World', 'Export in world space'),
('ARMATURE', 'Armature', 'Export in armature space'),
]
class ExportSpaceMixin:
export_space: EnumProperty(
name='Export Space',
description='Space to export the mesh in',
items=export_space_items,
default='WORLD'
)
def get_vector_from_axis_identifier(axis_identifier: str) -> Vector:
match axis_identifier:
case 'X':
return Vector((1.0, 0.0, 0.0))
case 'Y':
return Vector((0.0, 1.0, 0.0))
case 'Z':
return Vector((0.0, 0.0, 1.0))
case '-X':
return Vector((-1.0, 0.0, 0.0))
case '-Y':
return Vector((0.0, -1.0, 0.0))
case '-Z':
return Vector((0.0, 0.0, -1.0))
def get_coordinate_system_transform(forward_axis: str = 'X', up_axis: str = 'Z') -> Matrix:
forward = get_vector_from_axis_identifier(forward_axis)
up = get_vector_from_axis_identifier(up_axis)
left = up.cross(forward)
return Matrix((
(forward.x, forward.y, forward.z, 0.0),
(left.x, left.y, left.z, 0.0),
(up.x, up.y, up.z, 0.0),
(0.0, 0.0, 0.0, 1.0)
))

View File

@@ -4,6 +4,9 @@ import bpy
from bpy.props import CollectionProperty from bpy.props import CollectionProperty
from bpy.types import AnimData, Object from bpy.types import AnimData, Object
from bpy.types import Armature from bpy.types import Armature
from mathutils import Matrix
from .data import get_coordinate_system_transform
def rgb_to_srgb(c: float): def rgb_to_srgb(c: float):
@@ -208,3 +211,90 @@ class SemanticVersion(object):
def __hash__(self): def __hash__(self):
return hash((self.major, self.minor, self.patch)) return hash((self.major, self.minor, self.patch))
def convert_blender_bones_to_psx_bones(
bones: List[bpy.types.Bone],
bone_class: type,
export_space: str = 'WORLD', # perhaps export space should just be a transform matrix, since the below is not actually used unless we're using WORLD space.
armature_object_matrix_world: Matrix = Matrix.Identity(4),
scale = 1.0,
forward_axis: str = 'X',
up_axis: str = 'Z'
) -> Iterable[type]:
'''
Function that converts a Blender bone list into a bone list that
@param bones:
@return:
'''
scale_matrix = Matrix.Scale(scale, 4)
coordinate_system_transform = get_coordinate_system_transform(forward_axis, up_axis)
coordinate_system_default_rotation = coordinate_system_transform.to_quaternion()
psx_bones = []
for bone in bones:
psx_bone = bone_class()
try:
psx_bone.name = bytes(bone.name, encoding='windows-1252')
except UnicodeEncodeError:
raise RuntimeError(
f'Bone name "{bone.name}" contains characters that cannot be encoded in the Windows-1252 codepage')
# TODO: flags & children_count should be initialized to zero anyways, so we can probably remove these lines?
psx_bone.flags = 0
psx_bone.children_count = 0
try:
parent_index = bones.index(bone.parent)
psx_bone.parent_index = parent_index
psx_bones[parent_index].children_count += 1
except ValueError:
psx_bone.parent_index = 0
if bone.parent is not None:
rotation = bone.matrix.to_quaternion().conjugated()
inverse_parent_rotation = bone.parent.matrix.to_quaternion().inverted()
parent_head = inverse_parent_rotation @ bone.parent.head
parent_tail = inverse_parent_rotation @ bone.parent.tail
location = (parent_tail - parent_head) + bone.head
else:
def get_armature_local_matrix():
match export_space:
case 'WORLD':
return armature_object_matrix_world
case 'ARMATURE':
return Matrix.Identity(4)
case _:
raise ValueError(f'Invalid export space: {export_space}')
armature_local_matrix = get_armature_local_matrix()
location = armature_local_matrix @ bone.head
location = coordinate_system_transform @ location
bone_rotation = bone.matrix.to_quaternion().conjugated()
local_rotation = armature_local_matrix.to_3x3().to_quaternion().conjugated()
rotation = bone_rotation @ local_rotation
rotation.conjugate()
rotation = coordinate_system_default_rotation @ rotation
location = scale_matrix @ location
# If the armature object has been scaled, we need to scale the bone's location to match.
_, _, armature_object_scale = armature_object_matrix_world.decompose()
location.x *= armature_object_scale.x
location.y *= armature_object_scale.y
location.z *= armature_object_scale.z
psx_bone.location.x = location.x
psx_bone.location.y = location.y
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)
return psx_bones