Versioning for layered actions

This implements versioning code to go from legacy to layered action.
The versioning is only triggered when the experimental flag for
Multi-Slot actions is enabled.

All the actions are converted in place, which should be fine because
of backwards and forwards compatibility with layered actions.

Pull Request: https://projects.blender.org/blender/blender/pulls/127842
This commit is contained in:
Christoph Lendenfeld
2024-09-26 15:45:53 +02:00
committed by Christoph Lendenfeld
parent 5f8721d92a
commit bcd0d14943
4 changed files with 223 additions and 1 deletions

View File

@@ -79,6 +79,8 @@
#include "SEQ_sequencer.hh"
#include "SEQ_time.hh"
#include "ANIM_action.hh"
#include "ANIM_action_iterators.hh"
#include "ANIM_armature_iter.hh"
#include "ANIM_bone_collections.hh"
@@ -105,6 +107,126 @@ static void version_composite_nodetree_null_id(bNodeTree *ntree, Scene *scene)
}
}
struct ActionUserInfo {
ID *id;
blender::animrig::slot_handle_t *slot_handle;
bAction **action_ptr_ptr;
char *slot_name;
};
static void convert_action_in_place(blender::animrig::Action &action)
{
using namespace blender::animrig;
if (action.is_action_layered()) {
return;
}
Slot &slot = action.slot_add();
slot.idtype = action.idroot;
action.idroot = 0;
Layer &layer = action.layer_add("Layer");
blender::animrig::Strip &strip = layer.strip_add(action,
blender::animrig::Strip::Type::Keyframe);
ChannelBag &bag = strip.data<StripKeyframeData>(action).channelbag_for_slot_ensure(slot);
const int fcu_count = BLI_listbase_count(&action.curves);
const int group_count = BLI_listbase_count(&action.groups);
bag.fcurve_array = MEM_cnew_array<FCurve *>(fcu_count, "Action versioning - fcurves");
bag.fcurve_array_num = fcu_count;
bag.group_array = MEM_cnew_array<bActionGroup *>(group_count, "Action versioning - groups");
bag.group_array_num = group_count;
int group_index = 0;
int fcurve_index = 0;
LISTBASE_FOREACH_INDEX (bActionGroup *, group, &action.groups, group_index) {
bag.group_array[group_index] = group;
group->channel_bag = &bag;
group->fcurve_range_start = fcurve_index;
LISTBASE_FOREACH (FCurve *, fcu, &group->channels) {
if (fcu->grp != group) {
break;
}
bag.fcurve_array[fcurve_index++] = fcu;
}
group->fcurve_range_length = fcurve_index - group->fcurve_range_start;
}
LISTBASE_FOREACH (FCurve *, fcu, &action.curves) {
/* Any fcurves with groups have already been added to the fcurve array. */
if (fcu->grp) {
continue;
}
bag.fcurve_array[fcurve_index++] = fcu;
}
BLI_assert(fcurve_index == fcu_count);
action.curves = {nullptr, nullptr};
action.groups = {nullptr, nullptr};
}
static void version_legacy_actions_to_layered(Main *bmain)
{
using namespace blender::animrig;
blender::Map<bAction *, blender::Vector<ActionUserInfo>> action_users;
LISTBASE_FOREACH (bAction *, dna_action, &bmain->actions) {
Action &action = dna_action->wrap();
if (action.is_action_layered()) {
continue;
}
action_users.add(dna_action, {});
}
ID *id;
FOREACH_MAIN_ID_BEGIN (bmain, id) {
auto callback =
[&](bAction *&action_ptr_ref, slot_handle_t &slot_handle_ref, char *slot_name) -> bool {
blender::Vector<ActionUserInfo> *action_user_vector = action_users.lookup_ptr(
action_ptr_ref);
/* Only actions that need to be converted are in this map. */
if (!action_user_vector) {
return true;
}
ActionUserInfo user_info;
user_info.id = id;
user_info.action_ptr_ptr = &action_ptr_ref;
user_info.slot_handle = &slot_handle_ref;
user_info.slot_name = slot_name;
action_user_vector->append(user_info);
return true;
};
foreach_action_slot_use_with_references(*id, callback);
}
FOREACH_MAIN_ID_END;
for (const auto &item : action_users.items()) {
Action &action = item.key->wrap();
convert_action_in_place(action);
blender::Vector<ActionUserInfo> &user_infos = item.value;
if (user_infos.size() == 1) {
/* Rename the slot after its single user. If there are multiple users, the name is unchanged
* because there is no good way to determine a name. */
action.slot_name_set(*bmain, *action.slot(0), user_infos[0].id->name);
}
for (ActionUserInfo &action_user : user_infos) {
BLI_assert_msg(*action_user.slot_handle == Slot::unassigned,
"Because the action was just converted from legacy, none of the users of "
"that action should have a slot set yet.");
ActionSlotAssignmentResult result = generic_assign_action_slot_handle(
action.slot(0)->handle,
*action_user.id,
*action_user.action_ptr_ptr,
*action_user.slot_handle,
action_user.slot_name);
BLI_assert(result == ActionSlotAssignmentResult::OK);
UNUSED_VARS_NDEBUG(result);
}
}
}
/* Move bone-group color to the individual bones. */
static void version_bonegroup_migrate_color(Main *bmain)
{
@@ -1056,6 +1178,12 @@ void do_versions_after_linking_400(FileData *fd, Main *bmain)
*
* \note Keep this message at the bottom of the function.
*/
/* Keeping this block is without a `MAIN_VERSION_FILE_ATLEAST` until the experimental flag is
* removed. */
if (USER_EXPERIMENTAL_TEST(&U, use_animation_baklava)) {
version_legacy_actions_to_layered(bmain);
}
}
static void version_mesh_legacy_to_struct_of_array_format(Mesh &mesh)

View File

@@ -405,6 +405,8 @@ if(WITH_EXPERIMENTAL_FEATURES)
add_blender_test(
bl_animation_action
--python ${CMAKE_CURRENT_LIST_DIR}/bl_animation_action.py
--
--testdir "${TEST_SRC_DIR}/animation"
)
endif()

View File

@@ -4,6 +4,7 @@
import unittest
import sys
import pathlib
import bpy
@@ -432,6 +433,96 @@ class DataPathTest(unittest.TestCase):
self.assertEqual("bpy.data.actions['TestAction'].layers[\"Layer\"].strips[0].channelbags[0]", repr(channelbag))
class VersioningTest(unittest.TestCase):
def setUp(self):
enable_experimental_animation_baklava()
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "layered_action_versioning_42.blend"), load_ui=False)
def tearDown(self) -> None:
disable_experimental_animation_baklava()
def test_nla_conversion(self):
nla_object = bpy.data.objects["nla_object"]
nla_anim_data = nla_object.animation_data
self.assertTrue(nla_anim_data.action.is_action_layered)
self.assertNotEqual(nla_anim_data.action_slot_handle, 0)
# The action that is not pushed into an NLA strip.
active_action = nla_anim_data.action
strip = active_action.layers[0].strips[0]
for fcurve_index, fcurve in enumerate(strip.channelbags[0].fcurves):
self.assertEqual(fcurve.data_path, "rotation_euler")
self.assertEqual(fcurve.group.name, "Object Transforms")
self.assertEqual(fcurve.array_index, fcurve_index)
self.assertEqual(len(nla_anim_data.nla_tracks), 2)
self.assertTrue(nla_anim_data.nla_tracks[0].strips[0].action.is_action_layered)
self.assertNotEqual(nla_anim_data.nla_tracks[0].strips[0].action_slot_handle, 0)
self.assertTrue(nla_anim_data.nla_tracks[1].strips[0].action.is_action_layered)
self.assertNotEqual(nla_anim_data.nla_tracks[1].strips[0].action_slot_handle, 0)
def test_multi_use_action(self):
object_a = bpy.data.objects["multi_user_object_a"]
object_b = bpy.data.objects["multi_user_object_b"]
self.assertTrue(object_a.animation_data.action.is_action_layered)
self.assertNotEqual(object_a.animation_data.action_slot_handle, 0)
self.assertTrue(object_b.animation_data.action.is_action_layered)
self.assertNotEqual(object_b.animation_data.action_slot_handle, 0)
self.assertEqual(object_a.animation_data.action, object_b.animation_data.action)
self.assertEqual(object_a.animation_data.action_slot_handle, object_b.animation_data.action_slot_handle)
action = object_a.animation_data.action
strip = action.layers[0].strips[0]
self.assertEqual(len(strip.channelbags[0].fcurves), 9)
self.assertEqual(len(strip.channelbags[0].groups), 1)
self.assertEqual(len(strip.channelbags[0].groups[0].channels), 9)
# Multi user slots do not get named after their users.
self.assertEqual(action.slots[0].name, "OBSlot")
def test_action_constraint(self):
constrained_object = bpy.data.objects["action_constraint_constrained"]
action_constraint = constrained_object.constraints[0]
self.assertTrue(action_constraint.action.is_action_layered)
self.assertNotEqual(action_constraint.action_slot_handle, 0)
action_owner_object = bpy.data.objects["action_constraint_action_owner"]
action = action_owner_object.animation_data.action
self.assertTrue(action.is_action_layered)
self.assertEqual(action, action_constraint.action)
self.assertEqual(action_owner_object.animation_data.action_slot_handle, action_constraint.action_slot_handle)
strip = action.layers[0].strips[0]
self.assertEqual(len(strip.channelbags[0].fcurves), 1)
fcurve = strip.channelbags[0].fcurves[0]
self.assertEqual(fcurve.data_path, "location")
self.assertEqual(fcurve.array_index, 2)
self.assertEqual(fcurve.group.name, "Object Transforms")
def test_armature_action_conversion(self):
armature_object = bpy.data.objects["armature_object"]
action = armature_object.animation_data.action
self.assertTrue(action.is_action_layered)
strip = action.layers[0].strips[0]
self.assertEqual(len(strip.channelbags[0].groups), 2)
self.assertEqual(strip.channelbags[0].groups[0].name, "Bone")
self.assertEqual(strip.channelbags[0].groups[1].name, "Bone.001")
self.assertEqual(len(strip.channelbags[0].fcurves), 20)
self.assertEqual(len(strip.channelbags[0].groups[0].channels), 10)
self.assertEqual(len(strip.channelbags[0].groups[1].channels), 10)
# Slots with a single user are named after their user.
self.assertEqual(action.slots[0].name, "OBarmature_object")
for fcurve in strip.channelbags[0].groups[0].channels:
self.assertEqual(fcurve.group.name, "Bone")
for fcurve in strip.channelbags[0].groups[1].channels:
self.assertEqual(fcurve.group.name, "Bone.001")
def main():
global args
import argparse
@@ -441,6 +532,7 @@ def main():
argv += sys.argv[sys.argv.index('--') + 1:]
parser = argparse.ArgumentParser()
parser.add_argument('--testdir', required=True, type=pathlib.Path)
args, remaining = parser.parse_known_args(argv)
unittest.main(argv=remaining)