From f232c5f51a8f494376789febdcd95233a72398c5 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Fri, 28 Nov 2025 14:45:48 +0100 Subject: [PATCH] Expanded design documents. --- README.md | 16 +--- doc/design.md | 251 +++++++++++++++++++++++++------------------------- 2 files changed, 127 insertions(+), 140 deletions(-) diff --git a/README.md b/README.md index 2b2c245..0e18e6f 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,3 @@ # Synced Animation Graphs for Godot - -## Questions - -1. Given an animation "Walk" with a call-method track and given that it is used as an input to a Blend2 node: will the - method be called twice? -1. a) - -## Open Issues - -1. Dynamic Track Caches - -When AnimationMixer performs blends it c - -AnimationMixer still has all sampled Animations and therefore their Tracks individually. However for the SAG - evaluation all operations are +This is a very much work in progress repository. Very rough drafts of the design and API can be found in the doc folder. \ No newline at end of file diff --git a/doc/design.md b/doc/design.md index cd2dd5e..28d0fa7 100644 --- a/doc/design.md +++ b/doc/design.md @@ -55,6 +55,128 @@ Some nodes have special names in the Blend Tree: is called the parent node. The output socket has no parent and in the example above The Blend2 node is the parent of both AnimationA and TimeScale. Conversely, AnimationA and TimeScale are child nodes of the Blend2 node. +## Blend Tree Evaluation Process + +### Description + +Evaluation of a node happens in multiple phases: + +1. ActivateInputs(): right to left (i.e. from the root node via depth first to the leave nodes) +2. CalculateSyncTracks(): left to right (leave nodes to root node) +3. UpdateTime(): right to left +4. Evaluate(): left to right + +One question here is how to transport the actual data from one node to another. There are essentially two options: + +### Blend Tree Evaluation + +```c++ +// BlendTree.cpp +void BlendTree::initialize_tree() { + for (int i = 0; ci < num_connections; i++) { + const Connection& connection = connections[i]; + connection.target_node->set_input_node(connection.target_socket_name, connection.source_node); + } +} + +void BlendTree::activate_inputs() { + for (int i = 0; i < nodes.size(); i++) { + if (nodes[i].is_active()) { + nodes[i].activate_inputs() + } + } +} + +void BlendTree::calculate_sync_tracks() { + for (int i = nodes.size() - 1; i > 0; i--) { + if (nodes[i]->is_active()) { + nodes[i]->calculate_sync_track(); + } + } +} + +void BlendTree::update_time() { + for (int i = 1; i < nodes.size(); i++) { + if (nodes[i]->is_active()) { + if (nodes[i]->is_synced()) { + nodes[i]->update_time(node_parents[i]->node_time_info); + } else { + nodes[i]->update_time(node_parents[i]->node_time_info); + } + } + } +} + +void BlendTree::evaluate(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[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); + } + + nodes[i]->set_active(false); + } + } + + std::move(output, nodes[0].output); +} + +// Blend2Node.cpp +void Blend2Node::activate_inputs() { + input_node_0->set_active(weight < 1.0 - EPS); + input_node_1->set_active(weight > EPS); +} + +void Blend2Node::calculate_sync_track() { + if (input_node_0->is_active()) { + sync_track = input_node_0->sync_track; + } + + if (input_node_1->is_active()) { + sync_track.blend(input_node_1->sync_track, blend_weight); + } +} + +void Blend2Node::update_time(SyncedAnimationNode::NodeTimeInfo time_info) { + if (!sync_enabled) { + node_time_info.position = node_time_info.position + time_info.delta; + } else { + // TODO + } +} + +void Blend2Node::evaluate(AnimationData& output) { + output = lerp(input_node_0->get_output(), input_node_1_data->get_output(), blend_weight); +} + +// TimeScaleNode.cpp +void TimeScaleNode::activate_inputs() { + input_node_0->set_active(true); +} + +void TimeScaleNode::calculate_sync_track() { + sync_track = input_node_0.sync_track; + sync_track.duration *= time_scale; +} + +void TimeScaleNode::update_time(SyncedAnimationNode::NodeTimeInfo time_info) { + if (!sync_enabled) { + node_time_info.position = node_time_info.position + time_info.delta; + } else { + // TODO + } +} + +void TimeScaleNode::evaluate(AnimationData& output) { + std::swap(output, input_node_0->output); +} + +``` + ## State Machines ```plantuml @@ -77,6 +199,10 @@ Run -up-> Fall @enduml ``` +# Feature Considerations + +This section contains design decisions and their tradeoffs on what the animation graphs should support. + ## 1. Generalized data connections / Support of math nodes (or non-AnimationNodes in general) ### Description @@ -327,131 +453,6 @@ also general input values. * Inputs to embedded state machines? -## 6. Blend Tree Evaluation Process - -### Description - -Evaluation of a node happens in multiple phases: - -1. ActivateInputs(): right to left (i.e. from the root node via depth first to the leave nodes) -2. CalculateSyncTracks(): left to right (leave nodes to root node) -3. UpdateTime(): right to left -4. Evaluate(): left to right - -One question here is how to transport the actual data from one node to another. There are essentially two options: - -#### Indirect input node references - -```c++ -// BlendTree.cpp -void BlendTree::initialize_tree() { - for (int i = 0; ci < num_connections; i++) { - const Connection& connection = connections[i]; - connection.target_node->set_input_node(connection.target_socket_name, connection.source_node); - } -} - -void BlendTree::activate_inputs() { - for (int i = 0; i < nodes.size(); i++) { - if (nodes[i].is_active()) { - nodes[i].activate_inputs() - } - } -} - -void BlendTree::calculate_sync_tracks() { - for (int i = nodes.size() - 1; i > 0; i--) { - if (nodes[i]->is_active()) { - nodes[i]->calculate_sync_track(); - } - } -} - -void BlendTree::update_time() { - for (int i = 1; i < nodes.size(); i++) { - if (nodes[i]->is_active()) { - if (nodes[i]->is_synced()) { - nodes[i]->update_time(node_parents[i]->node_time_info); - } else { - nodes[i]->update_time(node_parents[i]->node_time_info); - } - } - } -} - -void BlendTree::evaluate(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[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); - } - - nodes[i]->set_active(false); - } - } - - std::move(output, nodes[0].output); -} - -void Blend2Node::activate_inputs() { - input_node_0->set_active(weight < 1.0 - EPS); - input_node_1->set_active(weight > EPS); -} - -void Blend2Node::calculate_sync_track() { - if (input_node_0->is_active()) { - sync_track = input_node_0->sync_track; - } - - if (input_node_1->is_active()) { - sync_track.blend(input_node_1->sync_track, blend_weight); - } -} - -void Blend2Node::update_time(double p_delta) { - if (!sync_enabled) { - node_time_info.position = node_time_info.position + p_delta; - } else { - node_time_info.position = node_time_info.position + p_delta; - double sync_time = sync_track.calculate_sync_time(node_time_info.position); - } -} - -void Blend2Node::evaluate(AnimationData& output) { - output = lerp(input_node_0->get_output(), input_node_1_data->get_output(), blend_weight); -} - -void TimeScaleNode::evaluate(AnimationData& output) { - std::swap(output, input_node_0->output); -} -``` - -```c++ -// Node.cpp -void Node::evaluate(AnimationData& output) { - output = lerp(input_node_0->get_output(), input_node_1_data->get_output(), blend_weight); -} -``` - -#### Data injected by Blend Tree - -Nodes store references or pointers to all input nodes. - -```c++ -void Node::evaluate(const Array& animation_inputs, const Array& data_inputs>, AnimationData& output) { - output = lerp(animation_inputs[0], animation_inputs[1], data_inputs[0]); -} -``` - -* [+] This would allow easy extension of animation nodes via GDScript or GDExtension based nodes. - * [-] Though this could maybe be achieved using a specific customizable node for other approaches. -* [-] Easy to mess up indices. -* [-] Type safety of data_inputs messy. - ## Glossary ### Animation Data