FBX IO: Speed up animation simplification using NumPy #104904

Merged
Thomas Barlow merged 17 commits from Mysteryem/blender-addons:fbx_anim_export_numpy_simplify into blender-v4.0-release 2023-10-06 17:53:02 +02:00
Showing only changes of commit 0fb53e49f0 - Show all commits

View File

@ -1318,88 +1318,154 @@ class AnimationCurveNodeWrapper:
min_reldiff_fac = fac * 1.0e-3 # min relative value evolution: 0.1% of current 'order of magnitude'. min_reldiff_fac = fac * 1.0e-3 # min relative value evolution: 0.1% of current 'order of magnitude'.
min_absdiff_fac = 0.1 # A tenth of reldiff... min_absdiff_fac = 0.1 # A tenth of reldiff...
for values, frame_write_mask in zip(self._frame_values_array, self._frame_write_mask_array): # Initialise to no values enabled for writing.
# Initialise to no frames written. self._frame_write_mask_array[:] = False
frame_write_mask[:] = False
abs_values = np.abs(values) # Values are enabled for writing if they differ enough from either of their adjacent values or if they differ
# enough from the closest previous value that is enabled due to either of these conditions.
for sampled_values, enabled_mask in zip(self._frame_values_array, self._frame_write_mask_array):
# Create overlapping views of the 'previous' (all but the last) and 'current' (all but the first)
# `sampled_values` and `enabled_mask`.
# Calculate absolute values from `sampled_values` so that the 'previous' and 'current' absolute arrays can
# be views into the same array instead of separately calculated arrays.
abs_sampled_values = np.abs(sampled_values)
# 'previous' views.
p_val_view = sampled_values[:-1]
p_abs_val_view = abs_sampled_values[:-1]
p_enabled_mask_view = enabled_mask[:-1]
# 'current' views.
c_val_view = sampled_values[1:]
c_abs_val_view = abs_sampled_values[1:]
c_enabled_mask_view = enabled_mask[1:]
# Create views of the 'previous' and 'current' mask and values. # If enough difference from previous sampled value, enable the current value *and* the previous one!
p_val = values[:-1] # The difference check is symmetrical, so this will compare each value to both of its adjacent values.
abs_p_val = abs_values[:-1] # Unless it is forcefully enabled later, this is the only way that the first value can be enabled.
p_write_mask = frame_write_mask[:-1] # This is a contracted form of relative + absolute-near-zero difference:
val = values[1:] # def is_different(a, b):
abs_val = abs_values[1:] # abs_diff = abs(a - b)
write_mask = frame_write_mask[1:] # if abs_diff < min_reldiff_fac * min_absdiff_fac:
# This is contracted form of relative + absolute-near-zero difference:
# absdiff = abs(a - b)
# if absdiff < min_reldiff_fac * min_absdiff_fac:
# return False # return False
# return (absdiff / ((abs(a) + abs(b)) / 2)) > min_reldiff_fac # return (abs_diff / ((abs(a) + abs(b)) / 2)) > min_reldiff_fac
# Note that we ignore the '/ 2' part here, since it's not much significant for us. # Note that we ignore the '/ 2' part here, since it's not much significant for us.
enough_diff_p_val_mask = ( # Contracted form using only builtin Python functions:
np.abs(val - p_val) > (min_reldiff_fac * np.maximum(abs_val + abs_p_val, min_absdiff_fac)) # return abs(a - b) > (min_reldiff_fac * max(abs(a) + abs(b), min_absdiff_fac))
) abs_diff = np.abs(c_val_view - p_val_view)
# If enough difference from previous sampled value, key this value *and* the previous one! different_if_greater_than = min_reldiff_fac * np.maximum(c_abs_val_view + p_abs_val_view, min_absdiff_fac)
# Unless it is forcefully keyed later, this is the only way that the first value can be keyed. enough_diff_p_val_mask = abs_diff > different_if_greater_than
p_write_mask[enough_diff_p_val_mask] = True # Enable both the current values *and* the previous values where `enough_diff_p_val_mask` is True. Some
write_mask[enough_diff_p_val_mask] = True # values may get set to True twice because the views overlap, but this is not a problem.
p_enabled_mask_view[enough_diff_p_val_mask] = True
c_enabled_mask_view[enough_diff_p_val_mask] = True
# The other case where we key a value is if there is enough difference between it and the previous keyed # Else, if enough difference from previous enabled value, enable the current value only!
# value. # For each 'current' value, get the index of the nearest previous enabled value in `sampled_values` (or
# Values that equal their previous value can be skipped, so the remaining values to check are those which # itself if the value is enabled).
# are currently not keyed. # Start with an array that is the index of the 'current' value in `sampled_values`. The 'current' values are
not_keyed_mask = ~write_mask # all but the first value, so the indices will be from 1 to `len(sampled_values)` exclusive.
check_diff_mask = np.logical_and(not_keyed_mask, p_val != val) # Let len(sampled_values) == 9:
val_check_idx = np.flatnonzero(check_diff_mask) # [1, 2, 3, 4, 5, 6, 7, 8]
val_check = val[val_check_idx] p_enabled_idx_in_sampled_values = np.arange(1, len(sampled_values))
abs_val_check = abs_val[val_check_idx] # Replace the indices of all disabled values with 0 in preparation of filling them in with the index of the
# nearest previous enabled value. We choose to replace with 0 so that if there is no nearest previous
# For each current value, get the index of the previous keyed value in `values`. # enabled value, we instead default to `values[0]`.
p_keyed_idx = np.arange(1, len(values)) c_val_disabled_mask = ~c_enabled_mask_view
# The first 'previous keyed value' defaults to values[0], even if it's not actually keyed. # Let `c_val_disabled_mask` be:
p_keyed_idx[not_keyed_mask] = 0 # [F, F, T, F, F, T, T, T]
# Accumulative maximum fills in the zeroed indices with the closest previous non-zero index because the # Set indices to 0 where `c_val_disabled_mask` is True:
# indices must be increasing. # [1, 2, 3, 4, 5, 6, 7, 8]
p_keyed_idx = np.maximum.accumulate(p_keyed_idx) # v v v v
# Extract only the indices that need checking. # [1, 2, 0, 4, 5, 0, 0, 0]
p_keyed_idx_check = p_keyed_idx[val_check_idx] p_enabled_idx_in_sampled_values[c_val_disabled_mask] = 0
p_keyed_val_check = values[p_keyed_idx_check] # Accumulative maximum travels across the array from left to right, filling in the zeroed indices with the
abs_p_keyed_val_check = np.abs(p_keyed_val_check) # maximum value so far, which will be the closest previous enabled index because the non-zero indices are
# strictly increasing.
# We check the relative + absolute-near-zero difference again, but against the previous keyed value this # [1, 2, 0, 4, 5, 0, 0, 0]
# time. # v v v v
enough_diff_p_keyed_val_mask = ( # [1, 2, 2, 4, 5, 5, 5, 5]
np.abs(val_check - p_keyed_val_check) p_enabled_idx_in_sampled_values = np.maximum.accumulate(p_enabled_idx_in_sampled_values)
> (min_reldiff_fac * np.maximum(abs_val_check + abs_p_keyed_val_check, min_absdiff_fac)) # Only disabled values need to be checked against their nearest previous enabled values.
) # We can additionally ignore all values which equal their immediately previous value because those values
if np.any(enough_diff_p_keyed_val_mask): # will never be enabled if they were not enabled by the earlier difference check against immediately
# If there are any that are different enough from the previous keyed value, then we have to check them # previous values.
# all iteratively because keying a new value can change the previous keyed value of some elements, which p_enabled_diff_to_check_mask = np.logical_and(c_val_disabled_mask, p_val_view != c_val_view)
# changes their relative + absolute-near-zero difference. # Convert from a mask to indices because we need the indices later and because the array of indices will
new_p_keyed_idx = -1 # usually be smaller than the mask array making it faster to index other arrays with.
new_p_keyed_val = -1 p_enabled_diff_to_check_idx = np.flatnonzero(p_enabled_diff_to_check_mask)
new_p_keyed_val_abs = -1 # `p_enabled_idx_in_sampled_values` from earlier:
# [1, 2, 2, 4, 5, 5, 5, 5]
# `p_enabled_diff_to_check_mask` assuming no values equal their immediately previous value:
# [F, F, T, F, F, T, T, T]
# `p_enabled_diff_to_check_idx`:
# [ 2, 5, 6, 7]
# `p_enabled_idx_in_sampled_values_to_check`:
# [ 2, 5, 5, 5]
p_enabled_idx_in_sampled_values_to_check = p_enabled_idx_in_sampled_values[p_enabled_diff_to_check_idx]
# Get the 'current' disabled values that need to be checked.
c_val_to_check = c_val_view[p_enabled_diff_to_check_idx]
c_abs_val_to_check = c_abs_val_view[p_enabled_diff_to_check_idx]
# Get the nearest previous enabled value for each value to be checked.
nearest_p_enabled_val = sampled_values[p_enabled_idx_in_sampled_values_to_check]
abs_nearest_p_enabled_val = np.abs(nearest_p_enabled_val)
# Check the relative + absolute-near-zero difference again, but against the nearest previous enabled value
# this time.
abs_diff = np.abs(c_val_to_check - nearest_p_enabled_val)
different_if_greater_than = (min_reldiff_fac
* np.maximum(c_abs_val_to_check + abs_nearest_p_enabled_val, min_absdiff_fac))
enough_diff_p_enabled_val_mask = abs_diff > different_if_greater_than
# If there are any that are different enough from the previous enabled value, then we have to check them all
# iteratively because enabling a new value can change the nearest previous enabled value of some elements,
# which changes their relative + absolute-near-zero difference:
# `p_enabled_diff_to_check_idx`:
# [2, 5, 6, 7]
# `p_enabled_idx_in_sampled_values_to_check`:
# [2, 5, 5, 5]
# Let `enough_diff_p_enabled_val_mask` be:
# [F, F, T, T]
# The first index that is newly enabled is 6:
# [2, 5,>6<,5]
# But 6 > 5, so the next value's nearest previous enabled index is also affected:
# [2, 5, 6,>6<]
# We had calculated a newly enabled index of 7 too, but that was calculated against the old nearest previous
# enabled index of 5, which has now been updated to 6, so whether 7 is enabled or not needs to be
# recalculated:
# [F, F, T, ?]
if np.any(enough_diff_p_enabled_val_mask):
# Accessing .data, the memoryview of the array, iteratively or by individual index is faster than doing # Accessing .data, the memoryview of the array, iteratively or by individual index is faster than doing
# the same with the array itself. # the same with the array itself.
key_write_mv = write_mask.data zipped = zip(p_enabled_diff_to_check_idx.data,
zipped = zip(val_check_idx.data, val_check.data, abs_val_check.data, p_keyed_idx_check.data, c_val_to_check.data,
enough_diff_p_keyed_val_mask.data) c_abs_val_to_check.data,
for cur_idx, cur_val, abs_cur_val, old_p_keyed_idx, enough_diff in zipped: p_enabled_idx_in_sampled_values_to_check.data,
if new_p_keyed_idx > old_p_keyed_idx: enough_diff_p_enabled_val_mask.data)
# The previous keyed value is new and was not included when enough_diff_p_keyed_val_mask was # While iterating, we could set updated values into `enough_diff_p_enabled_val_mask` as we go and then
# calculated, so whether the current value is different enough needs to be calculated. # update `enabled_mask` in bulk after the iteration, but if we're going to update an array while
# Check if the relative + absolute-near-zero difference is enough to key this value. # iterating, we may as well update `enabled_mask` directly instead and skip the bulk update.
enough_diff = (abs(cur_val - new_p_keyed_val) # Additionally, the number of `True` writes to `enabled_mask` is usually much less than the number of
> (min_reldiff_fac * max(abs_cur_val + new_p_keyed_val_abs, min_absdiff_fac))) # updates that would be required to `enough_diff_p_enabled_val_mask`.
c_enabled_mask_view_mv = c_enabled_mask_view.data
# While iterating, keep track of the most recent newly enabled index, so we can tell when we need to
# recalculate whether the current value needs to be enabled.
new_p_enabled_idx = -1
# Keep track of its value too for performance.
new_p_enabled_val = -1
new_abs_p_enabled_val = -1
for cur_idx, c_val, c_abs_val, old_p_enabled_idx, enough_diff in zipped:
if new_p_enabled_idx > old_p_enabled_idx:
# The nearest previous enabled value is newly enabled and was not included when
# `enough_diff_p_enabled_val_mask` was calculated, so whether the current value is different
# enough needs to be recalculated using the newly enabled value.
# Check if the relative + absolute-near-zero difference is enough to enable this value.
enough_diff = (abs(c_val - new_p_enabled_val)
> (min_reldiff_fac * max(c_abs_val + new_abs_p_enabled_val, min_absdiff_fac)))
if enough_diff: if enough_diff:
# The current value needs to be keyed. # The current value needs to be enabled.
key_write_mv[cur_idx] = True c_enabled_mask_view_mv[cur_idx] = True
# Update the index and values for this newly keyed value. # Update the index and values for this newly enabled value.
new_p_keyed_idx = cur_idx new_p_enabled_idx = cur_idx
new_p_keyed_val = cur_val new_p_enabled_val = c_val
new_p_keyed_val_abs = abs_cur_val new_abs_p_enabled_val = c_abs_val
# If we write nothing (action doing nothing) and are in 'force_keep' mode, we key everything! :P # If we write nothing (action doing nothing) and are in 'force_keep' mode, we key everything! :P
# See T41766. # See T41766.