diff --git a/source/blender/animrig/ANIM_animation.hh b/source/blender/animrig/ANIM_animation.hh index 216d758584c..9729f30b39e 100644 --- a/source/blender/animrig/ANIM_animation.hh +++ b/source/blender/animrig/ANIM_animation.hh @@ -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 bool is() const; template T &as(); template 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"); diff --git a/source/blender/animrig/ANIM_evaluation.hh b/source/blender/animrig/ANIM_evaluation.hh new file mode 100644 index 00000000000..4f864b90b43 --- /dev/null +++ b/source/blender/animrig/ANIM_evaluation.hh @@ -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 diff --git a/source/blender/animrig/CMakeLists.txt b/source/blender/animrig/CMakeLists.txt index 25974854f11..673326cea8f 100644 --- a/source/blender/animrig/CMakeLists.txt +++ b/source/blender/animrig/CMakeLists.txt @@ -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 diff --git a/source/blender/animrig/intern/animation.cc b/source/blender/animrig/intern/animation.cc index 4e25ef51eb5..e754e93e375 100644 --- a/source/blender/animrig/intern/animation.cc +++ b/source/blender/animrig/intern/animation.cc @@ -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::infinity(), + "only the end frame can be at positive infinity"); + BLI_assert_msg(frame_end > -std::numeric_limits::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) diff --git a/source/blender/animrig/intern/animation_test.cc b/source/blender/animrig/intern/animation_test.cc index 0bdade8d566..4028068c4c4 100644 --- a/source/blender/animrig/intern/animation_test.cc +++ b/source/blender/animrig/intern/animation_test.cc @@ -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::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( diff --git a/source/blender/animrig/intern/evaluation.cc b/source/blender/animrig/intern/evaluation.cc new file mode 100644 index 00000000000..d3b36310072 --- /dev/null +++ b/source/blender/animrig/intern/evaluation.cc @@ -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 ¤t_result, + const Layer ¤t_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(); + 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 ¤t_result, + const Layer ¤t_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 diff --git a/source/blender/animrig/intern/evaluation_internal.hh b/source/blender/animrig/intern/evaluation_internal.hh new file mode 100644 index 00000000000..8a5e4a49955 --- /dev/null +++ b/source/blender/animrig/intern/evaluation_internal.hh @@ -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; + 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 diff --git a/source/blender/animrig/intern/evaluation_test.cc b/source/blender/animrig/intern/evaluation_test.cc new file mode 100644 index 00000000000..670780bace4 --- /dev/null +++ b/source/blender/animrig/intern/evaluation_test.cc @@ -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 + +#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 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 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 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(); + + /* 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(); + 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(); + 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(); + 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(); + 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(); + 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 diff --git a/source/blender/blenkernel/intern/anim_sys.cc b/source/blender/blenkernel/intern/anim_sys.cc index a221c49b7df..95bb6770c4c 100644 --- a/source/blender/blenkernel/intern/anim_sys.cc +++ b/source/blender/blenkernel/intern/anim_sys.cc @@ -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); + } } } diff --git a/source/blender/makesrna/intern/rna_animation.cc b/source/blender/makesrna/intern/rna_animation.cc index 827c53b9f83..425a0b11564 100644 --- a/source/blender/makesrna/intern/rna_animation.cc +++ b/source/blender/makesrna/intern/rna_animation.cc @@ -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; }