FBX IO: Speed up animation import using NumPy #104856
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user