From 1384d4a156454b19428860a58b6e12e7c19d319e Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Sat, 13 Dec 2025 22:38:45 +0100 Subject: [PATCH] WIP: blend tree setup and evaluation tests. --- synced_animation_node.h | 208 ++++++++++++++++++++++++---- tests/test_synced_animation_graph.h | 30 +++- 2 files changed, 209 insertions(+), 29 deletions(-) diff --git a/synced_animation_node.h b/synced_animation_node.h index 5e172ed..d9786d8 100644 --- a/synced_animation_node.h +++ b/synced_animation_node.h @@ -107,8 +107,18 @@ public: virtual ~SyncedAnimationNode() = default; virtual void initialize(GraphEvaluationContext &context) {} - virtual void activate_inputs() {} - virtual void calculate_sync_track() {} + virtual void activate_inputs(Vector> input_nodes) { + // By default, all inputs nodes are activated. + for (Ref node: input_nodes) { + node->active = true; + } + } + virtual void calculate_sync_track(Vector> input_nodes) { + // By default, use the SyncTrack of the first input. + if (input_nodes.size() > 0) { + node_time_info.sync_track = input_nodes[0]->node_time_info.sync_track; + } + } virtual void update_time(double p_delta) { node_time_info.delta = p_delta; node_time_info.position += p_delta; @@ -132,9 +142,14 @@ public: } } } - virtual void evaluate(GraphEvaluationContext &context, const Vector &inputs, AnimationData &output) {} + virtual void evaluate(GraphEvaluationContext &context, const Vector &input_datas, AnimationData &output_data) { + // By default, use the AnimationData of the first input. + if (input_datas.size() > 0) { + output_data = *input_datas[0]; + } + } - bool is_active() const { return active; } + bool active = false; bool set_input_node(const StringName &socket_name, SyncedAnimationNode *node); virtual void get_input_names(Vector &inputs) const {} @@ -148,9 +163,6 @@ public: get_input_names(inputs); return inputs.size(); } - -private: - bool active = false; }; class AnimationSamplerNode : public SyncedAnimationNode { @@ -187,11 +199,15 @@ struct BlendTreeConnection { const StringName target_port_name = ""; }; -struct SortedTreeConstructor { +/** + * @class BlendTreeBuilder + * Helper class that is used to build runtime blend trees and also to validate connections. + */ +struct BlendTreeBuilder { struct NodeConnectionInfo { int parent_node_index = -1; - HashSet input_subtree_node_indices; - LocalVector connected_child_node_index_at_port; + HashSet input_subtree_node_indices; // Contains all nodes down to the tree leaves that influence this node. + LocalVector connected_child_node_index_at_port; // Contains for each input port the index of the node that is connected to it. NodeConnectionInfo() = default; @@ -215,13 +231,32 @@ struct SortedTreeConstructor { } print_line(result); } + + void apply_node_mapping(LocalVector node_index_mapping) { + if (parent_node_index != -1) { + parent_node_index = node_index_mapping[parent_node_index]; + } + + // Map connected node indices + for (unsigned int j = 0; j < connected_child_node_index_at_port.size(); j++) { + int connected_node_index = connected_child_node_index_at_port[j]; + connected_child_node_index_at_port[j] = node_index_mapping.find(connected_node_index); + } + + // Map connected subtrees + HashSet old_indices = input_subtree_node_indices; + input_subtree_node_indices.clear(); + for (int old_index: old_indices) { + input_subtree_node_indices.insert(node_index_mapping.find(old_index)); + } + } }; Vector> nodes; // All added nodes LocalVector node_connection_info; Vector connections; - SortedTreeConstructor() { + BlendTreeBuilder() { Ref output_node; output_node.instantiate(); output_node->name = "Output"; @@ -247,6 +282,43 @@ struct SortedTreeConstructor { node_connection_info.push_back(NodeConnectionInfo(node.ptr())); } + void sort_nodes_and_references() { + LocalVector sorted_node_indices = get_sorted_node_indices(); + LocalVector index_mapping; + for (int i : sorted_node_indices) { + index_mapping.push_back(sorted_node_indices.find(i)); + } + + Vector> sorted_nodes; + Vector old_node_connection_info = node_connection_info; + for (unsigned int i = 0; i < sorted_node_indices.size(); i++) { + int node_index = sorted_node_indices[i]; + sorted_nodes.push_back(nodes[node_index]); + node_connection_info[i] = old_node_connection_info[node_index]; + } + nodes = sorted_nodes; + + for (NodeConnectionInfo& connection_info: node_connection_info) { + connection_info.apply_node_mapping(sorted_node_indices); + } + } + + LocalVector get_sorted_node_indices() { + LocalVector result; + + sort_nodes_recursive(0, result); + result.reverse(); + + return result; + } + + void sort_nodes_recursive(int node_index, LocalVector &result) { + for (int input_node_index : node_connection_info[node_index].connected_child_node_index_at_port) { + sort_nodes_recursive(input_node_index, result); + } + result.push_back(node_index); + } + void add_index_and_update_subtrees_recursive(int node, int node_parent) { if (node_parent == -1) { return; @@ -331,17 +403,48 @@ class SyncedBlendTree : public SyncedAnimationNode { Vector tree_connections; Vector> nodes; - Vector node_parent_index; - Vector> node_subgraph; - Vector>> node_input_nodes; - Vector>> node_input_data; - Vector> node_output_data; - void _setup_graph_evaluation() { - // After this functions we must have: - // * nodes sorted by evaluation order - // * node_parent filled - // * Arrays for node_input_data and node_output_data are filled with empty values + struct NodeRuntimeData { + Vector> input_nodes; + Vector input_data; + AnimationData *output_data = nullptr; + }; + LocalVector _node_runtime_data; + + BlendTreeBuilder tree_builder; + bool tree_initialized = false; + + void setup_tree() { + nodes.clear(); + _node_runtime_data.clear(); + + tree_builder.sort_nodes_and_references(); + + // Add nodes and allocate runtime data + for (int i = 0; i < tree_builder.nodes.size(); i++) { + Ref node = tree_builder.nodes[i]; + nodes.push_back(node); + + NodeRuntimeData node_runtime_data; + for (int ni = 0; ni < node->get_node_input_count(); ni++) { + node_runtime_data.input_data.push_back(nullptr); + } + + node_runtime_data.output_data = nullptr; + _node_runtime_data.push_back(node_runtime_data); + } + + // Populate runtime data (only now is this.nodes populated to retrieve the nodes) + for (int i = 0; i < nodes.size(); i++) { + Ref node = nodes[i]; + NodeRuntimeData& node_runtime_data = _node_runtime_data[i]; + + for (int port_index = 0; port_index < node->get_node_input_count(); port_index++) { + node_runtime_data.input_nodes.push_back(nodes[tree_builder.node_connection_info[i].connected_child_node_index_at_port[port_index]]); + } + } + + tree_initialized = true; } public: @@ -366,10 +469,22 @@ public: return -1; } - int add_node(const Ref &node) { - nodes.push_back(node); - int node_index = nodes.size() - 1; - return node_index; + void add_node(const Ref &node) { + if (tree_initialized) { + print_error("Cannot add node to BlendTree: BlendTree already initialized."); + return; + } + + tree_builder.add_node(node); + } + + bool add_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) { + if (tree_initialized) { + print_error("Cannot add connection to BlendTree: BlendTree already initialized."); + return false; + } + + return tree_builder.add_connection(source_node, target_node, target_port_name); } // overrides from SyncedAnimationNode @@ -379,15 +494,52 @@ public: } } - void activate_inputs() override { + void activate_inputs(Vector> input_nodes) override { + nodes[0]->active = true; + for (int i = 0; i < nodes.size(); i++) { + Ref node = nodes[i]; + + if (!node->active) { + continue; + } + + NodeRuntimeData& node_runtime_data = _node_runtime_data[i]; + node_runtime_data.output_data = memnew(AnimationData); + + node->activate_inputs(node_runtime_data.input_nodes); + } } - void calculate_sync_track() override { + void calculate_sync_track(Vector> input_nodes) override { + for (int i = nodes.size() - 1; i > 0; i--) { + Ref node = nodes[i]; + + if (!node->active) { + continue; + } + + NodeRuntimeData& node_runtime_data = _node_runtime_data[i]; + node_runtime_data.output_data = memnew(AnimationData); + + node->calculate_sync_track(node_runtime_data.input_nodes); + } } void update_time(double p_delta) override { + for (int i = 0; i < nodes.size(); i++) { + Ref node = nodes[i]; + + if (!node->active) { + continue; + } + + NodeRuntimeData& node_runtime_data = _node_runtime_data[i]; + node_runtime_data.output_data = memnew(AnimationData); + + node->update_time(node_runtime_data.input_nodes); + } } - void evaluate(GraphEvaluationContext &context, const Vector &inputs, AnimationData &output) override { + void evaluate(GraphEvaluationContext &context, const Vector &input_datas, AnimationData &output_data) override { } }; diff --git a/tests/test_synced_animation_graph.h b/tests/test_synced_animation_graph.h index 7f3feb2..e4fd036 100644 --- a/tests/test_synced_animation_graph.h +++ b/tests/test_synced_animation_graph.h @@ -56,7 +56,7 @@ struct SyncedAnimationGraphFixture { namespace TestSyncedAnimationGraph { TEST_CASE("[SyncedAnimationGraph] TestBlendTreeConstruction") { - SortedTreeConstructor tree_constructor; + BlendTreeBuilder tree_constructor; Ref animation_sampler_node0; animation_sampler_node0.instantiate(); @@ -123,6 +123,34 @@ TEST_CASE("[SyncedAnimationGraph] TestBlendTreeConstruction") { CHECK(tree_constructor.node_connection_info[0].input_subtree_node_indices.has(3)); CHECK(tree_constructor.node_connection_info[0].input_subtree_node_indices.has(4)); CHECK(tree_constructor.node_connection_info[0].input_subtree_node_indices.has(5)); + + print_line("-- Unsorted Nodes:"); + for (unsigned int i = 0; i < tree_constructor.nodes.size(); i++) { + print_line(vformat("%d: node %10s", i, tree_constructor.nodes[i]->name)); + tree_constructor.node_connection_info[i]._print_subtree(); + } + + LocalVector mapping = tree_constructor.get_sorted_node_indices(); + for (unsigned int i = 0; i < mapping.size(); i++) { + print_line(vformat("%2d -> %2d", i, mapping[i])); + } + print_line(vformat("node %d is at index %d", 4, mapping.find(4))); + + tree_constructor.sort_nodes_and_references(); + + print_line("-- Sorted Nodes"); + for (unsigned int i = 0; i < tree_constructor.nodes.size(); i++) { + print_line(vformat("%d: node %10s", i, tree_constructor.nodes[i]->name)); + tree_constructor.node_connection_info[i]._print_subtree(); + } + + // Check that for node i all input nodes have a node index j > i. + for (unsigned int i = 0; i < tree_constructor.nodes.size(); i++) { + for (int input_index: tree_constructor.node_connection_info[i].input_subtree_node_indices) { + CHECK(input_index > i); + } + } + } TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] SimpleAnimationSamplerTest" * doctest::skip(true)) {