Files
test/tests/python/bl_animation_bake.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

244 lines
11 KiB
Python
Raw Normal View History

# SPDX-FileCopyrightText: 2025 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import unittest
import sys
import pathlib
import bpy
from bpy_extras import anim_utils
"""
blender -b --factory-startup --python tests/python/bl_animation_bake.py
"""
OBJECT_BAKE_OPTIONS = anim_utils.BakeOptions(
only_selected=False,
do_pose=False,
do_object=True,
do_visual_keying=False,
do_constraint_clear=False,
do_parents_clear=False,
do_clean=False,
do_location=True,
do_rotation=True,
do_scale=True,
do_bbone=False,
do_custom_props=False,
)
class ObjectBakeTest(unittest.TestCase):
"""This tests the animation baking to document the current behavior without any attempt of declaring that behavior correct or good."""
obj: bpy.types.Object
def setUp(self) -> None:
bpy.ops.wm.read_homefile(use_factory_startup=True)
self.obj = bpy.data.objects.new("test_object", None)
bpy.context.scene.collection.objects.link(self.obj)
self.obj.animation_data_create()
def test_bake_object_without_animation(self):
self.assertEqual(self.obj.animation_data.action, None)
anim_utils.bake_action_objects([(self.obj, None)], frames=range(0, 10), bake_options=OBJECT_BAKE_OPTIONS)
action = self.obj.animation_data.action
self.assertIsNotNone(action, "Baking without an existing action should create an action")
self.assertEqual(len(action.slots), 1, "Baking should have created a slot")
self.assertEqual(action.slots[0], self.obj.animation_data.action_slot)
channelbag = anim_utils.action_get_channelbag_for_slot(action, action.slots[0])
self.assertIsNotNone(channelbag)
self.assertEqual(len(channelbag.fcurves), 9, "If no animation is present, FCurves are created for all channels")
for fcurve in channelbag.fcurves:
self.assertEqual(len(fcurve.keyframe_points), 10, f"Unexpected key count on {fcurve.data_path}")
self.assertAlmostEqual(fcurve.keyframe_points[0].co.x, 0, 6,
f"Unexpected key y position on {fcurve.data_path}")
self.assertAlmostEqual(fcurve.keyframe_points[-1].co.x, 9, 6, "Baking range is exclusive for the end")
def test_bake_object_animation_to_new_action(self):
action = bpy.data.actions.new("test_action")
self.obj.animation_data.action = action
bpy.context.scene.frame_set(0)
self.obj.keyframe_insert("location")
bpy.context.scene.frame_set(15)
self.obj.location = (1, 1, 1)
self.obj.keyframe_insert("location")
# Passing None here will create a new action.
anim_utils.bake_action_objects([(self.obj, None)], frames=range(0, 10), bake_options=OBJECT_BAKE_OPTIONS)
self.assertNotEqual(action, self.obj.animation_data.action, "Expected baking to result in a new action")
baked_action = self.obj.animation_data.action
self.assertEqual(len(baked_action.slots), 1)
self.assertEqual(baked_action.slots[0].name_display, action.slots[0].name_display)
channelbag = anim_utils.action_get_channelbag_for_slot(baked_action, self.obj.animation_data.action_slot)
self.assertIsNotNone(channelbag)
self.assertEqual(len(channelbag.fcurves), 9)
for fcurve in channelbag.fcurves:
self.assertEqual(len(fcurve.keyframe_points), 10, f"Unexpected key count on {fcurve.data_path}")
self.assertAlmostEqual(fcurve.keyframe_points[-1].co.x, 9,
6, f"Baking to a new action should delete all keys outside the given range ({fcurve.data_path})")
def test_bake_object_animation_to_existing_action(self):
action = bpy.data.actions.new("test_action")
self.obj.animation_data.action = action
bpy.context.scene.frame_set(0)
self.obj.keyframe_insert("location")
bpy.context.scene.frame_set(15)
self.obj.location = (1, 1, 1)
self.obj.keyframe_insert("location")
# Passing the action as the second element of the tuple means that it will be written into.
anim_utils.bake_action_objects([(self.obj, action)], frames=range(0, 10), bake_options=OBJECT_BAKE_OPTIONS)
self.assertEqual(self.obj.animation_data.action, action)
self.assertEqual(len(action.slots), 1)
channelbag = anim_utils.action_get_channelbag_for_slot(action, self.obj.animation_data.action_slot)
self.assertIsNotNone(channelbag)
self.assertEqual(len(channelbag.fcurves), 9)
for fcurve in channelbag.fcurves:
if fcurve.data_path == "location":
self.assertAlmostEqual(fcurve.keyframe_points[-1].co.x, 15,
6, f"Baking over an existing action should preserve all keys even those out of range ({fcurve.data_path})")
self.assertEqual(len(fcurve.keyframe_points), 11, f"Unexpected key count on {fcurve.data_path}")
else:
self.assertAlmostEqual(fcurve.keyframe_points[-1].co.x, 9,
6, f"Unexpected key y position on {fcurve.data_path}")
self.assertEqual(len(fcurve.keyframe_points), 10, f"Unexpected key count on {fcurve.data_path}")
def test_bake_object_multi_slot_to_new_action(self):
obj2 = bpy.data.objects.new("obj2", None)
bpy.context.scene.collection.objects.link(obj2)
action = bpy.data.actions.new("test_action")
self.obj.animation_data.action = action
obj2.animation_data_create().action = action
bpy.context.scene.frame_set(0)
self.obj.location = (0, 0, 0)
self.obj.keyframe_insert("location")
obj2.location = (0, 1, 0)
obj2.keyframe_insert("location")
bpy.context.scene.frame_set(9)
self.obj.location = (2, 0, 0)
self.obj.keyframe_insert("location")
obj2.location = (2, 1, 0)
obj2.keyframe_insert("location")
self.assertIsNotNone(self.obj.animation_data.action_slot)
self.assertIsNotNone(obj2.animation_data.action_slot)
self.assertNotEqual(self.obj.animation_data.action_slot, obj2.animation_data.action_slot)
original_slot = obj2.animation_data.action_slot
anim_utils.bake_action_objects([(obj2, None)], frames=range(0, 10), bake_options=OBJECT_BAKE_OPTIONS)
self.assertNotEqual(action, obj2.animation_data.action, "Expected baking to result in a new action")
baked_action = obj2.animation_data.action
self.assertEqual(len(baked_action.slots), 1)
self.assertEqual(original_slot.name_display, baked_action.slots[0].name_display)
channelbag = anim_utils.action_get_channelbag_for_slot(baked_action, baked_action.slots[0])
for fcurve in channelbag.fcurves:
if fcurve.data_path != "location":
continue
# The keyframes should match the animation of obj2, not self.obj.
if fcurve.array_index == 0:
self.assertAlmostEqual(fcurve.keyframe_points[0].co.y, 0,
6, f"Unexpected key y position on {fcurve.data_path}")
self.assertAlmostEqual(fcurve.keyframe_points[-1].co.y, 2,
6, f"Unexpected key y position on {fcurve.data_path}")
elif fcurve.array_index == 1:
self.assertAlmostEqual(fcurve.keyframe_points[0].co.y, 1,
6, f"Unexpected key y position on {fcurve.data_path}")
self.assertAlmostEqual(fcurve.keyframe_points[-1].co.y, 1,
6, f"Unexpected key y position on {fcurve.data_path}")
def test_bake_object_multi_slot_to_existing_action(self):
obj2 = bpy.data.objects.new("obj2", None)
bpy.context.scene.collection.objects.link(obj2)
action = bpy.data.actions.new("test_action")
self.obj.animation_data.action = action
obj2.animation_data_create().action = action
bpy.context.scene.frame_set(0)
self.obj.location = (0, 0, 0)
self.obj.keyframe_insert("location")
obj2.location = (0, 1, 0)
obj2.keyframe_insert("location")
bpy.context.scene.frame_set(15)
self.obj.location = (2, 0, 0)
self.obj.keyframe_insert("location")
obj2.location = (2, 1, 0)
obj2.keyframe_insert("location")
self.assertEqual(len(action.slots), 2)
self.assertIsNotNone(self.obj.animation_data.action_slot)
self.assertIsNotNone(obj2.animation_data.action_slot)
anim_utils.bake_action_objects([(obj2, action)], frames=range(0, 10), bake_options=OBJECT_BAKE_OPTIONS)
self.assertEqual(action, obj2.animation_data.action)
self.assertEqual(len(action.slots), 2, "Didn't expect baking to create a new slot")
self.assertNotEqual(obj2.animation_data.action_slot, self.obj.animation_data.action_slot)
channelbag = anim_utils.action_get_channelbag_for_slot(action, obj2.animation_data.action_slot)
self.assertIsNotNone(channelbag)
self.assertEqual(len(channelbag.fcurves), 9)
for fcurve in channelbag.fcurves:
# The keyframes should match the animation of obj2, not self.obj.
if fcurve.data_path == "location":
self.assertAlmostEqual(fcurve.keyframe_points[-1].co.x, 15,
6, f"Baking over an existing action should preserve all keys even those out of range ({fcurve.data_path})")
self.assertEqual(len(fcurve.keyframe_points), 11, f"Unexpected key count on {fcurve.data_path}")
if fcurve.array_index == 0:
self.assertAlmostEqual(fcurve.keyframe_points[-1].co.y, 2,
6, f"Unexpected key y position on {fcurve.data_path}")
elif fcurve.array_index == 1:
self.assertAlmostEqual(fcurve.keyframe_points[-1].co.y, 1,
6, f"Unexpected key y position on {fcurve.data_path}")
else:
self.assertAlmostEqual(fcurve.keyframe_points[-1].co.x, 9,
6, f"Unexpected key y position on {fcurve.data_path}")
self.assertEqual(len(fcurve.keyframe_points), 10, f"Unexpected key count on {fcurve.data_path}")
def main():
global args
import argparse
argv = [sys.argv[0]]
if '--' in sys.argv:
argv += sys.argv[sys.argv.index('--') + 1:]
parser = argparse.ArgumentParser()
parser.add_argument(
"--output-dir",
dest="output_dir",
type=pathlib.Path,
default=pathlib.Path("."),
help="Where to output temp saved blendfiles",
required=False,
)
args, remaining = parser.parse_known_args(argv)
unittest.main(argv=remaining)
if __name__ == "__main__":
main()