Added PSA resampling on import + some fixes for 4.1
This commit is contained in:
@@ -91,14 +91,15 @@ 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:
|
def get_sequence_fps(context: Context, fps_source: str, fps_custom: float, actions: Iterable[Action]) -> float:
|
||||||
if fps_source == 'SCENE':
|
match fps_source:
|
||||||
|
case 'SCENE':
|
||||||
return context.scene.render.fps
|
return context.scene.render.fps
|
||||||
elif fps_source == 'CUSTOM':
|
case 'CUSTOM':
|
||||||
return fps_custom
|
return fps_custom
|
||||||
elif fps_source == 'ACTION_METADATA':
|
case 'ACTION_METADATA':
|
||||||
# Get the minimum value of action metadata FPS values.
|
# Get the minimum value of action metadata FPS values.
|
||||||
return min([action.psa_export.fps for action in actions])
|
return min([action.psa_export.fps for action in actions])
|
||||||
else:
|
case _:
|
||||||
raise RuntimeError(f'Invalid FPS source "{fps_source}"')
|
raise RuntimeError(f'Invalid FPS source "{fps_source}"')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from bpy.props import StringProperty
|
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 bpy_extras.io_utils import ImportHelper
|
||||||
|
|
||||||
from .properties import get_visible_sequences
|
from .properties import get_visible_sequences
|
||||||
@@ -89,23 +89,6 @@ class PSA_OT_import_sequences_deselect_all(Operator):
|
|||||||
return {'FINISHED'}
|
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):
|
def load_psa_file(context, filepath: str):
|
||||||
pg = context.scene.psa_import
|
pg = context.scene.psa_import
|
||||||
pg.sequence_list.clear()
|
pg.sequence_list.clear()
|
||||||
@@ -270,10 +253,13 @@ class PSA_OT_import(Operator, ImportHelper):
|
|||||||
col.prop(pg, 'action_name_prefix')
|
col.prop(pg, 'action_name_prefix')
|
||||||
|
|
||||||
|
|
||||||
|
class PSA_FH_import(FileHandler):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
PSA_OT_import_sequences_select_all,
|
PSA_OT_import_sequences_select_all,
|
||||||
PSA_OT_import_sequences_deselect_all,
|
PSA_OT_import_sequences_deselect_all,
|
||||||
PSA_OT_import_sequences_from_text,
|
PSA_OT_import_sequences_from_text,
|
||||||
PSA_OT_import,
|
PSA_OT_import,
|
||||||
PSA_OT_import_select_file,
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import math
|
||||||
import typing
|
import typing
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import numpy
|
import numpy as np
|
||||||
from bpy.types import FCurve, Object, Context
|
from bpy.types import FCurve, Object, Context
|
||||||
from mathutils import Vector, Quaternion
|
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
|
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:
|
def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object, options: PsaImportOptions) -> PsaImportResult:
|
||||||
result = PsaImportResult()
|
result = PsaImportResult()
|
||||||
sequences = [psa_reader.sequences[x] for x in options.sequence_names]
|
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 _:
|
case _:
|
||||||
raise ValueError(f'Unknown FPS source: {options.fps_source}')
|
raise ValueError(f'Unknown FPS source: {options.fps_source}')
|
||||||
|
|
||||||
keyframe_time_dilation = target_fps / sequence.fps
|
|
||||||
|
|
||||||
if options.should_write_keyframes:
|
if options.should_write_keyframes:
|
||||||
# Remove existing f-curves (replace with action.fcurves.clear() in Blender 3.2)
|
# Remove existing f-curves.
|
||||||
while len(action.fcurves) > 0:
|
action.fcurves.clear()
|
||||||
action.fcurves.remove(action.fcurves[-1])
|
|
||||||
|
|
||||||
# Create f-curves for the rotation and location of each bone.
|
# 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():
|
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.
|
# Calculate the local-space key data for the bone.
|
||||||
sequence_data_matrix[frame_index, bone_index] = _calculate_fcurve_data(import_bone, key_data)
|
sequence_data_matrix[frame_index, bone_index] = _calculate_fcurve_data(import_bone, key_data)
|
||||||
|
|
||||||
# Write the keyframes out.
|
# Resample the sequence data to the target FPS.
|
||||||
fcurve_data = numpy.zeros(2 * sequence.frame_count, dtype=float)
|
# 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):
|
for bone_index, import_bone in enumerate(import_bones):
|
||||||
if import_bone is None:
|
if import_bone is None:
|
||||||
continue
|
continue
|
||||||
for fcurve_index, fcurve in enumerate(import_bone.fcurves):
|
for fcurve_index, fcurve in enumerate(import_bone.fcurves):
|
||||||
if fcurve is None:
|
if fcurve is None:
|
||||||
continue
|
continue
|
||||||
fcurve_data[1::2] = sequence_data_matrix[:, bone_index, fcurve_index]
|
fcurve_data[1::2] = resampled_sequence_data_matrix[:, bone_index, fcurve_index]
|
||||||
fcurve.keyframe_points.add(sequence.frame_count)
|
fcurve.keyframe_points.add(target_frame_count)
|
||||||
fcurve.keyframe_points.foreach_set('co', fcurve_data)
|
fcurve.keyframe_points.foreach_set('co', fcurve_data)
|
||||||
for fcurve_keyframe in fcurve.keyframe_points:
|
for fcurve_keyframe in fcurve.keyframe_points:
|
||||||
fcurve_keyframe.interpolation = 'LINEAR'
|
fcurve_keyframe.interpolation = 'LINEAR'
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ from ..reader import read_psk
|
|||||||
empty_set = set()
|
empty_set = set()
|
||||||
|
|
||||||
|
|
||||||
class PSX_FH_psk(FileHandler):
|
class PSK_FH_import(FileHandler):
|
||||||
bl_idname = 'PSX_FH_psk'
|
bl_idname = 'PSK_FH_import'
|
||||||
bl_label = 'Unreal PSK/PSKX'
|
bl_label = 'File handler for Unreal PSK/PSKX import'
|
||||||
bl_import_operator = 'import_scene.psk'
|
bl_import_operator = 'import_scene.psk'
|
||||||
bl_file_extensions = '.psk;.pskx'
|
bl_file_extensions = '.psk'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll_drop(cls, context: Context):
|
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):
|
class PSK_OT_import(Operator, ImportHelper):
|
||||||
@@ -143,10 +143,11 @@ class PSK_OT_import(Operator, ImportHelper):
|
|||||||
col.use_property_decorate = False
|
col.use_property_decorate = False
|
||||||
col.prop(self, 'scale')
|
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:
|
if mesh_panel and self.should_import_mesh:
|
||||||
row = layout.row()
|
row = mesh_panel.row()
|
||||||
col = row.column()
|
col = row.column()
|
||||||
col.use_property_split = True
|
col.use_property_split = True
|
||||||
col.use_property_decorate = False
|
col.use_property_decorate = False
|
||||||
@@ -158,9 +159,11 @@ class PSK_OT_import(Operator, ImportHelper):
|
|||||||
col.prop(self, 'vertex_color_space')
|
col.prop(self, 'vertex_color_space')
|
||||||
col.prop(self, 'should_import_shape_keys', text='Shape Keys')
|
col.prop(self, 'should_import_shape_keys', text='Shape Keys')
|
||||||
|
|
||||||
layout.prop(self, 'should_import_skeleton')
|
skeleton_header, skeleton_panel = layout.panel('skeleton_panel_id', default_closed=False)
|
||||||
if self.should_import_skeleton:
|
skeleton_header.prop(self, 'should_import_skeleton')
|
||||||
row = layout.row()
|
|
||||||
|
if skeleton_panel and self.should_import_skeleton:
|
||||||
|
row = skeleton_panel.row()
|
||||||
col = row.column()
|
col = row.column()
|
||||||
col.use_property_split = True
|
col.use_property_split = True
|
||||||
col.use_property_decorate = False
|
col.use_property_decorate = False
|
||||||
@@ -169,5 +172,5 @@ class PSK_OT_import(Operator, ImportHelper):
|
|||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
PSK_OT_import,
|
PSK_OT_import,
|
||||||
PSX_FH_psk,
|
PSK_FH_import,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user