Refactoring and fixing issues with PSK exports with non-default forward & up axes
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user