WIP: refactored test to use a fixture, started working on Blend Tree evaluation.

This commit is contained in:
Martin Felis 2025-12-08 22:47:00 +01:00
parent 33fae3458b
commit 5880dde6ec
5 changed files with 215 additions and 80 deletions

View File

@ -56,27 +56,14 @@ invalid.
### Example: ### Example:
```plantuml ```mermaid
@startuml flowchart LR
AnimationB --> TimeScale("TimeScale
left to right direction ----
*scale*")
abstract Output {} AnimationA --> Blend2
abstract Blend2 { TimeScale --> Blend2
bool is_synced Blend2 --> Output
float weight()
}
abstract AnimationA {}
abstract TimeScale {}
abstract AnimationB {}
AnimationA --> Blend2
AnimationB --> TimeScale
TimeScale --> Blend2
Blend2 --> Output
@enduml
``` ```
A Blend Tree always has a designated output node where the time delta is specified as an input and after the Blend Tree 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 ### Blend Tree Evaluation
```c++ ```c++
// BlendTree.h
class BlendTree: public SyncedAnimationNode {
private:
Vector<SyncedAnimationNode*> nodes;
Vector<int> node_parent; // node_parent[i] is the index of the parent of node i.
Vector<AnimationData*> node_output; // output for each node
Vector<Vector<SyncedAnimationNode*>> node_input_nodes;
Vector<Vector<AnimationData*>> node_input_data; // list of inputs for all nodes.
int get_index_for_node(const SyncedAnimationNode& node);
};
// BlendTree.cpp // BlendTree.cpp
void BlendTree::initialize_tree() { void BlendTree::initialize_tree() {
for (int i = 0; ci < num_connections; i++) { for (int i = 0; ci < num_connections; i++) {
@ -119,9 +118,11 @@ void BlendTree::initialize_tree() {
} }
void BlendTree::activate_inputs() { void BlendTree::activate_inputs() {
for (int i = 0; i < nodes.size(); i++) { nodes[0]->activate_inputs();
if (nodes[i].is_active()) {
nodes[i].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<AnimationData*>& inputs, AnimationData &output) {
for (int i = nodes.size() - 1; i > 0; i--) { for (int i = nodes.size() - 1; i > 0; i--) {
if (nodes[i]->is_active()) { if (nodes[i]->is_active()) {
nodes[i]->output = AnimationDataPool::allocate(); node_output[i] = AnimationDataPool::allocate();
nodes[i]->evaluate(); 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]. // 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]) { for (AnimationGraphnNode& input_node: input_nodes[i]) {
AnimationDataPool::deallocate(input_node.output); AnimationDataPool::deallocate(node_output[input_node.index]);
} }
nodes[i]->set_active(false); nodes[i]->set_active(false);
@ -188,7 +189,8 @@ void Blend2Node::update_time(SyncedAnimationNode::NodeTimeInfo time_info) {
} }
} }
void Blend2Node::evaluate(const Array<const AnimationData>& inputs, AnimationData& output) { void Blend2Node::evaluate(GraphEvaluationContext &context, const Vector<AnimationData*>& inputs, AnimationData &output) {
assert(inputs.size() == 2);
output = lerp(inputs[0]->get_output(), inputs[1], blend_weight); 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<const AnimationData>& inputs, AnimationData& output) { void TimeScaleNode::evaluate(GraphEvaluationContext &context, const Vector<AnimationData*>& inputs, AnimationData &output) {
std::swap(output, input_node_0->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 ### 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: * 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 * 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? to data that is not animation data dependent?
@ -471,7 +474,7 @@ when a node becomes active/deactivated.
Re-use of animation data ports Re-use of animation data ports
## 5. Inputs into Subgraphs ## 5. Inputs into embedded subgraphs
### Description ### Description

View File

@ -171,7 +171,7 @@ void SyncedAnimationGraph::_process_graph(double p_delta, bool p_update_only) {
graph_root_node->activate_inputs(); graph_root_node->activate_inputs();
graph_root_node->calculate_sync_track(); graph_root_node->calculate_sync_track();
graph_root_node->update_time(p_delta); graph_root_node->update_time(p_delta);
graph_root_node->evaluate(graph_context, graph_output); graph_root_node->evaluate(graph_context, Vector<AnimationData*>(), graph_output);
_apply_animation_data(graph_output); _apply_animation_data(graph_output);
} }

View File

@ -10,7 +10,9 @@ 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, const Vector<AnimationData*>& inputs, AnimationData &output) {
assert(inputs.size() == 0);
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();

View File

@ -12,7 +12,6 @@ struct GraphEvaluationContext {
Skeleton3D *skeleton_3d = nullptr; Skeleton3D *skeleton_3d = nullptr;
}; };
struct AnimationData { struct AnimationData {
enum TrackType : uint8_t { enum TrackType : uint8_t {
TYPE_VALUE, // Set a value in a property, can be interpolated. TYPE_VALUE, // Set a value in a property, can be interpolated.
@ -105,6 +104,8 @@ public:
Vector<InputPort> input_port; Vector<InputPort> input_port;
StringName name;
virtual ~SyncedAnimationNode() = default; virtual ~SyncedAnimationNode() = default;
virtual void initialize(GraphEvaluationContext &context) {} virtual void initialize(GraphEvaluationContext &context) {}
virtual void activate_inputs() {} virtual void activate_inputs() {}
@ -132,14 +133,13 @@ public:
} }
} }
} }
virtual void evaluate(GraphEvaluationContext &context, AnimationData &output) {} virtual void evaluate(GraphEvaluationContext &context, const Vector<AnimationData*>& inputs, AnimationData &output) {}
bool is_active() const { return active; } bool is_active() const { return active; }
bool set_input_node(const StringName &socket_name, SyncedAnimationNode *node); bool set_input_node(const StringName &socket_name, SyncedAnimationNode *node);
void get_input_names(Vector<StringName> &inputs); virtual void get_input_names(Vector<StringName> &inputs) {};
private: private:
AnimationData *output = nullptr;
bool active = false; bool active = false;
}; };
@ -153,28 +153,113 @@ private:
Ref<Animation> animation; Ref<Animation> animation;
void initialize(GraphEvaluationContext &context) override; void initialize(GraphEvaluationContext &context) override;
void evaluate(GraphEvaluationContext &context, AnimationData &output) override; void evaluate(GraphEvaluationContext &context, const Vector<AnimationData*>& inputs, AnimationData &output) override;
}; };
class BlendTree : public SyncedAnimationNode { class OutputNode : public SyncedAnimationNode {
struct Connection { public:
const SyncedAnimationNode* source_node = nullptr; void get_input_names(Vector<StringName> &inputs) override {
const SyncedAnimationNode* target_node = nullptr; inputs.push_back("Input");
}
};
class SyncedBlendTree : public SyncedAnimationNode {
Vector<Ref<SyncedAnimationNode>> nodes;
struct Connection {
const Ref<SyncedAnimationNode> source_node = nullptr;
const Ref<SyncedAnimationNode> target_node = nullptr;
const StringName target_socket_name = ""; const StringName target_socket_name = "";
}; };
Vector<SyncedAnimationNode> nodes;
Vector<int> node_parent;
Vector<Connection> connections; Vector<Connection> connections;
Vector<int> node_parent;
Vector<int> 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: public:
void connect_nodes(const SyncedAnimationNode* source_node, const SyncedAnimationNode* target_node, StringName target_socket_name) { SyncedBlendTree() {
// TODO Ref<OutputNode> output_node;
// connections.append(Connection{source_node, target_node, target_socket_name}); output_node.instantiate();
// sort_nodes_by_evaluation_order(); output_node->name = "Output";
nodes.push_back(output_node);
node_eval_order.push_back(0);
}
Ref<SyncedAnimationNode> get_output_node() {
return nodes[0];
}
int get_node_index(const Ref<SyncedAnimationNode> node) {
for (int i = 0; i < nodes.size(); i++) {
if (nodes[i] == node) {
return i;
}
}
return -1;
}
int add_node(const Ref<SyncedAnimationNode>& node) {
nodes.push_back(node);
int node_index = nodes.size() - 1;
return node_index;
}
bool connect_nodes(const Ref<SyncedAnimationNode>& source_node, const Ref<SyncedAnimationNode>& 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<StringName> 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() { void sort_nodes_by_evaluation_order() {
// TODO: sort nodes and node_parent s.t. for node i all children have index > i. // 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<SyncedAnimationNode> 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<AnimationData*>& inputs, AnimationData &output) override {
}
}; };

View File

@ -6,42 +6,56 @@
#include "tests/test_macros.h" #include "tests/test_macros.h"
namespace TestSyncedAnimationGraph { struct SyncedAnimationGraphFixture {
TEST_CASE("[SceneTree][SyncedAnimationGraph] Simple") { Node* character_node;
Node* character_node = memnew(Node); Skeleton3D* skeleton_node;
character_node->set_name("CharacterNode"); AnimationPlayer* player_node;
SceneTree::get_singleton()->get_root()->add_child(character_node);
Skeleton3D *skeleton_node = memnew(Skeleton3D); int hip_bone_index = -1;
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<Animation> test_animation;
Ref<AnimationLibrary> animation_library; Ref<AnimationLibrary> animation_library;
animation_library.instantiate();
animation_library->add_animation("TestAnimation", animation);
player_node->add_animation_library("animation_library", animation_library); SyncedAnimationGraph* synced_animation_graph;
SceneTree::get_singleton()->get_root()->add_child(player_node); 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); skeleton_node = memnew(Skeleton3D);
SceneTree::get_singleton()->get_root()->add_child(synced_animation_graph); skeleton_node->set_name("Skeleton");
character_node->add_child(skeleton_node);
synced_animation_graph->set_animation_player(player_node->get_path()); skeleton_node->add_bone("Root");
synced_animation_graph->set_skeleton(skeleton_node->get_path()); 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<AnimationSamplerNode> animation_sampler_node; Ref<AnimationSamplerNode> animation_sampler_node;
animation_sampler_node.instantiate(); animation_sampler_node.instantiate();
animation_sampler_node->animation_name = "animation_library/TestAnimation"; 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.y == doctest::Approx(0.02));
CHECK(hip_bone_position.z == doctest::Approx(0.03)); CHECK(hip_bone_position.z == doctest::Approx(0.03));
} }
TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] SimpleBlendTreeTest") {
Ref<SyncedBlendTree> synced_blend_tree_node;
synced_blend_tree_node.instantiate();
Ref<AnimationSamplerNode> 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));
}
} }