From 57a2179412b234b2a576cae05e73c7e5eed95a44 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Sun, 23 Jan 2022 18:15:07 -0800 Subject: [PATCH] * Fixed a bug where the keyframe cleaning process could write incorrect data to the resultant keyframes in some cases. * Added new options to the PSA Import pane: "Clean Keyframes", "Fake User" and "Stash". --- io_scene_psk_psa/psa/exporter.py | 2 + io_scene_psk_psa/psa/importer.py | 105 ++++++++++++++++++++----------- 2 files changed, 69 insertions(+), 38 deletions(-) diff --git a/io_scene_psk_psa/psa/exporter.py b/io_scene_psk_psa/psa/exporter.py index 3754b47..6349d2e 100644 --- a/io_scene_psk_psa/psa/exporter.py +++ b/io_scene_psk_psa/psa/exporter.py @@ -102,6 +102,7 @@ class PsaExportOperator(Operator, ExportHelper): box.label(text='Bones', icon='BONE_DATA') bone_filter_mode_items = property_group.bl_rna.properties['bone_filter_mode'].enum_items_static row = box.row(align=True) + for item in bone_filter_mode_items: identifier = item.identifier item_layout = row.row(align=True) @@ -114,6 +115,7 @@ class PsaExportOperator(Operator, ExportHelper): rows = max(3, min(len(property_group.bone_group_list), 10)) row.template_list('PSX_UL_BoneGroupList', '', property_group, 'bone_group_list', property_group, 'bone_group_list_index', rows=rows) + def is_action_for_armature(self, action): if len(action.fcurves) == 0: return False diff --git a/io_scene_psk_psa/psa/importer.py b/io_scene_psk_psa/psa/importer.py index 620e24c..3d890d5 100644 --- a/io_scene_psk_psa/psa/importer.py +++ b/io_scene_psk_psa/psa/importer.py @@ -10,12 +10,20 @@ from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerP from .reader import PsaReader +class PsaImportOptions(object): + def __init__(self): + self.should_clean_keys = True + self.should_use_fake_user = False + self.should_stash = False + self.sequence_names = [] + + class PsaImporter(object): def __init__(self): pass - def import_psa(self, psa_reader: PsaReader, sequence_names: List[AnyStr], armature_object): - sequences = map(lambda x: psa_reader.sequences[x], sequence_names) + def import_psa(self, psa_reader: PsaReader, armature_object, options: PsaImportOptions): + sequences = map(lambda x: psa_reader.sequences[x], options.sequence_names) armature_data = armature_object.data class ImportBone(object): @@ -104,9 +112,11 @@ class PsaImporter(object): import_bone.post_quat = import_bone.orig_quat.conjugated() # Create and populate the data for new sequences. + actions = [] for sequence in sequences: # Add the action. action = bpy.data.actions.new(name=sequence.name.decode()) + action.use_fake_user = options.should_use_fake_user # Create f-curves for the rotation and location of each bone. for psa_bone_index, armature_bone_index in psa_to_armature_bone_indices.items(): @@ -124,28 +134,39 @@ class PsaImporter(object): action.fcurves.new(location_data_path, index=2), # Lz ] - # Read the sequence keys from the PSA file. sequence_name = sequence.name.decode('windows-1252') # Read the sequence data matrix from the PSA. sequence_data_matrix = psa_reader.read_sequence_data_matrix(sequence_name) keyframe_write_matrix = np.ones(sequence_data_matrix.shape, dtype=np.int8) - # The first step is to determine the frames at which each bone will write out a keyframe. - threshold = 0.001 + # Convert the sequence's data from world-space to local-space. for bone_index, import_bone in enumerate(import_bones): if import_bone is None: continue - for fcurve_index, fcurve in enumerate(import_bone.fcurves): - # Get all the keyframe data for the bone's f-curve data from the sequence data matrix. - fcurve_frame_data = sequence_data_matrix[:, bone_index, fcurve_index] - last_written_datum = 0 - for frame_index, datum in enumerate(fcurve_frame_data): - # If the f-curve data is not different enough to the last written frame, un-mark this data for writing. - if frame_index > 0 and abs(datum - last_written_datum) < threshold: - keyframe_write_matrix[frame_index, bone_index, fcurve_index] = 0 - else: - last_written_datum = fcurve_frame_data[frame_index] + for frame_index in range(sequence.frame_count): + # This bone has writeable keyframes for this frame. + key_data = sequence_data_matrix[frame_index, bone_index] + # Calculate the local-space key data for the bone. + sequence_data_matrix[frame_index, bone_index] = calculate_fcurve_data(import_bone, key_data) + + # Clean the keyframe data. This is accomplished by writing zeroes to the write matrix when there is an + # insufficiently large change in the data from frame-to-frame. + if options.should_clean_keys: + threshold = 0.001 + for bone_index, import_bone in enumerate(import_bones): + if import_bone is None: + continue + for fcurve_index in range(len(import_bone.fcurves)): + # Get all the keyframe data for the bone's f-curve data from the sequence data matrix. + fcurve_frame_data = sequence_data_matrix[:, bone_index, fcurve_index] + last_written_datum = 0 + for frame_index, datum in enumerate(fcurve_frame_data): + # If the f-curve data is not different enough to the last written frame, un-mark this data for writing. + if frame_index > 0 and abs(datum - last_written_datum) < threshold: + keyframe_write_matrix[frame_index, bone_index, fcurve_index] = 0 + else: + last_written_datum = datum # Write the keyframes out! for frame_index in range(sequence.frame_count): @@ -156,12 +177,22 @@ class PsaImporter(object): if bone_has_writeable_keyframes: # This bone has writeable keyframes for this frame. key_data = sequence_data_matrix[frame_index, bone_index] - # Calculate the local-space key data for the bone. - fcurve_data = calculate_fcurve_data(import_bone, key_data) - for fcurve, should_write, datum in zip(import_bone.fcurves, keyframe_write_matrix[frame_index, bone_index], fcurve_data): + for fcurve, should_write, datum in zip(import_bone.fcurves, keyframe_write_matrix[frame_index, bone_index], key_data): if should_write: fcurve.keyframe_points.insert(frame_index, datum, options={'FAST'}) + actions.append(action) + + # If the user specifies, store the new animations as strips on a non-contributing NLA stack. + if options.should_stash: + if armature_object.animation_data is None: + armature_object.animation_data_create() + for action in actions: + nla_track = armature_object.animation_data.nla_tracks.new() + nla_track.name = action.name + nla_track.mute = True + nla_track.strips.new(name=action.name, start=0, action=action) + class PsaImportPsaBoneItem(PropertyGroup): bone_name: StringProperty() @@ -182,7 +213,6 @@ class PsaImportActionListItem(PropertyGroup): def on_psa_file_path_updated(property, context): - print('PATH UPDATED') property_group = context.scene.psa_import property_group.action_list.clear() property_group.psa_bones.clear() @@ -195,33 +225,23 @@ def on_psa_file_path_updated(property, context): item.action_name = sequence.name.decode('windows-1252') item.frame_count = sequence.frame_count item.is_selected = True - for psa_bone in psa_reader.bones: item = property_group.psa_bones.add() item.bone_name = psa_bone.name except IOError as e: - print('ERROR READING FILE') - print(e) # TODO: set an error somewhere so the user knows the PSA could not be read. pass -def on_armature_object_updated(property, context): - # TODO: ensure that there are matching bones between the two rigs. - property_group = context.scene.psa_import - armature_object = property_group.armature_object - if armature_object is not None: - armature_bone_names = set(map(lambda bone: bone.name, armature_object.data.bones)) - psa_bone_names = set(map(lambda psa_bone: psa_bone.name, property_group.psa_bones)) - - class PsaImportPropertyGroup(bpy.types.PropertyGroup): psa_file_path: StringProperty(default='', update=on_psa_file_path_updated, name='PSA File Path') psa_bones: CollectionProperty(type=PsaImportPsaBoneItem) - # armature_object: PointerProperty(name='Object', type=bpy.types.Object, update=on_armature_object_updated) action_list: CollectionProperty(type=PsaImportActionListItem) action_list_index: IntProperty(name='', default=0) action_filter_name: StringProperty(default='') + should_clean_keys: BoolProperty(default=True, name='Clean Keyframes', description='Exclude unnecessary keyframes from being written to the actions.') + should_use_fake_user: BoolProperty(default=True, name='Fake User', description='Assign each imported action a fake user so that the data block is saved even it has no users.') + should_stash: BoolProperty(default=False, name='Stash', description='Stash each imported action as a strip on a new non-contributing NLA track') class PSA_UL_ImportActionList(UIList): @@ -314,12 +334,10 @@ class PSA_PT_ImportPanel(Panel): row = layout.row() row.prop(property_group, 'psa_file_path', text='') row.enabled = False - # row.enabled = property_group.psa_file_path is not '' + row = layout.row() - - layout.separator() - row.operator('psa_import.select_file', text='Select PSA File', icon='FILEBROWSER') + if len(property_group.action_list) > 0: box = layout.box() box.label(text=f'Actions ({len(property_group.action_list)})', icon='ACTION') @@ -331,7 +349,13 @@ class PSA_PT_ImportPanel(Panel): row.operator('psa_import.actions_select_all', text='All') row.operator('psa_import.actions_deselect_all', text='None') - layout.separator() + row = layout.row() + row.prop(property_group, 'should_clean_keys') + + # DATA + row = layout.row() + row.prop(property_group, 'should_use_fake_user') + row.prop(property_group, 'should_stash') layout.operator('psa_import.import', text=f'Import') @@ -370,7 +394,12 @@ class PsaImportOperator(Operator): property_group = context.scene.psa_import psa_reader = PsaReader(property_group.psa_file_path) sequence_names = [x.action_name for x in property_group.action_list if x.is_selected] - PsaImporter().import_psa(psa_reader, sequence_names, context.view_layer.objects.active) + options = PsaImportOptions() + options.sequence_names = sequence_names + options.should_clean_keys = property_group.should_clean_keys + options.should_use_fake_user = property_group.should_use_fake_user + options.should_stash = property_group.should_stash + PsaImporter().import_psa(psa_reader, context.view_layer.objects.active, options) self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)') return {'FINISHED'}