Bake Action: operate on selected objects
authorCampbell Barton <ideasman42@gmail.com>
Sun, 10 Sep 2017 06:58:04 +0000 (16:58 +1000)
committerCampbell Barton <ideasman42@gmail.com>
Sun, 10 Sep 2017 07:16:47 +0000 (17:16 +1000)
Previously only the active object was used.

Use coroutines to support baking frames for multiple objects at once,
without having to playback the animation multiple times.

release/scripts/modules/bpy_extras/anim_utils.py
release/scripts/startup/bl_operators/anim.py

index ee270c6e8c7bdb12b1b060248538a00e68392694..f2df1bc16b38158aa1d807076b3103707db2394c 100644 (file)
 
 __all__ = (
     "bake_action",
-    )
+    "bake_action_objects",
+
+    "bake_action_iter",
+    "bake_action_objects_iter",
+)
 
 import bpy
 
 
-# XXX visual keying is actually always considered as True in this code...
 def bake_action(
         obj,
-        frame_start,
-        frame_end,
-        frame_step=1,
+        *,
+        action, frames,
+        **kwargs,
+):
+    """
+    :arg obj: Object to bake.
+    :type obj: :class:`bpy.types.Object`
+    :arg action: An action to bake the data into, or None for a new action
+       to be created.
+    :type action: :class:`bpy.types.Action` or None
+    :arg frames: Frames to bake.
+    :type frames: iterable of int
+
+    :return: an action or None
+    :rtype: :class:`bpy.types.Action`
+    """
+    if not (do_pose or do_object):
+        return None
+
+    action, = bake_action_objects(
+        [(obj, action)],
+        frames,
+        **kwargs,
+    )
+    return action
+
+
+def bake_action_objects(
+        object_action_pairs,
+        *,
+        frames,
+        **kwargs,
+):
+    """
+    A version of :func:`bake_action_objects_iter` that takes frames and returns the output.
+
+    :arg frames: Frames to bake.
+    :type frames: iterable of int
+
+    :return: A sequence of Action or None types (aligned with `object_action_pairs`)
+    :rtype: sequence of :class:`bpy.types.Action`
+    """
+    iter = bake_action_objects_iter(object_action_pairs, **kwargs)
+    iter.send(None)
+    for frame in frames:
+        iter.send(frame)
+    return iter.send(None)
+
+
+def bake_action_objects_iter(
+        object_action_pairs,
+        **kwargs,
+):
+    """
+    An coroutine that bakes actions for multiple objects.
+
+    :arg object_action_pairs: Sequence of object action tuples,
+       action is the destination for the baked data. When None a new action will be created.
+    :type object_action_pairs: Sequence of (:class:`bpy.types.Object`, :class:`bpy.types.Action`)
+    """
+    scene = bpy.context.scene
+    frame_back = scene.frame_current
+    iter_all = tuple(
+        bake_action_iter(obj, action=action, **kwargs)
+        for (obj, action) in object_action_pairs
+    )
+    for iter in iter_all:
+        iter.send(None)
+    while True:
+        frame = yield None
+        if frame is None:
+            break
+        scene.frame_set(frame)
+        scene.update()
+        for iter in iter_all:
+            iter.send(frame)
+    scene.frame_set(frame_back)
+    yield tuple(iter.send(None) for iter in iter_all)
+
+
+# XXX visual keying is actually always considered as True in this code...
+def bake_action_iter(
+        obj,
+        *,
+        action,
         only_selected=False,
         do_pose=True,
         do_object=True,
@@ -38,21 +123,15 @@ def bake_action(
         do_constraint_clear=False,
         do_parents_clear=False,
         do_clean=False,
-        action=None,
 ):
-
     """
-    Return an image from the file path with options to search multiple paths
-    and return a placeholder if its not found.
+    An coroutine that bakes action for a single object.
 
     :arg obj: Object to bake.
     :type obj: :class:`bpy.types.Object`
-    :arg frame_start: First frame to bake.
-    :type frame_start: int
-    :arg frame_end: Last frame to bake.
-    :type frame_end: int
-    :arg frame_step: Frame step.
-    :type frame_step: int
+    :arg action: An action to bake the data into, or None for a new action
+       to be created.
+    :type action: :class:`bpy.types.Action` or None
     :arg only_selected: Only bake selected bones.
     :type only_selected: bool
     :arg do_pose: Bake pose channels.
@@ -67,14 +146,10 @@ def bake_action(
     :type do_parents_clear: bool
     :arg do_clean: Remove redundant keyframes after baking.
     :type do_clean: bool
-    :arg action: An action to bake the data into, or None for a new action
-       to be created.
-    :type action: :class:`bpy.types.Action` or None
 
     :return: an action or None
     :rtype: :class:`bpy.types.Action`
     """
-
     # -------------------------------------------------------------------------
     # Helper Functions and vars
 
@@ -116,33 +191,32 @@ def bake_action(
     # -------------------------------------------------------------------------
     # Setup the Context
 
-    # TODO, pass data rather then grabbing from the context!
-    scene = bpy.context.scene
-    frame_back = scene.frame_current
-
     if obj.pose is None:
         do_pose = False
 
     if not (do_pose or do_object):
-        return None
+        raise Exception("Pose and object baking is disabled, no action needed")
 
     pose_info = []
     obj_info = []
 
     options = {'INSERTKEY_NEEDED'}
 
-    frame_range = range(frame_start, frame_end + 1, frame_step)
-
     # -------------------------------------------------------------------------
     # Collect transformations
 
-    for f in frame_range:
-        scene.frame_set(f)
-        scene.update()
+    while True:
+        # Caller is responsible for setting the frame and updating the scene.
+        frame = yield None
+
+        # Signal we're done!
+        if frame is None:
+            break
+
         if do_pose:
-            pose_info.append(pose_frame_info(obj))
+            pose_info.append((frame, pose_frame_info(obj)))
         if do_object:
-            obj_info.append(obj_frame_info(obj))
+            obj_info.append((frame, obj_frame_info(obj)))
 
     # -------------------------------------------------------------------------
     # Clean (store initial data)
@@ -181,7 +255,7 @@ def bake_action(
             # create compatible eulers
             euler_prev = None
 
-            for (f, matrix) in zip(frame_range, pose_info):
+            for (f, matrix) in pose_info:
                 pbone.matrix_basis = matrix[name].copy()
 
                 pbone.keyframe_insert("location", -1, f, name, options)
@@ -213,7 +287,7 @@ def bake_action(
         # create compatible eulers
         euler_prev = None
 
-        for (f, matrix) in zip(frame_range, obj_info):
+        for (f, matrix) in obj_info:
             name = "Action Bake"  # XXX: placeholder
             obj.matrix_basis = matrix
 
@@ -264,6 +338,4 @@ def bake_action(
                 else:
                     i += 1
 
-    scene.frame_set(frame_back)
-
-    return action
+    yield action
index 380c1257a3d17c01dd7175157067b1fb6a3a3ae8..0632f9bc3ca9105eee3623b28e960636632878b1 100644 (file)
@@ -198,7 +198,7 @@ class ANIM_OT_keying_set_export(Operator):
 
 
 class BakeAction(Operator):
-    """Bake object/pose loc/scale/rotation animation to a new action"""
+    """Bake all selected objects loc/scale/rotation animation to an action"""
     bl_idname = "nla.bake"
     bl_label = "Bake Action"
     bl_options = {'REGISTER', 'UNDO'}
@@ -222,7 +222,7 @@ class BakeAction(Operator):
             default=1,
             )
     only_selected = BoolProperty(
-            name="Only Selected",
+            name="Only Selected Bones",
             description="Only key selected bones (Pose baking only)",
             default=True,
             )
@@ -259,18 +259,16 @@ class BakeAction(Operator):
 
     def execute(self, context):
         from bpy_extras import anim_utils
+        objects = context.selected_editable_objects
+        object_action_pairs = (
+            [(obj, getattr(obj.animation_data, "action", None)) for obj in objects]
+            if self.use_current_action else
+            [(obj, None) for obj in objects]
+        )
 
-        obj = context.object
-        action = None
-        if self.use_current_action:
-            if obj.animation_data:
-                action = obj.animation_data.action
-
-        action = anim_utils.bake_action(
-            obj,
-            self.frame_start,
-            self.frame_end,
-            frame_step=self.step,
+        actions = anim_utils.bake_action_objects(
+            object_action_pairs,
+            frames=range(self.frame_start, self.frame_end + 1, self.step),
             only_selected=self.only_selected,
             do_pose='POSE' in self.bake_types,
             do_object='OBJECT' in self.bake_types,
@@ -278,10 +276,9 @@ class BakeAction(Operator):
             do_constraint_clear=self.clear_constraints,
             do_parents_clear=self.clear_parents,
             do_clean=True,
-            action=action,
         )
 
-        if action is None:
+        if not any(actions):
             self.report({'INFO'}, "Nothing to bake")
             return {'CANCELLED'}