Expanded design documents.
This commit is contained in:
parent
d61c7926fa
commit
f232c5f51a
16
README.md
16
README.md
@ -1,17 +1,3 @@
|
|||||||
# Synced Animation Graphs for Godot
|
# Synced Animation Graphs for Godot
|
||||||
|
|
||||||
|
This is a very much work in progress repository. Very rough drafts of the design and API can be found in the doc folder.
|
||||||
## 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
|
|
||||||
251
doc/design.md
251
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
|
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.
|
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
|
## State Machines
|
||||||
|
|
||||||
```plantuml
|
```plantuml
|
||||||
@ -77,6 +199,10 @@ Run -up-> Fall
|
|||||||
@enduml
|
@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)
|
## 1. Generalized data connections / Support of math nodes (or non-AnimationNodes in general)
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
@ -327,131 +453,6 @@ also general input values.
|
|||||||
|
|
||||||
* Inputs to embedded state machines?
|
* 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
|
## Glossary
|
||||||
|
|
||||||
### Animation Data
|
### Animation Data
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user