Fixed keyframe "bleeding" on PSA import
This commit is contained in:
@@ -44,6 +44,16 @@ class Psa(object):
|
|||||||
('time', c_float)
|
('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:
|
def __repr__(self) -> str:
|
||||||
return repr((self.location, self.rotation, self.time))
|
return repr((self.location, self.rotation, self.time))
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,23 @@ class PsaImporter(object):
|
|||||||
self.post_quat: Quaternion = Quaternion()
|
self.post_quat: Quaternion = Quaternion()
|
||||||
self.fcurves = []
|
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.
|
# Create an index mapping from bones in the PSA to bones in the target armature.
|
||||||
psa_to_armature_bone_indices = {}
|
psa_to_armature_bone_indices = {}
|
||||||
armature_bone_names = [x.name for x in armature_data.bones]
|
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.orig_quat = armature_bone.matrix_local.to_quaternion()
|
||||||
import_bone.post_quat = import_bone.orig_quat.conjugated()
|
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.
|
# Create and populate the data for new sequences.
|
||||||
for sequence in sequences:
|
for sequence in sequences:
|
||||||
# F-curve data buffer for all bones. This is used later on to avoid adding redundant keyframes.
|
# 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.
|
# Read the sequence keys from the PSA file.
|
||||||
sequence_name = sequence.name.decode('windows-1252')
|
sequence_name = sequence.name.decode('windows-1252')
|
||||||
sequence_keys = psa_reader.read_sequence_keys(sequence_name)
|
|
||||||
|
|
||||||
# Add keyframes for each frame of the sequence.
|
# Read the sequence data matrix from the PSA.
|
||||||
for frame_index in reversed(range(sequence.frame_count)):
|
start_datetime = datetime.datetime.now()
|
||||||
key_index = frame_index * len(import_bones)
|
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):
|
for bone_index, import_bone in enumerate(import_bones):
|
||||||
if import_bone is None:
|
if import_bone is None:
|
||||||
# bone does not exist in the armature, skip it
|
|
||||||
key_index += 1
|
|
||||||
continue
|
continue
|
||||||
|
bone_has_writeable_keyframes = any(keyframe_write_matrix[frame_index, bone_index])
|
||||||
# Convert world-space transforms to local-space transforms.
|
if bone_has_writeable_keyframes:
|
||||||
key_rotation = Quaternion(tuple(sequence_keys[key_index].rotation))
|
# This bone has writeable keyframes for this frame.
|
||||||
q = import_bone.post_quat.copy()
|
key_data = sequence_data_matrix[frame_index, bone_index]
|
||||||
q.rotate(import_bone.orig_quat)
|
# Calculate the local-space key data for the bone.
|
||||||
quat = q
|
start_datetime = datetime.datetime.now()
|
||||||
q = import_bone.post_quat.copy()
|
fcurve_data = calculate_fcurve_data(import_bone, key_data)
|
||||||
if import_bone.parent is None:
|
math_time += datetime.datetime.now() - start_datetime
|
||||||
q.rotate(key_rotation.conjugated())
|
for fcurve, should_write, datum in zip(import_bone.fcurves, keyframe_write_matrix[frame_index, bone_index], fcurve_data):
|
||||||
else:
|
if should_write:
|
||||||
q.rotate(key_rotation)
|
start_datetime = datetime.datetime.now()
|
||||||
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:
|
|
||||||
fcurve.keyframe_points.insert(frame_index, datum, options={'FAST'})
|
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
|
print(f'io_time: {io_time}')
|
||||||
|
print(f'math_time: {math_time}')
|
||||||
# Eliminate redundant final keyframe if the f-curve value is identical to the previous keyframe.
|
print(f'keyframe_time: {keyframe_time}')
|
||||||
for import_bone in filter(lambda x: x is not None, import_bones):
|
print(f'total_time: {total_time}')
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class PsaImportActionListItem(PropertyGroup):
|
class PsaImportActionListItem(PropertyGroup):
|
||||||
@@ -186,7 +205,6 @@ def on_psa_file_path_updated(property, context):
|
|||||||
try:
|
try:
|
||||||
# Read the file and populate the action list.
|
# Read the file and populate the action list.
|
||||||
p = os.path.abspath(context.scene.psa_import.psa_file_path)
|
p = os.path.abspath(context.scene.psa_import.psa_file_path)
|
||||||
print(p)
|
|
||||||
psa_reader = PsaReader(p)
|
psa_reader = PsaReader(p)
|
||||||
for sequence in psa_reader.sequences.values():
|
for sequence in psa_reader.sequences.values():
|
||||||
item = context.scene.psa_import.action_list.add()
|
item = context.scene.psa_import.action_list.add()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from .data import *
|
from .data import *
|
||||||
import ctypes
|
import ctypes
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
class PsaReader(object):
|
class PsaReader(object):
|
||||||
@@ -30,7 +31,19 @@ class PsaReader(object):
|
|||||||
data.append(data_class.from_buffer_copy(buffer, offset))
|
data.append(data_class.from_buffer_copy(buffer, offset))
|
||||||
offset += section.data_size
|
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.
|
""" Reads and returns the key data for a sequence.
|
||||||
|
|
||||||
:param sequence_name: The name of the sequence.
|
:param sequence_name: The name of the sequence.
|
||||||
|
|||||||
Reference in New Issue
Block a user