From 5880dde6ec5d6d729dbbd5588b89489623a48f21 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Mon, 8 Dec 2025 22:47:00 +0100 Subject: [PATCH] WIP: refactored test to use a fixture, started working on Blend Tree evaluation. --- doc/design.md | 69 ++++++++-------- synced_animation_graph.cpp | 2 +- synced_animation_node.cpp | 4 +- synced_animation_node.h | 117 ++++++++++++++++++++++++---- tests/test_synced_animation_graph.h | 103 +++++++++++++++++------- 5 files changed, 215 insertions(+), 80 deletions(-) diff --git a/doc/design.md b/doc/design.md index 537e925..d811a31 100644 --- a/doc/design.md +++ b/doc/design.md @@ -56,27 +56,14 @@ invalid. ### Example: -```plantuml -@startuml - -left to right direction - -abstract Output {} -abstract Blend2 { -bool is_synced - -float weight() -} -abstract AnimationA {} -abstract TimeScale {} -abstract AnimationB {} - -AnimationA --> Blend2 -AnimationB --> TimeScale -TimeScale --> Blend2 -Blend2 --> Output - -@enduml +```mermaid +flowchart LR + AnimationB --> TimeScale("TimeScale + ---- + *scale*") + AnimationA --> Blend2 + TimeScale --> Blend2 + Blend2 --> Output ``` A Blend Tree always has a designated output node where the time delta is specified as an input and after the Blend Tree @@ -110,6 +97,18 @@ that is only needed during evaluation to the Blend Tree. ### Blend Tree Evaluation ```c++ +// BlendTree.h +class BlendTree: public SyncedAnimationNode { +private: + Vector nodes; + Vector node_parent; // node_parent[i] is the index of the parent of node i. + Vector node_output; // output for each node + Vector> node_input_nodes; + Vector> node_input_data; // list of inputs for all nodes. + + int get_index_for_node(const SyncedAnimationNode& node); +}; + // BlendTree.cpp void BlendTree::initialize_tree() { for (int i = 0; ci < num_connections; i++) { @@ -119,9 +118,11 @@ void BlendTree::initialize_tree() { } void BlendTree::activate_inputs() { - for (int i = 0; i < nodes.size(); i++) { - if (nodes[i].is_active()) { - nodes[i].activate_inputs() + nodes[0]->activate_inputs(); + + for (int i = 1; i < nodes.size(); i++) { + if (nodes[i]->is_active()) { + nodes[i]->activate_inputs(node_input_nodes[i]); } } } @@ -146,15 +147,15 @@ void BlendTree::update_time() { } } -void BlendTree::evaluate(AnimationData& output) { +void BlendTree::evaluate(GraphEvaluationContext &context, const Vector& inputs, AnimationData &output) { for (int i = nodes.size() - 1; i > 0; i--) { if (nodes[i]->is_active()) { - nodes[i]->output = AnimationDataPool::allocate(); - nodes[i]->evaluate(); + node_output[i] = AnimationDataPool::allocate(); + nodes[i]->evaluate(context, node_inputs[i], node_output[i]); // node[i] is done, so we can deallocate the output handles of all input nodes of node[i]. for (AnimationGraphnNode& input_node: input_nodes[i]) { - AnimationDataPool::deallocate(input_node.output); + AnimationDataPool::deallocate(node_output[input_node.index]); } nodes[i]->set_active(false); @@ -188,7 +189,8 @@ void Blend2Node::update_time(SyncedAnimationNode::NodeTimeInfo time_info) { } } -void Blend2Node::evaluate(const Array& inputs, AnimationData& output) { +void Blend2Node::evaluate(GraphEvaluationContext &context, const Vector& inputs, AnimationData &output) { + assert(inputs.size() == 2); output = lerp(inputs[0]->get_output(), inputs[1], blend_weight); } @@ -210,8 +212,9 @@ void TimeScaleNode::update_time(SyncedAnimationNode::NodeTimeInfo time_info) { } } -void TimeScaleNode::evaluate(const Array& inputs, AnimationData& output) { - std::swap(output, input_node_0->output); +void TimeScaleNode::evaluate(GraphEvaluationContext &context, const Vector& inputs, AnimationData &output) { + assert(inputs.size() == 1); + output = inputs[0]->duplicate(); } ``` @@ -261,7 +264,7 @@ We use the term "value data" to distinguish from Animation Data. ### Effects on the graph topology -* Need to generalize Output Sockets to different types instead of only "Animation Data". +* Need to generalize Input and Output Ports to different types instead of only "Animation Data". * How to evaluate? Two types of subgraphs: * a) Data/value inputs (e.g. for blend weights) that have to be evaluated before UpdateConnections. Maybe restrict to data that is not animation data dependent? @@ -471,7 +474,7 @@ when a node becomes active/deactivated. Re-use of animation data ports -## 5. Inputs into Subgraphs +## 5. Inputs into embedded subgraphs ### Description diff --git a/synced_animation_graph.cpp b/synced_animation_graph.cpp index 6b09e84..2eed8b6 100644 --- a/synced_animation_graph.cpp +++ b/synced_animation_graph.cpp @@ -171,7 +171,7 @@ void SyncedAnimationGraph::_process_graph(double p_delta, bool p_update_only) { 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); + graph_root_node->evaluate(graph_context, Vector(), graph_output); _apply_animation_data(graph_output); } diff --git a/synced_animation_node.cpp b/synced_animation_node.cpp index f168842..44271e0 100644 --- a/synced_animation_node.cpp +++ b/synced_animation_node.cpp @@ -10,7 +10,9 @@ void AnimationSamplerNode::initialize(GraphEvaluationContext &context) { node_time_info.loop_mode = Animation::LOOP_LINEAR; } -void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, AnimationData &output) { +void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const Vector& inputs, AnimationData &output) { + assert(inputs.size() == 0); + const Vector tracks = animation->get_tracks(); Animation::Track *const *tracks_ptr = tracks.ptr(); diff --git a/synced_animation_node.h b/synced_animation_node.h index 171c51f..c18d7b4 100644 --- a/synced_animation_node.h +++ b/synced_animation_node.h @@ -12,7 +12,6 @@ struct GraphEvaluationContext { Skeleton3D *skeleton_3d = nullptr; }; - struct AnimationData { enum TrackType : uint8_t { TYPE_VALUE, // Set a value in a property, can be interpolated. @@ -105,6 +104,8 @@ public: Vector input_port; + StringName name; + virtual ~SyncedAnimationNode() = default; virtual void initialize(GraphEvaluationContext &context) {} virtual void activate_inputs() {} @@ -132,14 +133,13 @@ public: } } } - virtual void evaluate(GraphEvaluationContext &context, AnimationData &output) {} + virtual void evaluate(GraphEvaluationContext &context, const Vector& inputs, AnimationData &output) {} bool is_active() const { return active; } bool set_input_node(const StringName &socket_name, SyncedAnimationNode *node); - void get_input_names(Vector &inputs); + virtual void get_input_names(Vector &inputs) {}; private: - AnimationData *output = nullptr; bool active = false; }; @@ -153,28 +153,113 @@ private: Ref animation; void initialize(GraphEvaluationContext &context) override; - void evaluate(GraphEvaluationContext &context, AnimationData &output) override; + void evaluate(GraphEvaluationContext &context, const Vector& inputs, AnimationData &output) override; }; -class BlendTree : public SyncedAnimationNode { - struct Connection { - const SyncedAnimationNode* source_node = nullptr; - const SyncedAnimationNode* target_node = nullptr; +class OutputNode : public SyncedAnimationNode { +public: + void get_input_names(Vector &inputs) override { + inputs.push_back("Input"); + } +}; + +class SyncedBlendTree : public SyncedAnimationNode { + Vector> nodes; + + struct Connection { + const Ref source_node = nullptr; + const Ref target_node = nullptr; const StringName target_socket_name = ""; }; - - Vector nodes; - Vector node_parent; Vector connections; + Vector node_parent; + Vector node_eval_order; + + void _update_eval_order() { + // TODO: proof of concept code: currently assume nodes are properly added, though this is not guaranteed. + node_eval_order.clear(); + for (int i = 0; i < nodes.size(); i++) { + node_eval_order.push_back(i); + } + } public: - void connect_nodes(const SyncedAnimationNode* source_node, const SyncedAnimationNode* target_node, StringName target_socket_name) { - // TODO - // connections.append(Connection{source_node, target_node, target_socket_name}); - // sort_nodes_by_evaluation_order(); + SyncedBlendTree() { + Ref output_node; + output_node.instantiate(); + output_node->name = "Output"; + nodes.push_back(output_node); + node_eval_order.push_back(0); + } + + Ref get_output_node() { + return nodes[0]; + } + + int get_node_index(const Ref node) { + for (int i = 0; i < nodes.size(); i++) { + if (nodes[i] == node) { + return i; + } + } + + return -1; + } + + int add_node(const Ref& node) { + nodes.push_back(node); + int node_index = nodes.size() - 1; + return node_index; + } + + bool connect_nodes(const Ref& source_node, const Ref& target_node, StringName target_socket_name) { + _update_eval_order(); + + if (get_node_index(source_node) == -1) { + print_error("Cannot connect nodes: source node not found."); + return false; + } + + if (get_node_index(target_node) == -1) { + print_error("Cannot connect nodes: target node not found."); + return false; + } + + Vector target_inputs; + target_node->get_input_names(target_inputs); + + if (!target_inputs.has(target_socket_name)) { + print_error("Cannot connect nodes: target socket not found."); + return false; + } + + return true; } void sort_nodes_by_evaluation_order() { // TODO: sort nodes and node_parent s.t. for node i all children have index > i. } + + // overrides from SyncedAnimationNode + void initialize(GraphEvaluationContext &context) override { + for (Ref node : nodes) { + node->initialize(context); + } + } + + void activate_inputs() override { + + } + + void calculate_sync_track() override { + + } + + void update_time(double p_delta) override { + + } + + void evaluate(GraphEvaluationContext &context, const Vector& inputs, AnimationData &output) override { + + } }; diff --git a/tests/test_synced_animation_graph.h b/tests/test_synced_animation_graph.h index 974fb12..d83236a 100644 --- a/tests/test_synced_animation_graph.h +++ b/tests/test_synced_animation_graph.h @@ -6,42 +6,56 @@ #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); +struct SyncedAnimationGraphFixture { + Node* character_node; + Skeleton3D* skeleton_node; + AnimationPlayer* player_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 = 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"))); + int hip_bone_index = -1; + Ref test_animation; Ref 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; + SyncedAnimationGraphFixture() { + character_node = memnew(Node); + character_node->set_name("CharacterNode"); + SceneTree::get_singleton()->get_root()->add_child(character_node); - SyncedAnimationGraph *synced_animation_graph = memnew(SyncedAnimationGraph); - SceneTree::get_singleton()->get_root()->add_child(synced_animation_graph); + skeleton_node = memnew(Skeleton3D); + skeleton_node->set_name("Skeleton"); + character_node->add_child(skeleton_node); - synced_animation_graph->set_animation_player(player_node->get_path()); - synced_animation_graph->set_skeleton(skeleton_node->get_path()); + skeleton_node->add_bone("Root"); + hip_bone_index = skeleton_node->add_bone("Hips"); + player_node = memnew(AnimationPlayer); + player_node->set_name("AnimationPlayer"); + + test_animation = memnew(Animation); + const int track_index = test_animation->add_track(Animation::TYPE_POSITION_3D); + CHECK(track_index == 0); + test_animation->track_insert_key(track_index, 0.0, Vector3(0., 0., 0.)); + test_animation->track_insert_key(track_index, 1.0, Vector3(1., 2., 3.)); + test_animation->track_set_path(track_index, NodePath(vformat("%s:%s", skeleton_node->get_path().get_concatenated_names(),"Hips"))); + + animation_library.instantiate(); + animation_library->add_animation("TestAnimation", test_animation); + + player_node->add_animation_library("animation_library", animation_library); + SceneTree::get_singleton()->get_root()->add_child(player_node); + + 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()); + } +}; + +namespace TestSyncedAnimationGraph { + +TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] SimpleAnimationSamplerTest") { Ref animation_sampler_node; animation_sampler_node.instantiate(); animation_sampler_node->animation_name = "animation_library/TestAnimation"; @@ -62,4 +76,35 @@ TEST_CASE("[SceneTree][SyncedAnimationGraph] Simple") { CHECK(hip_bone_position.y == doctest::Approx(0.02)); CHECK(hip_bone_position.z == doctest::Approx(0.03)); } + +TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] SimpleBlendTreeTest") { + Ref synced_blend_tree_node; + synced_blend_tree_node.instantiate(); + + Ref animation_sampler_node; + animation_sampler_node.instantiate(); + animation_sampler_node->animation_name = "animation_library/TestAnimation"; + synced_blend_tree_node->add_node(animation_sampler_node); + + + synced_blend_tree_node->connect_nodes(animation_sampler_node, synced_blend_tree_node->get_output_node(), "Input"); + + synced_animation_graph->set_graph_root_node(synced_blend_tree_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)); } + + +} \ No newline at end of file