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
3 changed files with 176 additions and 105 deletions

View File

@ -5,7 +5,7 @@
bl_info = {
"name": "FBX format",
"author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem",
"version": (5, 7, 4),
"version": (5, 7, 5),
"blender": (3, 6, 0),
"location": "File > Import-Export",
"description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions",

View File

@ -1981,12 +1981,6 @@ def fbx_data_animation_elements(root, scene_data):
animations = scene_data.animations
if not animations:
return
scene = scene_data.scene
fps = scene.render.fps / scene.render.fps_base
def keys_to_ktimes(keys):
return (int(v) for v in convert_sec_to_ktime_iter((f / fps for f, _v in keys)))
# Animation stacks.
for astack_key, alayers, alayer_key, name, f_start, f_end in animations:
@ -2026,18 +2020,18 @@ def fbx_data_animation_elements(root, scene_data):
acn_tmpl = elem_props_template_init(scene_data.templates, b"AnimationCurveNode")
acn_props = elem_properties(acurvenode)
for fbx_item, (acurve_key, def_value, keys, _acurve_valid) in acurves.items():
for fbx_item, (acurve_key, def_value, (keys, values), _acurve_valid) in acurves.items():
elem_props_template_set(acn_tmpl, acn_props, "p_number", fbx_item.encode(),
def_value, animatable=True)
# Only create Animation curve if needed!
if keys:
nbr_keys = len(keys)
if nbr_keys:
acurve = elem_data_single_int64(root, b"AnimationCurve", get_fbx_uuid_from_key(acurve_key))
acurve.add_string(fbx_name_class(b"", b"AnimCurve"))
acurve.add_string(b"")
# key attributes...
nbr_keys = len(keys)
# flags...
keyattr_flags = (
1 << 2 | # interpolation mode, 1 = constant, 2 = linear, 3 = cubic.
@ -2052,8 +2046,8 @@ def fbx_data_animation_elements(root, scene_data):
# And now, the *real* data!
elem_data_single_float64(acurve, b"Default", def_value)
elem_data_single_int32(acurve, b"KeyVer", FBX_ANIM_KEY_VERSION)
elem_data_single_int64_array(acurve, b"KeyTime", keys_to_ktimes(keys))
elem_data_single_float32_array(acurve, b"KeyValueFloat", (v for _f, v in keys))
elem_data_single_int64_array(acurve, b"KeyTime", astype_view_signedness(keys, np.int64))
elem_data_single_float32_array(acurve, b"KeyValueFloat", values.astype(np.float32, copy=False))
elem_data_single_int32_array(acurve, b"KeyAttrFlags", keyattr_flags)
elem_data_single_float32_array(acurve, b"KeyAttrDataFloat", keyattr_datafloat)
elem_data_single_int32_array(acurve, b"KeyAttrRefCount", (nbr_keys,))
@ -2254,75 +2248,128 @@ def fbx_animations_do(scene_data, ref_id, f_start, f_end, start_zero, objects=No
dupli_parent_bdata = {dup.get_parent().bdata for dup in animdata_ob if dup.is_dupli}
has_animated_duplis = bool(dupli_parent_bdata)
currframe = f_start
while currframe <= f_end:
real_currframe = currframe - f_start if start_zero else currframe
scene.frame_set(int(currframe), subframe=currframe - int(currframe))
# Initialize keyframe times array. Each AnimationCurveNodeWrapper will share the same instance.
# `np.arange` excludes the `stop` argument like when using `range`, so we use np.nextafter to get the next
# representable value after f_end and use that as the `stop` argument instead.
currframes = np.arange(f_start, np.nextafter(f_end, np.inf), step=bake_step)
if has_animated_duplis:
# Changing the scene's frame invalidates existing dupli instances. To get the updated matrices of duplis for
# this frame, we must get the duplis from the depsgraph again.
for dup in depsgraph.object_instances:
if (parent := dup.parent) and parent.original in dupli_parent_bdata:
# ObjectWrapper caches its instances. Attempting to create a new instance updates the existing
# ObjectWrapper instance with the current frame's matrix and then returns the existing instance.
ObjectWrapper(dup)
for ob_obj, (anim_loc, anim_rot, anim_scale) in animdata_ob.items():
# We compute baked loc/rot/scale for all objects (rot being euler-compat with previous value!).
p_rot = p_rots.get(ob_obj, None)
loc, rot, scale, _m, _mr = ob_obj.fbx_object_tx(scene_data, rot_euler_compat=p_rot)
p_rots[ob_obj] = rot
anim_loc.add_keyframe(real_currframe, loc)
anim_rot.add_keyframe(real_currframe, tuple(convert_rad_to_deg_iter(rot)))
anim_scale.add_keyframe(real_currframe, scale)
for anim_shape, me, shape in animdata_shapes.values():
anim_shape.add_keyframe(real_currframe, (shape.value * 100.0,))
for anim_camera_lens, anim_camera_focus_distance, camera in animdata_cameras.values():
anim_camera_lens.add_keyframe(real_currframe, (camera.lens,))
anim_camera_focus_distance.add_keyframe(real_currframe, (camera.dof.focus_distance * 1000 * gscale,))
currframe += bake_step
# Convert from Blender time to FBX time.
fps = scene.render.fps / scene.render.fps_base
real_currframes = currframes - f_start if start_zero else currframes
real_currframes = (real_currframes / fps * FBX_KTIME).astype(np.int64)
# Generator that yields the animated values of each frame in order.
def frame_values_gen():
# 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 int_currframe, subframe in zip(int_currframes.data, subframes.data):
scene.frame_set(int_currframe, subframe=subframe)
if has_animated_duplis:
# Changing the scene's frame invalidates existing dupli instances. To get the updated matrices of duplis
# for this frame, we must get the duplis from the depsgraph again.
for dup in depsgraph.object_instances:
if (parent := dup.parent) and parent.original in dupli_parent_bdata:
# ObjectWrapper caches its instances. Attempting to create a new instance updates the existing
# ObjectWrapper instance with the current frame's matrix and then returns the existing instance.
ObjectWrapper(dup)
next_p_rots = []
for ob_obj, p_rot in zip(animdata_ob, animdata_ob_p_rots):
# We compute baked loc/rot/scale for all objects (rot being euler-compat with previous value!).
loc, rot, scale, _m, _mr = ob_obj.fbx_object_tx(scene_data, rot_euler_compat=p_rot)
next_p_rots.append(rot)
yield from loc
yield from rot
yield from scale
animdata_ob_p_rots = next_p_rots
for shape in animdata_shapes_only:
yield shape.value
for camera in animdata_cameras_only:
yield camera.lens
yield camera.dof.focus_distance
# 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)
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 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 = np.cumsum(split_at[:-1])
all_ob_values, all_shape_key_values, all_camera_values = np.split(all_values, split_at)
all_anims = []
# 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 anims, 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 from Blender rotation to FBX rotation.
np.rad2deg(rot_xyz, out=rot_xyz)
anim_loc, anim_rot, anim_scale = anims
anim_loc.set_keyframes(real_currframes, loc_xyz)
anim_rot.set_keyframes(real_currframes, rot_xyz)
anim_scale.set_keyframes(real_currframes, sca_xyz)
all_anims.extend(anims)
# 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)
all_anims.append(anim_shape)
# 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)
all_anims.append(anim_camera_lens)
all_anims.append(anim_camera_focus_distance)
animations = {}
# And now, produce final data (usable by FBX export code)
# Objects-like loc/rot/scale...
for ob_obj, anims in animdata_ob.items():
for anim in anims:
anim.simplify(simplify_fac, bake_step, force_keep)
if not anim:
continue
for obj_key, group_key, group, fbx_group, fbx_gname in anim.get_final_data(scene, ref_id, force_keep):
anim_data = animations.setdefault(obj_key, ("dummy_unused_key", {}))
anim_data[1][fbx_group] = (group_key, group, fbx_gname)
# And meshes' shape keys.
for channel_key, (anim_shape, me, shape) in animdata_shapes.items():
final_keys = {}
anim_shape.simplify(simplify_fac, bake_step, force_keep)
if not anim_shape:
for anim in all_anims:
anim.simplify(simplify_fac, bake_step, force_keep)
if not anim:
continue
for elem_key, group_key, group, fbx_group, fbx_gname in anim_shape.get_final_data(scene, ref_id, force_keep):
anim_data = animations.setdefault(elem_key, ("dummy_unused_key", {}))
for obj_key, group_key, group, fbx_group, fbx_gname in anim.get_final_data(scene, ref_id, force_keep):
anim_data = animations.setdefault(obj_key, ("dummy_unused_key", {}))
anim_data[1][fbx_group] = (group_key, group, fbx_gname)
# And cameras' lens and focus distance keys.
for cam_key, (anim_camera_lens, anim_camera_focus_distance, camera) in animdata_cameras.items():
final_keys = {}
anim_camera_lens.simplify(simplify_fac, bake_step, force_keep)
anim_camera_focus_distance.simplify(simplify_fac, bake_step, force_keep)
if anim_camera_lens:
for elem_key, group_key, group, fbx_group, fbx_gname in \
anim_camera_lens.get_final_data(scene, ref_id, force_keep):
anim_data = animations.setdefault(elem_key, ("dummy_unused_key", {}))
anim_data[1][fbx_group] = (group_key, group, fbx_gname)
if anim_camera_focus_distance:
for elem_key, group_key, group, fbx_group, fbx_gname in \
anim_camera_focus_distance.get_final_data(scene, ref_id, force_keep):
anim_data = animations.setdefault(elem_key, ("dummy_unused_key", {}))
anim_data[1][fbx_group] = (group_key, group, fbx_gname)
astack_key = get_blender_anim_stack_key(scene, ref_id)
alayer_key = get_blender_anim_layer_key(scene, ref_id)
name = (get_blenderID_name(ref_id) if ref_id else scene.name).encode()
@ -2848,8 +2895,8 @@ def fbx_data_from_scene(scene, depsgraph, settings):
for _alayer_key, alayer in astack.values():
for _acnode_key, acnode, _acnode_name in alayer.values():
nbr_acnodes += 1
for _acurve_key, _dval, acurve, acurve_valid in acnode.values():
if acurve:
for _acurve_key, _dval, (keys, _values), acurve_valid in acnode.values():
if len(keys):
nbr_acurves += 1
templates[b"AnimationStack"] = fbx_template_def_animstack(scene, settings, nbr_users=nbr_astacks)
@ -2983,8 +3030,8 @@ def fbx_data_from_scene(scene, depsgraph, settings):
connections.append((b"OO", acurvenode_id, alayer_id, None))
# Animcurvenode -> object property.
connections.append((b"OP", acurvenode_id, elem_id, fbx_prop.encode()))
for fbx_item, (acurve_key, default_value, acurve, acurve_valid) in acurves.items():
if acurve:
for fbx_item, (acurve_key, default_value, (keys, values), acurve_valid) in acurves.items():
if len(keys):
# Animcurve -> Animcurvenode.
connections.append((b"OP", get_fbx_uuid_from_key(acurve_key), acurvenode_id, fbx_item.encode()))

View File

@ -1234,8 +1234,10 @@ class AnimationCurveNodeWrapper:
and easy API to handle those.
"""
__slots__ = (
'elem_keys', '_keys', 'default_values', 'fbx_group', 'fbx_gname', 'fbx_props',
'force_keying', 'force_startend_keying')
'elem_keys', 'default_values', 'fbx_group', 'fbx_gname', 'fbx_props',
'force_keying', 'force_startend_keying',
'_frame_times_array', '_frame_values_array', '_frame_write_mask_array',
)
kinds = {
'LCL_TRANSLATION': ("Lcl Translation", "T", ("X", "Y", "Z")),
@ -1254,7 +1256,9 @@ class AnimationCurveNodeWrapper:
self.fbx_props = [self.kinds[kind][2]]
self.force_keying = force_keying
self.force_startend_keying = force_startend_keying
self._keys = [] # (frame, values, write_flags)
self._frame_times_array = None
self._frame_values_array = None
self._frame_write_mask_array = None
if default_values is not ...:
assert(len(default_values) == len(self.fbx_props[0]))
self.default_values = default_values
@ -1263,7 +1267,7 @@ class AnimationCurveNodeWrapper:
def __bool__(self):
# We are 'True' if we do have some validated keyframes...
return bool(self._keys) and (True in ((True in k[2]) for k in self._keys))
return self._frame_write_mask_array is not None and bool(np.any(self._frame_write_mask_array))
def add_group(self, elem_key, fbx_group, fbx_gname, fbx_props):
"""
@ -1276,19 +1280,31 @@ class AnimationCurveNodeWrapper:
self.fbx_gname.append(fbx_gname)
self.fbx_props.append(fbx_props)
def add_keyframe(self, frame, values):
def set_keyframes(self, keyframe_times, keyframe_values):
"""
Add a new keyframe to all curves of the group.
Set all keyframe times and values of the group.
Values can be a 2D array where each row is the values for a separate curve.
"""
assert(len(values) == len(self.fbx_props[0]))
self._keys.append((frame, values, [True] * len(values))) # write everything by default.
# View 1D keyframe_values as 2D with a single row, so that the same code can be used for both 1D and
# 2D inputs.
if len(keyframe_values.shape) == 1:
keyframe_values = keyframe_values[np.newaxis]
# There must be a time for each column of values.
assert(len(keyframe_times) == keyframe_values.shape[1])
# There must be as many rows of values as there are properties.
assert(len(self.fbx_props[0]) == len(keyframe_values))
write_mask = np.full_like(keyframe_values, True, dtype=bool) # write everything by default
self._frame_times_array = keyframe_times
self._frame_values_array = keyframe_values
self._frame_write_mask_array = write_mask
def simplify(self, fac, step, force_keep=False):
"""
Simplifies sampled curves by only enabling samples when:
* their values relatively differ from the previous sample ones.
"""
if not self._keys:
if self._frame_times_array is None:
# Keyframes have not been added yet.
return
if fac == 0.0:
@ -1297,15 +1313,22 @@ class AnimationCurveNodeWrapper:
# So that, with default factor and step values (1), we get:
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...
keys = self._keys
p_currframe, p_key, p_key_write = keys[0]
p_keyed = list(p_key)
are_keyed = [False] * len(p_key)
for currframe, key, key_write in keys:
are_keyed = []
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
# Create views of the 'previous' and 'current' mask and values. The memoryview, .data, of each array is used
# for its iteration and indexing performance compared to the array.
key = values[1:].data
p_key = values[:-1].data
key_write = frame_write_mask[1:].data
p_key_write = frame_write_mask[:-1].data
p_keyedval = values[0]
is_keyed = False
for idx, (val, p_val) in enumerate(zip(key, p_key)):
key_write[idx] = False
p_keyedval = p_keyed[idx]
if val == p_val:
# Never write keyframe when value is exactly the same as prev one!
continue
@ -1319,14 +1342,14 @@ class AnimationCurveNodeWrapper:
# If enough difference from previous sampled value, key this value *and* the previous one!
key_write[idx] = True
p_key_write[idx] = True
p_keyed[idx] = val
are_keyed[idx] = True
p_keyedval = val
is_keyed = True
elif abs(val - p_keyedval) > (min_reldiff_fac * max((abs(val) + abs(p_keyedval)), min_absdiff_fac)):
# Else, if enough difference from previous keyed value, key this value only!
key_write[idx] = True
p_keyed[idx] = val
are_keyed[idx] = True
p_currframe, p_key, p_key_write = currframe, key, key_write
p_keyedval = val
is_keyed = True
are_keyed.append(is_keyed)
# If we write nothing (action doing nothing) and are in 'force_keep' mode, we key everything! :P
# See T41766.
@ -1339,20 +1362,20 @@ class AnimationCurveNodeWrapper:
# If we did key something, ensure first and last sampled values are keyed as well.
if self.force_startend_keying:
for idx, is_keyed in enumerate(are_keyed):
for is_keyed, frame_write_mask in zip(are_keyed, self._frame_write_mask_array):
if is_keyed:
keys[0][2][idx] = keys[-1][2][idx] = True
frame_write_mask[:1] = True
frame_write_mask[-1:] = True
def get_final_data(self, scene, ref_id, force_keep=False):
"""
Yield final anim data for this 'curvenode' (for all curvenodes defined).
force_keep is to force to keep a curve even if it only has one valid keyframe.
"""
curves = [[] for k in self._keys[0][1]]
for currframe, key, key_write in self._keys:
for curve, val, wrt in zip(curves, key, key_write):
if wrt:
curve.append((currframe, val))
curves = [
(self._frame_times_array[write_mask], values[write_mask])
for values, write_mask in zip(self._frame_values_array, self._frame_write_mask_array)
]
force_keep = force_keep or self.force_keying
for elem_key, fbx_group, fbx_gname, fbx_props in \
@ -1363,8 +1386,9 @@ class AnimationCurveNodeWrapper:
fbx_item = FBX_ANIM_PROPSGROUP_NAME + "|" + fbx_item
curve_key = get_blender_anim_curve_key(scene, ref_id, elem_key, fbx_group, fbx_item)
# (curve key, default value, keyframes, write flag).
group[fbx_item] = (curve_key, def_val, c,
True if (len(c) > 1 or (len(c) > 0 and force_keep)) else False)
times = c[0]
write_flag = len(times) > (0 if force_keep else 1)
group[fbx_item] = (curve_key, def_val, c, write_flag)
yield elem_key, group_key, group, fbx_group, fbx_gname