|
|
|
@@ -2,7 +2,7 @@ import typing
|
|
|
|
from typing import List, Optional
|
|
|
|
from typing import List, Optional
|
|
|
|
|
|
|
|
|
|
|
|
import bpy
|
|
|
|
import bpy
|
|
|
|
import numpy as np
|
|
|
|
import numpy
|
|
|
|
from bpy.types import FCurve, Object, Context
|
|
|
|
from bpy.types import FCurve, Object, Context
|
|
|
|
from mathutils import Vector, Quaternion
|
|
|
|
from mathutils import Vector, Quaternion
|
|
|
|
|
|
|
|
|
|
|
|
@@ -64,12 +64,12 @@ class PsaImportResult:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_armature_bone_index_for_psa_bone(psa_bone_name: str, armature_bone_names: List[str], bone_mapping_mode: str = 'EXACT') -> Optional[int]:
|
|
|
|
def _get_armature_bone_index_for_psa_bone(psa_bone_name: str, armature_bone_names: List[str], bone_mapping_mode: str = 'EXACT') -> Optional[int]:
|
|
|
|
"""
|
|
|
|
'''
|
|
|
|
@param psa_bone_name: The name of the PSA bone.
|
|
|
|
@param psa_bone_name: The name of the PSA bone.
|
|
|
|
@param armature_bone_names: The names of the bones in the armature.
|
|
|
|
@param armature_bone_names: The names of the bones in the armature.
|
|
|
|
@param bone_mapping_mode: One of 'EXACT' or 'CASE_INSENSITIVE'.
|
|
|
|
@param bone_mapping_mode: One of 'EXACT' or 'CASE_INSENSITIVE'.
|
|
|
|
@return: The index of the armature bone that corresponds to the given PSA bone, or None if no such bone exists.
|
|
|
|
@return: The index of the armature bone that corresponds to the given PSA bone, or None if no such bone exists.
|
|
|
|
"""
|
|
|
|
'''
|
|
|
|
for armature_bone_index, armature_bone_name in enumerate(armature_bone_names):
|
|
|
|
for armature_bone_index, armature_bone_name in enumerate(armature_bone_names):
|
|
|
|
if bone_mapping_mode == 'CASE_INSENSITIVE':
|
|
|
|
if bone_mapping_mode == 'CASE_INSENSITIVE':
|
|
|
|
if armature_bone_name.lower() == psa_bone_name.lower():
|
|
|
|
if armature_bone_name.lower() == psa_bone_name.lower():
|
|
|
|
@@ -80,52 +80,6 @@ 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, time_step: float = 1.0) -> np.ndarray:
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
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, time_step: float) -> typing.Iterable[float]:
|
|
|
|
|
|
|
|
# TODO: for correctness, we should also emit the target frame time as well (because the last frame can be a
|
|
|
|
|
|
|
|
# fractional frame).
|
|
|
|
|
|
|
|
time = 0.0
|
|
|
|
|
|
|
|
while time < source_frame_count - 1:
|
|
|
|
|
|
|
|
yield time
|
|
|
|
|
|
|
|
time += time_step
|
|
|
|
|
|
|
|
yield source_frame_count - 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if time_step == 1.0:
|
|
|
|
|
|
|
|
# No resampling is necessary.
|
|
|
|
|
|
|
|
return sequence_data_matrix
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
source_frame_count, bone_count = sequence_data_matrix.shape[:2]
|
|
|
|
|
|
|
|
sample_times = list(get_sample_times(source_frame_count, time_step))
|
|
|
|
|
|
|
|
target_frame_count = len(sample_times)
|
|
|
|
|
|
|
|
resampled_sequence_data_matrix = np.zeros((target_frame_count, bone_count, 7), dtype=float)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for sample_index, sample_time in enumerate(sample_times):
|
|
|
|
|
|
|
|
frame_index = int(sample_time)
|
|
|
|
|
|
|
|
if sample_time % 1.0 == 0.0:
|
|
|
|
|
|
|
|
# Sample time has no fractional part, so just copy the frame.
|
|
|
|
|
|
|
|
resampled_sequence_data_matrix[sample_index, :, :] = sequence_data_matrix[frame_index, :, :]
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
# Sample time has a fractional part, so interpolate between two frames.
|
|
|
|
|
|
|
|
next_frame_index = frame_index + 1
|
|
|
|
|
|
|
|
for bone_index in range(bone_count):
|
|
|
|
|
|
|
|
source_frame_1_data = sequence_data_matrix[frame_index, bone_index, :]
|
|
|
|
|
|
|
|
source_frame_2_data = sequence_data_matrix[next_frame_index, 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[sample_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]
|
|
|
|
@@ -144,7 +98,7 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
|
|
|
|
if armature_bone_index is not None:
|
|
|
|
if armature_bone_index is not None:
|
|
|
|
# Ensure that no other PSA bone has been mapped to this armature bone yet.
|
|
|
|
# Ensure that no other PSA bone has been mapped to this armature bone yet.
|
|
|
|
if armature_bone_index not in armature_to_psa_bone_indices:
|
|
|
|
if armature_bone_index not in armature_to_psa_bone_indices:
|
|
|
|
psa_to_armature_bone_indices[psa_bone_index] = armature_bone_index
|
|
|
|
psa_to_armature_bone_indices[psa_bone_index] = armature_bone_names.index(psa_bone_name)
|
|
|
|
armature_to_psa_bone_indices[armature_bone_index] = psa_bone_index
|
|
|
|
armature_to_psa_bone_indices[armature_bone_index] = psa_bone_index
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
# This armature bone has already been mapped to a PSA bone.
|
|
|
|
# This armature bone has already been mapped to a PSA bone.
|
|
|
|
@@ -173,7 +127,7 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
|
|
|
|
|
|
|
|
|
|
|
|
# Create intermediate bone data for import operations.
|
|
|
|
# Create intermediate bone data for import operations.
|
|
|
|
import_bones = []
|
|
|
|
import_bones = []
|
|
|
|
psa_bone_names_to_import_bones = dict()
|
|
|
|
import_bones_dict = dict()
|
|
|
|
|
|
|
|
|
|
|
|
for (psa_bone_index, psa_bone), psa_bone_name in zip(enumerate(psa_reader.bones), psa_bone_names):
|
|
|
|
for (psa_bone_index, psa_bone), psa_bone_name in zip(enumerate(psa_reader.bones), psa_bone_names):
|
|
|
|
if psa_bone_index not in psa_to_armature_bone_indices:
|
|
|
|
if psa_bone_index not in psa_to_armature_bone_indices:
|
|
|
|
@@ -183,22 +137,15 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
|
|
|
|
import_bone = ImportBone(psa_bone)
|
|
|
|
import_bone = ImportBone(psa_bone)
|
|
|
|
import_bone.armature_bone = armature_data.bones[psa_bone_name]
|
|
|
|
import_bone.armature_bone = armature_data.bones[psa_bone_name]
|
|
|
|
import_bone.pose_bone = armature_object.pose.bones[psa_bone_name]
|
|
|
|
import_bone.pose_bone = armature_object.pose.bones[psa_bone_name]
|
|
|
|
psa_bone_names_to_import_bones[psa_bone_name] = import_bone
|
|
|
|
import_bones_dict[psa_bone_name] = import_bone
|
|
|
|
import_bones.append(import_bone)
|
|
|
|
import_bones.append(import_bone)
|
|
|
|
|
|
|
|
|
|
|
|
bones_with_missing_parents = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for import_bone in filter(lambda x: x is not None, import_bones):
|
|
|
|
for import_bone in filter(lambda x: x is not None, import_bones):
|
|
|
|
armature_bone = import_bone.armature_bone
|
|
|
|
armature_bone = import_bone.armature_bone
|
|
|
|
has_parent = armature_bone.parent is not None
|
|
|
|
if armature_bone.parent is not None and armature_bone.parent.name in psa_bone_names:
|
|
|
|
if has_parent:
|
|
|
|
import_bone.parent = import_bones_dict[armature_bone.parent.name]
|
|
|
|
if armature_bone.parent.name in psa_bone_names:
|
|
|
|
|
|
|
|
import_bone.parent = psa_bone_names_to_import_bones[armature_bone.parent.name]
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
# Add a warning if the parent bone is not in the PSA.
|
|
|
|
|
|
|
|
bones_with_missing_parents.append(armature_bone)
|
|
|
|
|
|
|
|
# Calculate the original location & rotation of each bone (in world-space maybe?)
|
|
|
|
# Calculate the original location & rotation of each bone (in world-space maybe?)
|
|
|
|
if has_parent:
|
|
|
|
if import_bone.parent is not None:
|
|
|
|
import_bone.original_location = armature_bone.matrix_local.translation - armature_bone.parent.matrix_local.translation
|
|
|
|
import_bone.original_location = armature_bone.matrix_local.translation - armature_bone.parent.matrix_local.translation
|
|
|
|
import_bone.original_location.rotate(armature_bone.parent.matrix_local.to_quaternion().conjugated())
|
|
|
|
import_bone.original_location.rotate(armature_bone.parent.matrix_local.to_quaternion().conjugated())
|
|
|
|
import_bone.original_rotation = armature_bone.matrix_local.to_quaternion()
|
|
|
|
import_bone.original_rotation = armature_bone.matrix_local.to_quaternion()
|
|
|
|
@@ -210,12 +157,6 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
|
|
|
|
|
|
|
|
|
|
|
|
import_bone.post_rotation = import_bone.original_rotation.conjugated()
|
|
|
|
import_bone.post_rotation = import_bone.original_rotation.conjugated()
|
|
|
|
|
|
|
|
|
|
|
|
# Warn about bones with missing parents.
|
|
|
|
|
|
|
|
if len(bones_with_missing_parents) > 0:
|
|
|
|
|
|
|
|
count = len(bones_with_missing_parents)
|
|
|
|
|
|
|
|
message = f'{count} bone(s) have parents that are not present in the PSA:\n' + str([x.name for x in bones_with_missing_parents])
|
|
|
|
|
|
|
|
result.warnings.append(message)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
context.window_manager.progress_begin(0, len(sequences))
|
|
|
|
context.window_manager.progress_begin(0, len(sequences))
|
|
|
|
|
|
|
|
|
|
|
|
# Create and populate the data for new sequences.
|
|
|
|
# Create and populate the data for new sequences.
|
|
|
|
@@ -246,9 +187,12 @@ 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.
|
|
|
|
# Remove existing f-curves (replace with action.fcurves.clear() in Blender 3.2)
|
|
|
|
action.fcurves.clear()
|
|
|
|
while len(action.fcurves) > 0:
|
|
|
|
|
|
|
|
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():
|
|
|
|
@@ -282,25 +226,19 @@ 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)
|
|
|
|
|
|
|
|
|
|
|
|
# Resample the sequence data to the target FPS.
|
|
|
|
|
|
|
|
# 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,
|
|
|
|
|
|
|
|
time_step=sequence.fps / target_fps)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Write the keyframes out.
|
|
|
|
# Write the keyframes out.
|
|
|
|
# Note that the f-curve data consists of alternating time and value data.
|
|
|
|
fcurve_data = numpy.zeros(2 * sequence.frame_count, dtype=float)
|
|
|
|
target_frame_count = resampled_sequence_data_matrix.shape[0]
|
|
|
|
|
|
|
|
fcurve_data = np.zeros(2 * target_frame_count, dtype=float)
|
|
|
|
|
|
|
|
fcurve_data[0::2] = range(0, 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] = resampled_sequence_data_matrix[:, bone_index, fcurve_index]
|
|
|
|
fcurve_data[1::2] = sequence_data_matrix[:, bone_index, fcurve_index]
|
|
|
|
fcurve.keyframe_points.add(target_frame_count)
|
|
|
|
fcurve.keyframe_points.add(sequence.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'
|
|
|
|
|