Files
test2/source/blender/animrig/intern/evaluation_test.cc
Nathan Vegdahl e0d5379ef8 Anim: change how action strip data is stored
This updates the layered action data model to store strip data differently.  Specifically:

- `Strip` is now just a single, POD type that only stores the data common to all
  strips, such as start/end frames.
- The data that might be of a completely different nature between strips (e.g.
  keyframe data vs modifier data) is now stored in arrays on the action itself.
- `Strip`s indicate their type with an enum, and specify their data with an
  index into the array on the action that stores data for that type.

This approach requires a little more data juggling, but has the advantage of
making `Strip`s themselves super simple POD types, and also opening the door to
trivial strip instancing later on: instances are just strips that point at the
same data.

The intention is that the RNA API remains the same: from RNA's perspective there
is no data storage separate from the strips, and a strip's data is presented as
fields and methods directly on the strip itself. Different strip types will be
presented as different subtypes of `ActionStrip`, each with their own fields and
methods specific to their underlying data's type. However, this PR doesn't
implement that sub-typing, leaving it for a future PR. It does, however, put the
fields and methods of the one strip type we have so far directly on the strip,
which avoids changing the APIs we have so far.

This PR implements the bulk of this new approach, and everything should be
functional and working correctly. However, there are two TODO items left over
that will be implemented in forthcoming PRs:

- Type refinement in the RNA api. This PR actually removes the existing type
  refinement code that was implemented in terms of the inheritance tree of the
  actual C++ types, and this will need to be reimplemented in terms of the new
  data model. The RNA API still works without the type refinement since there
  are only keyframe strips right now, but it will be needed in preparation for
  more strip types down the road.
- Strip data deletion. This PR only deletes data from the strip data arrays when
  the whole action is deleted, and otherwise just accumulates strip data as more
  and more strips are added, never removing the data when the corresponding
  strips get removed. That's fine in the short term, especially since we only
  support single strips right now. But it does need to be implemented in
  preparation for proper layered actions.

Pull Request: https://projects.blender.org/blender/blender/pulls/126559
2024-09-17 17:31:09 +02:00

324 lines
12 KiB
C++

/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "ANIM_action.hh"
#include "ANIM_evaluation.hh"
#include "evaluation_internal.hh"
#include "BKE_action.hh"
#include "BKE_animsys.h"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_object.hh"
#include "DNA_object_types.h"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "BLI_math_base.h"
#include "BLI_string_utf8.h"
#include <optional>
#include "CLG_log.h"
#include "testing/testing.h"
namespace blender::animrig::tests {
using namespace blender::animrig::internal;
class AnimationEvaluationTest : public testing::Test {
protected:
Main *bmain;
Action *action;
Object *cube;
Slot *slot;
Layer *layer;
KeyframeSettings settings = get_keyframe_settings(false);
AnimationEvalContext anim_eval_context = {};
PointerRNA cube_rna_ptr;
public:
static void SetUpTestSuite()
{
/* BKE_id_free() hits a code path that uses CLOG, which crashes if not initialized properly. */
CLG_init();
/* To make id_can_have_animdata() and friends work, the `id_types` array needs to be set up. */
BKE_idtype_init();
}
static void TearDownTestSuite()
{
CLG_exit();
}
void SetUp() override
{
bmain = BKE_main_new();
action = static_cast<Action *>(BKE_id_new(bmain, ID_AC, "ACÄnimåtië"));
cube = BKE_object_add_only_object(bmain, OB_EMPTY, "Küüübus");
slot = &action->slot_add();
ASSERT_EQ(assign_action_and_slot(action, slot, cube->id), ActionSlotAssignmentResult::OK);
layer = &action->layer_add("Kübus layer");
/* Make it easier to predict test values. */
settings.interpolation = BEZT_IPO_LIN;
cube_rna_ptr = RNA_pointer_create(&cube->id, &RNA_Object, &cube->id);
}
void TearDown() override
{
BKE_main_free(bmain);
}
/** Evaluate the layer, and return result for the given property. */
std::optional<float> evaluate_single_property(const StringRefNull rna_path,
const int array_index,
const float eval_time)
{
anim_eval_context.eval_time = eval_time;
EvaluationResult result = evaluate_layer(
cube_rna_ptr, *action, *layer, slot->handle, anim_eval_context);
const AnimatedProperty *loc0_result = result.lookup_ptr(PropIdentifier(rna_path, array_index));
if (!loc0_result) {
return {};
}
return loc0_result->value;
}
/** Evaluate the layer, and test that the given property evaluates to the expected value. */
testing::AssertionResult test_evaluate_layer(const StringRefNull rna_path,
const int array_index,
const float2 eval_time__expect_value)
{
const float eval_time = eval_time__expect_value[0];
const float expect_value = eval_time__expect_value[1];
const std::optional<float> opt_eval_value = evaluate_single_property(
rna_path, array_index, eval_time);
if (!opt_eval_value) {
return testing::AssertionFailure()
<< rna_path << "[" << array_index << "] should have been animated";
}
const float eval_value = *opt_eval_value;
const uint diff_ulps = ulp_diff_ff(expect_value, eval_value);
if (diff_ulps >= 4) {
return testing::AssertionFailure()
<< std::endl
<< " " << rna_path << "[" << array_index
<< "] evaluation did not produce the expected result:" << std::endl
<< " evaluted to: " << testing::PrintToString(eval_value) << std::endl
<< " expected : " << testing::PrintToString(expect_value) << std::endl;
}
return testing::AssertionSuccess();
};
/** Evaluate the layer, and test that the given property is not part of the result. */
testing::AssertionResult test_evaluate_layer_no_result(const StringRefNull rna_path,
const int array_index,
const float eval_time)
{
const std::optional<float> eval_value = evaluate_single_property(
rna_path, array_index, eval_time);
if (eval_value) {
return testing::AssertionFailure()
<< std::endl
<< " " << rna_path << "[" << array_index
<< "] evaluation should NOT produce a value:" << std::endl
<< " evaluted to: " << testing::PrintToString(*eval_value) << std::endl;
}
return testing::AssertionSuccess();
}
};
TEST_F(AnimationEvaluationTest, evaluate_layer__keyframes)
{
Strip &strip = layer->strip_add(*action, Strip::Type::Keyframe);
StripKeyframeData &strip_data = strip.data<StripKeyframeData>(*action);
/* Set some keys. */
strip_data.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.1f}, settings);
strip_data.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 47.5f}, settings);
strip_data.keyframe_insert(bmain, *slot, {"rotation_euler", 1}, {1.0f, 0.0f}, settings);
strip_data.keyframe_insert(bmain, *slot, {"rotation_euler", 1}, {5.0f, 3.14f}, settings);
/* Set the animated properties to some values. These should not be overwritten
* by the evaluation itself. */
cube->loc[0] = 3.0f;
cube->loc[1] = 2.0f;
cube->loc[2] = 7.0f;
cube->rot[0] = 3.0f;
cube->rot[1] = 2.0f;
cube->rot[2] = 7.0f;
/* Evaluate. */
anim_eval_context.eval_time = 3.0f;
EvaluationResult result = evaluate_layer(
cube_rna_ptr, *action, *layer, slot->handle, anim_eval_context);
/* Check the result. */
ASSERT_FALSE(result.is_empty());
AnimatedProperty *loc0_result = result.lookup_ptr(PropIdentifier("location", 0));
ASSERT_NE(nullptr, loc0_result) << "location[0] should have been animated";
EXPECT_EQ(47.3f, loc0_result->value);
EXPECT_EQ(3.0f, cube->loc[0]) << "Evaluation should not modify the animated ID";
EXPECT_EQ(2.0f, cube->loc[1]) << "Evaluation should not modify the animated ID";
EXPECT_EQ(7.0f, cube->loc[2]) << "Evaluation should not modify the animated ID";
EXPECT_EQ(3.0f, cube->rot[0]) << "Evaluation should not modify the animated ID";
EXPECT_EQ(2.0f, cube->rot[1]) << "Evaluation should not modify the animated ID";
EXPECT_EQ(7.0f, cube->rot[2]) << "Evaluation should not modify the animated ID";
}
TEST_F(AnimationEvaluationTest, strip_boundaries__single_strip)
{
/* Single finite strip, check first, middle, and last frame. */
Strip &strip = layer->strip_add(*action, Strip::Type::Keyframe);
strip.resize(1.0f, 10.0f);
/* Set some keys. */
StripKeyframeData &strip_data = strip.data<StripKeyframeData>(*action);
strip_data.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.0f}, settings);
strip_data.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 327.0f}, settings);
strip_data.keyframe_insert(bmain, *slot, {"location", 0}, {10.0f, 48.0f}, settings);
/* Evaluate the layer to see how it handles the boundaries + something in between. */
EXPECT_TRUE(test_evaluate_layer("location", 0, {1.0f, 47.0f}));
EXPECT_TRUE(test_evaluate_layer("location", 0, {3.0f, 187.0f}));
EXPECT_TRUE(test_evaluate_layer("location", 0, {10.0f, 48.0f}));
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 10.001f));
}
TEST_F(AnimationEvaluationTest, strip_boundaries__nonoverlapping)
{
/* Two finite strips that are strictly distinct. */
Strip &strip1 = layer->strip_add(*action, Strip::Type::Keyframe);
Strip &strip2 = layer->strip_add(*action, Strip::Type::Keyframe);
strip1.resize(1.0f, 10.0f);
strip2.resize(11.0f, 20.0f);
strip2.frame_offset = 10;
/* Set some keys. */
{
StripKeyframeData &strip_data1 = strip1.data<StripKeyframeData>(*action);
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.0f}, settings);
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 327.0f}, settings);
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {10.0f, 48.0f}, settings);
}
{
StripKeyframeData &strip_data2 = strip2.data<StripKeyframeData>(*action);
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.0f}, settings);
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 327.0f}, settings);
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {10.0f, 48.0f}, settings);
}
/* Check Strip 1. */
EXPECT_TRUE(test_evaluate_layer("location", 0, {1.0f, 47.0f}));
EXPECT_TRUE(test_evaluate_layer("location", 0, {3.0f, 187.0f}));
EXPECT_TRUE(test_evaluate_layer("location", 0, {10.0f, 48.0f}));
/* Check Strip 2. */
EXPECT_TRUE(test_evaluate_layer("location", 0, {11.0f, 47.0f}));
EXPECT_TRUE(test_evaluate_layer("location", 0, {13.0f, 187.0f}));
EXPECT_TRUE(test_evaluate_layer("location", 0, {20.0f, 48.0f}));
/* Check outside the range of the strips. */
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 0.999f));
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 10.001f));
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 10.999f));
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 20.001f));
}
TEST_F(AnimationEvaluationTest, strip_boundaries__overlapping_edge)
{
/* Two finite strips that are overlapping on their edge. */
Strip &strip1 = layer->strip_add(*action, Strip::Type::Keyframe);
Strip &strip2 = layer->strip_add(*action, Strip::Type::Keyframe);
strip1.resize(1.0f, 10.0f);
strip2.resize(10.0f, 19.0f);
strip2.frame_offset = 9;
/* Set some keys. */
{
StripKeyframeData &strip_data1 = strip1.data<StripKeyframeData>(*action);
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.0f}, settings);
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 327.0f}, settings);
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {10.0f, 48.0f}, settings);
}
{
StripKeyframeData &strip_data2 = strip2.data<StripKeyframeData>(*action);
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.0f}, settings);
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 327.0f}, settings);
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {10.0f, 48.0f}, settings);
}
/* Check Strip 1. */
EXPECT_TRUE(test_evaluate_layer("location", 0, {1.0f, 47.0f}));
EXPECT_TRUE(test_evaluate_layer("location", 0, {3.0f, 187.0f}));
/* Check overlapping frame. */
EXPECT_TRUE(test_evaluate_layer("location", 0, {10.0f, 47.0f}))
<< "On the overlapping frame, only Strip 2 should be evaluated.";
/* Check Strip 2. */
EXPECT_TRUE(test_evaluate_layer("location", 0, {12.0f, 187.0f}));
EXPECT_TRUE(test_evaluate_layer("location", 0, {19.0f, 48.0f}));
/* Check outside the range of the strips. */
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 0.999f));
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 19.001f));
}
class AccessibleEvaluationResult : public EvaluationResult {
public:
EvaluationMap &get_map()
{
return result_;
}
};
TEST(AnimationEvaluationResultTest, prop_identifier_hashing)
{
AccessibleEvaluationResult result;
/* Test storing the same result twice, with different memory locations of the RNA paths. This
* tests that the mapping uses the actual string, and not just pointer comparison. */
const char *rna_path_1 = "pose.bones['Root'].location";
const std::string rna_path_2(rna_path_1);
ASSERT_NE(rna_path_1, rna_path_2.c_str())
<< "This test requires different addresses for the RNA path strings";
PathResolvedRNA fake_resolved_rna;
result.store(rna_path_1, 0, 1.0f, fake_resolved_rna);
result.store(rna_path_2, 0, 2.0f, fake_resolved_rna);
EXPECT_EQ(1, result.get_map().size())
<< "Storing a result for the same property twice should just overwrite the previous value";
{
PropIdentifier key(rna_path_1, 0);
AnimatedProperty *anim_prop = result.lookup_ptr(key);
EXPECT_EQ(2.0f, anim_prop->value) << "The last-stored result should survive.";
}
{
PropIdentifier key(rna_path_2, 0);
AnimatedProperty *anim_prop = result.lookup_ptr(key);
EXPECT_EQ(2.0f, anim_prop->value) << "The last-stored result should survive.";
}
}
} // namespace blender::animrig::tests