FBX IO: Speed up animation import using NumPy #104856
@ -642,142 +642,6 @@ def _combine_same_property_curves(times_and_values_tuples):
|
||||
return times_and_values_tuples[0]
|
||||
|
||||
|
||||
def _interpolate_curves_linear(sorted_all_times, times_indices, times, values, initial_value):
|
||||
# Find the indices of all times that need their values to be interpolated
|
||||
needs_interpolation_mask = np.full(len(sorted_all_times), True)
|
||||
needs_interpolation_mask[times_indices] = False
|
||||
needs_interpolation_idx = np.flatnonzero(needs_interpolation_mask)
|
||||
|
||||
if not needs_interpolation_idx.size:
|
||||
# No indices need their values interpolated.
|
||||
# This can happen when a curve contains all keyframe times of all the other curves, a notable case would be
|
||||
# when all the imported curves have the same keyframe times.
|
||||
return values
|
||||
|
||||
# Create the extended values array that will contain `values` and the extra interpolated values for times in
|
||||
# `sorted_all_times` that are not in `times`.
|
||||
extended_values = np.empty_like(values, shape=len(sorted_all_times))
|
||||
|
||||
# Set the non-interpolated values
|
||||
extended_values[times_indices] = values
|
||||
|
||||
# We can use the fact that sorted_all_times, times_indices and times are all sorted and unique to perform linear
|
||||
# interpolation with a better scaling time complexity than np.interp, but np.interp is a C-compiled function and
|
||||
# will pretty much always outperform a step-by-step linear interpolation by calling various NumPy functions.
|
||||
interp_values = np.interp(sorted_all_times[needs_interpolation_idx], times, values, left=initial_value)
|
||||
|
||||
extended_values[needs_interpolation_idx] = interp_values
|
||||
extended_values[times_indices] = values
|
||||
return extended_values
|
||||
|
||||
|
||||
def _interpolate_curves(sorted_all_times, times_indices, _times, values, initial_value):
|
||||
extended_values = np.empty_like(values, shape=len(sorted_all_times))
|
||||
|
||||
# Because times was sorted, we can get the region within extended_values or sorted_all_times from the first
|
||||
# time in `times` to the last time in `times`.
|
||||
# Elements within this region may need interpolation.
|
||||
# Elements outside this region would result in extrapolation, which we do not do, instead setting an
|
||||
# `initial_value` or maintaining the last value in `values`
|
||||
interp_start_full_incl = times_indices[0]
|
||||
interp_end_full_excl = times_indices[-1] + 1
|
||||
|
||||
# Fill in the times that would result in extrapolation with their fixed values.
|
||||
extended_values[:interp_start_full_incl] = initial_value
|
||||
extended_values[interp_end_full_excl:] = values[-1]
|
||||
|
||||
# Get the regions of extended_values and sorted_all_times where interpolation will take place.
|
||||
extended_values_interp_region = extended_values[interp_start_full_incl:interp_end_full_excl]
|
||||
all_times_interp_region = sorted_all_times[interp_start_full_incl:interp_end_full_excl]
|
||||
|
||||
# The index in `extended_values_interp_region` of each value in `times`
|
||||
interp_region_times_indices = times_indices - times_indices[0]
|
||||
|
||||
# Fill in the times that already have values.
|
||||
# Same as `extended_values[times_indices] = values`.
|
||||
extended_values_interp_region[interp_region_times_indices] = values
|
||||
|
||||
# Construct a mask of the values within the interp_region that need interpolation
|
||||
needs_interpolation_mask = np.full(len(extended_values_interp_region), True, dtype=bool)
|
||||
needs_interpolation_mask[interp_region_times_indices] = False
|
||||
# When the number of elements needing interpolation is much smaller than the total number of elements, it can be
|
||||
# faster to calculate indices from the mask and then index using the indices instead of indexing using the mask.
|
||||
needs_interpolation_idx = np.flatnonzero(needs_interpolation_mask)
|
||||
|
||||
if not needs_interpolation_idx.size:
|
||||
# No times need interpolating, we're done.
|
||||
return extended_values
|
||||
|
||||
# Because both `all_times_sorted` and `times` are sorted, the index in `all_times_sorted` of each value in
|
||||
# `times` must be increasing. Using this fact, we can find the index of the previous and next non-interpolated
|
||||
# time for each interpolated time, by taking min/max accumulations across the indices of the non-interpolated
|
||||
# times.
|
||||
# This performs similarly to doing a binary search with np.searchsorted when `times` and `interp_times` are
|
||||
# small, but np.searchsorted scales worse with larger `times` and `interp_times`:
|
||||
# interp_times = all_times_interp_region[needs_interpolation_idx]
|
||||
# prev_indices = np.searchsorted(times, interp_times)
|
||||
# # This only works because `times` and `interp_times` are disjoint.
|
||||
# next_indices = prev_indices + 1
|
||||
# prev_times = times[prev_indices]
|
||||
# next_times = times[next_indices]
|
||||
# prev_values = values[prev_indices]
|
||||
# next_values = values[next_indices]
|
||||
# First create arrays of indices.
|
||||
prev_indices = np.arange(len(extended_values_interp_region))
|
||||
next_indices = prev_indices.copy()
|
||||
|
||||
# Example prev_indices
|
||||
# [0, 1, 2, 3, 4, 5, 6, 7]
|
||||
# Example needs_interpolation_mask:
|
||||
# [F, F, T, F, T, T, F, F]
|
||||
# Set interpolated times indices to zero (using needs_interpolation_idx for performance):
|
||||
# [0, 1, 0, 3, 0, 0, 6, 7]
|
||||
# maximum.accumulate:
|
||||
# [0, 1, 1, 3, 4, 4, 6, 7]
|
||||
# Extract the values at each index requiring interpolation (using needs_interpolation_idx for performance):
|
||||
# [ 1, 4, 4, ]
|
||||
# The extracted indices are the indices of the previous non-interpolated time/value.
|
||||
prev_indices[needs_interpolation_idx] = 0
|
||||
prev_indices = np.maximum.accumulate(prev_indices)[needs_interpolation_idx]
|
||||
|
||||
# The same as prev_value_indices, but using minimum and accumulating from right to left.
|
||||
# Example next_indices:
|
||||
# [0, 1, 2, 3, 4, 5, 6, 7]
|
||||
# Example needs_interpolation_mask:
|
||||
# [F, F, T, F, T, T, F, F]
|
||||
# Set interpolated times indices to the maximum index (using needs_interpolation_idx for performance):
|
||||
# [0, 1, 7, 3, 7, 7, 6, 7]
|
||||
# minimum.accumulate from right to left by creating a flipped view, running minimum.accumulate and then creating
|
||||
# a flipped view of the result:
|
||||
# flip:
|
||||
# [7, 6, 7, 7, 3, 7, 1, 0]
|
||||
# minimum.accumulate:
|
||||
# [7, 6, 6, 6, 3, 3, 1, 0]
|
||||
# flip:
|
||||
# [0, 1, 3, 3, 6, 6, 6, 7]
|
||||
# Extract the values at each index requiring interpolation (using needs_interpolation_idx for performance):
|
||||
# [ 3, 6, 6, ]
|
||||
# The extracted indices are the indices of the next non-interpolated time/value.
|
||||
next_indices[needs_interpolation_idx] = len(extended_values_interp_region) - 1
|
||||
next_indices = np.flip(np.minimum.accumulate(np.flip(next_indices)))[needs_interpolation_idx]
|
||||
|
||||
prev_times = all_times_interp_region[prev_indices]
|
||||
next_times = all_times_interp_region[next_indices]
|
||||
prev_values = extended_values_interp_region[prev_indices]
|
||||
next_values = extended_values_interp_region[next_indices]
|
||||
|
||||
# This linear interpolation is an example intended to be replaced with other kinds of interpolation once they are
|
||||
# supported.
|
||||
# - Begin linear interpolation
|
||||
interp_times = all_times_interp_region[needs_interpolation_idx]
|
||||
ifac = (interp_times - prev_times) / (next_times - prev_times)
|
||||
interp_values = ifac * (next_values - prev_values) + prev_values
|
||||
# - End linear interpolation
|
||||
|
||||
extended_values_interp_region[needs_interpolation_idx] = interp_values
|
||||
return extended_values
|
||||
|
||||
|
||||
def _combine_curve_keyframes(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
|
||||
@ -791,21 +655,19 @@ def _combine_curve_keyframes(times_and_values_tuples, initial_values):
|
||||
|
||||
all_times = [t[0] for t in times_and_values_tuples]
|
||||
|
||||
# Get sorted unique times and the index in sorted_all_times of each time in all_times
|
||||
sorted_all_times, all_times_indices = np.unique(np.concatenate(all_times), return_inverse=True)
|
||||
# Get the combined sorted unique times of all the curves.
|
||||
sorted_all_times = np.unique(np.concatenate(all_times))
|
||||
|
||||
# An alternative would be to concatenated filled arrays with the index of each array and then index that by perm,
|
||||
# then a mask for each array can be found by checking for values that equal the index of that array.
|
||||
values_arrays = []
|
||||
times_start = 0
|
||||
for (times, values), initial_value in zip(times_and_values_tuples, initial_values):
|
||||
times_end = times_start + len(times)
|
||||
# The index in `sorted_all_times` of each value in `times`.
|
||||
times_indices = all_times_indices[times_start:times_end]
|
||||
# Update times_start for the next array.
|
||||
times_start = times_end
|
||||
|
||||
extended_values = _interpolate_curves_linear(sorted_all_times, times_indices, times, values, initial_value)
|
||||
if sorted_all_times.size == times.size:
|
||||
# `sorted_all_times` will always contain all values in `times` and both `times` and `sorted_all_times` must
|
||||
# be strictly increasing, so if both arrays have the same size, they must be identical.
|
||||
extended_values = values
|
||||
else:
|
||||
# For now, linear interpolation is assumed. NumPy conveniently has a fast C-compiled function for this.
|
||||
# Efficiently implementing other FBX supported interpolation will most likely be much more complicated.
|
||||
extended_values = np.interp(sorted_all_times, times, values, left=initial_value)
|
||||
values_arrays.append(extended_values)
|
||||
return sorted_all_times, values_arrays
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user