Expanded design documents.
This commit is contained in:
parent
5dfd44d52a
commit
bc3eee9537
438
doc/design.md
438
doc/design.md
@ -1,73 +1,459 @@
|
||||
## Graph
|
||||
# AnimationGraph
|
||||
|
||||
### 1. Generalized data connections / Support of math nodes (or non-AnimNodes in general)
|
||||
## Blend Trees
|
||||
|
||||
Use case:
|
||||
A Blend Tree is a directed acyclic graph. Nodes produce or process "AnimationData" and the connections transport "
|
||||
AnimationData". "AnimationData" here is anything that can be sampled in an Animation.
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
A Blend Tree always has a designated output node where the time delta is specified as an input and after processing of
|
||||
the Blend Tree it emits the animation data.
|
||||
|
||||
Some nodes have special names in the Blend Tree:
|
||||
|
||||
* **Root node** The output node is also called the root node of the graph.
|
||||
* **Leaf nodes** These are the nodes that have no inputs. In the example these are the nodes AnimationA and AnimationB.
|
||||
|
||||
### Animation and Animation Data
|
||||
|
||||
For Godot an Animation has multiple tracks where each Track is of a specific type such as "Position", "Rotation", "
|
||||
Method Call", "Audio Playback", etc. Each Track is associated with a node path on which the value of a sampled Track
|
||||
acts.
|
||||
|
||||
Animation Data represents a sampled animation for a given time. For each track in an animation it contains the sampled
|
||||
values, e.g. a Vector3 for a position, a Quaternion for a rotation, a function name and its parameters, etc.
|
||||
|
||||
## State Machines
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
|
||||
State Idle
|
||||
State Walk
|
||||
State Run
|
||||
State Fall
|
||||
|
||||
[*] -right-> Idle
|
||||
Idle -right-> Walk
|
||||
Walk -left-> Idle
|
||||
Walk -right-> Run
|
||||
Run -left-> Walk
|
||||
Walk -up-> Fall
|
||||
'Idle -right-> Fall
|
||||
Run -up-> Fall
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
## 1. Generalized data connections / Support of math nodes (or non-AnimationNodes in general)
|
||||
|
||||
### Description
|
||||
|
||||
The connections in the current AnimationTree only support animation values. Node inputs are specified as parameters of
|
||||
the AnimationTree. E.g. it is not possible to do some math operation on the floating point value that ends up as blend
|
||||
weight input of a Blend2 node.
|
||||
|
||||
We use the term "value data" to distinguish from Animation Data.
|
||||
|
||||
### Use case
|
||||
|
||||
* Enables animators to add custom math for blend inputs or to adjust other inputs (e.g. LookAt or IK
|
||||
targets).
|
||||
* Together with "Support of multiple output sockets" this can be used to add input parameters that affect multiple node
|
||||
inputs.
|
||||
|
||||
Effects on the graph topology?
|
||||
### Effects on the graph topology
|
||||
|
||||
* Need to generalize Output Sockets to different types instead of only "Animation Data".
|
||||
* How to evaluate? Two types of subgraphs:
|
||||
a) Instant inputs (needed for blend node inputs) that have to be evaluated before
|
||||
UpdateConnections. Maybe restrict to data that is not animation data dependent?
|
||||
b) Processing nodes, e.g. for extracted bones.
|
||||
* 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?
|
||||
* b) Processing nodes, e.g. for extracted bones.
|
||||
|
||||
### 2. Support of multiple output sockets
|
||||
## 2. Support of multiple output sockets
|
||||
|
||||
Use case:
|
||||
### Description
|
||||
|
||||
Current AnimationTree nodes have a single designated output socket. A node cannot extract a value that then gets used as
|
||||
input at a laters tage in the graph.
|
||||
|
||||
**Depends on**: "Generalized data connections".
|
||||
|
||||
### Use case
|
||||
|
||||
* E.g. extract Bone transform and use in pose modifying nodes.
|
||||
* Chain IK.
|
||||
|
||||
Effects on graph topology?
|
||||
### Effects on graph topology
|
||||
|
||||
* Increases Node complexity:
|
||||
* AnimOutput
|
||||
* AnimOutput + Data
|
||||
* Data
|
||||
(Data = bool, float, vec3, quat or ...)
|
||||
* Can a BlendTree emit values?
|
||||
* If so: what happens with the output if the BlendTree is used in a State Machine?
|
||||
* => Initially: State Machines only emit Animation Data.
|
||||
* Simplest case:
|
||||
* All value data connections are evaluated always before UpdateConnections.
|
||||
* BlendTrees (and therefore embedded graphs) cannot emit values.
|
||||
|
||||
(Data = bool, float, vec3, quat or ...)
|
||||
|
||||
**Open Issues**
|
||||
### Open Issues
|
||||
|
||||
1. Unclear when this is actually needed. Using more specific nodes that perform the desired logic
|
||||
may be better (
|
||||
c.f. https://dev.epicgames.com/documentation/en-us/unreal-engine/animation-blueprint-bone-driven-controller-in-unreal-engine).
|
||||
Likely this is not crucial so should be avoided for now.
|
||||
|
||||
### 3. Multi-skeleton evaluation
|
||||
## 3. Multi-skeleton evaluation
|
||||
|
||||
Use case: riding on a horse, interaction between two characters.
|
||||
### Description
|
||||
|
||||
### 4. Output re-use
|
||||
Allow an animation graph to affect multiple skeletons.
|
||||
|
||||
Description:
|
||||
### Use case
|
||||
|
||||
Output of a single node may be used as input of two or more other nodes.
|
||||
Riding on a horse, interaction between two characters.
|
||||
|
||||
Use case:
|
||||
## 4. Output re-use
|
||||
|
||||
* (Related to 1.) an input that does some computation gets reused in two separate subtrees.
|
||||
### Description
|
||||
|
||||
### 5. Inputs into Subgraphs
|
||||
Output of a single node may be used as input of two or more other nodes. One has to consider re-use of Animation Data
|
||||
and general value data (see "Generalized data connections") separately.
|
||||
|
||||
Description:
|
||||
#### Animation Data Output re-use
|
||||
|
||||
An embedded blend tree can receive inputs of the surrounding blend tree. Inputs are animations or - depending on 1. - also general input values.
|
||||
Animation Data connections are generally more heavy weight than "Generalized data connections". The latter only contain
|
||||
a small data type such as a Vector3 or Quaternion, whereas Animation Data is a container of those (and possibly more,
|
||||
e.g. function names and their parameters). So it is desirable to have a minimal number of Animation Data objects
|
||||
allocated.
|
||||
|
||||
Use case:
|
||||
```plantuml
|
||||
@startuml
|
||||
|
||||
left to right direction
|
||||
|
||||
abstract Output {}
|
||||
abstract Blend2 {
|
||||
bool is_synced
|
||||
|
||||
float weight()
|
||||
}
|
||||
|
||||
abstract AnimationA {}
|
||||
abstract TwoBoneIK {}
|
||||
abstract BoneExtractor {}
|
||||
abstract AnimationB {}
|
||||
|
||||
AnimationA --> Blend2
|
||||
AnimationB --> Blend2
|
||||
AnimationB --> BoneExtractor
|
||||
BoneExtractor --> TwoBoneIK
|
||||
Blend2 --> TwoBoneIK
|
||||
TwoBoneIK --> Output
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
In this case the Animation Data output of AnimationB is used both in the Blend2 node and the BoneExtractor node. The
|
||||
extracted bone information is passed into the TwoBoneIK node. For this to work we have to ensure that the output of
|
||||
AnimationB is freed only when both the Blend2 node and the BoneExtractor have finished their work.
|
||||
|
||||
An alternative layout with the same nodes would not suffer from this:
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
|
||||
left to right direction
|
||||
|
||||
abstract Output {}
|
||||
abstract Blend2 {
|
||||
bool is_synced
|
||||
|
||||
float weight()
|
||||
}
|
||||
|
||||
abstract AnimationA {}
|
||||
abstract TwoBoneIK {}
|
||||
abstract BoneExtractor {}
|
||||
abstract AnimationB {}
|
||||
|
||||
AnimationA --> Blend2
|
||||
BoneExtractor --> Blend2
|
||||
AnimationB --> BoneExtractor
|
||||
BoneExtractor --> TwoBoneIK
|
||||
Blend2 --> TwoBoneIK
|
||||
TwoBoneIK --> Output
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
Here the AnimationData output of BoneExtractor is used by Blend2 and the extracted bone information is still passed to
|
||||
the TwoBoneIK.
|
||||
|
||||
A more complex case is the following setup where Animation Data is used by two nodes:
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
|
||||
left to right direction
|
||||
|
||||
abstract Output {}
|
||||
abstract Blend2_UpperBody {
|
||||
bool is_synced
|
||||
|
||||
float weight()
|
||||
}
|
||||
|
||||
abstract Blend2_LowerBody {
|
||||
bool is_synced
|
||||
|
||||
float weight()
|
||||
}
|
||||
|
||||
|
||||
abstract AnimationA {}
|
||||
abstract AnimationB {}
|
||||
|
||||
AnimationA --> Blend2_UpperBody
|
||||
AnimationB --> Blend2_UpperBody
|
||||
AnimationB --> Blend2_LowerBody
|
||||
Blend2_UpperBody --> Blend2_LowerBody
|
||||
|
||||
Blend2_LowerBody --> Output
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
Here the output of AnimationB is used in both Blend2_UpperBody and Blend2_LowerBody. However, if both blends are synced
|
||||
the Blend2_UpperBody and Blend2_LowerBody nodes may compute different time values as their SyncTracks differ. Generally
|
||||
a connection is invalid if the Animation Data of a node gets combined with itself via a different path in the tree.
|
||||
|
||||
#### Data value re-use
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
|
||||
left to right direction
|
||||
|
||||
abstract Output {}
|
||||
abstract Blend2 {
|
||||
bool is_synced
|
||||
|
||||
float weight()
|
||||
}
|
||||
|
||||
abstract AnimationA {}
|
||||
abstract Vector3Input {}
|
||||
abstract UnpackVector3 {
|
||||
x
|
||||
y
|
||||
z
|
||||
}
|
||||
|
||||
AnimationA --> Blend2
|
||||
Vector3Input --> UnpackVector3
|
||||
UnpackVector3 --> Blend2
|
||||
Blend2 --> Output
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
Here the UnpackVector3 provides access to the individual 3 components of a Vector3 such that e.g. the x value can be
|
||||
used as an input to a weight node.
|
||||
|
||||
Data value re-use is not a problem as the values would be stored directly in the nodes and are not allocated/deallocated
|
||||
when a node becomes active/deactivated.
|
||||
|
||||
### Use case
|
||||
|
||||
* Depends on "Generalized data connections" an input that does some computation gets reused in two separate subtrees.
|
||||
|
||||
## 5. Inputs into Subgraphs
|
||||
|
||||
### Description
|
||||
|
||||
An embedded blend tree can receive inputs of the surrounding blend tree. Inputs are animations or - depending on 1. -
|
||||
also general input values.
|
||||
|
||||
### Use case
|
||||
|
||||
* Reuse of some blend logic or to move logic of a part of a blend tree into its own node.
|
||||
|
||||
Effects on graph topology:
|
||||
### Effects on graph topology
|
||||
|
||||
* Great flexibility and possibly reusability.
|
||||
* Improves logical block building.
|
||||
* Probably only of bigger use with 1.
|
||||
|
||||
Open issues:
|
||||
### Open issues
|
||||
|
||||
* Inputs to embedded state machines?
|
||||
|
||||
## 6. Evaluation API
|
||||
|
||||
### Description
|
||||
|
||||
Evaluation of a node happens in multiple phases:
|
||||
|
||||
1. UpdateConnections(): 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:
|
||||
|
||||
#### Data owned by connections
|
||||
|
||||
#### Explicit input node references
|
||||
|
||||
Nodes store references or pointers to all input nodes.
|
||||
|
||||
``` C++
|
||||
void Node::evaluate(AnimationData& output) {
|
||||
AnimationData input_node_0_data;
|
||||
input_node_0->evaluate(input_node_0_data);
|
||||
|
||||
AnimationData input_node_1_data;
|
||||
input_node_1->evaluate(input_node_1_data);
|
||||
|
||||
output = lerp(input_node_0_data, input_node_1_data, blend_weight);
|
||||
}
|
||||
```
|
||||
|
||||
* [-] Makes Blend Tree evaluation recursive.
|
||||
|
||||
#### 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 < num_nodes; i++) {
|
||||
if (nodes[i].is_active()) {
|
||||
nodes[i].activate_inputs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Blend2Node::activate_inputs() {
|
||||
if (weight < EPS) {
|
||||
input_node_0->set_active(false);
|
||||
} else {
|
||||
input_node_0->set_active(true);
|
||||
}
|
||||
|
||||
if (weight > 1.0 - EPS) {
|
||||
input_node_1->set_active(false);
|
||||
} else {
|
||||
input_node_1->set_active(true);
|
||||
}
|
||||
}
|
||||
|
||||
void BlendTree::calculate_sync_tracks() {
|
||||
for (int i = num_nodes; i > 0; i--) {
|
||||
if (nodes[i]->is_active()) {
|
||||
nodes[i]->calculate_sync_track();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BlendTree::propagate_time() {
|
||||
for (int i = 1; i < num_nodes; i++) {
|
||||
if (nodes[i]->is_active()) {
|
||||
nodes[i]->update_time();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BlendTree::evaluate(AnimationData& output) {
|
||||
for (int i = num_nodes; 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_nodes: input_nodes[i]) {
|
||||
AnimationDataPool::deallocate(nodes[i].output);
|
||||
}
|
||||
}
|
||||
|
||||
nodes[i]->set_active(false);
|
||||
}
|
||||
|
||||
output = nodes[0].output;
|
||||
|
||||
// free output buffers
|
||||
for (int i = 1; i < num_nodes; i++) {
|
||||
AnimationDataPool::deallocate(nodes[i].output);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Output of an AnimationGraphNode. Contains everything that can be sampled from an animation such as positions, rotations
|
||||
but also method calls including function name and arguments.
|
||||
|
||||
79
doc/design.puml
Normal file
79
doc/design.puml
Normal file
@ -0,0 +1,79 @@
|
||||
@startuml
|
||||
|
||||
class Node {
|
||||
}
|
||||
|
||||
class AnimationGraph {
|
||||
AnimationGraphNode root_node
|
||||
NodePath animation_player
|
||||
|
||||
void _notification(int p_what)
|
||||
static void _bind_methods()
|
||||
}
|
||||
|
||||
class AnimationGraphNode {
|
||||
NodeTimeInfo time_info
|
||||
SyncTrack sync_track
|
||||
|
||||
virtual void initialize(GraphEvaluationContext &context)
|
||||
virtual void activate_inputs(GraphEvaluationContext &context)
|
||||
virtual void calculate_sync_track(GraphEvaluationContext &context)
|
||||
virtual void update_time(double p_delta)
|
||||
virtual void evaluate(GraphEvaluationContext &context, AnimationData &data)
|
||||
|
||||
virtual int get_num_inputs() { return 0; }
|
||||
virtual AnimationGraphNode* get_input_node(int input_index) { return nullptr; }
|
||||
}
|
||||
|
||||
class AnimationGraphBlendTree {
|
||||
// nodes are ordered by evaluation
|
||||
Array<AnimationGraphNode> nodes
|
||||
Array<Connection> connections
|
||||
|
||||
void initialize_tree()
|
||||
void activate_nodes()
|
||||
void calculate_sync_tracks()
|
||||
void propagate_time()
|
||||
void evaluate_nodes()
|
||||
}
|
||||
|
||||
class AnimationGraphStateMachine {
|
||||
Array<AnimationGraphNode> states
|
||||
Array<Transition> transitions
|
||||
|
||||
AnimationGraphNode *current_state
|
||||
AnimationGraphNode *previous_state
|
||||
|
||||
Transition* active_transition
|
||||
double transition_time
|
||||
|
||||
void initialize_states()
|
||||
void activate_transitions()
|
||||
void calculate_sync_tracks()
|
||||
void calculate_state_times()
|
||||
void evaluate_nodes_and_output()
|
||||
}
|
||||
|
||||
class AnimationGraphBlend2Node {
|
||||
AnimationGraphNode input_0
|
||||
AnimationGraphNode input_1
|
||||
double blend_weight
|
||||
}
|
||||
|
||||
class AnimationGraph2BoneIK {
|
||||
AnimationGraphNode input
|
||||
double blend_weight
|
||||
|
||||
Vector3 pole_target
|
||||
StringName bone_name
|
||||
}
|
||||
|
||||
Node <|-- AnimationGraph
|
||||
AnimationGraph *-- AnimationGraphNode
|
||||
|
||||
AnimationGraphNode <|-- AnimationGraphBlendTree
|
||||
AnimationGraphNode <|-- AnimationGraphStateMachine
|
||||
AnimationGraphNode <|-- AnimationGraphBlend2Node
|
||||
AnimationGraphNode <|-- AnimationGraph2BoneIK
|
||||
|
||||
@enduml
|
||||
@ -79,7 +79,7 @@ struct AnimationData {
|
||||
AnimationData() = default;
|
||||
~AnimationData() {
|
||||
_clear_values();
|
||||
};
|
||||
}
|
||||
|
||||
void set_value(Animation::TypeHash thash, TrackValue *value) {
|
||||
if (!track_values.has(thash)) {
|
||||
@ -101,7 +101,6 @@ protected:
|
||||
memdelete(K.value);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
struct GraphEvaluationContext {
|
||||
@ -110,7 +109,13 @@ struct GraphEvaluationContext {
|
||||
Skeleton3D *skeleton_3d = nullptr;
|
||||
};
|
||||
|
||||
struct SyncTrack {
|
||||
|
||||
};
|
||||
|
||||
class SyncedAnimationNode {
|
||||
friend class SyncedAnimationGraph;
|
||||
|
||||
public:
|
||||
struct NodeTimeInfo {
|
||||
double length = 0.0;
|
||||
@ -118,12 +123,13 @@ public:
|
||||
double delta = 0.0;
|
||||
|
||||
Animation::LoopMode loop_mode = Animation::LOOP_NONE;
|
||||
SyncTrack sync_track;
|
||||
};
|
||||
NodeTimeInfo node_time_info;
|
||||
|
||||
virtual ~SyncedAnimationNode() = default;
|
||||
virtual void initialize(GraphEvaluationContext &context) {}
|
||||
virtual void activate_inputs(Vector<StringName> input_names) {}
|
||||
virtual void activate_inputs(GraphEvaluationContext &context, Vector<StringName> input_names) {}
|
||||
virtual void calculate_sync_track() {}
|
||||
virtual void update_time(double p_delta) {
|
||||
node_time_info.position += p_delta;
|
||||
@ -147,7 +153,15 @@ public:
|
||||
}
|
||||
}
|
||||
}
|
||||
virtual void evaluate(GraphEvaluationContext &context, AnimationData &output) {}
|
||||
virtual void evaluate(GraphEvaluationContext &context) {}
|
||||
|
||||
bool is_active() const { return active; }
|
||||
bool set_input_node(const StringName &socket_name, SyncedAnimationNode *node);
|
||||
void get_input_names(Array<StringName> &inputs);
|
||||
|
||||
private:
|
||||
AnimationData *output = nullptr;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
class AnimationSamplerNode : public SyncedAnimationNode {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user