1
0
mirror of https://github.com/DarklightGames/io_scene_psk_psa.git synced 2025-02-21 11:39:37 +01:00

Fixed keyframe "bleeding" on PSA import

This commit is contained in:
Colin Basnett 2022-01-19 02:55:10 -08:00
parent 59af9d2145
commit 41e14d5e19
3 changed files with 89 additions and 48 deletions

View File

@ -44,6 +44,16 @@ class Psa(object):
('time', c_float)
]
@property
def data(self):
yield self.rotation.w
yield self.rotation.x
yield self.rotation.y
yield self.rotation.z
yield self.location.x
yield self.location.y
yield self.location.z
def __repr__(self) -> str:
return repr((self.location, self.rotation, self.time))

View File

@ -33,6 +33,23 @@ class PsaImporter(object):
self.post_quat: Quaternion = Quaternion()
self.fcurves = []
def calculate_fcurve_data(import_bone: ImportBone, key_data: []):
# Convert world-space transforms to local-space transforms.
key_rotation = Quaternion(key_data[0:4])
key_location = Vector(key_data[4:])
q = import_bone.post_quat.copy()
q.rotate(import_bone.orig_quat)
quat = q
q = import_bone.post_quat.copy()
if import_bone.parent is None:
q.rotate(key_rotation.conjugated())
else:
q.rotate(key_rotation)
quat.rotate(q.conjugated())
loc = key_location - import_bone.orig_loc
loc.rotate(import_bone.post_quat.conjugated())
return quat.w, quat.x, quat.y, quat.z, loc.x, loc.y, loc.z
# Create an index mapping from bones in the PSA to bones in the target armature.
psa_to_armature_bone_indices = {}
armature_bone_names = [x.name for x in armature_data.bones]
@ -90,6 +107,13 @@ class PsaImporter(object):
import_bone.orig_quat = armature_bone.matrix_local.to_quaternion()
import_bone.post_quat = import_bone.orig_quat.conjugated()
io_time = datetime.timedelta()
math_time = datetime.timedelta()
keyframe_time = datetime.timedelta()
total_time = datetime.timedelta()
total_datetime_start = datetime.datetime.now()
# Create and populate the data for new sequences.
for sequence in sequences:
# F-curve data buffer for all bones. This is used later on to avoid adding redundant keyframes.
@ -116,59 +140,54 @@ class PsaImporter(object):
# Read the sequence keys from the PSA file.
sequence_name = sequence.name.decode('windows-1252')
sequence_keys = psa_reader.read_sequence_keys(sequence_name)
# Add keyframes for each frame of the sequence.
for frame_index in reversed(range(sequence.frame_count)):
key_index = frame_index * len(import_bones)
# Read the sequence data matrix from the PSA.
start_datetime = datetime.datetime.now()
sequence_data_matrix = psa_reader.read_sequence_data_matrix(sequence_name)
keyframe_write_matrix = np.ones(sequence_data_matrix.shape, dtype=np.int8)
io_time += datetime.datetime.now() - start_datetime
# The first step is to determine the frames at which each bone will write out a keyframe.
threshold = 0.001
for bone_index, import_bone in enumerate(import_bones):
if import_bone is None:
continue
for fcurve_index, fcurve in enumerate(import_bone.fcurves):
# Get all the keyframe data for the bone's f-curve data from the sequence data matrix.
fcurve_frame_data = sequence_data_matrix[:, bone_index, fcurve_index]
last_written_datum = 0
for frame_index, datum in enumerate(fcurve_frame_data):
# If the f-curve data is not different enough to the last written frame, un-mark this data for writing.
if frame_index > 0 and abs(datum - last_written_datum) < threshold:
keyframe_write_matrix[frame_index, bone_index, fcurve_index] = 0
else:
last_written_datum = fcurve_frame_data[frame_index]
# Write the keyframes out!
for frame_index in range(sequence.frame_count):
for bone_index, import_bone in enumerate(import_bones):
if import_bone is None:
# bone does not exist in the armature, skip it
key_index += 1
continue
# Convert world-space transforms to local-space transforms.
key_rotation = Quaternion(tuple(sequence_keys[key_index].rotation))
q = import_bone.post_quat.copy()
q.rotate(import_bone.orig_quat)
quat = q
q = import_bone.post_quat.copy()
if import_bone.parent is None:
q.rotate(key_rotation.conjugated())
else:
q.rotate(key_rotation)
quat.rotate(q.conjugated())
key_location = Vector(tuple(sequence_keys[key_index].location))
loc = key_location - import_bone.orig_loc
loc.rotate(import_bone.post_quat.conjugated())
# Add keyframe data for each of the associated f-curves.
bone_fcurve_data = quat.w, quat.x, quat.y, quat.z, loc.x, loc.y, loc.z
if frame_index == 0:
# Always add a keyframe on the first frame.
for fcurve, datum in zip(import_bone.fcurves, bone_fcurve_data):
fcurve.keyframe_points.insert(frame_index, datum, options={'FAST'})
else:
# For each f-curve, check that the next frame has data that differs from the current frame.
# If so, add a keyframe for the current frame.
# Note that we are iterating the frames in reverse order.
threshold = 0.001
for fcurve, datum, old_datum in zip(import_bone.fcurves, bone_fcurve_data, next_frame_bones_fcurve_data[bone_index]):
# Only
if abs(datum - old_datum) > threshold:
bone_has_writeable_keyframes = any(keyframe_write_matrix[frame_index, bone_index])
if bone_has_writeable_keyframes:
# This bone has writeable keyframes for this frame.
key_data = sequence_data_matrix[frame_index, bone_index]
# Calculate the local-space key data for the bone.
start_datetime = datetime.datetime.now()
fcurve_data = calculate_fcurve_data(import_bone, key_data)
math_time += datetime.datetime.now() - start_datetime
for fcurve, should_write, datum in zip(import_bone.fcurves, keyframe_write_matrix[frame_index, bone_index], fcurve_data):
if should_write:
start_datetime = datetime.datetime.now()
fcurve.keyframe_points.insert(frame_index, datum, options={'FAST'})
keyframe_time += datetime.datetime.now() - start_datetime
next_frame_bones_fcurve_data[bone_index] = bone_fcurve_data
total_time = datetime.datetime.now() - total_datetime_start
key_index += 1
# Eliminate redundant final keyframe if the f-curve value is identical to the previous keyframe.
for import_bone in filter(lambda x: x is not None, import_bones):
for fcurve in filter(lambda x: len(x.keyframe_points) > 1, import_bone.fcurves):
second_to_last_keyframe, last_keyframe = fcurve.keyframe_points[-2:]
if second_to_last_keyframe.co[1] == last_keyframe.co[1]:
fcurve.keyframe_points.remove(last_keyframe)
print(f'io_time: {io_time}')
print(f'math_time: {math_time}')
print(f'keyframe_time: {keyframe_time}')
print(f'total_time: {total_time}')
class PsaImportActionListItem(PropertyGroup):
@ -186,7 +205,6 @@ def on_psa_file_path_updated(property, context):
try:
# Read the file and populate the action list.
p = os.path.abspath(context.scene.psa_import.psa_file_path)
print(p)
psa_reader = PsaReader(p)
for sequence in psa_reader.sequences.values():
item = context.scene.psa_import.action_list.add()

View File

@ -1,5 +1,6 @@
from .data import *
import ctypes
import numpy as np
class PsaReader(object):
@ -30,7 +31,19 @@ class PsaReader(object):
data.append(data_class.from_buffer_copy(buffer, offset))
offset += section.data_size
def read_sequence_keys(self, sequence_name) -> List[Psa.Key]:
def read_sequence_data_matrix(self, sequence_name: str):
sequence = self.psa.sequences[sequence_name]
keys = self.read_sequence_keys(sequence_name)
bone_count = len(self.bones)
matrix_size = sequence.frame_count, bone_count, 7
matrix = np.zeros(matrix_size)
keys_iter = iter(keys)
for frame_index in range(sequence.frame_count):
for bone_index in range(bone_count):
matrix[frame_index, bone_index, :] = list(next(keys_iter).data)
return matrix
def read_sequence_keys(self, sequence_name: str) -> List[Psa.Key]:
""" Reads and returns the key data for a sequence.
:param sequence_name: The name of the sequence.