From 7fd0c6de813fb65fb0e6627be3dca19fd0499652 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Tue, 18 Jan 2022 13:21:38 -0800 Subject: [PATCH] Bone group filtering appears to work correctly now --- io_export_psk_psa/__init__.py | 2 + io_export_psk_psa/psa/builder.py | 69 +++++++++---- io_export_psk_psa/psa/exporter.py | 81 +++++++++++++-- io_export_psk_psa/psa/importer.py | 166 +++++++++++++++++------------- io_export_psk_psa/psa/reader.py | 8 +- io_export_psk_psa/psk/builder.py | 2 +- io_export_psk_psa/psk/importer.py | 11 +- 7 files changed, 226 insertions(+), 113 deletions(-) diff --git a/io_export_psk_psa/__init__.py b/io_export_psk_psa/__init__.py index 75c0b89..10454f1 100644 --- a/io_export_psk_psa/__init__.py +++ b/io_export_psk_psa/__init__.py @@ -46,6 +46,7 @@ classes = [ psa_importer.PsaImportOperator, psa_importer.PsaImportFileSelectOperator, psa_exporter.PSA_UL_ExportActionList, + psa_exporter.PSA_UL_ExportBoneGroupList, psa_importer.PSA_UL_ImportActionList, psa_importer.PsaImportActionListItem, psa_importer.PsaImportSelectAll, @@ -56,6 +57,7 @@ classes = [ psa_exporter.PsaExportSelectAll, psa_exporter.PsaExportDeselectAll, psa_exporter.PsaExportActionListItem, + psa_exporter.PsaExportBoneGroupListItem, psa_exporter.PsaExportPropertyGroup, ] diff --git a/io_export_psk_psa/psa/builder.py b/io_export_psk_psa/psa/builder.py index 38f9c47..0ae9244 100644 --- a/io_export_psk_psa/psa/builder.py +++ b/io_export_psk_psa/psa/builder.py @@ -4,6 +4,8 @@ from .data import * class PsaBuilderOptions(object): def __init__(self): self.actions = [] + self.bone_filter_mode = 'NONE' + self.bone_group_indices = [] # https://git.cth451.me/cth451/blender-addons/blob/master/io_export_unreal_psk_psa.py @@ -12,7 +14,7 @@ class PsaBuilder(object): # TODO: add options in here (selected anims, eg.) pass - def build(self, context, options) -> Psa: + def build(self, context, options: PsaBuilderOptions) -> Psa: object = context.view_layer.objects.active if object.type != 'ARMATURE': @@ -35,28 +37,59 @@ class PsaBuilder(object): pose_bones.sort(key=lambda x: x[0]) pose_bones = [x[1] for x in pose_bones] - for bone in bones: + bone_indices = list(range(len(bones))) + + if options.bone_filter_mode == 'BONE_GROUPS': + # Get a list of the bone indices that are explicitly part of the bone groups we are including. + bone_index_stack = [] + for bone_index, pose_bone in enumerate(pose_bones): + if pose_bone.bone_group_index in options.bone_group_indices: + bone_index_stack.append(bone_index) + + # For each bone that is explicitly being added, recursively walk up the hierarchy and ensure that all of + # those bone indices are also in the list. + bone_indices = set() + while len(bone_index_stack) > 0: + 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) + + del bone_names + + # Sort out list of bone indices to be exported. + bone_indices = sorted(list(bone_indices)) + + # The bone lists now contains 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] + + for pose_bone in bones: psa_bone = Psa.Bone() - psa_bone.name = bytes(bone.name, encoding='utf-8') - psa_bone.children_count = len(bone.children) + psa_bone.name = bytes(pose_bone.name, encoding='utf-8') try: - psa_bone.parent_index = bones.index(bone.parent) + parent_index = bones.index(pose_bone.parent) + psa_bone.parent_index = parent_index + psa.bones[parent_index].children_count += 1 except ValueError: psa_bone.parent_index = -1 - if bone.parent is not None: - rotation = bone.matrix.to_quaternion() + if pose_bone.parent is not None: + rotation = pose_bone.matrix.to_quaternion() rotation.x = -rotation.x rotation.y = -rotation.y rotation.z = -rotation.z - 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 + 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 else: - location = armature.matrix_local @ bone.head - rot_matrix = bone.matrix @ armature.matrix_local.to_3x3() + location = armature.matrix_local @ pose_bone.head + rot_matrix = pose_bone.matrix @ armature.matrix_local.to_3x3() rotation = rot_matrix.to_quaternion() psa_bone.location.x = location.x @@ -92,18 +125,18 @@ class PsaBuilder(object): for frame in range(frame_count): context.scene.frame_set(frame_min + frame) - for bone in pose_bones: + for pose_bone in pose_bones: key = Psa.Key() - pose_bone_matrix = bone.matrix + pose_bone_matrix = pose_bone.matrix - if bone.parent is not None: - pose_bone_parent_matrix = bone.parent.matrix + if pose_bone.parent is not None: + pose_bone_parent_matrix = pose_bone.parent.matrix pose_bone_matrix = pose_bone_parent_matrix.inverted() @ pose_bone_matrix location = pose_bone_matrix.to_translation() rotation = pose_bone_matrix.to_quaternion().normalized() - if bone.parent is not None: + if pose_bone.parent is not None: rotation.x = -rotation.x rotation.y = -rotation.y rotation.z = -rotation.z diff --git a/io_export_psk_psa/psa/exporter.py b/io_export_psk_psa/psa/exporter.py index 4bab2d7..587a586 100644 --- a/io_export_psk_psa/psa/exporter.py +++ b/io_export_psk_psa/psa/exporter.py @@ -1,6 +1,6 @@ import bpy -from bpy.types import Operator, PropertyGroup, Action, UIList -from bpy.props import CollectionProperty, IntProperty, PointerProperty, StringProperty, BoolProperty +from bpy.types import Operator, PropertyGroup, Action, UIList, BoneGroup +from bpy.props import CollectionProperty, IntProperty, PointerProperty, StringProperty, BoolProperty, EnumProperty from bpy_extras.io_utils import ExportHelper from typing import Type from .builder import PsaBuilder, PsaBuilderOptions @@ -43,11 +43,28 @@ class PsaExportActionListItem(PropertyGroup): return self.action.name -class PsaExportPropertyGroup(bpy.types.PropertyGroup): +class PsaExportBoneGroupListItem(PropertyGroup): + name: StringProperty() + index: IntProperty() + is_selected: BoolProperty(default=False) + + @property + def name(self): + return self.bone_group.name + + +class PsaExportPropertyGroup(PropertyGroup): action_list: CollectionProperty(type=PsaExportActionListItem) - import_action_list: CollectionProperty(type=PsaExportActionListItem) - action_list_index: IntProperty(name='index for list??', default=0) - import_action_list_index: IntProperty(name='index for list??', default=0) + action_list_index: IntProperty(default=0) + bone_filter_mode: EnumProperty( + name='Bone Filter', + items={ + ('NONE', 'None', 'All bones will be exported.'), + ('BONE_GROUPS', 'Bone Groups', 'Only bones belonging to the selected bone groups will be exported.'), + } + ) + bone_group_list: CollectionProperty(type=PsaExportBoneGroupListItem) + bone_group_list_index: IntProperty(default=0) class PsaExportOperator(Operator, ExportHelper): @@ -68,14 +85,29 @@ class PsaExportOperator(Operator, ExportHelper): def draw(self, context): layout = self.layout scene = context.scene + box = layout.box() box.label(text='Actions', icon='ACTION') + row = box.row() row.template_list('PSA_UL_ExportActionList', 'asd', scene.psa_export, 'action_list', scene.psa_export, 'action_list_index', rows=10) + row = box.row() row.operator('psa_export.actions_select_all', text='Select All') row.operator('psa_export.actions_deselect_all', text='Deselect All') + box = layout.box() + box.label(text='Bone Filter', icon='FILTER') + + row = box.row() + row.alignment = 'EXPAND' + row.prop(scene.psa_export, 'bone_filter_mode', expand=True, text='Bone Filter') + + if scene.psa_export.bone_filter_mode == 'BONE_GROUPS': + row = box.row() + rows = max(3, min(len(scene.psa_export.bone_group_list), 10)) + row.template_list('PSA_UL_ExportBoneGroupList', 'asd', scene.psa_export, 'bone_group_list', scene.psa_export, 'bone_group_list_index', rows=rows) + def is_action_for_armature(self, action): if len(action.fcurves) == 0: return False @@ -90,12 +122,17 @@ class PsaExportOperator(Operator, ExportHelper): return False def invoke(self, context, event): + if context.view_layer.objects.active is None: + self.report({'ERROR_INVALID_CONTEXT'}, 'An armature must be selected') + return {'CANCELLED'} + if context.view_layer.objects.active.type != 'ARMATURE': self.report({'ERROR_INVALID_CONTEXT'}, 'The selected object must be an armature.') return {'CANCELLED'} self.armature = context.view_layer.objects.active + # Populate actions list. context.scene.psa_export.action_list.clear() for action in bpy.data.actions: item = context.scene.psa_export.action_list.add() @@ -105,10 +142,20 @@ class PsaExportOperator(Operator, ExportHelper): item.is_selected = True if len(context.scene.psa_export.action_list) == 0: + # If there are no actions at all, we have nothing to export, so just cancel the operation. self.report({'ERROR_INVALID_CONTEXT'}, 'There are no actions to export.') return {'CANCELLED'} + # Populate bone groups list. + context.scene.psa_export.bone_group_list.clear() + for bone_group_index, bone_group in enumerate(self.armature.pose.bone_groups): + item = context.scene.psa_export.bone_group_list.add() + item.name = bone_group.name + item.index = bone_group_index + item.is_selected = False + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} def execute(self, context): @@ -120,6 +167,8 @@ class PsaExportOperator(Operator, ExportHelper): options = PsaBuilderOptions() options.actions = actions + options.bone_filter_mode = context.scene.psa_export.bone_filter_mode + options.bone_group_indices = [x.index for x in context.scene.psa_export.bone_group_list if x.is_selected] builder = PsaBuilder() psa = builder.build(context, options) exporter = PsaExporter(psa) @@ -127,6 +176,13 @@ class PsaExportOperator(Operator, ExportHelper): return {'FINISHED'} +class PSA_UL_ExportBoneGroupList(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + layout.alignment = 'LEFT' + layout.prop(item, 'is_selected', icon_only=True) + layout.label(text=item.name, icon='GROUP_BONE') + + class PSA_UL_ExportActionList(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): layout.alignment = 'LEFT' @@ -134,7 +190,6 @@ class PSA_UL_ExportActionList(UIList): layout.label(text=item.action_name) def filter_items(self, context, data, property): - # TODO: returns two lists, apparently actions = getattr(data, property) flt_flags = [] flt_neworder = [] @@ -153,6 +208,12 @@ class PsaExportSelectAll(bpy.types.Operator): bl_idname = 'psa_export.actions_select_all' bl_label = 'Select All' + @classmethod + def poll(cls, context): + action_list = context.scene.psa_export.action_list + has_unselected_actions = any(map(lambda action: not action.is_selected, action_list)) + return len(action_list) > 0 and has_unselected_actions + def execute(self, context): for action in context.scene.psa_export.action_list: action.is_selected = True @@ -163,6 +224,12 @@ class PsaExportDeselectAll(bpy.types.Operator): bl_idname = 'psa_export.actions_deselect_all' bl_label = 'Deselect All' + @classmethod + def poll(cls, context): + action_list = context.scene.psa_export.action_list + has_selected_actions = any(map(lambda action: action.is_selected, action_list)) + return len(action_list) > 0 and has_selected_actions + def execute(self, context): for action in context.scene.psa_export.action_list: action.is_selected = False diff --git a/io_export_psk_psa/psa/importer.py b/io_export_psk_psa/psa/importer.py index a3593ca..7848fa8 100644 --- a/io_export_psk_psa/psa/importer.py +++ b/io_export_psk_psa/psa/importer.py @@ -4,10 +4,11 @@ from mathutils import Vector, Quaternion, Matrix from .data import Psa from typing import List, AnyStr, Optional import bpy -from bpy.types import Operator, Action, UIList, PropertyGroup, Panel, Armature +from bpy.types import Operator, Action, UIList, PropertyGroup, Panel, Armature, FileSelectParams from bpy_extras.io_utils import ExportHelper, ImportHelper from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty from .reader import PsaReader +import numpy as np class PsaImporter(object): @@ -30,14 +31,7 @@ class PsaImporter(object): self.orig_loc: Vector = Vector() self.orig_quat: Quaternion = Quaternion() self.post_quat: Quaternion = Quaternion() - # TODO: this is UGLY, come up with a way to just map indices for these - self.fcurve_quat_w = None - self.fcurve_quat_x = None - self.fcurve_quat_y = None - self.fcurve_quat_z = None - self.fcurve_location_x = None - self.fcurve_location_y = None - self.fcurve_location_z = None + self.fcurves = [] # create an index mapping from bones in the PSA to bones in the target armature. psa_to_armature_bone_indices = {} @@ -103,90 +97,105 @@ class PsaImporter(object): import_bone = import_bones[psa_bone_index] pose_bone = import_bone.pose_bone - # rotation + # create fcurves from rotation and location data rotation_data_path = pose_bone.path_from_id('rotation_quaternion') - import_bone.fcurve_quat_w = action.fcurves.new(rotation_data_path, index=0) - import_bone.fcurve_quat_x = action.fcurves.new(rotation_data_path, index=1) - import_bone.fcurve_quat_y = action.fcurves.new(rotation_data_path, index=2) - import_bone.fcurve_quat_z = action.fcurves.new(rotation_data_path, index=3) - - # location location_data_path = pose_bone.path_from_id('location') - import_bone.fcurve_location_x = action.fcurves.new(location_data_path, index=0) - import_bone.fcurve_location_y = action.fcurves.new(location_data_path, index=1) - import_bone.fcurve_location_z = action.fcurves.new(location_data_path, index=2) + import_bone.fcurves.extend([ + action.fcurves.new(rotation_data_path, index=0), # Qw + action.fcurves.new(rotation_data_path, index=1), # Qx + action.fcurves.new(rotation_data_path, index=2), # Qy + action.fcurves.new(rotation_data_path, index=3), # Qz + action.fcurves.new(location_data_path, index=0), # Lx + action.fcurves.new(location_data_path, index=1), # Ly + action.fcurves.new(location_data_path, index=2), # Lz + ]) - # add keyframes - import_bone.fcurve_quat_w.keyframe_points.add(sequence.frame_count) - import_bone.fcurve_quat_x.keyframe_points.add(sequence.frame_count) - import_bone.fcurve_quat_y.keyframe_points.add(sequence.frame_count) - import_bone.fcurve_quat_z.keyframe_points.add(sequence.frame_count) - import_bone.fcurve_location_x.keyframe_points.add(sequence.frame_count) - import_bone.fcurve_location_y.keyframe_points.add(sequence.frame_count) - import_bone.fcurve_location_z.keyframe_points.add(sequence.frame_count) - - should_invert_root = False key_index = 0 + + # Read the sequence keys from the PSA file. sequence_name = sequence.name.decode('windows-1252') - sequence_keys = psa_reader.get_sequence_keys(sequence_name) + sequence_keys = psa_reader.read_sequence_keys(sequence_name) for frame_index in range(sequence.frame_count): - for import_bone in import_bones: + for bone_index, import_bone in enumerate(import_bones): if import_bone is None: # bone does not exist in the armature, skip it key_index += 1 continue - key_location = Vector(tuple(sequence_keys[key_index].location)) + # Convert world-space transforms to local-space transforms. key_rotation = Quaternion(tuple(sequence_keys[key_index].rotation)) - q = import_bone.post_quat.copy() q.rotate(import_bone.orig_quat) quat = q q = import_bone.post_quat.copy() - if import_bone.parent is None and not should_invert_root: + if import_bone.parent is None: q.rotate(key_rotation.conjugated()) else: q.rotate(key_rotation) quat.rotate(q.conjugated()) + key_location = Vector(tuple(sequence_keys[key_index].location)) loc = key_location - import_bone.orig_loc loc.rotate(import_bone.post_quat.conjugated()) - import_bone.fcurve_quat_w.keyframe_points[frame_index].co = frame_index, quat.w - import_bone.fcurve_quat_x.keyframe_points[frame_index].co = frame_index, quat.x - import_bone.fcurve_quat_y.keyframe_points[frame_index].co = frame_index, quat.y - import_bone.fcurve_quat_z.keyframe_points[frame_index].co = frame_index, quat.z - import_bone.fcurve_location_x.keyframe_points[frame_index].co = frame_index, loc.x - import_bone.fcurve_location_y.keyframe_points[frame_index].co = frame_index, loc.y - import_bone.fcurve_location_z.keyframe_points[frame_index].co = frame_index, loc.z + bone_fcurve_data = quat.w, quat.x, quat.y, quat.z, loc.x, loc.y, loc.z + for fcurve, datum in zip(import_bone.fcurves, bone_fcurve_data): + fcurve.keyframe_points.insert(frame_index, datum) key_index += 1 class PsaImportActionListItem(PropertyGroup): action_name: StringProperty() - is_selected: BoolProperty(default=True) + frame_count: IntProperty() + is_selected: BoolProperty(default=False) @property def name(self): return self.action_name +def on_psa_filepath_updated(property, context): + context.scene.psa_import.action_list.clear() + try: + # Read the file and populate the action list. + psa = PsaReader(context.scene.psa_import.psa_filepath).psa + for sequence in psa.sequences.values(): + item = context.scene.psa_import.action_list.add() + item.action_name = sequence.name.decode('windows-1252') + item.frame_count = sequence.frame_count + item.is_selected = True + except IOError: + # TODO: set an error somewhere so the user knows the PSA could not be read. + pass + + class PsaImportPropertyGroup(bpy.types.PropertyGroup): - cool_filepath: StringProperty(default='') - armature_object: PointerProperty(type=bpy.types.Object) # TODO: figure out how to filter this to only objects of a specific type + psa_filepath: StringProperty(default='', subtype='FILE_PATH', update=on_psa_filepath_updated) + armature_object: PointerProperty(name='Armature', type=bpy.types.Object) action_list: CollectionProperty(type=PsaImportActionListItem) - import_action_list: CollectionProperty(type=PsaImportActionListItem) - action_list_index: IntProperty(name='index for list??', default=0) - import_action_list_index: IntProperty(name='index for list??', default=0) + action_list_index: IntProperty(name='', default=0) + action_filter_name: StringProperty(default='') class PSA_UL_ImportActionList(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - layout.alignment = 'LEFT' - layout.prop(item, 'is_selected', icon_only=True) - layout.label(text=item.action_name) + row = layout.row(align=True) + split = row.split(align=True, factor=0.75) + action_col = split.row(align=True) + action_col.alignment = 'LEFT' + action_col.prop(item, 'is_selected', icon_only=True) + action_col.label(text=item.action_name) + + def draw_filter(self, context, layout): + row = layout.row() + subrow = row.row(align=True) + subrow.prop(self, 'filter_name', text="") + subrow.prop(self, 'use_filter_invert', text="", icon='ARROW_LEFTRIGHT') + subrow = row.row(align=True) + subrow.prop(self, 'use_filter_sort_reverse', text='', icon='SORT_ASC') def filter_items(self, context, data, property): actions = getattr(data, property) @@ -205,7 +214,13 @@ class PSA_UL_ImportActionList(UIList): class PsaImportSelectAll(bpy.types.Operator): bl_idname = 'psa_import.actions_select_all' - bl_label = 'Select All' + bl_label = 'All' + + @classmethod + def poll(cls, context): + action_list = context.scene.psa_import.action_list + has_unselected_actions = any(map(lambda action: not action.is_selected, action_list)) + return len(action_list) > 0 and has_unselected_actions def execute(self, context): for action in context.scene.psa_import.action_list: @@ -215,7 +230,13 @@ class PsaImportSelectAll(bpy.types.Operator): class PsaImportDeselectAll(bpy.types.Operator): bl_idname = 'psa_import.actions_deselect_all' - bl_label = 'Deselect All' + bl_label = 'None' + + @classmethod + def poll(cls, context): + action_list = context.scene.psa_import.action_list + has_selected_actions = any(map(lambda action: action.is_selected, action_list)) + return len(action_list) > 0 and has_selected_actions def execute(self, context): for action in context.scene.psa_import.action_list: @@ -234,25 +255,34 @@ class PSA_PT_ImportPanel(Panel): layout = self.layout scene = context.scene row = layout.row() - row.operator('psa_import.file_select', icon='FILE_FOLDER', text='') - row.label(text=scene.psa_import.cool_filepath) + row.prop(scene.psa_import, 'psa_filepath', text='PSA File') + row = layout.row() + row.prop_search(scene.psa_import, 'armature_object', bpy.data, 'objects') box = layout.box() - box.label(text='Actions', icon='ACTION') + box.label(text=f'Actions ({len(scene.psa_import.action_list)})', icon='ACTION') row = box.row() - row.template_list('PSA_UL_ImportActionList', 'asd', scene.psa_import, 'action_list', scene.psa_import, 'action_list_index', rows=10) - row = box.row() - row.operator('psa_import.actions_select_all', text='Select All') - row.operator('psa_import.actions_deselect_all', text='Deselect All') - layout.prop(scene.psa_import, 'armature_object', icon_only=True) - layout.operator('psa_import.import', text='Import') + rows = max(3, min(len(scene.psa_import.action_list), 10)) + row.template_list('PSA_UL_ImportActionList', 'asd', scene.psa_import, 'action_list', scene.psa_import, 'action_list_index', rows=rows) + row = box.row(align=True) + row.label(text='Select') + row.operator('psa_import.actions_select_all', text='All') + row.operator('psa_import.actions_deselect_all', text='None') + layout.operator('psa_import.import', text=f'Import') class PsaImportOperator(Operator): bl_idname = 'psa_import.import' bl_label = 'Import' + @classmethod + def poll(cls, context): + action_list = context.scene.psa_import.action_list + has_selected_actions = any(map(lambda action: action.is_selected, action_list)) + armature_object = context.scene.psa_import.armature_object + return has_selected_actions and armature_object is not None + def execute(self, context): - psa_reader = PsaReader(context.scene.psa_import.cool_filepath) + psa_reader = PsaReader(context.scene.psa_import.psa_filepath) sequence_names = [x.action_name for x in context.scene.psa_import.action_list if x.is_selected] PsaImporter().import_psa(psa_reader, sequence_names, context) return {'FINISHED'} @@ -274,16 +304,6 @@ class PsaImportFileSelectOperator(Operator, ImportHelper): return {'RUNNING_MODAL'} def execute(self, context): - context.scene.psa_import.cool_filepath = self.filepath + context.scene.psa_import.psa_filepath = self.filepath # Load the sequence names from the selected file - sequence_names = [] - try: - sequence_names = PsaReader.scan_sequence_names(self.filepath) - except IOError: - pass - context.scene.psa_import.action_list.clear() - for sequence_name in sequence_names: - item = context.scene.psa_import.action_list.add() - item.action_name = sequence_name.decode('windows-1252') - item.is_selected = True return {'FINISHED'} diff --git a/io_export_psk_psa/psa/reader.py b/io_export_psk_psa/psa/reader.py index f127ccd..2f01c19 100644 --- a/io_export_psk_psa/psa/reader.py +++ b/io_export_psk_psa/psa/reader.py @@ -34,19 +34,13 @@ class PsaReader(object): fp.seek(section.data_size * section.data_count, 1) return [] - def get_sequence_keys(self, sequence_name) -> List[Psa.Key]: + def read_sequence_keys(self, sequence_name) -> List[Psa.Key]: # Set the file reader to the beginning of the keys data sequence = self.psa.sequences[sequence_name] data_size = sizeof(Psa.Key) bone_count = len(self.psa.bones) buffer_length = data_size * bone_count * sequence.frame_count - print(f'data_size: {data_size}') - print(f'buffer_length: {buffer_length}') - print(f'bone_count: {bone_count}') - print(f'sequence.frame_count: {sequence.frame_count}') - print(f'self.keys_data_offset: {self.keys_data_offset}') sequence_keys_offset = self.keys_data_offset + (sequence.frame_start_index * bone_count * data_size) - print(f'sequence_keys_offset: {sequence_keys_offset}') self.fp.seek(sequence_keys_offset, 0) buffer = self.fp.read(buffer_length) offset = 0 diff --git a/io_export_psk_psa/psk/builder.py b/io_export_psk_psa/psk/builder.py index 2cc82fb..c5d92a0 100644 --- a/io_export_psk_psa/psk/builder.py +++ b/io_export_psk_psa/psk/builder.py @@ -40,7 +40,7 @@ class PskBuilder(object): modifiers = [x for x in obj.modifiers if x.type == 'ARMATURE'] if len(modifiers) == 0: continue - elif len(modifiers) == 2: + elif len(modifiers) > 1: raise RuntimeError(f'Mesh "{obj.name}" must have only one armature modifier') armature_modifier_objects.add(modifiers[0].object) diff --git a/io_export_psk_psa/psk/importer.py b/io_export_psk_psa/psk/importer.py index f87dd31..1de4c7c 100644 --- a/io_export_psk_psa/psk/importer.py +++ b/io_export_psk_psa/psk/importer.py @@ -48,7 +48,6 @@ class PskImporter(object): self.post_quat: Quaternion = Quaternion() import_bones = [] - should_invert_root = False new_bone_size = 8.0 for bone_index, psk_bone in enumerate(psk.bones): @@ -57,10 +56,7 @@ class PskImporter(object): import_bone.local_rotation = Quaternion(tuple(psk_bone.rotation)) import_bone.local_translation = Vector(tuple(psk_bone.location)) if psk_bone.parent_index == 0 and bone_index == 0: - if should_invert_root: - import_bone.world_rotation_matrix = import_bone.local_rotation.conjugated().to_matrix() - else: - import_bone.world_rotation_matrix = import_bone.local_rotation.to_matrix() + import_bone.world_rotation_matrix = import_bone.local_rotation.to_matrix() import_bone.world_matrix = Matrix.Translation(import_bone.local_translation) import_bones.append(import_bone) @@ -82,7 +78,7 @@ class PskImporter(object): if import_bone.parent is not None: edit_bone.parent = armature_data.edit_bones[import_bone.psk_bone.parent_index] - elif not should_invert_root: + else: import_bone.local_rotation.conjugate() edit_bone.tail = Vector((0.0, new_bone_size, 0.0)) @@ -126,7 +122,8 @@ class PskImporter(object): degenerate_face_indices.add(face_index) pass - print(f'WARNING: Discarded {len(degenerate_face_indices)} degenerate face(s).') + if len(degenerate_face_indices) > 0: + print(f'WARNING: Discarded {len(degenerate_face_indices)} degenerate face(s).') bm.to_mesh(mesh_data)