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 cf782b4b21 - Show all commits

View File

@ -530,7 +530,7 @@ 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.
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."""
@ -561,7 +561,7 @@ def _transformation_curves_gen(item, values_arrays, channel_keys):
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.
# Pre-get/calculate these to slightly reduce the work done inside the loop.
anim_compensation_matrix = item.anim_compensation_matrix
do_anim_compensation_matrix = bool(anim_compensation_matrix)
@ -602,7 +602,7 @@ def _transformation_curves_gen(item, values_arrays, channel_keys):
if do_restmat_inv:
mat = restmat_inv @ mat
# Now we have a virtual matrix of transform from AnimCurves, we can insert keyframes!
# Now we have a virtual matrix of transform from AnimCurves, we can yield keyframe values!
loc, rot, sca = decompose(mat)
if rot_mode == 'QUATERNION':
if rot_quat_prev.dot(rot) < 0.0:
@ -622,12 +622,13 @@ def _transformation_curves_gen(item, values_arrays, channel_keys):
def blen_read_animation_channel_curves(curves):
"""Read one or (rarely) more animation curves, that affect the same channel of the same property, from FBX data.
"""Read one or (very rarely) more animation curves, that affect a single same channel of a single property, from FBX
data.
When there are multiple curves, they will be combined into a single sorted animation curve.
Though, it is expected that there will almost never be more than a single curve to read because multiple curves
affecting the same channel of the same property is not part of FBX's default animation system.
It is expected that there will almost never be more than a single curve to read because FBX's default animation
system only uses the first curve assigned to a channel.
Returns an array of sorted, unique FBX keyframe times and an array of values for each of those keyframe times."""
if len(curves) > 1:
@ -652,7 +653,7 @@ def blen_read_animation_channel_curves(curves):
return blen_read_single_animation_curve(curves[0])
def _combine_curve_keyframes(times_and_values_tuples, initial_values):
def _combine_curve_keyframe_times(times_and_values_tuples, initial_values):
"""Combine multiple sorted animation curves, that affect different properties, such that every animation curve
contains the keyframes from every other curve, interpolating the values for the newly inserted keyframes in each
curve.
@ -711,14 +712,13 @@ def blen_read_invalid_animation_curve(key_times, key_values):
indexed_times = key_times[indices]
indexed_values = key_values[indices]
# Interpolate the value for each time in sorted_unique_times according to the times and values at each index and
# the previous index.
# Interpolate the value for each time in sorted_unique_times according to the times and values at each index and the
# previous index.
interpolated_values = np.empty_like(indexed_values)
# Where the index is 0, there's no previous value to interpolate from, so we set the value without
# interpolating.
# Because the indices are in increasing order, all zeroes must be at the start, so we can find the index of the
# last zero and use that to index with a slice instead of a boolean array for performance.
# Where the index is 0, there's no previous value to interpolate from, so we set the value without interpolating.
# Because the indices are in increasing order, all zeroes must be at the start, so we can find the index of the last
# zero and use that to index with a slice instead of a boolean array for performance.
# Equivalent to, but as a slice:
# idx_zero_mask = indices == 0
# idx_nonzero_mask = ~idx_zero_mask
@ -757,7 +757,6 @@ def blen_read_invalid_animation_curve(key_times, key_values):
def _convert_fbx_time_to_blender_time(key_times, blen_start_offset, fbx_start_offset, fps):
# todo: Could move this into blen_store_keyframes since it probably doesn't need to be used anywhere else
from .fbx_utils import FBX_KTIME
timefac = fps / FBX_KTIME
@ -788,8 +787,6 @@ def blen_read_single_animation_curve(fbx_curve):
if all_times_strictly_increasing:
return key_times, key_values
else:
# todo: Print something to the console warning that the animation curve was invalid.
# FBX will still read animation curves even if they are invalid.
return blen_read_invalid_animation_curve(key_times, key_values)
@ -815,8 +812,10 @@ def blen_store_keyframes_multi(fbx_key_times, fcurve_and_key_values_pairs, blen_
bl_enum_dtype = np.byte
# The keyframe_points 'co' are accessed as flattened pairs of (time, value).
# The key times are the same for each (blen_fcurve, key_values) pair, so only the values need to be updatedfor each array of values.
# The key times are the same for each (blen_fcurve, key_values) pair, so only the values need to be updated for each
# array of values.
keyframe_points_co = np.empty(len(bl_key_times) * 2, dtype=bl_keyframe_dtype)
# Even indices are times.
keyframe_points_co[0::2] = bl_key_times
interpolation_array = np.full(num_keys, LINEAR_INTERPOLATION_VALUE, dtype=bl_enum_dtype)
@ -824,6 +823,8 @@ def blen_store_keyframes_multi(fbx_key_times, fcurve_and_key_values_pairs, blen_
for blen_fcurve, key_values in fcurve_and_key_values_pairs:
# The fcurve must be newly created and thus have no keyframe_points.
assert(len(blen_fcurve.keyframe_points) == 0)
# Odd indices are values.
keyframe_points_co[1::2] = key_values
# Add the keyframe points to the FCurve and then set the 'co' and 'interpolation' of each point.
@ -906,8 +907,8 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
values = values / 100.0
blen_store_keyframes(fbx_key_times, blen_curve, values, anim_offset, fps)
# Store the minimum and maximum shape key values, so that the shape key's slider range can be expanded if
# necessary after reading all animations.
# Store the minimum and maximum shape key values, so that the shape key's slider range can be expanded
# if necessary after reading all animations.
deform_values.append(values.min())
deform_values.append(values.max())
@ -956,12 +957,16 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
times_and_values_tuples.append((fbx_key_times, values))
if not times_and_values_tuples:
# If `times_and_values_tuples` is empty, all the imported animation curves are for properties other than
# transformation (e.g. animated custom properties), so there is nothing to do until support for these other
# transformation (e.g. animated custom properties), so there is nothing to do until support for those other
# properties is added.
return
combined_fbx_times, values_arrays = _combine_curve_keyframes(times_and_values_tuples, initial_values)
# Combine the keyframe times of all the transformation curves so that each curve has a value at every time.
combined_fbx_times, values_arrays = _combine_curve_keyframe_times(times_and_values_tuples, initial_values)
# Convert from FBX Lcl Translation/Lcl Rotation/Lcl Scaling to the Blender location/rotation/scaling properties
# of this Object/PoseBone.
# The number of fcurves for the Blender properties varies depending on the rotation mode.
num_loc_channels = 3
num_rot_channels = 4 if rot_mode in {'QUATERNION', 'AXIS_ANGLE'} else 3 # Variations of EULER are all 3
num_sca_channels = 3
@ -969,8 +974,10 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
num_frames = len(combined_fbx_times)
full_length = num_channels * num_frames
# Do the conversion.
flattened_channel_values_gen = _transformation_curves_gen(item, values_arrays, channel_keys)
flattened_channel_values = np.fromiter(flattened_channel_values_gen, dtype=np.single, count=full_length)
# Reshape to one row per frame and then view the transpose so that each row corresponds to a single channel.
# e.g.
# loc_channels = channel_values[:num_loc_channels]
@ -978,6 +985,8 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
# sca_channels = channel_values[num_loc_channels + num_rot_channels:]
channel_values = flattened_channel_values.reshape(num_frames, num_channels).T
# Each channel has the same keyframe times, so the combined times can be passed once along with all the curves
# and values arrays.
blen_store_keyframes_multi(combined_fbx_times, zip(blen_curves, channel_values), anim_offset, fps)