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_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 frames written.
frame_write_mask[:] = False
# Initialise to no values enabled for writing.
self._frame_write_mask_array[:] = 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.
p_val = values[:-1]
abs_p_val = abs_values[:-1]
p_write_mask = frame_write_mask[:-1]
val = values[1:]
abs_val = abs_values[1:]
write_mask = frame_write_mask[1:]
# This is contracted form of relative + absolute-near-zero difference:
# absdiff = abs(a - b)
# if absdiff < min_reldiff_fac * min_absdiff_fac:
# If enough difference from previous sampled value, enable the current value *and* the previous one!
# The difference check is symmetrical, so this will compare each value to both of its adjacent values.
# Unless it is forcefully enabled later, this is the only way that the first value can be enabled.
# This is a contracted form of relative + absolute-near-zero difference:
# def is_different(a, b):
# abs_diff = abs(a - b)
# if abs_diff < min_reldiff_fac * min_absdiff_fac:
# 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.
enough_diff_p_val_mask = (
np.abs(val - p_val) > (min_reldiff_fac * np.maximum(abs_val + abs_p_val, min_absdiff_fac))
)
# If enough difference from previous sampled value, key this value *and* the previous one!
# Unless it is forcefully keyed later, this is the only way that the first value can be keyed.
p_write_mask[enough_diff_p_val_mask] = True
write_mask[enough_diff_p_val_mask] = True
# Contracted form using only builtin Python functions:
# 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)
different_if_greater_than = min_reldiff_fac * np.maximum(c_abs_val_view + p_abs_val_view, min_absdiff_fac)
enough_diff_p_val_mask = abs_diff > different_if_greater_than
# Enable both the current values *and* the previous values where `enough_diff_p_val_mask` is True. Some
# 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
# value.
# Values that equal their previous value can be skipped, so the remaining values to check are those which
# are currently not keyed.
not_keyed_mask = ~write_mask
check_diff_mask = np.logical_and(not_keyed_mask, p_val != val)
val_check_idx = np.flatnonzero(check_diff_mask)
val_check = val[val_check_idx]
abs_val_check = abs_val[val_check_idx]
# For each current value, get the index of the previous keyed value in `values`.
p_keyed_idx = np.arange(1, len(values))
# The first 'previous keyed value' defaults to values[0], even if it's not actually keyed.
p_keyed_idx[not_keyed_mask] = 0
# Accumulative maximum fills in the zeroed indices with the closest previous non-zero index because the
# indices must be increasing.
p_keyed_idx = np.maximum.accumulate(p_keyed_idx)
# Extract only the indices that need checking.
p_keyed_idx_check = p_keyed_idx[val_check_idx]
p_keyed_val_check = values[p_keyed_idx_check]
abs_p_keyed_val_check = np.abs(p_keyed_val_check)
# We check the relative + absolute-near-zero difference again, but against the previous keyed value this
# time.
enough_diff_p_keyed_val_mask = (
np.abs(val_check - p_keyed_val_check)
> (min_reldiff_fac * np.maximum(abs_val_check + abs_p_keyed_val_check, min_absdiff_fac))
)
if np.any(enough_diff_p_keyed_val_mask):
# If there are any that are different enough from the previous keyed value, then we have to check them
# all iteratively because keying a new value can change the previous keyed value of some elements, which
# changes their relative + absolute-near-zero difference.
new_p_keyed_idx = -1
new_p_keyed_val = -1
new_p_keyed_val_abs = -1
# Else, if enough difference from previous enabled value, enable the current value only!
# For each 'current' value, get the index of the nearest previous enabled value in `sampled_values` (or
# itself if the value is enabled).
# Start with an array that is the index of the 'current' value in `sampled_values`. The 'current' values are
# all but the first value, so the indices will be from 1 to `len(sampled_values)` exclusive.
# Let len(sampled_values) == 9:
# [1, 2, 3, 4, 5, 6, 7, 8]
p_enabled_idx_in_sampled_values = np.arange(1, len(sampled_values))
# 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
# enabled value, we instead default to `values[0]`.
c_val_disabled_mask = ~c_enabled_mask_view
# Let `c_val_disabled_mask` be:
# [F, F, T, F, F, T, T, T]
# Set indices to 0 where `c_val_disabled_mask` is True:
# [1, 2, 3, 4, 5, 6, 7, 8]
# v v v v
# [1, 2, 0, 4, 5, 0, 0, 0]
p_enabled_idx_in_sampled_values[c_val_disabled_mask] = 0
# Accumulative maximum travels across the array from left to right, filling in the zeroed indices with the
# maximum value so far, which will be the closest previous enabled index because the non-zero indices are
# strictly increasing.
# [1, 2, 0, 4, 5, 0, 0, 0]
# v v v v
# [1, 2, 2, 4, 5, 5, 5, 5]
p_enabled_idx_in_sampled_values = np.maximum.accumulate(p_enabled_idx_in_sampled_values)
# 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
# will never be enabled if they were not enabled by the earlier difference check against immediately
# previous values.
p_enabled_diff_to_check_mask = np.logical_and(c_val_disabled_mask, p_val_view != c_val_view)
# Convert from a mask to indices because we need the indices later and because the array of indices will
# usually be smaller than the mask array making it faster to index other arrays with.
p_enabled_diff_to_check_idx = np.flatnonzero(p_enabled_diff_to_check_mask)
# `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
# the same with the array itself.
key_write_mv = write_mask.data
zipped = zip(val_check_idx.data, val_check.data, abs_val_check.data, p_keyed_idx_check.data,
enough_diff_p_keyed_val_mask.data)
for cur_idx, cur_val, abs_cur_val, old_p_keyed_idx, enough_diff in zipped:
if new_p_keyed_idx > old_p_keyed_idx:
# The previous keyed value is new and was not included when enough_diff_p_keyed_val_mask was
# calculated, so whether the current value is different enough needs to be calculated.
# Check if the relative + absolute-near-zero difference is enough to key this value.
enough_diff = (abs(cur_val - new_p_keyed_val)
> (min_reldiff_fac * max(abs_cur_val + new_p_keyed_val_abs, min_absdiff_fac)))
zipped = zip(p_enabled_diff_to_check_idx.data,
c_val_to_check.data,
c_abs_val_to_check.data,
p_enabled_idx_in_sampled_values_to_check.data,
enough_diff_p_enabled_val_mask.data)
# While iterating, we could set updated values into `enough_diff_p_enabled_val_mask` as we go and then
# update `enabled_mask` in bulk after the iteration, but if we're going to update an array while
# iterating, we may as well update `enabled_mask` directly instead and skip the bulk update.
# Additionally, the number of `True` writes to `enabled_mask` is usually much less than the number of
# 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:
# The current value needs to be keyed.
key_write_mv[cur_idx] = True
# Update the index and values for this newly keyed value.
new_p_keyed_idx = cur_idx
new_p_keyed_val = cur_val
new_p_keyed_val_abs = abs_cur_val
# The current value needs to be enabled.
c_enabled_mask_view_mv[cur_idx] = True
# Update the index and values for this newly enabled value.
new_p_enabled_idx = cur_idx
new_p_enabled_val = c_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
# See T41766.