FBX IO: Speed up animation export using NumPy #104884

Merged
Thomas Barlow merged 12 commits from Mysteryem/blender-addons:fbx_anim_export_numpy_intermediate into main 2023-09-19 01:13:25 +02:00
Showing only changes of commit b3d4aa8d64 - Show all commits

View File

@ -2260,17 +2260,20 @@ def fbx_animations_do(scene_data, ref_id, f_start, f_end, start_zero, objects=No
currframes = np.arange(f_start, np.nextafter(f_end, np.inf), step=bake_step)
real_currframes = currframes - f_start if start_zero else currframes
# Get all animated values
# Generator that yields the animated values of each frame in order.
def frame_values_gen():
# Iterate through each frame and yield the values for that frame.
# Precalculate integer frames and subframes.
int_currframes = currframes.astype(int)
subframes = currframes - int_currframes
# Create simpler iterables that return only the values we care about.
animdata_shapes_only = [shape for _anim_shape, _me, shape in animdata_shapes.values()]
animdata_cameras_only = [camera for _anim_camera_lens, _anim_camera_focus_distance, camera
in animdata_cameras.values()]
# Previous frame's rotation for each object in animdata_ob, this will be updated each frame.
animdata_ob_p_rots = p_rots.values()
# Iterate through each frame and yield the values for that frame.
# Iterating .data, the memoryview of an array, is faster than iterating the array directly.
for real_currframe, int_currframe, subframe in zip(real_currframes.data, int_currframes.data, subframes.data):
scene.frame_set(int_currframe, subframe=subframe)
@ -2298,59 +2301,55 @@ def fbx_animations_do(scene_data, ref_id, f_start, f_end, start_zero, objects=No
yield camera.lens
yield camera.dof.focus_distance
# Calculating the total expected number of values reduces memory allocations while iterating and ensures the array
# ends up the size we're expecting.
num_ob_loc_values = num_ob_rot_values = num_ob_scale_values = 3
num_values_per_ob = num_ob_loc_values + num_ob_rot_values + num_ob_scale_values
num_ob_values = len(animdata_ob) * num_values_per_ob
num_shape_values = len(animdata_shapes)
num_values_per_camera = 2
num_camera_values = len(animdata_cameras) * num_values_per_camera
# Providing `count` to np.fromiter pre-allocates the array, avoiding extra memory allocations while iterating.
num_ob_values = len(animdata_ob) * 9 # Location, rotation and scale, each of which have x, y, and z components
num_shape_values = len(animdata_shapes) # Only 1 value per shape key
num_camera_values = len(animdata_cameras) * 2 # Focal length (`.lens`) and focus distance
num_values_per_frame = num_ob_values + num_shape_values + num_camera_values
num_frames = len(real_currframes)
total_num_values = num_frames * num_values_per_frame
all_values = np.fromiter(frame_values_gen(), dtype=np.float64, count=total_num_values)
all_values_flat = np.fromiter(frame_values_gen(), dtype=float, count=num_frames * num_values_per_frame)
# Restore the scene's current frame.
scene.frame_set(back_currframe, subframe=0.0)
# View as each column being the values for a single frame and each row being all values for a single property in a
# curve.
all_values = all_values.reshape(num_frames, -1).T
# View such that each column is all values for a single frame and each row is all values for a single curve.
all_values = all_values_flat.reshape(num_frames, num_values_per_frame).T
# Split into views of the arrays for each curve type.
split_at = [num_ob_values, num_shape_values, num_camera_values]
# For unequal sized splits, np.split takes indices to split at, which can be acquired through a cumulative sum
# across the list.
# The last value isn't needed, because the last split is assumed to go to the end of the array.
split_at = split_at[:-1]
# For uneven splits, np.split takes indices to split at, which can be acquired through a cumulative sum across the
# list.
split_at = np.cumsum(split_at)
split_at = np.cumsum(split_at[:-1])
all_ob_values, all_shape_key_values, all_camera_values = np.split(all_values, split_at)
# Set location/rotation/scale curves
# Further split into views of the arrays for each object.
num_animdata_ob = len(animdata_ob)
all_ob_values = np.split(all_ob_values, num_animdata_ob) if num_animdata_ob else ()
for (anim_loc, anim_rot, anim_scale), ob_values in zip(animdata_ob.values(), all_ob_values):
# Further split into views of the location, rotation and scaling arrays.
# Set location/rotation/scale curves.
# Split into equal sized views of the arrays for each object.
split_into = len(animdata_ob)
per_ob_values = np.split(all_ob_values, split_into) if split_into > 0 else ()
for (anim_loc, anim_rot, anim_scale), ob_values in zip(animdata_ob.values(), per_ob_values):
# Split again into equal sized views of the location, rotation and scaling arrays.
loc_xyz, rot_xyz, sca_xyz = np.split(ob_values, 3)
# In-place convert to degrees.
# In-place convert from Blender rotation to FBX rotation.
np.rad2deg(rot_xyz, out=rot_xyz)
anim_loc.set_keyframes(real_currframes, loc_xyz)
anim_rot.set_keyframes(real_currframes, rot_xyz)
anim_scale.set_keyframes(real_currframes, sca_xyz)
# Set shape key curves
# Set shape key curves.
# There's only one array per shape key, so there's no need to split `all_shape_key_values`.
for (anim_shape, _me, _shape), shape_key_values in zip(animdata_shapes.values(), all_shape_key_values):
# In-place convert from Blender Shape Key Value to FBX Deform Percent.
shape_key_values *= 100.0
anim_shape.set_keyframes(real_currframes, shape_key_values)
# Set camera curves
# Further split into views of the arrays for each camera.
num_animdata_cameras = len(animdata_cameras)
all_camera_values = np.split(all_camera_values, num_animdata_cameras) if num_animdata_cameras else ()
for (anim_camera_lens, anim_camera_focus_distance, camera), camera_values in zip(animdata_cameras.values(), all_camera_values):
lens_values, focus_distance_values = camera_values
# In-place convert from Blender to FBX
# Set camera curves.
# Split into equal sized views of the arrays for each camera.
split_into = len(animdata_cameras)
per_camera_values = np.split(all_camera_values, split_into) if split_into > 0 else ()
zipped = zip(animdata_cameras.values(), per_camera_values)
for (anim_camera_lens, anim_camera_focus_distance, _camera), (lens_values, focus_distance_values) in zipped:
# In-place convert from Blender focus distance to FBX.
focus_distance_values *= (1000 * gscale)
anim_camera_lens.set_keyframes(real_currframes, lens_values)
anim_camera_focus_distance.set_keyframes(real_currframes, focus_distance_values)