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')
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):
return True
def configure(env):
pass

View File

@ -5,8 +5,6 @@
#include "scene/animation/animation_player.h"
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("is_active"), &SyncedAnimationGraph::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());
}
} 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));
_setup_evaluation_context();
_setup_graph();
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));
_setup_evaluation_context();
_setup_graph();
emit_signal(SNAME("skeleton_changed")); // Needs to unpin AnimationPlayerEditor.
}
@ -139,22 +152,32 @@ NodePath SyncedAnimationGraph::get_skeleton() const {
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) {
if (root_node == nullptr) {
if (!graph_root_node.is_valid()) {
return;
}
root_node->activate_inputs();
root_node->calculate_sync_track();
root_node->update_time(p_delta);
AnimationData output_data;
root_node->evaluate(graph_context, output_data);
graph_root_node->activate_inputs();
graph_root_node->calculate_sync_track();
graph_root_node->update_time(p_delta);
graph_root_node->evaluate(graph_context, graph_output);
_apply_animation_data(output_data);
_apply_animation_data(graph_output);
}
void SyncedAnimationGraph::_apply_animation_data(AnimationData output_data) const {
for (KeyValue<Animation::TypeHash, AnimationData::TrackValue *> &K : output_data.track_values) {
void SyncedAnimationGraph::_apply_animation_data(const AnimationData& output_data) const {
for (const KeyValue<Animation::TypeHash, AnimationData::TrackValue *> &K : output_data.track_values) {
const AnimationData::TrackValue *track_value = K.value;
switch (track_value->type) {
case AnimationData::TrackType::TYPE_POSITION_3D: {
@ -219,24 +242,11 @@ void SyncedAnimationGraph::_cleanup_evaluation_context() {
}
void SyncedAnimationGraph::_setup_graph() {
if (root_node != nullptr) {
_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) {
if (graph_context.animation_player == nullptr || graph_context.skeleton_3d == nullptr || !graph_root_node.is_valid()) {
return;
}
memfree(root_node);
graph_root_node->initialize(graph_context);
}
SyncedAnimationGraph::SyncedAnimationGraph() {

View File

@ -15,13 +15,8 @@ private:
NodePath skeleton_path;
GraphEvaluationContext graph_context = {};
SyncedAnimationNode* root_node = nullptr;
void set_animation_player(const NodePath &p_path);
NodePath get_animation_player() const;
void set_skeleton(const NodePath &p_path);
NodePath get_skeleton() const;
Ref<SyncedAnimationNode> graph_root_node = nullptr;
AnimationData graph_output;
protected:
void _notification(int p_what);
@ -37,11 +32,20 @@ protected:
public:
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);
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);
AnimationMixer::AnimationCallbackModeProcess get_callback_mode_process() const;
@ -60,5 +64,4 @@ private:
void _cleanup_evaluation_context();
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;
}
void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, AnimationData &output) {
const Vector<Animation::Track *> tracks = animation->get_tracks();
Animation::Track *const *tracks_ptr = tracks.ptr();
// real_t a_length = animation->get_length();
int count = tracks.size();
for (int i = 0; i < count; i++) {
AnimationData::TrackValue *track_value = nullptr;

View File

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