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:
parent
8879654dd0
commit
631f72265d
@ -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");
|
||||
|
35
source/blender/animrig/ANIM_evaluation.hh
Normal file
35
source/blender/animrig/ANIM_evaluation.hh
Normal file
@ -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(
|
||||
|
303
source/blender/animrig/intern/evaluation.cc
Normal file
303
source/blender/animrig/intern/evaluation.cc
Normal file
@ -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<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 ¤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
|
128
source/blender/animrig/intern/evaluation_internal.hh
Normal file
128
source/blender/animrig/intern/evaluation_internal.hh
Normal file
@ -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
|
313
source/blender/animrig/intern/evaluation_test.cc
Normal file
313
source/blender/animrig/intern/evaluation_test.cc
Normal file
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user