FBX IO: Speed up transformation animation import #104870

Merged
Thomas Barlow merged 6 commits from Mysteryem/blender-addons:fbx_import_anim_numpy_p1.5 into main 2023-09-11 13:53:12 +02:00
Showing only changes of commit acffdf2725 - Show all commits

View File

@ -526,6 +526,56 @@ def blen_read_object_transform_preprocess(fbx_props, fbx_obj, rot_alt_mat, use_p
# ---------
# Animation
def _blen_read_object_transform_do_anim(transform_data, lcl_translation_mat, lcl_rot_euler, lcl_scale_mat):
"""Specialized version of blen_read_object_transform_do for animation that pre-calculates the non-animated matrices
and returns a function that returns base_mat @ geom_mat.
The lcl_translation_mat, lcl_rot_euler and lcl_scale_mat arguments should have their values updated each frame and
then the returned function will calculate the updated matrix."""
# translation
geom_loc = Matrix.Translation(transform_data.geom_loc)
# rotation
def to_rot(rot, rot_ord):
return Euler(convert_deg_to_rad_iter(rot), rot_ord).to_matrix().to_4x4()
pre_rot = to_rot(transform_data.pre_rot, 'XYZ')
pst_rot = to_rot(transform_data.pst_rot, 'XYZ')
pst_rot_inv = pst_rot.inverted_safe()
geom_rot = to_rot(transform_data.geom_rot, 'XYZ')
rot_ofs = Matrix.Translation(transform_data.rot_ofs)
rot_piv = Matrix.Translation(transform_data.rot_piv)
rot_piv_inv = rot_piv.inverted_safe()
sca_ofs = Matrix.Translation(transform_data.sca_ofs)
sca_piv = Matrix.Translation(transform_data.sca_piv)
sca_piv_inv = sca_piv.inverted_safe()
# scale
geom_scale = Matrix()
geom_scale[0][0], geom_scale[1][1], geom_scale[2][2] = transform_data.geom_sca
# Some matrices can be combined in advance, using the associative property of matrix multiplication, so that less
# matrix multiplication is required each frame.
geom_mat = geom_loc @ geom_rot @ geom_scale
post_lcl_translation = rot_ofs @ rot_piv @ pre_rot
post_lcl_rotation = transform_data.rot_alt_mat @ pst_rot_inv @ rot_piv_inv @ sca_ofs @ sca_piv
post_lcl_scaling = sca_piv_inv @ geom_mat
# Get the bound to_matrix method to avoid re-binding it on each call.
lcl_rot_to_matrix = lcl_rot_euler.to_matrix
# Get the unbound Matrix.to_4x4 method to avoid having to look it up again on each call.
matrix_to_4x4 = Matrix.to_4x4
return lambda: (lcl_translation_mat @
post_lcl_translation @
matrix_to_4x4(lcl_rot_to_matrix()) @
post_lcl_rotation @
lcl_scale_mat @
post_lcl_scaling)
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.
@ -550,21 +600,39 @@ def _transformation_curves_gen(item, values_arrays, channel_keys):
# Pre-compute inverted local rest matrix of the bone, if relevant.
restmat_inv = item.get_bind_matrix().inverted_safe() if item.is_bone else None
transform_prop_to_attr = {
b'Lcl Translation': transform_data.loc,
b'Lcl Rotation': transform_data.rot,
b'Lcl Scaling': transform_data.sca,
}
# Create matrices/euler from the initial transformation values of this item.
# These variables will be updated in-place as we iterate through each frame.
translation_matrix = Matrix.Translation(transform_data.loc)
rotation_euler = Euler(transform_data.rot, transform_data.rot_ord)
scaling_matrix = Matrix()
scaling_matrix[0][0], scaling_matrix[1][1], scaling_matrix[2][2] = transform_data.sca
# Create setters into translation_matrix, rotation_euler and scaling_matrix for each values_array and convert any
# rotation values into radians.
setters = []
values_arrays_converted = []
for values_array, (fbx_prop, channel) in zip(values_arrays, channel_keys):
if fbx_prop == b'Lcl Translation':
# translation_matrix.translation[channel] = value
setter = partial(setitem, translation_matrix.translation, channel)
elif fbx_prop == b'Lcl Rotation':
# FBX rotations are in degrees, but Blender uses radians, so convert all rotation values in advance.
values_array = np.deg2rad(values_array)
# rotation_euler[channel] = value
setter = partial(setitem, rotation_euler, channel)
else:
assert(fbx_prop == b'Lcl Scaling')
# scaling_matrix[channel][channel] = value
setter = partial(setitem, scaling_matrix[channel], channel)
setters.append(setter)
values_arrays_converted.append(values_array)
# 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].
setters = [partial(setitem, transform_prop_to_attr[fbx_prop], channel) for fbx_prop, channel in channel_keys]
# Create an iterator that gets one value from each array. Each iterated tuple will be all the imported
# Lcl Translation/Lcl Rotation/Lcl Scaling values for a single frame, in that order.
# Note that an FBX animation does not have to animate all the channels, so only the animated channels of each
# property will be present.
# .data, the memoryview of an np.ndarray, is faster to iterate than the ndarray itself.
frame_values_it = zip(*(arr.data for arr in values_arrays))
frame_values_it = zip(*(arr.data for arr in values_arrays_converted))
# Pre-get/calculate these to slightly reduce the work done inside the loop.
anim_compensation_matrix = item.anim_compensation_matrix
@ -582,6 +650,8 @@ def _transformation_curves_gen(item, values_arrays, channel_keys):
to_axis_angle = Quaternion.to_axis_angle
to_euler = Quaternion.to_euler
calc_mat = _blen_read_object_transform_do_anim(transform_data, translation_matrix, rotation_euler, scaling_matrix)
# Iterate through the values for each frame.
for frame_values in frame_values_it:
# Set each value into its corresponding attribute in transform_data.
@ -589,7 +659,7 @@ def _transformation_curves_gen(item, values_arrays, channel_keys):
setter(value)
# Calculate the updated matrix for this frame.
mat, _, _ = blen_read_object_transform_do(transform_data)
mat = calc_mat()
# compensate for changes in the local matrix during processing
if do_anim_compensation_matrix: