Added key compression functionality

This commit is contained in:
Colin Basnett
2023-04-30 20:18:59 -07:00
parent 15e20cdefc
commit 5cb9714597
4 changed files with 83 additions and 33 deletions

View File

@@ -103,12 +103,14 @@ def register():
bpy.types.Scene.psa_import = PointerProperty(type=psa_importer.PsaImportPropertyGroup) bpy.types.Scene.psa_import = PointerProperty(type=psa_importer.PsaImportPropertyGroup)
bpy.types.Scene.psa_export = PointerProperty(type=psa_exporter.PsaExportPropertyGroup) bpy.types.Scene.psa_export = PointerProperty(type=psa_exporter.PsaExportPropertyGroup)
bpy.types.Scene.psk_export = PointerProperty(type=psk_exporter.PskExportPropertyGroup) bpy.types.Scene.psk_export = PointerProperty(type=psk_exporter.PskExportPropertyGroup)
bpy.types.Action.psa_export = PointerProperty(type=psx_types.PSX_PG_ActionExportPropertyGroup)
def unregister(): def unregister():
del bpy.types.Scene.psa_import del bpy.types.Scene.psa_import
del bpy.types.Scene.psa_export del bpy.types.Scene.psa_export
del bpy.types.Scene.psk_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_export.remove(psk_export_menu_func)
bpy.types.TOPBAR_MT_file_import.remove(psk_import_menu_func) bpy.types.TOPBAR_MT_file_import.remove(psk_import_menu_func)
bpy.types.TOPBAR_MT_file_export.remove(psa_export_menu_func) bpy.types.TOPBAR_MT_file_export.remove(psa_export_menu_func)

View File

@@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from bpy.types import Armature, Bone, Action from bpy.types import Armature, Bone, Action, PoseBone
from .data import * from .data import *
from ..helpers import * from ..helpers import *
@@ -16,6 +16,8 @@ class PsaExportSequence:
def __init__(self): def __init__(self):
self.name: str = '' self.name: str = ''
self.nla_state: PsaExportSequence.NlaState = PsaExportSequence.NlaState() self.nla_state: PsaExportSequence.NlaState = PsaExportSequence.NlaState()
self.compression_ratio: float = 1.0
self.key_quota: int = 0
self.fps: float = 30.0 self.fps: float = 30.0
@@ -31,6 +33,28 @@ class PsaBuildOptions:
self.root_motion: bool = False 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: def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
active_object = context.view_layer.objects.active 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_start = export_sequence.nla_state.frame_start
frame_end = export_sequence.nla_state.frame_end 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 = Psa.Sequence()
psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252') psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252')
psa_sequence.frame_count = frame_count psa_sequence.frame_count = frame_count
psa_sequence.frame_start_index = frame_start_index 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): for _ in range(frame_count):
context.scene.frame_set(frame) context.scene.frame_set(frame=int(frame), subframe=frame % 1.0)
frame += frame_step
for pose_bone in pose_bones: for pose_bone in pose_bones:
location, rotation = get_pose_bone_location_and_rotation(pose_bone, armature_object, options)
key = Psa.Key() 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.x = location.x
key.location.y = location.y key.location.y = location.y
key.location.z = location.z 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.z = rotation.z
key.rotation.w = rotation.w key.rotation.w = rotation.w
key.time = 1.0 / psa_sequence.fps key.time = 1.0 / psa_sequence.fps
psa.keys.append(key) psa.keys.append(key)
psa_sequence.bone_count = len(pose_bones) frame += frame_step
psa_sequence.track_time = frame_count
frame_start_index += frame_count frame_start_index += frame_count

View File

@@ -500,6 +500,8 @@ class PsaExportOperator(Operator, ExportHelper):
export_sequence.nla_state.frame_start = action.frame_start export_sequence.nla_state.frame_start = action.frame_start
export_sequence.nla_state.frame_end = action.frame_end 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.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) export_sequences.append(export_sequence)
elif pg.sequence_source == 'TIMELINE_MARKERS': elif pg.sequence_source == 'TIMELINE_MARKERS':
for marker in pg.marker_list: for marker in pg.marker_list:

View File

@@ -1,6 +1,6 @@
import bpy.props import bpy.props
from bpy.props import StringProperty, IntProperty, BoolProperty from bpy.props import StringProperty, IntProperty, BoolProperty, FloatProperty
from bpy.types import PropertyGroup, UIList, UILayout, Context, AnyType, Operator from bpy.types import PropertyGroup, UIList, UILayout, Context, AnyType, Operator, Panel
class PSX_UL_BoneGroupList(UIList): class PSX_UL_BoneGroupList(UIList):
@@ -69,10 +69,36 @@ class BoneGroupListItem(PropertyGroup):
is_selected: BoolProperty(default=False) 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 = ( classes = (
PSX_PG_ActionExportPropertyGroup,
BoneGroupListItem, BoneGroupListItem,
PSX_UL_BoneGroupList, PSX_UL_BoneGroupList,
PSX_UL_MaterialPathList, PSX_UL_MaterialPathList,
PSX_OT_MaterialPathAdd, PSX_OT_MaterialPathAdd,
PSX_OT_MaterialPathRemove PSX_OT_MaterialPathRemove,
PSX_PT_ActionPropertyPanel
) )