diff --git a/io_scene_psk_psa/__init__.py b/io_scene_psk_psa/__init__.py index 05c6b55..96eaaf0 100644 --- a/io_scene_psk_psa/__init__.py +++ b/io_scene_psk_psa/__init__.py @@ -103,12 +103,14 @@ def register(): bpy.types.Scene.psa_import = PointerProperty(type=psa_importer.PsaImportPropertyGroup) bpy.types.Scene.psa_export = PointerProperty(type=psa_exporter.PsaExportPropertyGroup) bpy.types.Scene.psk_export = PointerProperty(type=psk_exporter.PskExportPropertyGroup) + bpy.types.Action.psa_export = PointerProperty(type=psx_types.PSX_PG_ActionExportPropertyGroup) def unregister(): del bpy.types.Scene.psa_import del bpy.types.Scene.psa_export del bpy.types.Scene.psk_export + del bpy.types.Action.psa_export bpy.types.TOPBAR_MT_file_export.remove(psk_export_menu_func) bpy.types.TOPBAR_MT_file_import.remove(psk_import_menu_func) bpy.types.TOPBAR_MT_file_export.remove(psa_export_menu_func) diff --git a/io_scene_psk_psa/psa/builder.py b/io_scene_psk_psa/psa/builder.py index 2f33675..41fe190 100644 --- a/io_scene_psk_psa/psa/builder.py +++ b/io_scene_psk_psa/psa/builder.py @@ -1,6 +1,6 @@ from typing import Optional -from bpy.types import Armature, Bone, Action +from bpy.types import Armature, Bone, Action, PoseBone from .data import * from ..helpers import * @@ -16,6 +16,8 @@ class PsaExportSequence: def __init__(self): self.name: str = '' self.nla_state: PsaExportSequence.NlaState = PsaExportSequence.NlaState() + self.compression_ratio: float = 1.0 + self.key_quota: int = 0 self.fps: float = 30.0 @@ -31,6 +33,28 @@ class PsaBuildOptions: self.root_motion: bool = False +def get_pose_bone_location_and_rotation(pose_bone: PoseBone, armature_object: Object, options: PsaBuildOptions): + if pose_bone.parent is not None: + pose_bone_matrix = pose_bone.matrix + pose_bone_parent_matrix = pose_bone.parent.matrix + pose_bone_matrix = pose_bone_parent_matrix.inverted() @ pose_bone_matrix + else: + if options.root_motion: + # 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 + else: + # Use the bind pose matrix for the root bone. + pose_bone_matrix = pose_bone.matrix + + location = pose_bone_matrix.to_translation() + rotation = pose_bone_matrix.to_quaternion().normalized() + + if pose_bone.parent is not None: + rotation.conjugate() + + return location, rotation + + def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa: active_object = context.view_layer.objects.active @@ -121,42 +145,40 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa: frame_start = export_sequence.nla_state.frame_start frame_end = export_sequence.nla_state.frame_end - frame_count = abs(frame_end - frame_start) + 1 - frame_step = 1 if frame_start < frame_end else -1 + + # Calculate the frame step based on the compression factor. + frame_extents = abs(frame_end - frame_start) + frame_count_raw = frame_extents + 1 + frame_count = max(export_sequence.key_quota, int(frame_count_raw * export_sequence.compression_ratio)) + + try: + frame_step = frame_extents / (frame_count - 1) + except ZeroDivisionError: + frame_step = 0.0 + + sequence_duration = frame_count_raw / export_sequence.fps + + # If this is a reverse sequence, we need to reverse the frame step. + if frame_start > frame_end: + frame_step = -frame_step psa_sequence = Psa.Sequence() psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252') psa_sequence.frame_count = frame_count psa_sequence.frame_start_index = frame_start_index - psa_sequence.fps = export_sequence.fps + psa_sequence.fps = frame_count / sequence_duration + psa_sequence.bone_count = len(pose_bones) + psa_sequence.track_time = frame_count + + frame = float(frame_start) - frame = frame_start for _ in range(frame_count): - context.scene.frame_set(frame) - - frame += frame_step + context.scene.frame_set(frame=int(frame), subframe=frame % 1.0) for pose_bone in pose_bones: + location, rotation = get_pose_bone_location_and_rotation(pose_bone, armature_object, options) + key = Psa.Key() - - if pose_bone.parent is not None: - pose_bone_matrix = pose_bone.matrix - pose_bone_parent_matrix = pose_bone.parent.matrix - pose_bone_matrix = pose_bone_parent_matrix.inverted() @ pose_bone_matrix - else: - if options.root_motion: - # 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 - else: - # Use the bind pose matrix for the root bone. - pose_bone_matrix = armature_data.bones[pose_bone.name].matrix_local - - location = pose_bone_matrix.to_translation() - rotation = pose_bone_matrix.to_quaternion().normalized() - - if pose_bone.parent is not None: - rotation.conjugate() - key.location.x = location.x key.location.y = location.y key.location.z = location.z @@ -165,11 +187,9 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa: key.rotation.z = rotation.z key.rotation.w = rotation.w key.time = 1.0 / psa_sequence.fps - psa.keys.append(key) - psa_sequence.bone_count = len(pose_bones) - psa_sequence.track_time = frame_count + frame += frame_step frame_start_index += frame_count diff --git a/io_scene_psk_psa/psa/exporter.py b/io_scene_psk_psa/psa/exporter.py index fa867a9..f114515 100644 --- a/io_scene_psk_psa/psa/exporter.py +++ b/io_scene_psk_psa/psa/exporter.py @@ -500,6 +500,8 @@ class PsaExportOperator(Operator, ExportHelper): export_sequence.nla_state.frame_start = action.frame_start export_sequence.nla_state.frame_end = action.frame_end export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [action.action]) + export_sequence.compression_ratio = action.action.psa_export.compression_ratio + export_sequence.key_quota = action.action.psa_export.key_quota export_sequences.append(export_sequence) elif pg.sequence_source == 'TIMELINE_MARKERS': for marker in pg.marker_list: diff --git a/io_scene_psk_psa/types.py b/io_scene_psk_psa/types.py index c7e1cbe..88fde62 100644 --- a/io_scene_psk_psa/types.py +++ b/io_scene_psk_psa/types.py @@ -1,6 +1,6 @@ import bpy.props -from bpy.props import StringProperty, IntProperty, BoolProperty -from bpy.types import PropertyGroup, UIList, UILayout, Context, AnyType, Operator +from bpy.props import StringProperty, IntProperty, BoolProperty, FloatProperty +from bpy.types import PropertyGroup, UIList, UILayout, Context, AnyType, Operator, Panel class PSX_UL_BoneGroupList(UIList): @@ -69,10 +69,36 @@ class BoneGroupListItem(PropertyGroup): is_selected: BoolProperty(default=False) +class PSX_PG_ActionExportPropertyGroup(PropertyGroup): + compression_ratio: FloatProperty(name='Compression Ratio', default=1.0, min=0.0, max=1.0, subtype='FACTOR', description='The ratio of frames to be exported.\n\nA compression ratio of 1.0 will export all frames, while a compression ratio of 0.5 will export half of the frames') + key_quota: IntProperty(name='Key Quota', default=0, min=1, description='The minimum number of frames to be exported') + + +class PSX_PT_ActionPropertyPanel(Panel): + bl_idname = 'PSX_PT_ActionPropertyPanel' + bl_label = 'PSA Export' + bl_space_type = 'DOPESHEET_EDITOR' + bl_region_type = 'UI' + bl_context = 'action' + bl_category = 'Action' + + @classmethod + def poll(cls, context: 'Context'): + return context.active_object and context.active_object.type == 'ARMATURE' and context.active_action is not None + + def draw(self, context: 'Context'): + action = context.active_action + layout = self.layout + layout.prop(action.psa_export, 'compression_ratio') + layout.prop(action.psa_export, 'key_quota') + + classes = ( + PSX_PG_ActionExportPropertyGroup, BoneGroupListItem, PSX_UL_BoneGroupList, PSX_UL_MaterialPathList, PSX_OT_MaterialPathAdd, - PSX_OT_MaterialPathRemove + PSX_OT_MaterialPathRemove, + PSX_PT_ActionPropertyPanel )