Expanded design documents.

This commit is contained in:
Martin Felis 2025-11-28 14:45:48 +01:00
parent d61c7926fa
commit f232c5f51a
2 changed files with 127 additions and 140 deletions

View File

@ -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.

View File

@ -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<const AnimationData*>& animation_inputs, const Array<const Variant>& 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