FBX IO: Speed up animation import using NumPy #104856

Merged
Thomas Barlow merged 12 commits from Mysteryem/blender-addons:fbx_import_anim_numpy_p1 into main 2023-09-04 22:07:45 +02:00
Showing only changes of commit fded8b6eb5 - Show all commits

View File

@ -16,7 +16,7 @@ if "bpy" in locals():
import bpy
from bpy.app.translations import pgettext_tip as tip_
from mathutils import Matrix, Euler, Vector
from mathutils import Matrix, Euler, Vector, Quaternion
# Also imported in .fbx_utils, so importing here is unlikely to further affect Blender startup time.
import numpy as np
@ -527,8 +527,16 @@ def blen_read_object_transform_preprocess(fbx_props, fbx_obj, rot_alt_mat, use_p
# ---------
# Animation
def _transformation_curves_gen(item, values_arrays, channel_keys):
"""Yields flattened location/rotation/scaling values for imported PoseBone/Object Lcl Translation/Rotation/Scaling
animation curve values.
The value arrays must have the same lengths where each index of each array corresponds to a single keyframe.
Each value array must have a corresponding channel key tuple that identifies the fbx property
(b'Lcl Translation'/b'Lcl Rotation'/b'Lcl Scaling') and the channel (x/y/z as 0/1/2) of that property."""
from operator import setitem
from functools import partial
if item.is_bone:
bl_obj = item.bl_obj.pose.bones[item.bl_bone]
else:
@ -548,6 +556,11 @@ def _transformation_curves_gen(item, values_arrays, channel_keys):
b'Lcl Scaling': transform_data.sca,
}
# Create a setter into transform_data for each values array. e.g. a values array for 'Lcl Scaling' with channel == 2
# would set transform_data.sca[2].
Review

transform_data.scale[2] I believe?

`transform_data.scale[2]` I believe?
Review

In this case .sca is correct, the FBXTransformData namedtuple uses rather short attribute names.

In this case `.sca` is correct, the `FBXTransformData` namedtuple uses rather short attribute names.
setters = [partial(setitem, transform_prop_to_attr[fbx_prop], channel) for fbx_prop, channel in channel_keys]
frame_values_it = zip(*(iter(arr.data) for arr in values_arrays))
# Pre-get/calculate these to reduce the work done inside the hot loop.
anim_compensation_matrix = item.anim_compensation_matrix
do_anim_compensation_matrix = bool(anim_compensation_matrix)
@ -560,24 +573,17 @@ def _transformation_curves_gen(item, values_arrays, channel_keys):
do_restmat_inv = bool(restmat_inv)
# Create a setter into transform_data for each values array. e.g. a values array for 'Lcl Scaling' with channel == 2
# would set transform_data.sca[2].
# TODO: Might be faster to create a list of each transform_prop_to_attr[fbx_prop] and a list of channels, then zip
# both and in the main loop, do transform_data_attr[channel] = value
setters = [partial(setitem, transform_prop_to_attr[fbx_prop], channel) for fbx_prop, channel in channel_keys]
zipped_values_iterators = zip(*(iter(arr.data) for arr in values_arrays))
decompose = Matrix.decompose
to_axis_angle = Quaternion.to_axis_angle
to_euler = Quaternion.to_euler
# todo: Rather than having to get the Matrix/Quaternion methods upon each call within the loop, we can instead get
# them in advance.
# Before the loop:
# `mat_decompose = Matrix.decompose`
# then within the loop:
# `mat_decompose(mat)`
for values in zipped_values_iterators:
for setter, value in zip(setters, values):
# Iterate through the values for each frame.
for frame_values in frame_values_it:
# Set each value into its corresponding attribute in transform_data.
for setter, value in zip(setters, frame_values):
setter(value)
# Calculate the updated matrix for this frame.
mat, _, _ = blen_read_object_transform_do(transform_data)
# compensate for changes in the local matrix during processing
@ -597,16 +603,16 @@ def _transformation_curves_gen(item, values_arrays, channel_keys):
mat = restmat_inv @ mat
# Now we have a virtual matrix of transform from AnimCurves, we can insert keyframes!
loc, rot, sca = mat.decompose()
loc, rot, sca = decompose(mat)
if rot_mode == 'QUATERNION':
if rot_quat_prev.dot(rot) < 0.0:
rot = -rot
rot_quat_prev = rot
elif rot_mode == 'AXIS_ANGLE':
vec, ang = rot.to_axis_angle()
vec, ang = to_axis_angle(rot)
rot = ang, vec.x, vec.y, vec.z
else: # Euler
rot = rot.to_euler(rot_mode, rot_eul_prev)
rot = to_euler(rot, rot_mode, rot_eul_prev)
rot_eul_prev = rot
# Yield order matches the order that the location/rotation/scale FCurves are created in.