Added PSA resampling on import + some fixes for 4.1
This commit is contained in:
@@ -91,15 +91,16 @@ def update_actions_and_timeline_markers(context: Context, armature: Armature):
|
||||
|
||||
|
||||
def get_sequence_fps(context: Context, fps_source: str, fps_custom: float, actions: Iterable[Action]) -> float:
|
||||
if fps_source == 'SCENE':
|
||||
return context.scene.render.fps
|
||||
elif fps_source == 'CUSTOM':
|
||||
return fps_custom
|
||||
elif fps_source == 'ACTION_METADATA':
|
||||
# Get the minimum value of action metadata FPS values.
|
||||
return min([action.psa_export.fps for action in actions])
|
||||
else:
|
||||
raise RuntimeError(f'Invalid FPS source "{fps_source}"')
|
||||
match fps_source:
|
||||
case 'SCENE':
|
||||
return context.scene.render.fps
|
||||
case 'CUSTOM':
|
||||
return fps_custom
|
||||
case 'ACTION_METADATA':
|
||||
# Get the minimum value of action metadata FPS values.
|
||||
return min([action.psa_export.fps for action in actions])
|
||||
case _:
|
||||
raise RuntimeError(f'Invalid FPS source "{fps_source}"')
|
||||
|
||||
|
||||
def get_animation_data_object(context: Context) -> Object:
|
||||
|
||||
@@ -2,7 +2,7 @@ import os
|
||||
from pathlib import Path
|
||||
|
||||
from bpy.props import StringProperty
|
||||
from bpy.types import Operator, Event, Context
|
||||
from bpy.types import Operator, Event, Context, FileHandler
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
|
||||
from .properties import get_visible_sequences
|
||||
@@ -89,23 +89,6 @@ class PSA_OT_import_sequences_deselect_all(Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class PSA_OT_import_select_file(Operator):
|
||||
bl_idname = 'psa_import.select_file'
|
||||
bl_label = 'Select'
|
||||
bl_options = {'INTERNAL'}
|
||||
bl_description = 'Select a PSA file from which to import animations'
|
||||
filepath: StringProperty(subtype='FILE_PATH')
|
||||
filter_glob: StringProperty(default='*.psa', options={'HIDDEN'})
|
||||
|
||||
def execute(self, context):
|
||||
getattr(context.scene, 'psa_import').psa_file_path = self.filepath
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
def load_psa_file(context, filepath: str):
|
||||
pg = context.scene.psa_import
|
||||
pg.sequence_list.clear()
|
||||
@@ -270,10 +253,13 @@ class PSA_OT_import(Operator, ImportHelper):
|
||||
col.prop(pg, 'action_name_prefix')
|
||||
|
||||
|
||||
class PSA_FH_import(FileHandler):
|
||||
|
||||
|
||||
|
||||
classes = (
|
||||
PSA_OT_import_sequences_select_all,
|
||||
PSA_OT_import_sequences_deselect_all,
|
||||
PSA_OT_import_sequences_from_text,
|
||||
PSA_OT_import,
|
||||
PSA_OT_import_select_file,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import math
|
||||
import typing
|
||||
from typing import List, Optional
|
||||
|
||||
import bpy
|
||||
import numpy
|
||||
import numpy as np
|
||||
from bpy.types import FCurve, Object, Context
|
||||
from mathutils import Vector, Quaternion
|
||||
|
||||
@@ -80,6 +81,50 @@ def _get_armature_bone_index_for_psa_bone(psa_bone_name: str, armature_bone_name
|
||||
return None
|
||||
|
||||
|
||||
def _resample_sequence_data_matrix(sequence_data_matrix: np.ndarray, target_frame_count: int):
|
||||
'''
|
||||
Resamples the sequence data matrix to the target frame count.
|
||||
@param sequence_data_matrix: FxBx7 matrix where F is the number of frames, B is the number of bones, and X is the
|
||||
number of data elements per bone.
|
||||
@param target_frame_count: The number of frames to resample to.
|
||||
@return: The resampled sequence data matrix, or sequence_data_matrix if no resampling is necessary.
|
||||
'''
|
||||
def get_sample_times(source_frame_count: int, target_frame_count: int) -> typing.Iterable[float]:
|
||||
time = 0.0
|
||||
time_step = source_frame_count / target_frame_count
|
||||
while time <= target_frame_count - 1:
|
||||
yield time
|
||||
time += time_step
|
||||
yield 1.0
|
||||
|
||||
source_frame_count, bone_count = sequence_data_matrix.shape[:1]
|
||||
|
||||
if target_frame_count == source_frame_count:
|
||||
# No resampling is necessary.
|
||||
return sequence_data_matrix
|
||||
|
||||
resampled_sequence_data_matrix = np.zeros((target_frame_count, bone_count, 7), dtype=float)
|
||||
sample_times = get_sample_times(source_frame_count, target_frame_count)
|
||||
|
||||
for frame_index, sample_time in enumerate(sample_times):
|
||||
if sample_time % 1.0 == 0.0:
|
||||
# Sample time has no fractional part, so just copy the frame.
|
||||
resampled_sequence_data_matrix[frame_index, :, :] = sequence_data_matrix[int(sample_time), :, :]
|
||||
else:
|
||||
# Sample time has a fractional part, so interpolate between two frames.
|
||||
for bone_index in range(bone_count):
|
||||
frame_index = int(sample_time)
|
||||
source_frame_1_data = sequence_data_matrix[frame_index, bone_index, :]
|
||||
source_frame_2_data = sequence_data_matrix[frame_index + 1, bone_index, :]
|
||||
factor = sample_time - frame_index
|
||||
q = Quaternion((source_frame_1_data[:4])).slerp(Quaternion((source_frame_2_data[:4])), factor)
|
||||
q.normalize()
|
||||
l = Vector(source_frame_1_data[4:]).lerp(Vector(source_frame_2_data[4:]), factor)
|
||||
resampled_sequence_data_matrix[frame_index, bone_index, :] = q.w, q.x, q.y, q.z, l.x, l.y, l.z
|
||||
|
||||
return resampled_sequence_data_matrix
|
||||
|
||||
|
||||
def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object, options: PsaImportOptions) -> PsaImportResult:
|
||||
result = PsaImportResult()
|
||||
sequences = [psa_reader.sequences[x] for x in options.sequence_names]
|
||||
@@ -186,12 +231,9 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
|
||||
case _:
|
||||
raise ValueError(f'Unknown FPS source: {options.fps_source}')
|
||||
|
||||
keyframe_time_dilation = target_fps / sequence.fps
|
||||
|
||||
if options.should_write_keyframes:
|
||||
# Remove existing f-curves (replace with action.fcurves.clear() in Blender 3.2)
|
||||
while len(action.fcurves) > 0:
|
||||
action.fcurves.remove(action.fcurves[-1])
|
||||
# Remove existing f-curves.
|
||||
action.fcurves.clear()
|
||||
|
||||
# Create f-curves for the rotation and location of each bone.
|
||||
for psa_bone_index, armature_bone_index in psa_to_armature_bone_indices.items():
|
||||
@@ -225,19 +267,27 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
|
||||
# Calculate the local-space key data for the bone.
|
||||
sequence_data_matrix[frame_index, bone_index] = _calculate_fcurve_data(import_bone, key_data)
|
||||
|
||||
# Write the keyframes out.
|
||||
fcurve_data = numpy.zeros(2 * sequence.frame_count, dtype=float)
|
||||
# Resample the sequence data to the target FPS.
|
||||
# TODO: target frame count can be fractional.
|
||||
target_frame_count = math.ceil(sequence.frame_count * (target_fps / sequence.fps))
|
||||
|
||||
# Write the keyframes out.
|
||||
# Note that the f-curve data consists of alternating time and value data.
|
||||
fcurve_data = np.zeros(2 * target_frame_count, dtype=float)
|
||||
fcurve_data[0::2] = range(0, target_frame_count)
|
||||
|
||||
# Resample the sequence data matrix to the target frame count.
|
||||
# If the target frame count is the same as the source frame count, this will be a no-op.
|
||||
resampled_sequence_data_matrix = _resample_sequence_data_matrix(sequence_data_matrix, target_frame_count)
|
||||
|
||||
# Populate the keyframe time data.
|
||||
fcurve_data[0::2] = [x * keyframe_time_dilation for x in range(sequence.frame_count)]
|
||||
for bone_index, import_bone in enumerate(import_bones):
|
||||
if import_bone is None:
|
||||
continue
|
||||
for fcurve_index, fcurve in enumerate(import_bone.fcurves):
|
||||
if fcurve is None:
|
||||
continue
|
||||
fcurve_data[1::2] = sequence_data_matrix[:, bone_index, fcurve_index]
|
||||
fcurve.keyframe_points.add(sequence.frame_count)
|
||||
fcurve_data[1::2] = resampled_sequence_data_matrix[:, bone_index, fcurve_index]
|
||||
fcurve.keyframe_points.add(target_frame_count)
|
||||
fcurve.keyframe_points.foreach_set('co', fcurve_data)
|
||||
for fcurve_keyframe in fcurve.keyframe_points:
|
||||
fcurve_keyframe.interpolation = 'LINEAR'
|
||||
|
||||
@@ -11,15 +11,15 @@ from ..reader import read_psk
|
||||
empty_set = set()
|
||||
|
||||
|
||||
class PSX_FH_psk(FileHandler):
|
||||
bl_idname = 'PSX_FH_psk'
|
||||
bl_label = 'Unreal PSK/PSKX'
|
||||
class PSK_FH_import(FileHandler):
|
||||
bl_idname = 'PSK_FH_import'
|
||||
bl_label = 'File handler for Unreal PSK/PSKX import'
|
||||
bl_import_operator = 'import_scene.psk'
|
||||
bl_file_extensions = '.psk;.pskx'
|
||||
bl_file_extensions = '.psk'
|
||||
|
||||
@classmethod
|
||||
def poll_drop(cls, context: Context):
|
||||
return context.area.type == 'VIEW_3D'
|
||||
return context.area and context.area.type == 'VIEW_3D'
|
||||
|
||||
|
||||
class PSK_OT_import(Operator, ImportHelper):
|
||||
@@ -143,10 +143,11 @@ class PSK_OT_import(Operator, ImportHelper):
|
||||
col.use_property_decorate = False
|
||||
col.prop(self, 'scale')
|
||||
|
||||
layout.prop(self, 'should_import_mesh')
|
||||
mesh_header, mesh_panel = layout.panel('mesh_panel_id', default_closed=False)
|
||||
mesh_header.prop(self, 'should_import_mesh')
|
||||
|
||||
if self.should_import_mesh:
|
||||
row = layout.row()
|
||||
if mesh_panel and self.should_import_mesh:
|
||||
row = mesh_panel.row()
|
||||
col = row.column()
|
||||
col.use_property_split = True
|
||||
col.use_property_decorate = False
|
||||
@@ -158,9 +159,11 @@ class PSK_OT_import(Operator, ImportHelper):
|
||||
col.prop(self, 'vertex_color_space')
|
||||
col.prop(self, 'should_import_shape_keys', text='Shape Keys')
|
||||
|
||||
layout.prop(self, 'should_import_skeleton')
|
||||
if self.should_import_skeleton:
|
||||
row = layout.row()
|
||||
skeleton_header, skeleton_panel = layout.panel('skeleton_panel_id', default_closed=False)
|
||||
skeleton_header.prop(self, 'should_import_skeleton')
|
||||
|
||||
if skeleton_panel and self.should_import_skeleton:
|
||||
row = skeleton_panel.row()
|
||||
col = row.column()
|
||||
col.use_property_split = True
|
||||
col.use_property_decorate = False
|
||||
@@ -169,5 +172,5 @@ class PSK_OT_import(Operator, ImportHelper):
|
||||
|
||||
classes = (
|
||||
PSK_OT_import,
|
||||
PSX_FH_psk,
|
||||
PSK_FH_import,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user