Anim: Reuse action between related data
When inserting keys, look on related IDs for an action to reuse that. This will make use of the slot system on the new layered action to ensure the animation data doesn't collide. This is done on the `id_action_ensure` function since that is the common function to get an action off an `ID`. IDs with more than 1 user will be skipped. "Related ID" in this case is hardcoded with a `switch` statement for each ID type. The system builds a list starting from the ID that should be keyed and keeps expanding the list until an action is found or no more non-duplicate IDs can be added. (This is using linear search for duplicate checks, but I don't think we will deal with a lot of IDs in this case) Pull Request: https://projects.blender.org/blender/blender/pulls/126655
This commit is contained in:
committed by
Christoph Lendenfeld
parent
d911d673a4
commit
cb6ed12ef1
@@ -5,14 +5,17 @@
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
#include "ANIM_animdata.hh"
|
||||
|
||||
#include "BKE_action.hh"
|
||||
#include "BKE_anim_data.hh"
|
||||
#include "BKE_fcurve.hh"
|
||||
#include "BKE_key.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_main.hh"
|
||||
#include "BKE_material.h"
|
||||
#include "BKE_node.hh"
|
||||
|
||||
#include "BLT_translation.hh"
|
||||
|
||||
@@ -23,6 +26,10 @@
|
||||
#include "DEG_depsgraph_build.hh"
|
||||
|
||||
#include "DNA_anim_types.h"
|
||||
#include "DNA_key_types.h"
|
||||
#include "DNA_material_types.h"
|
||||
#include "DNA_mesh_types.h"
|
||||
#include "DNA_particle_types.h"
|
||||
|
||||
#include "ED_anim_api.hh"
|
||||
|
||||
@@ -35,6 +42,148 @@ namespace blender::animrig {
|
||||
/** \name Public F-Curves API
|
||||
* \{ */
|
||||
|
||||
/* Find the users of the given ID within the objects of `bmain` and add non-duplicates to the end
|
||||
* of `related_ids`. */
|
||||
static void add_object_data_users(const Main &bmain, const ID &id, Vector<ID *> &related_ids)
|
||||
{
|
||||
if (ID_REAL_USERS(&id) != 1) {
|
||||
/* Only find objects if this ID is only used once. */
|
||||
return;
|
||||
}
|
||||
|
||||
Object *ob;
|
||||
ID *object_id;
|
||||
FOREACH_MAIN_LISTBASE_ID_BEGIN (&bmain.objects, object_id) {
|
||||
ob = (Object *)object_id;
|
||||
if (ob->data != &id) {
|
||||
continue;
|
||||
}
|
||||
related_ids.append_non_duplicates(&ob->id);
|
||||
}
|
||||
FOREACH_MAIN_LISTBASE_ID_END;
|
||||
}
|
||||
|
||||
/* Find an action on an ID that is related to the given ID. Related things are e.g. Object<->Data,
|
||||
* Mesh<->Material and so on. The exact relationships are defined per ID type. Only relationships
|
||||
* of 1:1 are traced. The case of multiple users for 1 ID is treated as not related. */
|
||||
static bAction *find_related_action(Main &bmain, ID &id)
|
||||
{
|
||||
Vector<ID *> related_ids({&id});
|
||||
|
||||
/* `related_ids` can grow during an iteration if the ID of the current iteration has associated
|
||||
* code that defines relationships. */
|
||||
for (int i = 0; i < related_ids.size(); i++) {
|
||||
ID *related_id = related_ids[i];
|
||||
|
||||
Action *action = get_action(*related_id);
|
||||
if (action && action->is_action_layered()) {
|
||||
/* Returning the first action found means highest priority has the action closest in the
|
||||
* relationship graph. */
|
||||
return action;
|
||||
}
|
||||
|
||||
if (related_id->flag & ID_FLAG_EMBEDDED_DATA) {
|
||||
/* No matter the type of embedded ID, their owner can always be added to the related IDs. */
|
||||
BLI_assert(ID_REAL_USERS(related_id) == 0);
|
||||
ID *owner_id = BKE_id_owner_get(related_id);
|
||||
/* Embedded IDs should always have an owner. */
|
||||
BLI_assert(owner_id != nullptr);
|
||||
related_ids.append_non_duplicates(owner_id);
|
||||
}
|
||||
|
||||
/* No action found on current ID, add related IDs to the ID Vector. */
|
||||
switch (GS(related_id->name)) {
|
||||
case ID_OB: {
|
||||
Object *ob = (Object *)related_id;
|
||||
if (!ob->data) {
|
||||
break;
|
||||
}
|
||||
ID *data = (ID *)ob->data;
|
||||
if (ID_REAL_USERS(data) == 1) {
|
||||
related_ids.append_non_duplicates(data);
|
||||
}
|
||||
LISTBASE_FOREACH (ParticleSystem *, particle_system, &ob->particlesystem) {
|
||||
if (!particle_system) {
|
||||
continue;
|
||||
}
|
||||
if (ID_REAL_USERS(&particle_system->part->id) != 1) {
|
||||
continue;
|
||||
}
|
||||
related_ids.append_non_duplicates(&particle_system->part->id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ID_KE: {
|
||||
/* Shapekeys. */
|
||||
Key *key = (Key *)related_id;
|
||||
/* Shapekeys are not embedded but there is currently no way to reuse them. */
|
||||
BLI_assert(ID_REAL_USERS(related_id) == 1);
|
||||
related_ids.append_non_duplicates(key->from);
|
||||
break;
|
||||
}
|
||||
|
||||
case ID_MA: {
|
||||
/* Explicitly not relating materials and material users. */
|
||||
Material *mat = (Material *)related_id;
|
||||
if (mat->nodetree && ID_REAL_USERS(&mat->nodetree->id) == 1) {
|
||||
related_ids.append_non_duplicates(&mat->nodetree->id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ID_PA: {
|
||||
if (ID_REAL_USERS(related_id) != 1) {
|
||||
continue;
|
||||
}
|
||||
Object *ob;
|
||||
ID *object_id;
|
||||
/* Find users of this particle setting. */
|
||||
FOREACH_MAIN_LISTBASE_ID_BEGIN (&bmain.objects, object_id) {
|
||||
ob = (Object *)object_id;
|
||||
bool object_uses_particle_settings = false;
|
||||
LISTBASE_FOREACH (ParticleSystem *, particle_system, &ob->particlesystem) {
|
||||
if (!particle_system) {
|
||||
continue;
|
||||
}
|
||||
if (&particle_system->part->id != related_id) {
|
||||
continue;
|
||||
}
|
||||
object_uses_particle_settings = true;
|
||||
break;
|
||||
}
|
||||
if (object_uses_particle_settings) {
|
||||
related_ids.append_non_duplicates(&ob->id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
FOREACH_MAIN_LISTBASE_ID_END;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
/* Just check if the ID is used as object data somewhere. */
|
||||
add_object_data_users(bmain, *related_id, related_ids);
|
||||
bNodeTree *node_tree = bke::node_tree_from_id(related_id);
|
||||
if (node_tree && ID_REAL_USERS(&node_tree->id) == 1) {
|
||||
related_ids.append_non_duplicates(&node_tree->id);
|
||||
}
|
||||
|
||||
Key *key = BKE_key_from_id(related_id);
|
||||
if (key) {
|
||||
/* No check for multi user because the Shapekey cannot be shared. */
|
||||
BLI_assert(ID_REAL_USERS(&key->id) == 1);
|
||||
related_ids.append_non_duplicates(&key->id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bAction *id_action_ensure(Main *bmain, ID *id)
|
||||
{
|
||||
AnimData *adt;
|
||||
@@ -53,18 +202,37 @@ bAction *id_action_ensure(Main *bmain, ID *id)
|
||||
/* init action if none available yet */
|
||||
/* TODO: need some wizardry to handle NLA stuff correct */
|
||||
if (adt->action == nullptr) {
|
||||
/* init action name from name of ID block */
|
||||
char actname[sizeof(id->name) - 2];
|
||||
SNPRINTF(actname, DATA_("%sAction"), id->name + 2);
|
||||
bAction *action = nullptr;
|
||||
if (USER_EXPERIMENTAL_TEST(&U, use_animation_baklava)) {
|
||||
action = find_related_action(*bmain, *id);
|
||||
}
|
||||
if (action == nullptr) {
|
||||
/* init action name from name of ID block */
|
||||
char actname[sizeof(id->name) - 2];
|
||||
if (id->flag & ID_FLAG_EMBEDDED_DATA && USER_EXPERIMENTAL_TEST(&U, use_animation_baklava)) {
|
||||
/* When the ID is embedded, use the name of the owner ID for clarity. */
|
||||
ID *owner_id = BKE_id_owner_get(id);
|
||||
/* If the ID is embedded it should have an owner. */
|
||||
BLI_assert(owner_id != nullptr);
|
||||
SNPRINTF(actname, DATA_("%sAction"), owner_id->name + 2);
|
||||
}
|
||||
else if (GS(id->name) == ID_KE && USER_EXPERIMENTAL_TEST(&U, use_animation_baklava)) {
|
||||
Key *key = (Key *)id;
|
||||
SNPRINTF(actname, DATA_("%sAction"), key->from->name + 2);
|
||||
}
|
||||
else {
|
||||
SNPRINTF(actname, DATA_("%sAction"), id->name + 2);
|
||||
}
|
||||
|
||||
/* create action */
|
||||
adt->action = BKE_action_add(bmain, actname);
|
||||
|
||||
/* set ID-type from ID-block that this is going to be assigned to
|
||||
* so that users can't accidentally break actions by assigning them
|
||||
* to the wrong places
|
||||
*/
|
||||
BKE_animdata_action_ensure_idroot(id, adt->action);
|
||||
/* create action */
|
||||
action = BKE_action_add(bmain, actname);
|
||||
/* set ID-type from ID-block that this is going to be assigned to
|
||||
* so that users can't accidentally break actions by assigning them
|
||||
* to the wrong places
|
||||
*/
|
||||
BKE_animdata_action_ensure_idroot(id, adt->action);
|
||||
}
|
||||
adt->action = action;
|
||||
|
||||
/* Tag depsgraph to be rebuilt to include time dependency. */
|
||||
DEG_relations_tag_update(bmain);
|
||||
|
||||
@@ -13,10 +13,13 @@
|
||||
#include "BKE_idtype.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_main.hh"
|
||||
#include "BKE_material.h"
|
||||
#include "BKE_mesh.hh"
|
||||
#include "BKE_nla.hh"
|
||||
#include "BKE_object.hh"
|
||||
|
||||
#include "DNA_anim_types.h"
|
||||
#include "DNA_material_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "RNA_access.hh"
|
||||
@@ -50,6 +53,14 @@ class KeyframingTest : public testing::Test {
|
||||
PointerRNA object_with_nla_rna_pointer;
|
||||
bAction *nla_action;
|
||||
|
||||
/* For action reuse testing. */
|
||||
Object *cube;
|
||||
PointerRNA cube_rna_pointer;
|
||||
Mesh *cube_mesh;
|
||||
PointerRNA cube_mesh_rna_pointer;
|
||||
Material *material;
|
||||
PointerRNA material_rna_pointer;
|
||||
|
||||
static void SetUpTestSuite()
|
||||
{
|
||||
/* BKE_id_free() hits a code path that uses CLOG, which crashes if not initialized properly. */
|
||||
@@ -96,6 +107,20 @@ class KeyframingTest : public testing::Test {
|
||||
object_with_nla_rna_pointer = RNA_id_pointer_create(&object_with_nla->id);
|
||||
nla_action = static_cast<bAction *>(BKE_id_new(bmain, ID_AC, "NLAAction"));
|
||||
|
||||
cube = BKE_object_add_only_object(bmain, OB_MESH, "cube");
|
||||
cube_rna_pointer = RNA_id_pointer_create(&cube->id);
|
||||
cube_mesh = BKE_mesh_add(bmain, "cube_mesh");
|
||||
cube_mesh_rna_pointer = RNA_id_pointer_create(&cube_mesh->id);
|
||||
/* Removing the implicit id user. Using BKE_mesh_assign_object increments the user count which
|
||||
* would leave it at 2 otherwise. */
|
||||
id_us_min(&cube_mesh->id);
|
||||
BKE_mesh_assign_object(bmain, cube, cube_mesh);
|
||||
material = BKE_material_add(bmain, "material");
|
||||
material_rna_pointer = RNA_id_pointer_create(&material->id);
|
||||
|
||||
id_us_min(&material->id);
|
||||
BKE_object_material_assign(bmain, cube, material, 0, BKE_MAT_ASSIGN_OBDATA);
|
||||
|
||||
/* Set up an NLA system with a single NLA track with a single offset-in-time
|
||||
* NLA strip, and make that strip active and in tweak mode. */
|
||||
AnimData *adt = BKE_animdata_ensure_id(&object_with_nla->id);
|
||||
@@ -218,6 +243,190 @@ TEST_F(KeyframingTest, insert_keyframes__layered_action__non_array_property)
|
||||
EXPECT_EQ(7.0, fcurve->bezt[1].vec[1][1]);
|
||||
}
|
||||
|
||||
TEST_F(KeyframingTest, insert_keyframes__layered_action__action_reuse)
|
||||
{
|
||||
/* Turn on Baklava experimental flag. */
|
||||
U.flag |= USER_DEVELOPER_UI;
|
||||
U.experimental.use_animation_baklava = 1;
|
||||
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
CombinedKeyingResult result_ob;
|
||||
result_ob = insert_keyframes(bmain,
|
||||
&armature_object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"location"}},
|
||||
10.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 3);
|
||||
ASSERT_TRUE(armature_object->adt != nullptr);
|
||||
ASSERT_TRUE(armature_object->adt->action != nullptr);
|
||||
|
||||
PointerRNA armature_rna_pointer = RNA_id_pointer_create(&armature->id);
|
||||
|
||||
result_ob = insert_keyframes(bmain,
|
||||
&armature_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"display_type"}},
|
||||
10.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 1);
|
||||
ASSERT_TRUE(armature->adt != nullptr);
|
||||
ASSERT_TRUE(armature->adt->action != nullptr);
|
||||
|
||||
/* Action is expected to be reused between object and data. */
|
||||
ASSERT_EQ(armature->adt->action, armature_object->adt->action);
|
||||
|
||||
Action &action = armature->adt->action->wrap();
|
||||
/* Should have two slots now. */
|
||||
ASSERT_EQ(action.slot_array_num, 2);
|
||||
for (Slot *slot : action.slots()) {
|
||||
ASSERT_TRUE(slot->idtype == ID_AR || slot->idtype == ID_OB);
|
||||
}
|
||||
|
||||
U.experimental.use_animation_baklava = 0;
|
||||
U.flag &= ~USER_DEVELOPER_UI;
|
||||
}
|
||||
|
||||
TEST_F(KeyframingTest, insert_keyframes__layered_action__action_reuse_material)
|
||||
{
|
||||
U.flag |= USER_DEVELOPER_UI;
|
||||
U.experimental.use_animation_baklava = 1;
|
||||
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
CombinedKeyingResult result_ob;
|
||||
|
||||
result_ob = insert_keyframes(bmain,
|
||||
&material_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"pass_index"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 1);
|
||||
ASSERT_TRUE(material->adt != nullptr);
|
||||
ASSERT_TRUE(material->adt->action != nullptr);
|
||||
|
||||
result_ob = insert_keyframes(bmain,
|
||||
&cube_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"location"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 3);
|
||||
ASSERT_TRUE(cube->adt != nullptr);
|
||||
ASSERT_TRUE(cube->adt->action != nullptr);
|
||||
|
||||
/* Actions are not shared between object and material. */
|
||||
ASSERT_NE(cube->adt->action, material->adt->action);
|
||||
|
||||
result_ob = insert_keyframes(bmain,
|
||||
&cube_mesh_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"remesh_voxel_size"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 1);
|
||||
ASSERT_TRUE(cube_mesh->adt != nullptr);
|
||||
ASSERT_TRUE(cube_mesh->adt->action != nullptr);
|
||||
|
||||
/* Reuse between Object and object data. */
|
||||
ASSERT_EQ(cube_mesh->adt->action, cube->adt->action);
|
||||
/* Still no reuse from mesh to material. */
|
||||
ASSERT_NE(cube_mesh->adt->action, material->adt->action);
|
||||
|
||||
Action &action = cube->adt->action->wrap();
|
||||
/* Should have two slots now. */
|
||||
ASSERT_EQ(action.slot_array_num, 2);
|
||||
|
||||
/* Material action should have only 1 slot. */
|
||||
ASSERT_EQ(material->adt->action->wrap().slot_array_num, 1);
|
||||
|
||||
for (Slot *slot : action.slots()) {
|
||||
ASSERT_TRUE(slot->idtype == ID_ME || slot->idtype == ID_OB);
|
||||
ASSERT_NE(slot->idtype, ID_MA);
|
||||
}
|
||||
|
||||
U.experimental.use_animation_baklava = 0;
|
||||
U.flag &= ~USER_DEVELOPER_UI;
|
||||
}
|
||||
|
||||
TEST_F(KeyframingTest, insert_keyframes__layered_action__action_reuse_multiuser)
|
||||
{
|
||||
U.flag |= USER_DEVELOPER_UI;
|
||||
U.experimental.use_animation_baklava = 1;
|
||||
|
||||
Object *another_object = BKE_object_add_only_object(bmain, OB_MESH, "another_object");
|
||||
PointerRNA another_object_rna_pointer = RNA_id_pointer_create(&another_object->id);
|
||||
BKE_mesh_assign_object(bmain, another_object, cube_mesh);
|
||||
|
||||
ASSERT_EQ(ID_REFCOUNTING_USERS(&cube_mesh->id), 2);
|
||||
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
CombinedKeyingResult result_ob;
|
||||
|
||||
result_ob = insert_keyframes(bmain,
|
||||
&cube_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"location"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 3);
|
||||
ASSERT_TRUE(cube->adt != nullptr);
|
||||
ASSERT_TRUE(cube->adt->action != nullptr);
|
||||
|
||||
result_ob = insert_keyframes(bmain,
|
||||
&cube_mesh_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"remesh_voxel_size"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 1);
|
||||
ASSERT_TRUE(cube_mesh->adt != nullptr);
|
||||
ASSERT_TRUE(cube_mesh->adt->action != nullptr);
|
||||
|
||||
/* When an ID is used more than once, the action should not be reused. */
|
||||
ASSERT_NE(cube->adt->action, cube_mesh->adt->action);
|
||||
|
||||
result_ob = insert_keyframes(bmain,
|
||||
&another_object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"location"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 3);
|
||||
ASSERT_TRUE(another_object->adt != nullptr);
|
||||
ASSERT_TRUE(another_object->adt->action != nullptr);
|
||||
|
||||
/* Given that those two objects are connected by a mesh (which due to this has two users) the
|
||||
* action shouldn't be reused between them. */
|
||||
ASSERT_NE(cube->adt->action, another_object->adt->action);
|
||||
|
||||
U.experimental.use_animation_baklava = 0;
|
||||
U.flag &= ~USER_DEVELOPER_UI;
|
||||
}
|
||||
|
||||
/* Keying a single element of an array property. */
|
||||
TEST_F(KeyframingTest, insert_keyframes__layered_action__single_element)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user