Animation: Improve performance of Bake Action operator
This dramatically improves baking performance by batch-adding keyframes instead of adding them individually, reducing unnecessary overhead. Testing indicates an approximate 4x performance uplift. Reviewed By: sybren, RiggingDojo Differential Revision: https://developer.blender.org/D8808
This commit is contained in:
parent
81754a0fc5
commit
58c8c4fde3
Notes:
blender-bot
2023-04-24 15:48:59 +02:00
Referenced by commit 2d21fc3f5d
, Cleanup: avoid multiplying lists multiple times
Referenced by issue #107286, Animation: The keyframes of the bake action will overlap with the original keyframes
|
@ -9,7 +9,13 @@ __all__ = (
|
|||
)
|
||||
|
||||
import bpy
|
||||
from typing import Mapping, List, Tuple, Sequence
|
||||
|
||||
# (fcurve.data_path, fcurve.array_index)
|
||||
FCurveKey = Tuple[str, int]
|
||||
# [frame0, value0, frame1, value1, ...]
|
||||
ListKeyframes = List[float]
|
||||
Action = bpy.types.Action
|
||||
|
||||
def bake_action(
|
||||
obj,
|
||||
|
@ -143,6 +149,18 @@ def bake_action_iter(
|
|||
'bbone_scalein', 'bbone_scaleout',
|
||||
'bbone_easein', 'bbone_easeout'
|
||||
]
|
||||
BBONE_PROPS_LENGTHS = {
|
||||
"bbone_curveinx": 1,
|
||||
"bbone_curveoutx": 1,
|
||||
"bbone_curveinz": 1,
|
||||
"bbone_curveoutz": 1,
|
||||
"bbone_rollin": 1,
|
||||
"bbone_rollout": 1,
|
||||
"bbone_scalein": 3,
|
||||
"bbone_scaleout": 3,
|
||||
"bbone_easein": 1,
|
||||
"bbone_easeout": 1,
|
||||
}
|
||||
|
||||
def pose_frame_info(obj):
|
||||
matrix = {}
|
||||
|
@ -225,7 +243,8 @@ def bake_action_iter(
|
|||
|
||||
# in case animation data hasn't been created
|
||||
atd = obj.animation_data_create()
|
||||
if action is None:
|
||||
is_new_action = action is None
|
||||
if is_new_action:
|
||||
action = bpy.data.actions.new("Action")
|
||||
|
||||
# Only leave tweak mode if we actually need to modify the action (T57159)
|
||||
|
@ -244,6 +263,7 @@ def bake_action_iter(
|
|||
# Apply transformations to action
|
||||
|
||||
# pose
|
||||
lookup_fcurves = {(fcurve.data_path, fcurve.array_index): fcurve for fcurve in action.fcurves}
|
||||
if do_pose:
|
||||
for name, pbone in obj.pose.bones.items():
|
||||
if only_selected and not pbone.bone.select:
|
||||
|
@ -257,12 +277,32 @@ def bake_action_iter(
|
|||
euler_prev = None
|
||||
quat_prev = None
|
||||
|
||||
base_fcurve_path = pbone.path_from_id() + "."
|
||||
path_location = base_fcurve_path + "location"
|
||||
path_quaternion = base_fcurve_path + "rotation_quaternion"
|
||||
path_axis_angle = base_fcurve_path + "rotation_axis_angle"
|
||||
path_euler = base_fcurve_path + "rotation_euler"
|
||||
path_scale = base_fcurve_path + "scale"
|
||||
paths_bbprops = [(base_fcurve_path + bbprop) for bbprop in BBONE_PROPS]
|
||||
|
||||
keyframes = KeyframesCo()
|
||||
keyframes.add_paths(path_location, 3)
|
||||
keyframes.add_paths(path_quaternion, 4)
|
||||
keyframes.add_paths(path_axis_angle, 4)
|
||||
keyframes.add_paths(path_euler, 3)
|
||||
keyframes.add_paths(path_scale, 3)
|
||||
|
||||
if pbone.bone.bbone_segments > 1:
|
||||
for prop_name, path in zip(BBONE_PROPS, paths_bbprops):
|
||||
keyframes.add_paths(path, BBONE_PROPS_LENGTHS[prop_name])
|
||||
|
||||
rotation_mode = pbone.rotation_mode
|
||||
total_new_keys = len(pose_info)
|
||||
for (f, matrix, bbones) in pose_info:
|
||||
pbone.matrix_basis = matrix[name].copy()
|
||||
|
||||
pbone.keyframe_insert("location", index=-1, frame=f, group=name)
|
||||
keyframes.extend_co_values(path_location, 3, f, pbone.location)
|
||||
|
||||
rotation_mode = pbone.rotation_mode
|
||||
if rotation_mode == 'QUATERNION':
|
||||
if quat_prev is not None:
|
||||
quat = pbone.rotation_quaternion.copy()
|
||||
|
@ -272,26 +312,37 @@ def bake_action_iter(
|
|||
del quat
|
||||
else:
|
||||
quat_prev = pbone.rotation_quaternion.copy()
|
||||
pbone.keyframe_insert("rotation_quaternion", index=-1, frame=f, group=name)
|
||||
keyframes.extend_co_values(path_quaternion, 4, f, pbone.rotation_quaternion)
|
||||
elif rotation_mode == 'AXIS_ANGLE':
|
||||
pbone.keyframe_insert("rotation_axis_angle", index=-1, frame=f, group=name)
|
||||
keyframes.extend_co_values(path_axis_angle, 4, f, pbone.rotation_axis_angle)
|
||||
else: # euler, XYZ, ZXY etc
|
||||
if euler_prev is not None:
|
||||
euler = pbone.matrix_basis.to_euler(pbone.rotation_mode, euler_prev)
|
||||
pbone.rotation_euler = euler
|
||||
del euler
|
||||
euler_prev = pbone.rotation_euler.copy()
|
||||
pbone.keyframe_insert("rotation_euler", index=-1, frame=f, group=name)
|
||||
keyframes.extend_co_values(path_euler, 3, f, pbone.rotation_euler)
|
||||
|
||||
pbone.keyframe_insert("scale", index=-1, frame=f, group=name)
|
||||
keyframes.extend_co_values(path_scale, 3, f, pbone.scale)
|
||||
|
||||
# Bendy Bones
|
||||
if pbone.bone.bbone_segments > 1:
|
||||
bbone_shape = bbones[name]
|
||||
for bb_prop in BBONE_PROPS:
|
||||
# update this property with value from bbone_shape, then key it
|
||||
setattr(pbone, bb_prop, bbone_shape[bb_prop])
|
||||
pbone.keyframe_insert(bb_prop, index=-1, frame=f, group=name)
|
||||
for prop_index, prop_name in enumerate(BBONE_PROPS):
|
||||
prop_len = BBONE_PROPS_LENGTHS[prop_name]
|
||||
if prop_len > 1:
|
||||
keyframes.extend_co_values(
|
||||
paths_bbprops[prop_index], prop_len, f, bbone_shape[prop_name]
|
||||
)
|
||||
else:
|
||||
keyframes.extend_co_value(
|
||||
paths_bbprops[prop_index], f, bbone_shape[prop_name]
|
||||
)
|
||||
|
||||
if is_new_action:
|
||||
keyframes.insert_keyframes_into_new_action(total_new_keys, action, name)
|
||||
else:
|
||||
keyframes.insert_keyframes_into_existing_action(lookup_fcurves, total_new_keys, action, name)
|
||||
|
||||
# object. TODO. multiple objects
|
||||
if do_object:
|
||||
|
@ -303,13 +354,27 @@ def bake_action_iter(
|
|||
euler_prev = None
|
||||
quat_prev = None
|
||||
|
||||
path_location = "location"
|
||||
path_quaternion = "rotation_quaternion"
|
||||
path_axis_angle = "rotation_axis_angle"
|
||||
path_euler = "rotation_euler"
|
||||
path_scale = "scale"
|
||||
|
||||
keyframes = KeyframesCo()
|
||||
keyframes.add_paths(path_location, 3)
|
||||
keyframes.add_paths(path_quaternion, 4)
|
||||
keyframes.add_paths(path_axis_angle, 4)
|
||||
keyframes.add_paths(path_euler, 3)
|
||||
keyframes.add_paths(path_scale, 3)
|
||||
|
||||
rotation_mode = obj.rotation_mode
|
||||
total_new_keys = len(obj_info)
|
||||
for (f, matrix) in obj_info:
|
||||
name = "Action Bake" # XXX: placeholder
|
||||
obj.matrix_basis = matrix
|
||||
|
||||
obj.keyframe_insert("location", index=-1, frame=f, group=name)
|
||||
keyframes.extend_co_values(path_location, 3, f, obj.location)
|
||||
|
||||
rotation_mode = obj.rotation_mode
|
||||
if rotation_mode == 'QUATERNION':
|
||||
if quat_prev is not None:
|
||||
quat = obj.rotation_quaternion.copy()
|
||||
|
@ -319,16 +384,22 @@ def bake_action_iter(
|
|||
del quat
|
||||
else:
|
||||
quat_prev = obj.rotation_quaternion.copy()
|
||||
obj.keyframe_insert("rotation_quaternion", index=-1, frame=f, group=name)
|
||||
keyframes.extend_co_values(path_quaternion, 4, f, obj.rotation_quaternion)
|
||||
|
||||
elif rotation_mode == 'AXIS_ANGLE':
|
||||
obj.keyframe_insert("rotation_axis_angle", index=-1, frame=f, group=name)
|
||||
keyframes.extend_co_values(path_axis_angle, 4, f, obj.rotation_axis_angle)
|
||||
else: # euler, XYZ, ZXY etc
|
||||
if euler_prev is not None:
|
||||
obj.rotation_euler = matrix.to_euler(obj.rotation_mode, euler_prev)
|
||||
euler_prev = obj.rotation_euler.copy()
|
||||
obj.keyframe_insert("rotation_euler", index=-1, frame=f, group=name)
|
||||
keyframes.extend_co_values(path_euler, 3, f, obj.rotation_euler)
|
||||
|
||||
obj.keyframe_insert("scale", index=-1, frame=f, group=name)
|
||||
keyframes.extend_co_values(path_scale, 3, f, obj.scale)
|
||||
|
||||
if is_new_action:
|
||||
keyframes.insert_keyframes_into_new_action(total_new_keys, action, name)
|
||||
else:
|
||||
keyframes.insert_keyframes_into_existing_action(lookup_fcurves, total_new_keys, action, name)
|
||||
|
||||
if do_parents_clear:
|
||||
obj.parent = None
|
||||
|
@ -358,3 +429,127 @@ def bake_action_iter(
|
|||
i += 1
|
||||
|
||||
yield action
|
||||
|
||||
class KeyframesCo:
|
||||
"""A buffer for keyframe Co unpacked values per FCurveKey. FCurveKeys are added using
|
||||
add_paths(), Co values stored using extend_co_values(), then finally use
|
||||
insert_keyframes_into_*_action() for efficiently inserting keys into the fcurves.
|
||||
|
||||
Users are limited to one Action Group per instance.
|
||||
"""
|
||||
|
||||
# keyframes[(rna_path, array_index)] = list(time0,value0, time1,value1,...)
|
||||
keyframes_from_fcurve: Mapping[FCurveKey, ListKeyframes]
|
||||
|
||||
def __init__(self):
|
||||
self.keyframes_from_fcurve = {}
|
||||
|
||||
def add_paths(
|
||||
self,
|
||||
rna_path: str,
|
||||
total_indices: int,
|
||||
) -> None:
|
||||
keyframes_from_fcurve = self.keyframes_from_fcurve
|
||||
for array_index in range(0, total_indices):
|
||||
keyframes_from_fcurve[(rna_path, array_index)] = []
|
||||
|
||||
def extend_co_values(
|
||||
self,
|
||||
rna_path: str,
|
||||
total_indices: int,
|
||||
frame: float,
|
||||
values: Sequence[float],
|
||||
) -> None:
|
||||
keyframes_from_fcurve = self.keyframes_from_fcurve
|
||||
for array_index in range(0, total_indices):
|
||||
keyframes_from_fcurve[(rna_path, array_index)].extend((frame, values[array_index]))
|
||||
|
||||
def extend_co_value(
|
||||
self,
|
||||
rna_path: str,
|
||||
frame: float,
|
||||
value: float,
|
||||
) -> None:
|
||||
self.keyframes_from_fcurve[(rna_path, 0)].extend((frame, value))
|
||||
|
||||
def insert_keyframes_into_new_action(
|
||||
self,
|
||||
total_new_keys: int,
|
||||
action: Action,
|
||||
action_group_name: str,
|
||||
) -> None:
|
||||
"""Assumes the action is new, that it has no fcurves. Otherwise, the only difference between versions is
|
||||
performance and implementation simplicity.
|
||||
|
||||
Args:
|
||||
action_group_name (str): Name of Action Group that fcurves are added to.
|
||||
"""
|
||||
linear_enum_values = [
|
||||
bpy.types.Keyframe.bl_rna.properties["interpolation"].enum_items["LINEAR"].value
|
||||
] * total_new_keys
|
||||
|
||||
for fc_key, key_values in self.keyframes_from_fcurve.items():
|
||||
if len(key_values) == 0:
|
||||
continue
|
||||
|
||||
data_path, array_index = fc_key
|
||||
keyframe_points = action.fcurves.new(
|
||||
data_path, index=array_index, action_group=action_group_name
|
||||
).keyframe_points
|
||||
|
||||
keyframe_points.add(total_new_keys)
|
||||
keyframe_points.foreach_set("co", key_values)
|
||||
keyframe_points.foreach_set("interpolation", linear_enum_values)
|
||||
|
||||
# There's no need to do fcurve.update() because the keys are already ordered, have
|
||||
# no duplicates and all handles are Linear.
|
||||
|
||||
def insert_keyframes_into_existing_action(
|
||||
self,
|
||||
lookup_fcurves: Mapping[FCurveKey, bpy.types.FCurve],
|
||||
total_new_keys: int,
|
||||
action: Action,
|
||||
action_group_name: str,
|
||||
) -> None:
|
||||
"""Assumes the action already exists, that it might already have fcurves. Otherwise, the
|
||||
only difference between versions is performance and implementation simplicity.
|
||||
|
||||
Args:
|
||||
lookup_fcurves (Mapping[FCurveKey, bpy.types.FCurve]): This is only used for efficiency.
|
||||
It's a substitute for action.fcurves.find() which is a potentially expensive linear
|
||||
search.
|
||||
action_group_name (str): Name of Action Group that fcurves are added to.
|
||||
"""
|
||||
linear_enum_values = [
|
||||
bpy.types.Keyframe.bl_rna.properties["interpolation"].enum_items["LINEAR"].value
|
||||
] * total_new_keys
|
||||
|
||||
for fc_key, key_values in self.keyframes_from_fcurve.items():
|
||||
if len(key_values) == 0:
|
||||
continue
|
||||
|
||||
fcurve = lookup_fcurves.get(fc_key, None)
|
||||
if fcurve is None:
|
||||
data_path, array_index = fc_key
|
||||
fcurve = action.fcurves.new(
|
||||
data_path, index=array_index, action_group=action_group_name
|
||||
)
|
||||
|
||||
keyframe_points = fcurve.keyframe_points
|
||||
|
||||
co_buffer = [0] * 2 * len(keyframe_points)
|
||||
keyframe_points.foreach_get("co", co_buffer)
|
||||
co_buffer.extend(key_values)
|
||||
|
||||
ipo_buffer = [None] * len(keyframe_points)
|
||||
keyframe_points.foreach_get("interpolation", ipo_buffer)
|
||||
ipo_buffer.extend(linear_enum_values)
|
||||
|
||||
# XXX: Currently baking inserts the same number of keys for all baked properties.
|
||||
# This block of code breaks if that's no longer true since we then will not be properly
|
||||
# initializing all the data.
|
||||
keyframe_points.add(total_new_keys)
|
||||
keyframe_points.foreach_set("co", co_buffer)
|
||||
keyframe_points.foreach_set("interpolation", ipo_buffer)
|
||||
|
||||
fcurve.update()
|
||||
|
|
Loading…
Reference in New Issue