Added key compression functionality
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user