[Bf-blender-cvs] [58c8c4fde35] master: Animation: Improve performance of Bake Action operator

Colin Basnett noreply at git.blender.org
Thu Nov 24 20:27:26 CET 2022


Commit: 58c8c4fde35c158407ca2ba0c0bc099d1455f691
Author: Colin Basnett
Date:   Thu Nov 24 11:26:17 2022 -0800
Branches: master
https://developer.blender.org/rB58c8c4fde35c158407ca2ba0c0bc099d1455f691

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

===================================================================

M	release/scripts/modules/bpy_extras/anim_utils.py

===================================================================

diff --git a/release/scripts/modules/bpy_extras/anim_utils.py b/release/scripts/modules/bpy_extras/anim_utils.py
index f66dfd6eb0a..7bc9125a767 100644
--- a/release/scripts/modules/bpy_extras/anim_utils.py
+++ b/release/scripts/modules/bpy_extras/anim_utils.py
@@ -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,
+    ) -

@@ Diff output truncated at 10240 characters. @@



More information about the Bf-blender-cvs mailing list