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:
parent
7ad8f0238a
commit
5e7c2535e2
@ -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
|
||||
|
@ -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():
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user