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:
committed by
Christoph Lendenfeld
parent
5f8721d92a
commit
bcd0d14943
@@ -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)
|
||||
|
||||
Submodule tests/data updated: f3eaf4c561...0ff59eafc7
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user