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) currframes = np.arange(f_start, np.nextafter(f_end, np.inf), step=bake_step)
real_currframes = currframes - f_start if start_zero else currframes 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(): 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) int_currframes = currframes.astype(int)
subframes = currframes - int_currframes subframes = currframes - int_currframes
# Create simpler iterables that return only the values we care about. # 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_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 animdata_cameras_only = [camera for _anim_camera_lens, _anim_camera_focus_distance, camera
in animdata_cameras.values()] in animdata_cameras.values()]
# Previous frame's rotation for each object in animdata_ob, this will be updated each frame. # Previous frame's rotation for each object in animdata_ob, this will be updated each frame.
animdata_ob_p_rots = p_rots.values() 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. # 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): for real_currframe, int_currframe, subframe in zip(real_currframes.data, int_currframes.data, subframes.data):
scene.frame_set(int_currframe, subframe=subframe) 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.lens
yield camera.dof.focus_distance yield camera.dof.focus_distance
# Calculating the total expected number of values reduces memory allocations while iterating and ensures the array # Providing `count` to np.fromiter pre-allocates the array, avoiding extra memory allocations while iterating.
# ends up the size we're expecting. num_ob_values = len(animdata_ob) * 9 # Location, rotation and scale, each of which have x, y, and z components
num_ob_loc_values = num_ob_rot_values = num_ob_scale_values = 3 num_shape_values = len(animdata_shapes) # Only 1 value per shape key
num_values_per_ob = num_ob_loc_values + num_ob_rot_values + num_ob_scale_values num_camera_values = len(animdata_cameras) * 2 # Focal length (`.lens`) and focus distance
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
num_values_per_frame = num_ob_values + num_shape_values + num_camera_values num_values_per_frame = num_ob_values + num_shape_values + num_camera_values
num_frames = len(real_currframes) num_frames = len(real_currframes)
total_num_values = num_frames * num_values_per_frame all_values_flat = np.fromiter(frame_values_gen(), dtype=float, count=num_frames * num_values_per_frame)
all_values = np.fromiter(frame_values_gen(), dtype=np.float64, count=total_num_values)
# Restore the scene's current frame.
scene.frame_set(back_currframe, subframe=0.0) 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 # View such that each column is all values for a single frame and each row is all values for a single curve.
# curve. all_values = all_values_flat.reshape(num_frames, num_values_per_frame).T
all_values = all_values.reshape(num_frames, -1).T
# Split into views of the arrays for each curve type. # Split into views of the arrays for each curve type.
split_at = [num_ob_values, num_shape_values, num_camera_values] 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. # 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] split_at = np.cumsum(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)
all_ob_values, all_shape_key_values, all_camera_values = np.split(all_values, split_at) all_ob_values, all_shape_key_values, all_camera_values = np.split(all_values, split_at)
# Set location/rotation/scale curves # Set location/rotation/scale curves.
# Further split into views of the arrays for each object. # Split into equal sized views of the arrays for each object.
num_animdata_ob = len(animdata_ob) split_into = len(animdata_ob)
all_ob_values = np.split(all_ob_values, num_animdata_ob) if num_animdata_ob else () 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(), all_ob_values): for (anim_loc, anim_rot, anim_scale), ob_values in zip(animdata_ob.values(), per_ob_values):
# Further split into views of the location, rotation and scaling arrays. # Split again into equal sized views of the location, rotation and scaling arrays.
loc_xyz, rot_xyz, sca_xyz = np.split(ob_values, 3) 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) np.rad2deg(rot_xyz, out=rot_xyz)
anim_loc.set_keyframes(real_currframes, loc_xyz) anim_loc.set_keyframes(real_currframes, loc_xyz)
anim_rot.set_keyframes(real_currframes, rot_xyz) anim_rot.set_keyframes(real_currframes, rot_xyz)
anim_scale.set_keyframes(real_currframes, sca_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): 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. # In-place convert from Blender Shape Key Value to FBX Deform Percent.
shape_key_values *= 100.0 shape_key_values *= 100.0
anim_shape.set_keyframes(real_currframes, shape_key_values) anim_shape.set_keyframes(real_currframes, shape_key_values)
# Set camera curves # Set camera curves.
# Further split into views of the arrays for each camera. # Split into equal sized views of the arrays for each camera.
num_animdata_cameras = len(animdata_cameras) split_into = len(animdata_cameras)
all_camera_values = np.split(all_camera_values, num_animdata_cameras) if num_animdata_cameras else () per_camera_values = np.split(all_camera_values, split_into) if split_into > 0 else ()
for (anim_camera_lens, anim_camera_focus_distance, camera), camera_values in zip(animdata_cameras.values(), all_camera_values): zipped = zip(animdata_cameras.values(), per_camera_values)
lens_values, focus_distance_values = camera_values for (anim_camera_lens, anim_camera_focus_distance, _camera), (lens_values, focus_distance_values) in zipped:
# In-place convert from Blender to FBX # In-place convert from Blender focus distance to FBX.
focus_distance_values *= (1000 * gscale) focus_distance_values *= (1000 * gscale)
anim_camera_lens.set_keyframes(real_currframes, lens_values) anim_camera_lens.set_keyframes(real_currframes, lens_values)
anim_camera_focus_distance.set_keyframes(real_currframes, focus_distance_values) anim_camera_focus_distance.set_keyframes(real_currframes, focus_distance_values)