Added sampling mode and changed the default sampling mode to be "Interpolated" rather than "Subframe"

This commit is contained in:
Colin Basnett
2025-01-05 18:22:03 -08:00
parent 515ee17203
commit 35ac0bf86c
3 changed files with 97 additions and 19 deletions

View File

@@ -1,6 +1,7 @@
from typing import Optional
from bpy.types import Bone, Action, PoseBone
from mathutils import Vector
from .data import *
from ..shared.helpers import *
@@ -33,6 +34,7 @@ class PsaBuildOptions:
self.sequence_name_suffix: str = ''
self.root_motion: bool = False
self.scale = 1.0
self.sampling_mode: str = 'INTERPOLATED' # One of ('INTERPOLATED', 'SUBFRAME')
def _get_pose_bone_location_and_rotation(pose_bone: PoseBone, armature_object: Object, options: PsaBuildOptions):
@@ -184,24 +186,83 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
frame = float(frame_start)
for _ in range(frame_count):
context.scene.frame_set(frame=int(frame), subframe=frame % 1.0)
def add_key(location: Vector, rotation: Quaternion):
key = Psa.Key()
key.location.x = location.x
key.location.y = location.y
key.location.z = location.z
key.rotation.x = rotation.x
key.rotation.y = rotation.y
key.rotation.z = rotation.z
key.rotation.w = rotation.w
key.time = 1.0 / psa_sequence.fps
psa.keys.append(key)
for pose_bone in pose_bones:
location, rotation = _get_pose_bone_location_and_rotation(pose_bone, export_sequence.armature_object, options)
match options.sampling_mode:
case 'INTERPOLATED':
# Used as a store for the last frame's pose bone locations and rotations.
last_frame: Optional[int] = None
last_frame_bone_poses: List[Tuple[Vector, Quaternion]] = []
key = Psa.Key()
key.location.x = location.x
key.location.y = location.y
key.location.z = location.z
key.rotation.x = rotation.x
key.rotation.y = rotation.y
key.rotation.z = rotation.z
key.rotation.w = rotation.w
key.time = 1.0 / psa_sequence.fps
psa.keys.append(key)
next_frame: Optional[int] = None
next_frame_bone_poses: List[Tuple[Vector, Quaternion]] = []
frame += frame_step
for _ in range(frame_count):
if last_frame is None or last_frame != int(frame):
# Populate the bone poses for frame A.
last_frame = int(frame)
# TODO: simplify this code and make it easier to follow!
if next_frame == last_frame:
# Simply transfer the data from next_frame to the last_frame so that we don't need to
# resample anything.
last_frame_bone_poses = next_frame_bone_poses.copy()
else:
last_frame_bone_poses.clear()
context.scene.frame_set(frame=last_frame)
for pose_bone in pose_bones:
location, rotation = _get_pose_bone_location_and_rotation(pose_bone,
export_sequence.armature_object,
options)
last_frame_bone_poses.append((location, rotation))
next_frame = None
next_frame_bone_poses.clear()
# If this is not a subframe, just use the last frame's bone poses.
if frame % 1.0 == 0:
for i in range(len(pose_bones)):
add_key(*last_frame_bone_poses[i])
else:
# Otherwise, this is a subframe, so we need to interpolate the pose between the next frame and the last frame.
if next_frame is None:
next_frame = last_frame + 1
context.scene.frame_set(frame=next_frame)
for pose_bone in pose_bones:
location, rotation = _get_pose_bone_location_and_rotation(pose_bone, export_sequence.armature_object, options)
next_frame_bone_poses.append((location, rotation))
factor = frame % 1.0
for i in range(len(pose_bones)):
last_location, last_rotation = last_frame_bone_poses[i]
next_location, next_rotation = next_frame_bone_poses[i]
location = last_location.lerp(next_location, factor)
rotation = last_rotation.slerp(next_rotation, factor)
add_key(location, rotation)
frame += frame_step
case 'SUBFRAME':
for _ in range(frame_count):
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, export_sequence.armature_object, options)
add_key(location, rotation)
frame += frame_step
frame_start_index += frame_count

View File

@@ -314,13 +314,16 @@ class PSA_OT_export(Operator, ExportHelper):
layout.label(text=f'Duplicate action: {action_name}', icon='ERROR')
break
data_source_header, data_source_panel = layout.panel('Data Source', default_closed=False)
data_source_header.label(text='Data Sources')
if data_source_panel:
flow = data_source_panel.grid_flow()
sampling_header, sampling_panel = layout.panel('Data Source', default_closed=False)
sampling_header.label(text='Sampling')
if sampling_panel:
flow = sampling_panel.grid_flow()
flow.use_property_split = True
flow.use_property_decorate = False
# SAMPLING MODE
flow.prop(pg, 'sampling_mode', text='Sampling Mode')
# FPS
col = flow.row(align=True)
col.prop(pg, 'fps_source', text='FPS')
@@ -378,6 +381,9 @@ class PSA_OT_export(Operator, ExportHelper):
f'\The armature data block for "{obj.name}\" (\'{obj.data.name}\') does not match '
f'the active armature data block (\'{context.view_layer.objects.active.name}\')')
if context.scene.is_nla_tweakmode:
raise RuntimeError('Cannot export PSA while in NLA tweak mode')
def invoke(self, context, _event):
try:
self._check_context(context)
@@ -482,6 +488,7 @@ class PSA_OT_export(Operator, ExportHelper):
options.sequence_name_suffix = pg.sequence_name_suffix
options.root_motion = pg.root_motion
options.scale = pg.scale
options.sampling_mode = pg.sampling_mode
try:
psa = build_psa(context, options)

View File

@@ -217,6 +217,16 @@ class PSA_PG_export(PropertyGroup):
min=0.0,
soft_max=100.0
)
sampling_mode: EnumProperty(
name='Sampling Mode',
options=empty_set,
description='The method by which frames are sampled',
items=(
('INTERPOLATED', 'Interpolated', 'Sampling is performed by interpolating the evaluated bone poses from the adjacent whole frames.', 'INTERPOLATED', 0),
('SUBFRAME', 'Subframe', 'Sampling is performed by evaluating the bone poses at the subframe time.\n\nNot recommended unless you are also animating with subframes enabled.', 'SUBFRAME', 1),
),
default='INTERPOLATED'
)
def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]: