1
0
mirror of https://github.com/DarklightGames/io_scene_psk_psa.git synced 2025-02-01 03:55:44 +01:00

* Unified how bones are filtered for export based on export settings (bone filter mode + bone groups)

* Bone Group filtering now works properly for PSK export
* Fixed a number of bugs that broke animation export
This commit is contained in:
Colin Basnett 2022-02-12 01:45:19 -08:00
parent 7ad8f0238a
commit 5e7c2535e2
5 changed files with 123 additions and 63 deletions

View File

@ -1,5 +1,5 @@
from bpy.types import NlaStrip
from typing import List
from typing import List, Tuple, Optional
from collections import Counter
@ -59,13 +59,14 @@ def get_psa_sequence_name(action, should_use_original_sequence_name):
return action.name
def get_export_bone_indices_for_bone_groups(armature_object, bone_group_indices: List[int]) -> List[int]:
def get_export_bone_names(armature_object, bone_filter_mode, bone_group_indices: List[int]) -> List[str]:
"""
Returns a sorted list of bone indices that should be exported for the given bone groups.
Returns a sorted list of bone indices that should be exported for the given bone filter mode and bone groups.
Note that the ancestors of bones within the bone groups will also be present in the returned list.
:param armature_object: Blender object with type 'ARMATURE'
:param bone_filter_mode: One of ['ALL', 'BONE_GROUPS']
:param bone_group_indices: List of bone group indices to be exported.
:return: A sorted list of bone indices that should be exported.
"""
@ -76,24 +77,58 @@ def get_export_bone_indices_for_bone_groups(armature_object, bone_group_indices:
pose_bones = armature_object.pose.bones
bone_names = [x.name for x in bones]
# Get a list of the bone indices that are explicitly part of the bone groups we are including.
# Get a list of the bone indices that we are explicitly including.
bone_index_stack = []
is_exporting_none_bone_groups = -1 in bone_group_indices
for bone_index, pose_bone in enumerate(pose_bones):
if (pose_bone.bone_group is None and is_exporting_none_bone_groups) or \
if bone_filter_mode == 'ALL' or \
(pose_bone.bone_group is None and is_exporting_none_bone_groups) or \
(pose_bone.bone_group is not None and pose_bone.bone_group_index in bone_group_indices):
bone_index_stack.append(bone_index)
bone_index_stack.append((bone_index, None))
# For each bone that is explicitly being added, recursively walk up the hierarchy and ensure that all of
# those ancestor bone indices are also in the list.
bone_indices = set()
bone_indices = dict()
while len(bone_index_stack) > 0:
bone_index = bone_index_stack.pop()
bone_index, instigator_bone_index = bone_index_stack.pop()
bone = bones[bone_index]
if bone.parent is not None:
parent_bone_index = bone_names.index(bone.parent.name)
if parent_bone_index not in bone_indices:
bone_index_stack.append(parent_bone_index)
bone_indices.add(bone_index)
bone_index_stack.append((parent_bone_index, bone_index))
bone_indices[bone_index] = instigator_bone_index
return list(sorted(list(bone_indices)))
# Sort the bone index list in-place.
bone_indices = [(x[0], x[1]) for x in bone_indices.items()]
bone_indices.sort(key=lambda x: x[0])
# Split out the bone indices and the instigator bone names into separate lists.
# We use the bone names for the return values because the bone name is a more universal way of referencing them.
# For example, users of this function may modify bone lists, which would invalidate the indices and require a
# index mapping scheme to resolve it. Using strings is more comfy and results in less code downstream.
instigator_bone_names = [bones[x[1]].name if x[1] is not None else None for x in bone_indices]
bone_names = [bones[x[0]].name for x in bone_indices]
# Ensure that the hierarchy we are sending back has a single root bone.
bone_indices = [x[0] for x in bone_indices]
root_bones = [bones[bone_index] for bone_index in bone_indices if bones[bone_index].parent is None]
if len(root_bones) > 1:
# There is more than one root bone.
# Print out why each root bone was included by linking it to one of the explicitly included bones.
root_bone_names = [bone.name for bone in root_bones]
for root_bone_name in root_bone_names:
bone_name = root_bone_name
while True:
# Traverse the instigator chain until the end to find the true instigator bone.
# TODO: in future, it would be preferential to have a readout of *all* instigator bones.
instigator_bone_name = instigator_bone_names[bone_names.index(bone_name)]
if instigator_bone_name is None:
print(f'Root bone "{root_bone_name}" was included because {bone_name} was marked for export')
break
bone_name = instigator_bone_name
raise RuntimeError('Exported bone hierarchy must have a single root bone.\n'
f'The bone hierarchy marked for export has {len(root_bones)} root bones: {root_bone_names}.\n'
f'Additional debugging information has been written to the console.')
return bone_names

View File

@ -44,55 +44,45 @@ class PsaBuilder(object):
# armature bones.
bone_names = [x.name for x in bones]
pose_bones = [(bone_names.index(bone.name), bone) for bone in armature.pose.bones]
del bone_names
pose_bones.sort(key=lambda x: x[0])
pose_bones = [x[1] for x in pose_bones]
bone_indices = list(range(len(bones)))
# If bone groups are specified, get only the bones that are in that specified bone groups and their ancestors.
if options.bone_filter_mode == 'BONE_GROUPS':
bone_indices = get_export_bone_indices_for_bone_groups(armature, options.bone_group_indices)
# Get a list of all the bone indices and instigator bones for the bone filter settings.
bone_names = get_export_bone_names(armature, options.bone_filter_mode, options.bone_group_indices)
bone_indices = [bone_names.index(x) for x in bone_names]
# Make the bone lists contain only the bones that are going to be exported.
bones = [bones[bone_index] for bone_index in bone_indices]
pose_bones = [pose_bones[bone_index] for bone_index in bone_indices]
if len(bones) == 0:
# No bones are going to be exported.
if len(bones) == 0:
raise RuntimeError('No bones available for export')
# Ensure that the exported hierarchy has a single root bone.
root_bones = [x for x in bones if x.parent is None]
if len(root_bones) > 1:
root_bone_names = [x.name for x in root_bones]
raise RuntimeError('Exported bone hierarchy must have a single root bone.'
f'The bone hierarchy marked for export has {len(root_bones)} root bones: {root_bone_names}')
# Build list of PSA bones.
for pose_bone in bones:
for bone in bones:
psa_bone = Psa.Bone()
psa_bone.name = bytes(pose_bone.name, encoding='utf-8')
psa_bone.name = bytes(bone.name, encoding='utf-8')
try:
parent_index = bones.index(pose_bone.parent)
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 = -1
if pose_bone.parent is not None:
rotation = pose_bone.matrix.to_quaternion()
if bone.parent is not None:
rotation = bone.matrix.to_quaternion()
rotation.x = -rotation.x
rotation.y = -rotation.y
rotation.z = -rotation.z
quat_parent = pose_bone.parent.matrix.to_quaternion().inverted()
parent_head = quat_parent @ pose_bone.parent.head
parent_tail = quat_parent @ pose_bone.parent.tail
location = (parent_tail - parent_head) + pose_bone.head
quat_parent = bone.parent.matrix.to_quaternion().inverted()
parent_head = quat_parent @ bone.parent.head
parent_tail = quat_parent @ bone.parent.tail
location = (parent_tail - parent_head) + bone.head
else:
location = armature.matrix_local @ pose_bone.head
rot_matrix = pose_bone.matrix @ armature.matrix_local.to_3x3()
location = armature.matrix_local @ bone.head
rot_matrix = bone.matrix @ armature.matrix_local.to_3x3()
rotation = rot_matrix.to_quaternion()
psa_bone.location.x = location.x
@ -195,8 +185,8 @@ class PsaBuilder(object):
psa.keys.append(key)
export_sequence.bone_count = len(pose_bones)
export_sequence.track_time = frame_count
psa_sequence.bone_count = len(pose_bones)
psa_sequence.track_time = frame_count
# Restore the mute state of the NLA strips we muted beforehand.
for nla_strip, mute in nla_strip_mute_statuses.items():

View File

@ -129,8 +129,8 @@ class PsaExportOperator(Operator, ExportHelper):
layout.label(text='Actions', icon='ACTION')
row = layout.row(align=True)
row.label(text='Select')
row.operator(PsaExportSelectAll.bl_idname, text='All')
row.operator(PsaExportDeselectAll.bl_idname, text='None')
row.operator(PsaExportActionsSelectAll.bl_idname, text='All')
row.operator(PsaExportActionsDeselectAll.bl_idname, text='None')
row = layout.row()
rows = max(3, min(len(pg.action_list), 10))
row.template_list('PSA_UL_ExportActionList', '', pg, 'action_list', pg, 'action_list_index', rows=rows)
@ -172,6 +172,10 @@ class PsaExportOperator(Operator, ExportHelper):
if pg.bone_filter_mode == 'BONE_GROUPS':
rows = max(3, min(len(pg.bone_group_list), 10))
layout.template_list('PSX_UL_BoneGroupList', '', pg, 'bone_group_list', pg, 'bone_group_list_index', rows=rows)
row = layout.row(align=True)
row.label(text='Select')
row.operator(PsaExportBoneGroupsSelectAll.bl_idname, text='All')
row.operator(PsaExportBoneGroupsDeselectAll.bl_idname, text='None')
def is_action_for_armature(self, action):
if len(action.fcurves) == 0:
@ -297,7 +301,7 @@ class PSA_UL_ExportActionList(UIList):
return flt_flags, flt_neworder
class PsaExportSelectAll(Operator):
class PsaExportActionsSelectAll(Operator):
bl_idname = 'psa_export.actions_select_all'
bl_label = 'Select All'
bl_description = 'Select all actions'
@ -307,17 +311,17 @@ class PsaExportSelectAll(Operator):
def poll(cls, context):
pg = context.scene.psa_export
item_list = pg.action_list
has_unselected_actions = any(map(lambda action: not action.is_selected, item_list))
return len(item_list) > 0 and has_unselected_actions
has_unselected_items = any(map(lambda item: not item.is_selected, item_list))
return len(item_list) > 0 and has_unselected_items
def execute(self, context):
pg = context.scene.psa_export
for action in pg.action_list:
action.is_selected = True
for item in pg.action_list:
item.is_selected = True
return {'FINISHED'}
class PsaExportDeselectAll(Operator):
class PsaExportActionsDeselectAll(Operator):
bl_idname = 'psa_export.actions_deselect_all'
bl_label = 'Deselect All'
bl_description = 'Deselect all actions'
@ -327,12 +331,52 @@ class PsaExportDeselectAll(Operator):
def poll(cls, context):
pg = context.scene.psa_export
item_list = pg.action_list
has_selected_items = any(map(lambda item: item.is_selected, item_list))
return len(item_list) > 0 and has_selected_items
def execute(self, context):
pg = context.scene.psa_export
for item in pg.action_list:
item.is_selected = False
return {'FINISHED'}
class PsaExportBoneGroupsSelectAll(Operator):
bl_idname = 'psa_export.bone_groups_select_all'
bl_label = 'Select All'
bl_description = 'Select all bone groups'
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
pg = context.scene.psa_export
item_list = pg.bone_group_list
has_unselected_items = any(map(lambda action: not action.is_selected, item_list))
return len(item_list) > 0 and has_unselected_items
def execute(self, context):
pg = context.scene.psa_export
for item in pg.bone_group_list:
item.is_selected = True
return {'FINISHED'}
class PsaExportBoneGroupsDeselectAll(Operator):
bl_idname = 'psa_export.bone_groups_deselect_all'
bl_label = 'Deselect All'
bl_description = 'Deselect all bone groups'
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
pg = context.scene.psa_export
item_list = pg.bone_group_list
has_selected_actions = any(map(lambda action: action.is_selected, item_list))
return len(item_list) > 0 and has_selected_actions
def execute(self, context):
pg = context.scene.psa_export
for action in pg.action_list:
for action in pg.bone_group_list:
action.is_selected = False
return {'FINISHED'}
@ -344,6 +388,8 @@ classes = (
PsaExportOperator,
PSA_UL_ExportActionList,
PSA_UL_ExportTimelineMarkerList,
PsaExportSelectAll,
PsaExportDeselectAll,
PsaExportActionsSelectAll,
PsaExportActionsDeselectAll,
PsaExportBoneGroupsSelectAll,
PsaExportBoneGroupsDeselectAll,
)

View File

@ -78,20 +78,8 @@ class PskBuilder(object):
psk_bone.rotation = Quaternion(0, 0, 0, 1)
psk.bones.append(psk_bone)
else:
bones = list(armature_object.data.bones)
# If we are filtering by bone groups, get only the bones that are in the specified bone groups and their
# ancestors.
if options.bone_filter_mode == 'BONE_GROUPS':
bone_indices = get_export_bone_indices_for_bone_groups(armature_object, options.bone_group_indices)
bones = [bones[bone_index] for bone_index in bone_indices]
# Ensure that the exported hierarchy has a single root bone.
root_bones = [x for x in bones if x.parent is None]
if len(root_bones) > 1:
root_bone_names = [x.name for x in root_bones]
raise RuntimeError('Exported bone hierarchy must have a single root bone.'
f'The bone hierarchy marked for export has {len(root_bones)} root bones: {root_bone_names}')
bone_names = get_export_bone_names(armature_object, options.bone_filter_mode, options.bone_group_indices)
bones = [armature_object.data.bones[bone_name] for bone_name in bone_names]
for bone in bones:
psk_bone = Psk.Bone()

View File

@ -125,6 +125,7 @@ class PskExportOperator(Operator, ExportHelper):
pg = context.scene.psk_export
builder = PskBuilder()
options = PskBuilderOptions()
options.bone_filter_mode = pg.bone_filter_mode
options.bone_group_indices = [x.index for x in pg.bone_group_list if x.is_selected]
try:
psk = builder.build(context, options)