Added simple unit test that uses an AnimationSamplerNode and a procedural animation.

This commit is contained in:
Martin Felis 2025-12-05 17:20:35 +01:00
parent 757c5ee51c
commit 1732ecb8bd
7 changed files with 127 additions and 40 deletions

8
SCsub
View File

@ -2,4 +2,10 @@
Import('env') Import('env')
env.add_source_files(env.modules_sources, "*.cpp") # Add all cpp files to the build module_env = env.Clone()
module_env.add_source_files(env.modules_sources, "*.cpp") # Add all cpp files to the build
if env["tests"]:
module_env.Append(CPPDEFINES=["TESTS_ENABLED"])
module_env.add_source_files(env.modules_sources, "./tests/*.cpp")

View File

@ -1,6 +1,5 @@
def can_build(env, platform): def can_build(env, platform):
return True return True
def configure(env): def configure(env):
pass pass

View File

@ -5,8 +5,6 @@
#include "scene/animation/animation_player.h" #include "scene/animation/animation_player.h"
void SyncedAnimationGraph::_bind_methods() { void SyncedAnimationGraph::_bind_methods() {
print_line(vformat("binding methods"));
ClassDB::bind_method(D_METHOD("set_active", "active"), &SyncedAnimationGraph::set_active); ClassDB::bind_method(D_METHOD("set_active", "active"), &SyncedAnimationGraph::set_active);
ClassDB::bind_method(D_METHOD("is_active"), &SyncedAnimationGraph::is_active); ClassDB::bind_method(D_METHOD("is_active"), &SyncedAnimationGraph::is_active);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "active"), "set_active", "is_active"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "active"), "set_active", "is_active");
@ -50,6 +48,15 @@ void SyncedAnimationGraph::_notification(int p_what) {
_process_graph(get_physics_process_delta_time()); _process_graph(get_physics_process_delta_time());
} }
} break; } break;
case Node::NOTIFICATION_EXIT_TREE: {
_cleanup_evaluation_context();
break;
}
default: {
break;
}
} }
} }
@ -115,6 +122,9 @@ void SyncedAnimationGraph::set_animation_player(const NodePath &p_path) {
} }
graph_context.animation_player = Object::cast_to<AnimationPlayer>(get_node_or_null(animation_player_path)); graph_context.animation_player = Object::cast_to<AnimationPlayer>(get_node_or_null(animation_player_path));
_setup_evaluation_context();
_setup_graph();
emit_signal(SNAME("animation_player_changed")); // Needs to unpin AnimationPlayerEditor. emit_signal(SNAME("animation_player_changed")); // Needs to unpin AnimationPlayerEditor.
} }
@ -132,6 +142,9 @@ void SyncedAnimationGraph::set_skeleton(const NodePath &p_path) {
} }
graph_context.skeleton_3d = Object::cast_to<Skeleton3D>(get_node_or_null(skeleton_path)); graph_context.skeleton_3d = Object::cast_to<Skeleton3D>(get_node_or_null(skeleton_path));
_setup_evaluation_context();
_setup_graph();
emit_signal(SNAME("skeleton_changed")); // Needs to unpin AnimationPlayerEditor. emit_signal(SNAME("skeleton_changed")); // Needs to unpin AnimationPlayerEditor.
} }
@ -139,22 +152,32 @@ NodePath SyncedAnimationGraph::get_skeleton() const {
return skeleton_path; return skeleton_path;
} }
void SyncedAnimationGraph::set_graph_root_node(const Ref<SyncedAnimationNode> &p_animation_node) {
if (graph_root_node != p_animation_node) {
graph_root_node = p_animation_node;
_setup_graph();
}
}
Ref<SyncedAnimationNode> SyncedAnimationGraph::get_graph_root_node() const {
return graph_root_node;
}
void SyncedAnimationGraph::_process_graph(double p_delta, bool p_update_only) { void SyncedAnimationGraph::_process_graph(double p_delta, bool p_update_only) {
if (root_node == nullptr) { if (!graph_root_node.is_valid()) {
return; return;
} }
root_node->activate_inputs(); graph_root_node->activate_inputs();
root_node->calculate_sync_track(); graph_root_node->calculate_sync_track();
root_node->update_time(p_delta); graph_root_node->update_time(p_delta);
AnimationData output_data; graph_root_node->evaluate(graph_context, graph_output);
root_node->evaluate(graph_context, output_data);
_apply_animation_data(output_data); _apply_animation_data(graph_output);
} }
void SyncedAnimationGraph::_apply_animation_data(AnimationData output_data) const { void SyncedAnimationGraph::_apply_animation_data(const AnimationData& output_data) const {
for (KeyValue<Animation::TypeHash, AnimationData::TrackValue *> &K : output_data.track_values) { for (const KeyValue<Animation::TypeHash, AnimationData::TrackValue *> &K : output_data.track_values) {
const AnimationData::TrackValue *track_value = K.value; const AnimationData::TrackValue *track_value = K.value;
switch (track_value->type) { switch (track_value->type) {
case AnimationData::TrackType::TYPE_POSITION_3D: { case AnimationData::TrackType::TYPE_POSITION_3D: {
@ -219,24 +242,11 @@ void SyncedAnimationGraph::_cleanup_evaluation_context() {
} }
void SyncedAnimationGraph::_setup_graph() { void SyncedAnimationGraph::_setup_graph() {
if (root_node != nullptr) { if (graph_context.animation_player == nullptr || graph_context.skeleton_3d == nullptr || !graph_root_node.is_valid()) {
_cleanup_graph();
}
AnimationSamplerNode *sampler_node = memnew(AnimationSamplerNode);
sampler_node->animation_name = "animation_library/Walk-InPlace";
root_node = sampler_node;
root_node->initialize(graph_context);
}
void SyncedAnimationGraph::_cleanup_graph() {
if (root_node == nullptr) {
return; return;
} }
memfree(root_node); graph_root_node->initialize(graph_context);
} }
SyncedAnimationGraph::SyncedAnimationGraph() { SyncedAnimationGraph::SyncedAnimationGraph() {

View File

@ -15,13 +15,8 @@ private:
NodePath skeleton_path; NodePath skeleton_path;
GraphEvaluationContext graph_context = {}; GraphEvaluationContext graph_context = {};
SyncedAnimationNode* root_node = nullptr; Ref<SyncedAnimationNode> graph_root_node = nullptr;
AnimationData graph_output;
void set_animation_player(const NodePath &p_path);
NodePath get_animation_player() const;
void set_skeleton(const NodePath &p_path);
NodePath get_skeleton() const;
protected: protected:
void _notification(int p_what); void _notification(int p_what);
@ -37,11 +32,20 @@ protected:
public: public:
void _process_graph(double p_delta, bool p_update_only = false); void _process_graph(double p_delta, bool p_update_only = false);
void _apply_animation_data(AnimationData output_data) const; void _apply_animation_data(const AnimationData& output_data) const;
void set_active(bool p_active); void set_active(bool p_active);
bool is_active() const; bool is_active() const;
void set_animation_player(const NodePath &p_path);
NodePath get_animation_player() const;
void set_skeleton(const NodePath &p_path);
NodePath get_skeleton() const;
void set_graph_root_node(const Ref<SyncedAnimationNode> &p_animation_node);
Ref<SyncedAnimationNode> get_graph_root_node() const;
void set_callback_mode_process(AnimationMixer::AnimationCallbackModeProcess p_mode); void set_callback_mode_process(AnimationMixer::AnimationCallbackModeProcess p_mode);
AnimationMixer::AnimationCallbackModeProcess get_callback_mode_process() const; AnimationMixer::AnimationCallbackModeProcess get_callback_mode_process() const;
@ -60,5 +64,4 @@ private:
void _cleanup_evaluation_context(); void _cleanup_evaluation_context();
void _setup_graph(); void _setup_graph();
void _cleanup_graph();
}; };

View File

@ -10,11 +10,10 @@ void AnimationSamplerNode::initialize(GraphEvaluationContext &context) {
node_time_info.loop_mode = Animation::LOOP_LINEAR; node_time_info.loop_mode = Animation::LOOP_LINEAR;
} }
void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, AnimationData &output) { void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, AnimationData &output) {
const Vector<Animation::Track *> tracks = animation->get_tracks(); const Vector<Animation::Track *> tracks = animation->get_tracks();
Animation::Track *const *tracks_ptr = tracks.ptr(); Animation::Track *const *tracks_ptr = tracks.ptr();
// real_t a_length = animation->get_length();
int count = tracks.size(); int count = tracks.size();
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
AnimationData::TrackValue *track_value = nullptr; AnimationData::TrackValue *track_value = nullptr;

View File

@ -2,6 +2,7 @@
#include "scene/animation/animation_player.h" #include "scene/animation/animation_player.h"
#include "core/io/resource.h"
#include "scene/3d/skeleton_3d.h" #include "scene/3d/skeleton_3d.h"
#include <cassert> #include <cassert>
@ -79,7 +80,9 @@ struct SyncTrack {
}; };
class SyncedAnimationNode { class SyncedAnimationNode: public Resource {
GDCLASS(SyncedAnimationNode, Resource);
friend class SyncedAnimationGraph; friend class SyncedAnimationGraph;
public: public:
@ -141,6 +144,8 @@ private:
}; };
class AnimationSamplerNode : public SyncedAnimationNode { class AnimationSamplerNode : public SyncedAnimationNode {
GDCLASS(AnimationSamplerNode, SyncedAnimationNode);
public: public:
StringName animation_name; StringName animation_name;

View File

@ -0,0 +1,65 @@
#pragma once
#include "../synced_animation_graph.h"
#include "scene/main/window.h"
#include "servers/rendering/rendering_server_default.h"
#include "tests/test_macros.h"
namespace TestSyncedAnimationGraph {
TEST_CASE("[SceneTree][SyncedAnimationGraph] Simple") {
Node* character_node = memnew(Node);
character_node->set_name("CharacterNode");
SceneTree::get_singleton()->get_root()->add_child(character_node);
Skeleton3D *skeleton_node = memnew(Skeleton3D);
skeleton_node->set_name("Skeleton");
character_node->add_child(skeleton_node);
skeleton_node->add_bone("Root");
int hip_bone_index = skeleton_node->add_bone("Hips");
AnimationPlayer *player_node = memnew(AnimationPlayer);
player_node->set_name("AnimationPlayer");
Ref<Animation> animation = memnew(Animation);
const int track_index = animation->add_track(Animation::TYPE_POSITION_3D);
CHECK(track_index == 0);
animation->track_insert_key(track_index, 0.0, Vector3(0., 0., 0.));
animation->track_insert_key(track_index, 1.0, Vector3(1., 2., 3.));
animation->track_set_path(track_index, NodePath(vformat("%s:%s", skeleton_node->get_path().get_concatenated_names(),"Hips")));
Ref<AnimationLibrary> animation_library;
animation_library.instantiate();
animation_library->add_animation("TestAnimation", animation);
player_node->add_animation_library("animation_library", animation_library);
SceneTree::get_singleton()->get_root()->add_child(player_node);
SyncedAnimationGraph *synced_animation_graph = memnew(SyncedAnimationGraph);
SceneTree::get_singleton()->get_root()->add_child(synced_animation_graph);
synced_animation_graph->set_animation_player(player_node->get_path());
synced_animation_graph->set_skeleton(skeleton_node->get_path());
Ref<AnimationSamplerNode> animation_sampler_node;
animation_sampler_node.instantiate();
animation_sampler_node->animation_name = "animation_library/TestAnimation";
synced_animation_graph->set_graph_root_node(animation_sampler_node);
Vector3 hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin;
CHECK(hip_bone_position.x == doctest::Approx(0.0));
CHECK(hip_bone_position.y == doctest::Approx(0.0));
CHECK(hip_bone_position.z == doctest::Approx(0.0));
SceneTree::get_singleton()->process(0.01);
hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin;
CHECK(hip_bone_position.x == doctest::Approx(0.01));
CHECK(hip_bone_position.y == doctest::Approx(0.02));
CHECK(hip_bone_position.z == doctest::Approx(0.03));
}
}