Added sampling mode and changed the default sampling mode to be "Interpolated" rather than "Subframe"
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from bpy.types import Bone, Action, PoseBone
|
from bpy.types import Bone, Action, PoseBone
|
||||||
|
from mathutils import Vector
|
||||||
|
|
||||||
from .data import *
|
from .data import *
|
||||||
from ..shared.helpers import *
|
from ..shared.helpers import *
|
||||||
@@ -33,6 +34,7 @@ class PsaBuildOptions:
|
|||||||
self.sequence_name_suffix: str = ''
|
self.sequence_name_suffix: str = ''
|
||||||
self.root_motion: bool = False
|
self.root_motion: bool = False
|
||||||
self.scale = 1.0
|
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):
|
def _get_pose_bone_location_and_rotation(pose_bone: PoseBone, armature_object: Object, options: PsaBuildOptions):
|
||||||
@@ -184,12 +186,7 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
|
|||||||
|
|
||||||
frame = float(frame_start)
|
frame = float(frame_start)
|
||||||
|
|
||||||
for _ in range(frame_count):
|
def add_key(location: Vector, rotation: Quaternion):
|
||||||
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)
|
|
||||||
|
|
||||||
key = Psa.Key()
|
key = Psa.Key()
|
||||||
key.location.x = location.x
|
key.location.x = location.x
|
||||||
key.location.y = location.y
|
key.location.y = location.y
|
||||||
@@ -201,6 +198,70 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
|
|||||||
key.time = 1.0 / psa_sequence.fps
|
key.time = 1.0 / psa_sequence.fps
|
||||||
psa.keys.append(key)
|
psa.keys.append(key)
|
||||||
|
|
||||||
|
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]] = []
|
||||||
|
|
||||||
|
next_frame: Optional[int] = None
|
||||||
|
next_frame_bone_poses: List[Tuple[Vector, Quaternion]] = []
|
||||||
|
|
||||||
|
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 += frame_step
|
||||||
|
|
||||||
frame_start_index += frame_count
|
frame_start_index += frame_count
|
||||||
|
|||||||
@@ -314,13 +314,16 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
layout.label(text=f'Duplicate action: {action_name}', icon='ERROR')
|
layout.label(text=f'Duplicate action: {action_name}', icon='ERROR')
|
||||||
break
|
break
|
||||||
|
|
||||||
data_source_header, data_source_panel = layout.panel('Data Source', default_closed=False)
|
sampling_header, sampling_panel = layout.panel('Data Source', default_closed=False)
|
||||||
data_source_header.label(text='Data Sources')
|
sampling_header.label(text='Sampling')
|
||||||
if data_source_panel:
|
if sampling_panel:
|
||||||
flow = data_source_panel.grid_flow()
|
flow = sampling_panel.grid_flow()
|
||||||
flow.use_property_split = True
|
flow.use_property_split = True
|
||||||
flow.use_property_decorate = False
|
flow.use_property_decorate = False
|
||||||
|
|
||||||
|
# SAMPLING MODE
|
||||||
|
flow.prop(pg, 'sampling_mode', text='Sampling Mode')
|
||||||
|
|
||||||
# FPS
|
# FPS
|
||||||
col = flow.row(align=True)
|
col = flow.row(align=True)
|
||||||
col.prop(pg, 'fps_source', text='FPS')
|
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 armature data block for "{obj.name}\" (\'{obj.data.name}\') does not match '
|
||||||
f'the active armature data block (\'{context.view_layer.objects.active.name}\')')
|
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):
|
def invoke(self, context, _event):
|
||||||
try:
|
try:
|
||||||
self._check_context(context)
|
self._check_context(context)
|
||||||
@@ -482,6 +488,7 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
options.sequence_name_suffix = pg.sequence_name_suffix
|
options.sequence_name_suffix = pg.sequence_name_suffix
|
||||||
options.root_motion = pg.root_motion
|
options.root_motion = pg.root_motion
|
||||||
options.scale = pg.scale
|
options.scale = pg.scale
|
||||||
|
options.sampling_mode = pg.sampling_mode
|
||||||
|
|
||||||
try:
|
try:
|
||||||
psa = build_psa(context, options)
|
psa = build_psa(context, options)
|
||||||
|
|||||||
@@ -217,6 +217,16 @@ class PSA_PG_export(PropertyGroup):
|
|||||||
min=0.0,
|
min=0.0,
|
||||||
soft_max=100.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]:
|
def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:
|
||||||
|
|||||||
Reference in New Issue
Block a user