Anim: add evaluation of Animation data-blocks

Include Animation data-block handling in Blender's animation evaluation
stack. If an `Animation` is assigned to an `ID`, it will take precedence
over the NLA and/or any `Action` that might be assigned as well.

For more info, see #113594.

Pull Request: https://projects.blender.org/blender/blender/pulls/118677
This commit is contained in:
Sybren A. Stüvel 2024-03-04 18:05:12 +01:00
parent 8879654dd0
commit 631f72265d
10 changed files with 892 additions and 11 deletions

@ -178,7 +178,17 @@ static_assert(sizeof(Animation) == sizeof(::Animation),
*/
class Strip : public ::AnimationStrip {
public:
Strip() = default;
/**
* Strip instances should not be created via this constructor. Create a sub-class like
* #KeyframeStrip instead.
*
* The reason is that various functions will assume that the `Strip` is actually a down-cast
* instance of another strip class, and that `Strip::type()` will say which type. To avoid having
* to explcitly deal with an 'invalid' type everywhere, creating a `Strip` directly is simply not
* allowed.
*/
Strip() = delete;
/**
* Strip cannot be duplicated via the copy constructor. Either use a concrete
* strip type's copy constructor, or use Strip::duplicate().
@ -213,6 +223,18 @@ class Strip : public ::AnimationStrip {
template<typename T> bool is() const;
template<typename T> T &as();
template<typename T> const T &as() const;
bool contains_frame(float frame_time) const;
bool is_last_frame(float frame_time) const;
/**
* Set the start and end frame.
*
* Note that this does not do anything else. There is no check whether the
* frame numbers are valid (i.e. frame_start <= frame_end). Infinite values
* (negative for frame_start, positive for frame_end) are supported.
*/
void resize(float frame_start, float frame_end);
};
static_assert(sizeof(Strip) == sizeof(::AnimationStrip),
"DNA struct and its C++ wrapper must have the same size");

@ -0,0 +1,35 @@
/* SPDX-FileCopyrightText: 2023 Blender Developers
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*
* \brief Animation data-block evaluation.
*/
#pragma once
#include "DNA_anim_types.h"
#include "ANIM_animation.hh"
struct AnimationEvalContext;
struct PointerRNA;
namespace blender::animrig {
/**
* Top level animation evaluation function.
*
* Animate the given ID, using the animation data-block and the given binding.
*
* \param flush_to_original when true, look up the original data-block (assuming
* the given one is an evaluated copy) and update that too.
*/
void evaluate_and_apply_animation(PointerRNA &animated_id_ptr,
Animation &animation,
binding_handle_t binding_handle,
const AnimationEvalContext &anim_eval_context,
bool flush_to_original);
} // namespace blender::animrig

@ -27,6 +27,7 @@ set(SRC
intern/bone_collections.cc
intern/bonecolor.cc
intern/driver.cc
intern/evaluation.cc
intern/fcurve.cc
intern/keyframing.cc
intern/keyframing_auto.cc
@ -39,11 +40,13 @@ set(SRC
ANIM_bone_collections.hh
ANIM_bonecolor.hh
ANIM_driver.hh
ANIM_evaluation.hh
ANIM_fcurve.hh
ANIM_keyframing.hh
ANIM_rna.hh
ANIM_visualkey.hh
intern/bone_collections_internal.hh
intern/evaluation_internal.hh
)
set(LIB
@ -67,6 +70,7 @@ if(WITH_GTESTS)
set(TEST_SRC
intern/animation_test.cc
intern/bone_collections_test.cc
intern/evaluation_test.cc
)
set(TEST_LIB
PRIVATE bf::animrig

@ -597,6 +597,29 @@ Strip::~Strip()
BLI_assert_unreachable();
}
bool Strip::contains_frame(const float frame_time) const
{
return this->frame_start <= frame_time && frame_time <= this->frame_end;
}
bool Strip::is_last_frame(const float frame_time) const
{
/* Maybe this needs a more advanced equality check. Implement that when
* we have an actual example case that breaks. */
return this->frame_end == frame_time;
}
void Strip::resize(const float frame_start, const float frame_end)
{
BLI_assert(frame_start <= frame_end);
BLI_assert_msg(frame_start < std::numeric_limits<float>::infinity(),
"only the end frame can be at positive infinity");
BLI_assert_msg(frame_end > -std::numeric_limits<float>::infinity(),
"only the start frame can be at negative infinity");
this->frame_start = frame_start;
this->frame_end = frame_end;
}
/* ----- KeyframeAnimationStrip implementation ----------- */
KeyframeStrip::KeyframeStrip(const KeyframeStrip &other)

@ -321,6 +321,44 @@ TEST_F(AnimationLayersTest, find_suitable_binding)
EXPECT_EQ(&binding, anim->find_suitable_binding_for(cube->id));
}
TEST_F(AnimationLayersTest, strip)
{
constexpr float inf = std::numeric_limits<float>::infinity();
Layer &layer0 = anim->layer_add("Test Læür nul");
Strip &strip = layer0.strip_add(Strip::Type::Keyframe);
strip.resize(-inf, inf);
EXPECT_TRUE(strip.contains_frame(0.0f));
EXPECT_TRUE(strip.contains_frame(-100000.0f));
EXPECT_TRUE(strip.contains_frame(100000.0f));
EXPECT_TRUE(strip.is_last_frame(inf));
strip.resize(1.0f, 2.0f);
EXPECT_FALSE(strip.contains_frame(0.0f))
<< "Strip should not contain frames before its first frame";
EXPECT_TRUE(strip.contains_frame(1.0f)) << "Strip should contain its first frame.";
EXPECT_TRUE(strip.contains_frame(2.0f)) << "Strip should contain its last frame.";
EXPECT_FALSE(strip.contains_frame(2.0001f))
<< "Strip should not contain frames after its last frame";
EXPECT_FALSE(strip.is_last_frame(1.0f));
EXPECT_FALSE(strip.is_last_frame(1.5f));
EXPECT_FALSE(strip.is_last_frame(1.9999f));
EXPECT_TRUE(strip.is_last_frame(2.0f));
EXPECT_FALSE(strip.is_last_frame(2.0001f));
/* Same test as above, but with much larger end frame number. This is 2 hours at 24 FPS. */
strip.resize(1.0f, 172800.0f);
EXPECT_TRUE(strip.contains_frame(172800.0f)) << "Strip should contain its last frame.";
EXPECT_FALSE(strip.contains_frame(172800.1f))
<< "Strip should not contain frames after its last frame";
/* You can't get much closer to the end frame before it's considered equal. */
EXPECT_FALSE(strip.is_last_frame(172799.925f));
EXPECT_TRUE(strip.is_last_frame(172800.0f));
EXPECT_FALSE(strip.is_last_frame(172800.075f));
}
TEST_F(AnimationLayersTest, KeyframeStrip__keyframe_insert)
{
Binding &binding = anim->binding_add();
@ -346,7 +384,10 @@ TEST_F(AnimationLayersTest, KeyframeStrip__keyframe_insert)
binding, "location", 0, {5.0f, 47.1f}, settings);
ASSERT_EQ(fcurve_loc_a, fcurve_loc_b)
<< "Expect same (binding/rna path/array index) tuple to return the same FCurve.";
EXPECT_EQ(2, fcurve_loc_b->totvert);
EXPECT_EQ(47.0f, evaluate_fcurve(fcurve_loc_a, 1.0f));
EXPECT_EQ(47.1f, evaluate_fcurve(fcurve_loc_a, 5.0f));
/* Insert another key for another property, should create another FCurve. */
FCurve *fcurve_rot = key_strip.keyframe_insert(

@ -0,0 +1,303 @@
/* SPDX-FileCopyrightText: 2023 Blender Developers
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "ANIM_evaluation.hh"
#include "RNA_access.hh"
#include "BKE_animsys.h"
#include "BKE_fcurve.hh"
#include "BLI_map.hh"
#include "evaluation_internal.hh"
namespace blender::animrig {
using namespace internal;
/**
* Blend the 'current layer' with the 'last evaluation result', returning the
* blended result.
*/
EvaluationResult blend_layer_results(const EvaluationResult &last_result,
const EvaluationResult &current_result,
const Layer &current_layer);
/**
* Apply the result of the animation evaluation to the given data-block.
*
* \param flush_to_original when true, look up the original data-block (assuming the given one is
* an evaluated copy) and update that too.
*/
void apply_evaluation_result(const EvaluationResult &evaluation_result,
PointerRNA &animated_id_ptr,
bool flush_to_original);
static EvaluationResult evaluate_animation(PointerRNA &animated_id_ptr,
Animation &animation,
const binding_handle_t binding_handle,
const AnimationEvalContext &anim_eval_context)
{
EvaluationResult last_result;
/* Evaluate each layer in order. */
for (Layer *layer : animation.layers()) {
if (layer->influence <= 0.0f) {
/* Don't bother evaluating layers without influence. */
continue;
}
auto layer_result = evaluate_layer(animated_id_ptr, *layer, binding_handle, anim_eval_context);
if (!layer_result) {
continue;
}
if (!last_result) {
/* Simple case: no results so far, so just use this layer as-is. There is
* nothing to blend/combine with, so ignore the influence and combination
* options. */
last_result = layer_result;
continue;
}
/* Complex case: blend this layer's result into the previous layer's result. */
last_result = blend_layer_results(last_result, layer_result, *layer);
}
return last_result;
}
void evaluate_and_apply_animation(PointerRNA &animated_id_ptr,
Animation &animation,
const binding_handle_t binding_handle,
const AnimationEvalContext &anim_eval_context,
const bool flush_to_original)
{
EvaluationResult evaluation_result = evaluate_animation(
animated_id_ptr, animation, binding_handle, anim_eval_context);
if (!evaluation_result) {
return;
}
apply_evaluation_result(evaluation_result, animated_id_ptr, flush_to_original);
}
/* Copy of the same-named function in anim_sys.cc, with the check on action groups removed. */
static bool is_fcurve_evaluatable(const FCurve *fcu)
{
if (fcu->flag & (FCURVE_MUTED | FCURVE_DISABLED)) {
return false;
}
if (BKE_fcurve_is_empty(fcu)) {
return false;
}
return true;
}
/* Copy of the same-named function in anim_sys.cc, but with the special handling for NLA strips
* removed. */
static void animsys_construct_orig_pointer_rna(const PointerRNA *ptr, PointerRNA *ptr_orig)
{
*ptr_orig = *ptr;
/* Original note from anim_sys.cc:
* -----------
* NOTE: nlastrip_evaluate_controls() creates PointerRNA with ID of nullptr. Technically, this is
* not a valid pointer, but there are exceptions in various places of this file which handles
* such pointers.
* We do special trickery here as well, to quickly go from evaluated to original NlaStrip.
* -----------
* And this is all not ported to the new layered animation system. */
BLI_assert_msg(ptr->owner_id, "NLA support was not ported to the layered animation system");
ptr_orig->owner_id = ptr_orig->owner_id->orig_id;
ptr_orig->data = ptr_orig->owner_id;
}
/* Copy of the same-named function in anim_sys.cc. */
static void animsys_write_orig_anim_rna(PointerRNA *ptr,
const char *rna_path,
const int array_index,
const float value)
{
PointerRNA ptr_orig;
animsys_construct_orig_pointer_rna(ptr, &ptr_orig);
PathResolvedRNA orig_anim_rna;
/* TODO(sergey): Should be possible to cache resolved path in dependency graph somehow. */
if (BKE_animsys_rna_path_resolve(&ptr_orig, rna_path, array_index, &orig_anim_rna)) {
BKE_animsys_write_to_rna_path(&orig_anim_rna, value);
}
}
static EvaluationResult evaluate_keyframe_strip(PointerRNA &animated_id_ptr,
KeyframeStrip &key_strip,
const binding_handle_t binding_handle,
const AnimationEvalContext &offset_eval_context)
{
ChannelBag *channelbag_for_binding = key_strip.channelbag_for_binding(binding_handle);
if (!channelbag_for_binding) {
return {};
}
EvaluationResult evaluation_result;
for (FCurve *fcu : channelbag_for_binding->fcurves()) {
/* Blatant copy of animsys_evaluate_fcurves(). */
if (!is_fcurve_evaluatable(fcu)) {
continue;
}
PathResolvedRNA anim_rna;
if (!BKE_animsys_rna_path_resolve(
&animated_id_ptr, fcu->rna_path, fcu->array_index, &anim_rna))
{
printf("Cannot resolve RNA path %s[%d] on ID %s\n",
fcu->rna_path,
fcu->array_index,
animated_id_ptr.owner_id->name);
continue;
}
const float curval = calculate_fcurve(&anim_rna, fcu, &offset_eval_context);
evaluation_result.store(fcu->rna_path, fcu->array_index, curval, anim_rna);
}
return evaluation_result;
}
void apply_evaluation_result(const EvaluationResult &evaluation_result,
PointerRNA &animated_id_ptr,
const bool flush_to_original)
{
for (auto channel_result : evaluation_result.items()) {
const PropIdentifier &prop_ident = channel_result.key;
const AnimatedProperty &anim_prop = channel_result.value;
const float animated_value = anim_prop.value;
PathResolvedRNA anim_rna = anim_prop.prop_rna;
BKE_animsys_write_to_rna_path(&anim_rna, animated_value);
if (flush_to_original) {
/* Convert the StringRef to a `const char *`, as the rest of the RNA path handling code in
* BKE still uses `char *` instead of `StringRef`. */
animsys_write_orig_anim_rna(&animated_id_ptr,
StringRefNull(prop_ident.rna_path).c_str(),
prop_ident.array_index,
animated_value);
}
}
}
static EvaluationResult evaluate_strip(PointerRNA &animated_id_ptr,
Strip &strip,
const binding_handle_t binding_handle,
const AnimationEvalContext &anim_eval_context)
{
AnimationEvalContext offset_eval_context = anim_eval_context;
/* Positive offset means the entire strip is pushed "to the right", so
* evaluation needs to happen further "to the left". */
offset_eval_context.eval_time -= strip.frame_offset;
switch (strip.type()) {
case Strip::Type::Keyframe: {
KeyframeStrip &key_strip = strip.as<KeyframeStrip>();
return evaluate_keyframe_strip(
animated_id_ptr, key_strip, binding_handle, offset_eval_context);
}
}
return {};
}
EvaluationResult blend_layer_results(const EvaluationResult &last_result,
const EvaluationResult &current_result,
const Layer &current_layer)
{
/* TODO?: store the layer results sequentially, so that we can step through
* them in parallel, instead of iterating over one and doing map lookups on
* the other. */
EvaluationResult blend = last_result;
for (auto channel_result : current_result.items()) {
const PropIdentifier &prop_ident = channel_result.key;
AnimatedProperty *last_prop = blend.lookup_ptr(prop_ident);
const AnimatedProperty &anim_prop = channel_result.value;
if (!last_prop) {
/* Nothing to blend with, so just take (influence * value). */
blend.store(prop_ident.rna_path,
prop_ident.array_index,
anim_prop.value * current_layer.influence,
anim_prop.prop_rna);
continue;
}
/* TODO: move this to a separate function. And write more smartness for rotations. */
switch (current_layer.mix_mode()) {
case Layer::MixMode::Replace:
last_prop->value = anim_prop.value * current_layer.influence;
break;
case Layer::MixMode::Offset:
last_prop->value = math::interpolate(
current_layer.influence, last_prop->value, anim_prop.value);
break;
case Layer::MixMode::Add:
last_prop->value += anim_prop.value * current_layer.influence;
break;
case Layer::MixMode::Subtract:
last_prop->value -= anim_prop.value * current_layer.influence;
break;
case Layer::MixMode::Multiply:
last_prop->value *= anim_prop.value * current_layer.influence;
break;
};
}
return blend;
}
namespace internal {
EvaluationResult evaluate_layer(PointerRNA &animated_id_ptr,
Layer &layer,
const binding_handle_t binding_handle,
const AnimationEvalContext &anim_eval_context)
{
/* TODO: implement cross-blending between overlapping strips. For now, this is not supported.
* Instead, the first strong result is taken (see below), and if that is not available, the last
* weak result will be used.
*
* Weak result: obtained from evaluating the final frame of the strip.
* Strong result: any result that is not a weak result. */
EvaluationResult last_weak_result;
for (Strip *strip : layer.strips()) {
if (!strip->contains_frame(anim_eval_context.eval_time)) {
continue;
}
const EvaluationResult strip_result = evaluate_strip(
animated_id_ptr, *strip, binding_handle, anim_eval_context);
if (!strip_result) {
continue;
}
const bool is_weak_result = strip->is_last_frame(anim_eval_context.eval_time);
if (is_weak_result) {
/* Keep going until a strong result is found. */
last_weak_result = strip_result;
continue;
}
/* Found a strong result, just return it. */
return strip_result;
}
return last_weak_result;
}
} // namespace internal
} // namespace blender::animrig

@ -0,0 +1,128 @@
/* SPDX-FileCopyrightText: 2024 Blender Developers
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_map.hh"
#include "BLI_string_ref.hh"
#include "RNA_access.hh"
namespace blender::animrig::internal {
class PropIdentifier {
public:
/**
* Reference to the RNA path of the property.
*
* This string is typically owned by the FCurve that animates the property.
*/
StringRefNull rna_path;
int array_index;
PropIdentifier() = default;
PropIdentifier(const StringRefNull rna_path, const int array_index)
: rna_path(rna_path), array_index(array_index)
{
}
bool operator==(const PropIdentifier &other) const
{
return rna_path == other.rna_path && array_index == other.array_index;
}
bool operator!=(const PropIdentifier &other) const
{
return !(*this == other);
}
uint64_t hash() const
{
return get_default_hash(rna_path, array_index);
}
};
class AnimatedProperty {
public:
float value;
PathResolvedRNA prop_rna;
AnimatedProperty(const float value, const PathResolvedRNA &prop_rna)
: value(value), prop_rna(prop_rna)
{
}
};
/* Evaluated FCurves for some animation binding.
* Mapping from property identifier to its float value.
*
* Can be fed to the evaluation of the next layer, mixed with another strip, or
* used to modify actual RNA properties.
*
* TODO: see if this is efficient, and contains enough info, for mixing. For now
* this just captures the FCurve evaluation result, but doesn't have any info
* about how to do the mixing (LERP, quaternion SLERP, etc.).
*/
class EvaluationResult {
protected:
using EvaluationMap = Map<PropIdentifier, AnimatedProperty>;
EvaluationMap result_;
public:
EvaluationResult() = default;
EvaluationResult(const EvaluationResult &other) = default;
~EvaluationResult() = default;
public:
operator bool() const
{
return !this->is_empty();
}
bool is_empty() const
{
return result_.is_empty();
}
void store(const StringRefNull rna_path,
const int array_index,
const float value,
const PathResolvedRNA &prop_rna)
{
PropIdentifier key(rna_path, array_index);
AnimatedProperty anim_prop(value, prop_rna);
result_.add_overwrite(key, anim_prop);
}
AnimatedProperty value(const StringRefNull rna_path, const int array_index) const
{
PropIdentifier key(rna_path, array_index);
return result_.lookup(key);
}
const AnimatedProperty *lookup_ptr(const PropIdentifier &key) const
{
return result_.lookup_ptr(key);
}
AnimatedProperty *lookup_ptr(const PropIdentifier &key)
{
return result_.lookup_ptr(key);
}
EvaluationMap::ItemIterator items() const
{
return result_.items();
}
};
/**
* Evaluate the animation data on the given layer, for the given binding. This
* just returns the evaluation result, without taking any other layers,
* blending, influence, etc. into account.
*/
EvaluationResult evaluate_layer(PointerRNA &animated_id_ptr,
Layer &layer,
binding_handle_t binding_handle,
const AnimationEvalContext &anim_eval_context);
} // namespace blender::animrig::internal

@ -0,0 +1,313 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "ANIM_animation.hh"
#include "ANIM_evaluation.hh"
#include "evaluation_internal.hh"
#include "BKE_animation.hh"
#include "BKE_animsys.h"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_object.hh"
#include "DNA_object_types.h"
#include "RNA_access.hh"
#include "RNA_prototypes.h"
#include "BLI_math_base.h"
#include "BLI_string_utf8.h"
#include <optional>
#include "testing/testing.h"
namespace blender::animrig::tests {
using namespace blender::animrig::internal;
class AnimationEvaluationTest : public testing::Test {
protected:
Animation anim = {};
Object *cube;
Binding *binding;
Layer *layer;
KeyframeSettings settings = get_keyframe_settings(false);
AnimationEvalContext anim_eval_context = {};
PointerRNA cube_rna_ptr;
public:
static void SetUpTestSuite()
{
/* To make id_can_have_animdata() and friends work, the `id_types` array needs to be set up. */
BKE_idtype_init();
}
void SetUp() override
{
anim = {};
STRNCPY_UTF8(anim.id.name, "ANÄnimåtië");
cube = BKE_object_add_only_object(nullptr, OB_EMPTY, "Küüübus");
binding = &anim.binding_add();
anim.assign_id(binding, cube->id);
layer = &anim.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_id_free(nullptr, &cube->id);
anim.wrap().free_data();
}
/** 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, *layer, binding->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(Strip::Type::Keyframe);
KeyframeStrip &key_strip = strip.as<KeyframeStrip>();
/* Set some keys. */
key_strip.keyframe_insert(*binding, "location", 0, {1.0f, 47.1f}, settings);
key_strip.keyframe_insert(*binding, "location", 0, {5.0f, 47.5f}, settings);
key_strip.keyframe_insert(*binding, "rotation_euler", 1, {1.0f, 0.0f}, settings);
key_strip.keyframe_insert(*binding, "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, *layer, binding->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(Strip::Type::Keyframe);
strip.resize(1.0f, 10.0f);
/* Set some keys. */
KeyframeStrip &key_strip = strip.as<KeyframeStrip>();
key_strip.keyframe_insert(*binding, "location", 0, {1.0f, 47.0f}, settings);
key_strip.keyframe_insert(*binding, "location", 0, {5.0f, 327.0f}, settings);
key_strip.keyframe_insert(*binding, "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(Strip::Type::Keyframe);
Strip &strip2 = layer->strip_add(Strip::Type::Keyframe);
strip1.resize(1.0f, 10.0f);
strip2.resize(11.0f, 20.0f);
strip2.frame_offset = 10;
/* Set some keys. */
{
KeyframeStrip &key_strip1 = strip1.as<KeyframeStrip>();
key_strip1.keyframe_insert(*binding, "location", 0, {1.0f, 47.0f}, settings);
key_strip1.keyframe_insert(*binding, "location", 0, {5.0f, 327.0f}, settings);
key_strip1.keyframe_insert(*binding, "location", 0, {10.0f, 48.0f}, settings);
}
{
KeyframeStrip &key_strip2 = strip2.as<KeyframeStrip>();
key_strip2.keyframe_insert(*binding, "location", 0, {1.0f, 47.0f}, settings);
key_strip2.keyframe_insert(*binding, "location", 0, {5.0f, 327.0f}, settings);
key_strip2.keyframe_insert(*binding, "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(Strip::Type::Keyframe);
Strip &strip2 = layer->strip_add(Strip::Type::Keyframe);
strip1.resize(1.0f, 10.0f);
strip2.resize(10.0f, 19.0f);
strip2.frame_offset = 9;
/* Set some keys. */
{
KeyframeStrip &key_strip1 = strip1.as<KeyframeStrip>();
key_strip1.keyframe_insert(*binding, "location", 0, {1.0f, 47.0f}, settings);
key_strip1.keyframe_insert(*binding, "location", 0, {5.0f, 327.0f}, settings);
key_strip1.keyframe_insert(*binding, "location", 0, {10.0f, 48.0f}, settings);
}
{
KeyframeStrip &key_strip2 = strip2.as<KeyframeStrip>();
key_strip2.keyframe_insert(*binding, "location", 0, {1.0f, 47.0f}, settings);
key_strip2.keyframe_insert(*binding, "location", 0, {5.0f, 327.0f}, settings);
key_strip2.keyframe_insert(*binding, "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

@ -51,6 +51,8 @@
#include "BKE_report.hh"
#include "BKE_texture.h"
#include "ANIM_evaluation.hh"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_query.hh"
@ -3923,16 +3925,26 @@ void BKE_animsys_evaluate_animdata(ID *id,
*/
/* TODO: need to double check that this all works correctly */
if (recalc & ADT_RECALC_ANIM) {
/* evaluate NLA data */
if ((adt->nla_tracks.first) && !(adt->flag & ADT_NLA_EVAL_OFF)) {
/* evaluate NLA-stack
* - active action is evaluated as part of the NLA stack as the last item
*/
animsys_calculate_nla(&id_ptr, adt, anim_eval_context, flush_to_original);
if (adt->animation && adt->binding_handle) {
/* Animation data-blocks take precedence over the old Action + NLA system. */
blender::animrig::evaluate_and_apply_animation(id_ptr,
adt->animation->wrap(),
adt->binding_handle,
*anim_eval_context,
flush_to_original);
}
/* evaluate Active Action only */
else if (adt->action) {
animsys_evaluate_action(&id_ptr, adt->action, anim_eval_context, flush_to_original);
else {
/* evaluate NLA data */
if ((adt->nla_tracks.first) && !(adt->flag & ADT_NLA_EVAL_OFF)) {
/* evaluate NLA-stack
* - active action is evaluated as part of the NLA stack as the last item
*/
animsys_calculate_nla(&id_ptr, adt, anim_eval_context, flush_to_original);
}
/* evaluate Active Action only */
else if (adt->action) {
animsys_evaluate_action(&id_ptr, adt->action, anim_eval_context, flush_to_original);
}
}
}

@ -250,7 +250,7 @@ static void rna_AnimData_animation_binding_handle_set(
BLI_assert_msg(adt, "ID.animation_data is unexpectedly empty");
if (!adt) {
WM_reportf(RPT_ERROR,
"Data-block '%s' does not have any animation data, how did you set this property?",
"Data-block '%s' does not have any animation data, use animation_data_create()",
animated_id.name + 2);
return;
}