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

View File

@ -50,6 +50,8 @@ from .fbx_utils import (
expand_shape_key_range,
)
LINEAR_INTERPOLATION_VALUE = bpy.types.Keyframe.bl_rna.properties['interpolation'].enum_items['LINEAR'].value
# global singleton, assign on execution
fbx_elem_nil = None
@ -766,11 +768,6 @@ def blen_read_single_animation_curve(fbx_curve):
"""Read a single animation curve from FBX data.
The parsed keyframe times are guaranteed to be strictly increasing."""
# TODO: Remove these, we can do all time conversion at the very end, just before combining times and values into a
# single array
# from .fbx_utils import FBX_KTIME
# timefac = fps / FBX_KTIME
key_times = parray_as_ndarray(elem_prop_first(elem_find_first(fbx_curve, b'KeyTime')))
key_values = parray_as_ndarray(elem_prop_first(elem_find_first(fbx_curve, b'KeyValueFloat')))
@ -788,30 +785,6 @@ def blen_read_single_animation_curve(fbx_curve):
# FBX will still read animation curves even if they are invalid.
return blen_read_invalid_animation_curve(key_times, key_values)
# todo When we have transformation curves (or more than one curve per channel (optional support)) separately combine
# singular parsed curves and fill in the gaps with linear interpolation. .concatenate and .unique the key_times
# arrays with return_inverse=True. Use the lengths of each key_times array and their order in the concatenation to
# get the index of each of their elements in the sorted, unique concatenation.
# For each key_times array, create an all True array and use those indices to set values to False.
# Copy the sorted, unique concatenation and use this new mask to effectively delete all times that didn't come from
# this key_times array. Use .maximum.accumulate and a reversed .minimum.accumulate to get the first time before and
# first time after each time that needs its value to be interpolated. These two arrays get the start and end times
# to interpolate from. For each time that needs its value to be interpolated, get the values for the start and end
# times and then use those and the times that needs their values interpolated to calculate the interpolated values.
# Care will need to be taken for times where there is no first value before or where there is no first value after,
# in which case interpolation can't take place and we'll either need to start set values at the very start and end
# or otherwise fill the values that can't be interpolated with a default value or the first/last value in
# key_times.
if not all_times_strictly_increasing:
# We try to match how FBX behaves when it encounters an invalid KeyTime array. This doesn't quite match when the
# maximum value is not the last value (FBX discards some keyframes whereas we don't), but it's close enough.
# Start the curve from the index of the smallest KeyTime value.
min_idx = np.amin(key_times) if key_times.size else 0
key_times = key_times[min_idx:]
key_values = key_values[min_idx:]
max_idx = np.amax(key_times) if key_times.size else 0
# If the largest KeyTime value is at the last index then it's simple.
if max_idx == key_times.size - 1:
@ -990,76 +963,45 @@ def blen_read_single_animation_curve(fbx_curve):
return key_times, key_values
def blen_store_keyframes(blen_fcurve, key_times, key_values):
def blen_store_keyframes(fbx_key_times, blen_fcurve, key_values, blen_start_offset, fps, fbx_start_offset=0):
"""Set all keyframe times and values for a newly created FCurve.
Linear interpolation is currently assumed.
This is a convenience function for calling blen_store_keyframes_multi with only a single fcurve and values array."""
blen_store_keyframes_multi(fbx_key_times, [(blen_fcurve, key_values)], blen_start_offset, fps, fbx_start_offset)
def blen_store_keyframes_multi(fbx_key_times, fcurve_and_key_values_pairs, blen_start_offset, fps, fbx_start_offset=0):
"""Set all keyframe times and values for multiple pairs of newly created FCurves and keyframe values arrays, where
each pair has the same keyframe times.
Linear interpolation is currently assumed."""
# The fcurve must be newly created and thus have no keyframe_points.
assert(len(blen_fcurve.keyframe_points) == 0)
num_keys = len(key_times)
bl_key_times = _convert_fbx_time_to_blender_time(fbx_key_times, blen_start_offset, fbx_start_offset, fps)
num_keys = len(bl_key_times)
# Compatible with C float type
bl_keyframe_dtype = np.single
# Compatible with C char type
bl_enum_dtype = np.byte
# TODO: get this value once and store it as a global variable
linear_enum_value = bpy.types.Keyframe.bl_rna.properties['interpolation'].enum_items['LINEAR'].value
# 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.
keyframe_points_co = np.empty(len(bl_key_times) * 2, dtype=bl_keyframe_dtype)
keyframe_points_co[0::2] = bl_key_times
# Stack the arrays into a flattened array of flattened (frame, value) pairs
# Same as `np.column_stack(key_times, key_values).ravel()`, but allows specifying the dtype.
full_key_frame_array = np.concatenate((key_times.reshape(-1, 1), key_values.reshape(-1, 1)),
dtype=bl_keyframe_dtype, casting='unsafe', axis=1).ravel()
interpolation_array = np.full(num_keys, LINEAR_INTERPOLATION_VALUE, dtype=bl_enum_dtype)
# Add the keyframe points to the FCurve and then set the 'co' and 'interpolation' of each point.
blen_fcurve.keyframe_points.add(num_keys)
blen_fcurve.keyframe_points.foreach_set('co', full_key_frame_array.ravel())
blen_fcurve.keyframe_points.foreach_set('interpolation', np.full(num_keys, linear_enum_value, dtype=bl_enum_dtype))
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)
keyframe_points_co[1::2] = key_values
# Since we inserted our keyframes in 'ultra-fast' mode, we have to update the fcurves now.
blen_fcurve.update()
# Add the keyframe points to the FCurve and then set the 'co' and 'interpolation' of each point.
blen_fcurve.keyframe_points.add(num_keys)
blen_fcurve.keyframe_points.foreach_set('co', keyframe_points_co)
blen_fcurve.keyframe_points.foreach_set('interpolation', interpolation_array)
# TODO: Remove this function
def blen_read_animations_curves_iter(fbx_curves, blen_start_offset, fbx_start_offset, fps):
"""
Get raw FBX AnimCurve list, and yield values for all curves at each singular curves' keyframes,
together with (blender) timing, in frames.
blen_start_offset is expected in frames, while fbx_start_offset is expected in FBX ktime.
"""
# As a first step, assume linear interpolation between key frames, we'll (try to!) handle more
# of FBX curves later.
from .fbx_utils import FBX_KTIME
timefac = fps / FBX_KTIME
curves = tuple([0,
elem_prop_first(elem_find_first(c[2], b'KeyTime')),
elem_prop_first(elem_find_first(c[2], b'KeyValueFloat')),
c]
for c in fbx_curves)
allkeys = sorted({item for sublist in curves for item in sublist[1]})
for curr_fbxktime in allkeys:
curr_values = []
for item in curves:
idx, times, values, fbx_curve = item
if times[idx] < curr_fbxktime:
if idx >= 0:
idx += 1
if idx >= len(times):
# We have reached our last element for this curve, stay on it from now on...
idx = -1
item[0] = idx
if times[idx] >= curr_fbxktime:
if idx == 0:
curr_values.append((values[idx], fbx_curve))
else:
# Interpolate between this key and the previous one.
ifac = (curr_fbxktime - times[idx - 1]) / (times[idx] - times[idx - 1])
curr_values.append(((values[idx] - values[idx - 1]) * ifac + values[idx - 1], fbx_curve))
curr_blenkframe = (curr_fbxktime - fbx_start_offset) * timefac + blen_start_offset
yield (curr_blenkframe, curr_values)
# Since we inserted our keyframes in 'ultra-fast' mode, we have to update the fcurves now.
blen_fcurve.update()
def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, global_scale, shape_key_deforms):
@ -1155,10 +1097,11 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
for channel, curves in channel_to_curves.items():
assert(channel in {0, 1, 2})
blen_curve = blen_curves[channel]
parsed_curves = tuple(map(blen_read_single_animation_curve, curves))
fbx_key_times, values = _combine_same_property_curves(parsed_curves)
bl_key_times = _convert_fbx_time_to_blender_time(fbx_key_times, anim_offset, 0, fps)
blen_store_keyframes(blen_curve, bl_key_times, values)
blen_store_keyframes(fbx_key_times, blen_curve, values, anim_offset, fps)
elif isinstance(item, ShapeKey):
deform_values = shape_key_deforms.setdefault(item, [])
@ -1167,12 +1110,13 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
for channel, curves in channel_to_curves.items():
assert(channel == 0)
blen_curve = blen_curves[channel]
parsed_curves = tuple(map(blen_read_single_animation_curve, curves))
fbx_key_times, values = _combine_same_property_curves(parsed_curves)
bl_key_times = _convert_fbx_time_to_blender_time(fbx_key_times, anim_offset, 0, fps)
# A fully activated shape key in FBX DeformPercent is 100.0 whereas it is 1.0 in Blender.
values = values / 100.0
blen_store_keyframes(blen_curve, bl_key_times, values)
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.
deform_values.append(values.min())
@ -1186,14 +1130,14 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
assert(channel == 0)
# The indices are determined by the creation of the `props` list above.
blen_curve = blen_curves[1 if is_focus_distance else 0]
parsed_curves = tuple(map(blen_read_single_animation_curve, curves))
fbx_key_times, values = _combine_same_property_curves(parsed_curves)
bl_key_times = _convert_fbx_time_to_blender_time(fbx_key_times, anim_offset, 0, fps)
if is_focus_distance:
# Remap the imported values from FBX to Blender.
values = values / 1000.0
values *= global_scale
blen_store_keyframes(blen_curve, bl_key_times, values)
blen_store_keyframes(fbx_key_times, blen_curve, values, anim_offset, fps)
else: # Object or PoseBone:
transform_data = item.fbx_transform_data
@ -1223,13 +1167,14 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
initial_values.append(transform_prop_to_attr[fbxprop][channel])
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
# properties is added.
return
combined_fbx_times, values_arrays = _combine_curve_keyframes(times_and_values_tuples, initial_values)
bl_key_times = _convert_fbx_time_to_blender_time(combined_fbx_times, anim_offset, 0, fps)
flattened_channel_values_gen = _transformation_curves_gen(item, values_arrays, channel_keys)
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
@ -1237,7 +1182,7 @@ 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
# TODO: It may be beneficial to iterate into np.float64 since the generator yields Python floats
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.
@ -1246,9 +1191,7 @@ 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
for blen_curve, values in zip(blen_curves, channel_values):
# TODO: The bl_key_times is used more than once, meaning we duplicate some of the work
blen_store_keyframes(blen_curve, bl_key_times, values)
blen_store_keyframes_multi(combined_fbx_times, zip(blen_curves, channel_values), anim_offset, fps)
def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, anim_offset, global_scale):