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:
```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<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
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<AnimationData*>& 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<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);
}
@ -210,8 +212,9 @@ void TimeScaleNode::update_time(SyncedAnimationNode::NodeTimeInfo time_info) {
}
}
void TimeScaleNode::evaluate(const Array<const AnimationData>& inputs, AnimationData& output) {
std::swap(output, input_node_0->output);
void TimeScaleNode::evaluate(GraphEvaluationContext &context, const Vector<AnimationData*>& 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

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->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<AnimationData*>(), 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;
}
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();
Animation::Track *const *tracks_ptr = tracks.ptr();

View File

@ -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<InputPort> 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<AnimationData*>& inputs, AnimationData &output) {}
bool is_active() const { return active; }
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:
AnimationData *output = nullptr;
bool active = false;
};
@ -153,28 +153,113 @@ private:
Ref<Animation> animation;
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 {
struct Connection {
const SyncedAnimationNode* source_node = nullptr;
const SyncedAnimationNode* target_node = nullptr;
class OutputNode : public SyncedAnimationNode {
public:
void get_input_names(Vector<StringName> &inputs) override {
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 = "";
};
Vector<SyncedAnimationNode> nodes;
Vector<int> node_parent;
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:
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<OutputNode> output_node;
output_node.instantiate();
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() {
// 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"
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> 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<Animation> test_animation;
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;
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<AnimationSamplerNode> 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<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));
}
}