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 691c3d0c45 - Show all commits

View File

@ -529,22 +529,25 @@ def blen_read_object_transform_preprocess(fbx_props, fbx_obj, rot_alt_mat, use_p
def _blen_read_object_transform_do_anim(transform_data, lcl_translation_mat, lcl_rot_euler, lcl_scale_mat, def _blen_read_object_transform_do_anim(transform_data, lcl_translation_mat, lcl_rot_euler, lcl_scale_mat,
extra_pre_matrix, extra_post_matrix): extra_pre_matrix, extra_post_matrix):
"""Specialized version of blen_read_object_transform_do for animation that pre-calculates the non-animated matrices """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. and returns a function that calculates (base_mat @ geom_mat). See the comments in blen_read_object_transform_do for
a full description of what this function is doing.
The lcl_translation_mat, lcl_rot_euler and lcl_scale_mat arguments should have their values updated each frame and 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.""" then calling the returned function will calculate the matrix for the current frame.
# translation
extra_pre_matrix and extra_post_matrix are any extra matrices to multiply first/last."""
# Translation
geom_loc = Matrix.Translation(transform_data.geom_loc) geom_loc = Matrix.Translation(transform_data.geom_loc)
# rotation # Rotation
def to_rot(rot, rot_ord): def to_rot_xyz(rot):
return Euler(convert_deg_to_rad_iter(rot), rot_ord).to_matrix().to_4x4() # All the rotations that can be precalculated have a fixed XYZ order.
return Euler(convert_deg_to_rad_iter(rot), 'XYZ').to_matrix().to_4x4()
pre_rot = to_rot(transform_data.pre_rot, 'XYZ') pre_rot = to_rot_xyz(transform_data.pre_rot)
pst_rot = to_rot(transform_data.pst_rot, 'XYZ') pst_rot_inv = to_rot_xyz(transform_data.pst_rot).inverted_safe()
pst_rot_inv = pst_rot.inverted_safe() geom_rot = to_rot_xyz(transform_data.geom_rot)
geom_rot = to_rot(transform_data.geom_rot, 'XYZ')
# Offsets and pivots
rot_ofs = Matrix.Translation(transform_data.rot_ofs) rot_ofs = Matrix.Translation(transform_data.rot_ofs)
rot_piv = Matrix.Translation(transform_data.rot_piv) rot_piv = Matrix.Translation(transform_data.rot_piv)
rot_piv_inv = rot_piv.inverted_safe() rot_piv_inv = rot_piv.inverted_safe()
@ -552,37 +555,36 @@ def _blen_read_object_transform_do_anim(transform_data, lcl_translation_mat, lcl
sca_piv = Matrix.Translation(transform_data.sca_piv) sca_piv = Matrix.Translation(transform_data.sca_piv)
sca_piv_inv = sca_piv.inverted_safe() sca_piv_inv = sca_piv.inverted_safe()
# scale # Scale
geom_scale = Matrix() geom_scale = Matrix()
geom_scale[0][0], geom_scale[1][1], geom_scale[2][2] = transform_data.geom_sca 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 # Some matrices can be combined in advance, using the associative property of matrix multiplication, so that less
# matrix multiplication is required each frame. # matrix multiplication is required each frame.
geom_mat = geom_loc @ geom_rot @ geom_scale geom_mat = geom_loc @ geom_rot @ geom_scale
post_lcl_translation = rot_ofs @ rot_piv @ pre_rot 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_rotation = transform_data.rot_alt_mat @ pst_rot_inv @ rot_piv_inv @ sca_ofs @ sca_piv
post_lcl_scaling = sca_piv_inv @ geom_mat @ extra_post_matrix post_lcl_scaling = sca_piv_inv @ geom_mat @ extra_post_matrix
# Get the bound to_matrix method to avoid re-binding it on each call. # Get the bound to_matrix method to avoid re-binding it on each call.
lcl_rot_to_matrix = lcl_rot_euler.to_matrix lcl_rot_euler_to_matrix_3x3 = lcl_rot_euler.to_matrix
# Get the unbound Matrix.to_4x4 method to avoid having to look it up again on each call. # Get the unbound Matrix.to_4x4 method to avoid having to look it up again on each call.
matrix_to_4x4 = Matrix.to_4x4 matrix_to_4x4 = Matrix.to_4x4
if extra_pre_matrix != Matrix(): if extra_pre_matrix == Matrix():
# There aren't any other matrices that must be multiplied before lcl_translation_mat that extra_pre_matrix can # There aren't any other matrices that must be multiplied before lcl_translation_mat that extra_pre_matrix can
# be combined with, so a separate lambda is required. # be combined with, so skip extra_pre_matrix when it's the identity matrix.
return lambda: (extra_pre_matrix @ return lambda: (lcl_translation_mat @
lcl_translation_mat @
post_lcl_translation @ post_lcl_translation @
matrix_to_4x4(lcl_rot_to_matrix()) @ matrix_to_4x4(lcl_rot_euler_to_matrix_3x3()) @
post_lcl_rotation @ post_lcl_rotation @
lcl_scale_mat @ lcl_scale_mat @
post_lcl_scaling) post_lcl_scaling)
else: else:
return lambda: (lcl_translation_mat @ return lambda: (extra_pre_matrix @
lcl_translation_mat @
post_lcl_translation @ post_lcl_translation @
matrix_to_4x4(lcl_rot_to_matrix()) @ matrix_to_4x4(lcl_rot_euler_to_matrix_3x3()) @
post_lcl_rotation @ post_lcl_rotation @
lcl_scale_mat @ lcl_scale_mat @
post_lcl_scaling) post_lcl_scaling)
@ -609,48 +611,46 @@ def _transformation_curves_gen(item, values_arrays, channel_keys):
rot_eul_prev = bl_obj.rotation_euler.copy() rot_eul_prev = bl_obj.rotation_euler.copy()
rot_quat_prev = bl_obj.rotation_quaternion.copy() rot_quat_prev = bl_obj.rotation_quaternion.copy()
# Pre-compute combined pre matrix # Pre-compute combined pre-matrix
# Remove that rest pose matrix from current matrix (also in parent space) by computing the inverted local rest # Remove that rest pose matrix from current matrix (also in parent space) by computing the inverted local rest
# matrix of the bone, if relevant. # matrix of the bone, if relevant.
combined_pre_matrix = item.get_bind_matrix().inverted_safe() if item.is_bone else Matrix() combined_pre_matrix = item.get_bind_matrix().inverted_safe() if item.is_bone else Matrix()
# item.pre_matrix will contain any correction for a parent's correction matrix or the global matrix
# pre-matrix will contain any correction for a parent's correction matrix or the global matrix
if item.pre_matrix: if item.pre_matrix:
combined_pre_matrix @= item.pre_matrix combined_pre_matrix @= item.pre_matrix
# Pre-compute combined post matrix # Pre-compute combined post-matrix
# compensate for changes in the local matrix during processing # Compensate for changes in the local matrix during processing
combined_post_matrix = item.anim_compensation_matrix if item.anim_compensation_matrix else Matrix() combined_post_matrix = item.anim_compensation_matrix if item.anim_compensation_matrix else Matrix()
# item.post_matrix will contain any correction for lights, camera and bone orientation
# post-matrix will contain any correction for lights, camera and bone orientation
if item.post_matrix: if item.post_matrix:
combined_post_matrix @= item.post_matrix combined_post_matrix @= item.post_matrix
# Create matrices/euler from the initial transformation values of this item. # Create matrices/euler from the initial transformation values of this item.
# These variables will be updated in-place as we iterate through each frame. # These variables will be updated in-place as we iterate through each frame.
translation_matrix = Matrix.Translation(transform_data.loc) lcl_translation_mat = Matrix.Translation(transform_data.loc)
rotation_euler = Euler(transform_data.rot, transform_data.rot_ord) lcl_rotation_eul = Euler(transform_data.rot, transform_data.rot_ord)
scaling_matrix = Matrix() lcl_scaling_mat = Matrix()
scaling_matrix[0][0], scaling_matrix[1][1], scaling_matrix[2][2] = transform_data.sca lcl_scaling_mat[0][0], lcl_scaling_mat[1][1], lcl_scaling_mat[2][2] = transform_data.sca
# Create setters into translation_matrix, rotation_euler and scaling_matrix for each values_array and convert any # Create setters into lcl_translation_mat, lcl_rotation_eul and lcl_scaling_mat for each values_array and convert
# rotation values into radians. # any rotation values into radians.
setters = [] lcl_setters = []
values_arrays_converted = [] values_arrays_converted = []
for values_array, (fbx_prop, channel) in zip(values_arrays, channel_keys): for values_array, (fbx_prop, channel) in zip(values_arrays, channel_keys):
if fbx_prop == b'Lcl Translation': if fbx_prop == b'Lcl Translation':
# translation_matrix.translation[channel] = value # lcl_translation_mat.translation[channel] = value
setter = partial(setitem, translation_matrix.translation, channel) setter = partial(setitem, lcl_translation_mat.translation, channel)
elif fbx_prop == b'Lcl Rotation': elif fbx_prop == b'Lcl Rotation':
# FBX rotations are in degrees, but Blender uses radians, so convert all rotation values in advance. # FBX rotations are in degrees, but Blender uses radians, so convert all rotation values in advance.
values_array = np.deg2rad(values_array) values_array = np.deg2rad(values_array)
# rotation_euler[channel] = value # lcl_rotation_eul[channel] = value
setter = partial(setitem, rotation_euler, channel) setter = partial(setitem, lcl_rotation_eul, channel)
else: else:
assert(fbx_prop == b'Lcl Scaling') assert(fbx_prop == b'Lcl Scaling')
# scaling_matrix[channel][channel] = value # lcl_scaling_mat[channel][channel] = value
setter = partial(setitem, scaling_matrix[channel], channel) setter = partial(setitem, lcl_scaling_mat[channel], channel)
setters.append(setter) lcl_setters.append(setter)
values_arrays_converted.append(values_array) values_arrays_converted.append(values_array)
# Create an iterator that gets one value from each array. Each iterated tuple will be all the imported # Create an iterator that gets one value from each array. Each iterated tuple will be all the imported
@ -660,33 +660,36 @@ def _transformation_curves_gen(item, values_arrays, channel_keys):
# .data, the memoryview of an np.ndarray, is faster to iterate than the ndarray itself. # .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_converted)) frame_values_it = zip(*(arr.data for arr in values_arrays_converted))
decompose = Matrix.decompose # Getting the unbound methods in advance avoids having to look them up again on each call within the loop.
to_axis_angle = Quaternion.to_axis_angle mat_decompose = Matrix.decompose
to_euler = Quaternion.to_euler quat_to_axis_angle = Quaternion.to_axis_angle
quat_to_euler = Quaternion.to_euler
quat_dot = Quaternion.dot
calc_mat = _blen_read_object_transform_do_anim(transform_data, translation_matrix, rotation_euler, scaling_matrix, calc_mat = _blen_read_object_transform_do_anim(transform_data,
lcl_translation_mat, lcl_rotation_eul, lcl_scaling_mat,
combined_pre_matrix, combined_post_matrix) combined_pre_matrix, combined_post_matrix)
# Iterate through the values for each frame. # Iterate through the values for each frame.
for frame_values in frame_values_it: for frame_values in frame_values_it:
# Set each value into its corresponding attribute in transform_data. # Set each value into its corresponding lcl matrix/euler.
for setter, value in zip(setters, frame_values): for lcl_setter, value in zip(lcl_setters, frame_values):
setter(value) lcl_setter(value)
# Calculate the updated matrix for this frame. # Calculate the updated matrix for this frame.
mat = calc_mat() mat = calc_mat()
# Now we have a virtual matrix of transform from AnimCurves, we can yield keyframe values! # Now we have a virtual matrix of transform from AnimCurves, we can yield keyframe values!
loc, rot, sca = decompose(mat) loc, rot, sca = mat_decompose(mat)
if rot_mode == 'QUATERNION': if rot_mode == 'QUATERNION':
if rot_quat_prev.dot(rot) < 0.0: if quat_dot(rot_quat_prev, rot) < 0.0:
rot = -rot rot = -rot
rot_quat_prev = rot rot_quat_prev = rot
elif rot_mode == 'AXIS_ANGLE': elif rot_mode == 'AXIS_ANGLE':
vec, ang = to_axis_angle(rot) vec, ang = quat_to_axis_angle(rot)
rot = ang, vec.x, vec.y, vec.z rot = ang, vec.x, vec.y, vec.z
else: # Euler else: # Euler
rot = to_euler(rot, rot_mode, rot_eul_prev) rot = quat_to_euler(rot, rot_mode, rot_eul_prev)
rot_eul_prev = rot rot_eul_prev = rot
# Yield order matches the order that the location/rotation/scale FCurves are created in. # Yield order matches the order that the location/rotation/scale FCurves are created in.