Compare commits

...

4 Commits

Author SHA1 Message Date
Martin Felis
56fde580c3 WIP: Blend2 node and blending of AnimationData. 2025-12-22 00:37:27 +01:00
Martin Felis
f4eea6d2d4 Minor cleanup. 2025-12-21 18:12:34 +01:00
Martin Felis
e09995c3fa Initial support of actual blend tree evaluation. 2025-12-19 10:53:19 +01:00
Martin Felis
ea2cb6b8e8 Extended documentation of animation syncing. 2025-12-19 10:52:41 +01:00
6 changed files with 306 additions and 130 deletions

View File

@ -28,20 +28,43 @@ scale). However, when blending a humanoid walk and a run animation this generall
must semantically match to produce sensible result. If input `A` has the left foot on the ground and `B` the right foot
the resulting pose is confusing at best.
Instead, by annotating animations using a "SyncTrack" the phase space (e.g. `left foot contact phase` in the time
interval [0.0s, 1.2s] and
`right foot contact phase` form from [1.2s, 2.4s] the additional information can be used for blending. To produce
plausible poses one has to blend poses from matching fractional positions within the phase spaces.
A solution to this problem is presented by Bobby Anguelov at https://www.youtube.com/watch?v=Jkv0pbp0ckQ&t=7998s.
In short: animations are using a "SyncTrack" to annotate the periods (or phases) of an animation (e.g. `left foot down`,
`right foot cross`, `right foot down`, `left foot cross`). Two animations that are blended must have the same order of
these periods, however each animation may have different durations for these phases. SyncTracks can then be blended and
used to determine for each animation the actual sampling time.
#### Example
Given two animations `W` (a walking animation) and `R` (a running animation) annotated with SyncTracks on phases
`LeftFoot` and `RightFoot` phases. Then blending a pose of `W` that is 64% through the `LeftFoot` phase with a pose of
`R`
that is 64% through its `LeftFoot` phase results in a plausible pose.
Given two animations:
Both animations do not need to have matching durations and neither do each phases
have to be of the same duration. However, both have to have the same phases (number and order).
* `Walk` with SyncTrack
* `left foot down` [0.0, 1.2], duration = `1.2`
* `right foot down` [1.2, 2.4], duration = `1.2`
* `ZombieWalk` with SyncTrack
* `left foot down` [0.0, 1.8], duration = `1.8`
* `right foot down` [1.8, 1.9], duration = `0.1`
Then blending these two animations, e.g. with a blend weight `w=0.2` we obtain the following SyncTrack:
* `left foot down` [0.0, 1.32] (`1.32 = (1-w) * 1.2 + w * 1.8`), duration = 1.32
* `right foot down` [1.32, 2.3] (`2.3 = (1-w) * 2.4 + w * 1.9`), duration = 0.98
A blend factor of `w=0.0` results in the `Walk` animation's SyncTrack, similarly `w=1.0` results in the `ZombieWalk`
SyncTrack.
Time progresses normally in the blended SyncTrack. However, the resulting time is then interpreted relative to the
phases of the SyncTrack. E.g. for the blended SyncTrack with `w=0.2`, a time at 1.5 would be in the `right foot down`
phase with a relative progression of `(1.5-1.32)/0.98 =~ 0.18 = 18%` through the `right foot down` phase.
When sampling the `Walk` or the `ZombieWalk` animation the relative phase progression is used to sample the inputs. For
each animation we sample at 18% of the `right foot down` phase. For the `Walk` animation we would sample at
`1.2 + 1.2*0.18 = 1.416 ~= 1.42` and for the `ZombieWalk` we sample at `1.8 + 0.1*0.18 = 1.818 ~= 1.82`.
What is crucial to note here is that for synchronized blending one first has to have the blended SyncTrack, progress
time there and then use the relative phase progression when sampling the actual animation that serve as input for the
blend operation.
## Blend Trees
@ -80,23 +103,12 @@ Some nodes have special names in the Blend Tree:
## Blend Tree Evaluation Process
### Description
Evaluation of the Blend Tree happens in multiple phases to ensure we have syncing dependent timing information available
before performing the actual evaluation. Essentially the Blend Tree has to call the following function on all nodes:
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
### Ownership of evaluation data (inputs and outputs)
Except for the output node of a Blend Tree the following properties hold:
Except for the output node of a Blend Tree the following properties hold:
* all Blend Tree nodes only operate on properties they own and any other data (e.g. inputs and outputs) are specified via arguments to `SyncedAnimationNode::evaluate(context, inputs, output)`
function of the node.
*
* all Blend Tree nodes only operate on properties they own and any other data (e.g. inputs and outputs) are specified
via arguments to `SyncedAnimationNode::evaluate(context, inputs, output)` function of the node.
Advantages:
@ -106,10 +118,17 @@ Advantages:
Disadvantages:
* Data has to be managed by the Blend Tree => additional bookkeeping
*
* Data has to be managed by the Blend Tree => additional bookkeeping there.
### Blend Tree Evaluation
### Evaluation
Evaluation of the Blend Tree happens in multiple phases to ensure we have syncing dependent timing information available
before performing the actual evaluation. Essentially the Blend Tree has to call the following function on all nodes:
1. ActivateInputs(): right to left (i.e. from the root node via depth first to the leaf nodes)
2. CalculateSyncTracks(): left to right (leaf nodes to root node)
3. UpdateTime(): right to left
4. Evaluate(): left to right
```c++
// BlendTree.h

View File

@ -168,15 +168,15 @@ void SyncedAnimationGraph::_process_graph(double p_delta, bool p_update_only) {
return;
}
graph_root_node->activate_inputs();
graph_root_node->calculate_sync_track();
graph_root_node->activate_inputs(Vector<Ref<SyncedAnimationNode>>());
graph_root_node->calculate_sync_track(Vector<Ref<SyncedAnimationNode>>());
graph_root_node->update_time(p_delta);
graph_root_node->evaluate(graph_context, Vector<AnimationData*>(), graph_output);
graph_root_node->evaluate(graph_context, Vector<AnimationData *>(), graph_output);
_apply_animation_data(graph_output);
}
void SyncedAnimationGraph::_apply_animation_data(const AnimationData& output_data) const {
void SyncedAnimationGraph::_apply_animation_data(const AnimationData &output_data) const {
for (const KeyValue<Animation::TypeHash, AnimationData::TrackValue *> &K : output_data.track_values) {
const AnimationData::TrackValue *track_value = K.value;
switch (track_value->type) {

View File

@ -32,7 +32,7 @@ protected:
public:
void _process_graph(double p_delta, bool p_update_only = false);
void _apply_animation_data(const AnimationData& output_data) const;
void _apply_animation_data(const AnimationData &output_data) const;
void set_active(bool p_active);
bool is_active() const;
@ -55,6 +55,10 @@ public:
void set_callback_mode_discrete(AnimationMixer::AnimationCallbackModeDiscrete p_mode);
AnimationMixer::AnimationCallbackModeDiscrete get_callback_mode_discrete() const;
GraphEvaluationContext &get_context() {
return graph_context;
}
SyncedAnimationGraph();
private:

View File

@ -4,15 +4,7 @@
#include "synced_animation_node.h"
void AnimationSamplerNode::initialize(GraphEvaluationContext &context) {
animation = context.animation_player->get_animation(animation_name);
node_time_info.length = animation->get_length();
node_time_info.loop_mode = Animation::LOOP_LINEAR;
}
void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const Vector<AnimationData*>& inputs, AnimationData &output) {
assert(inputs.size() == 0);
void AnimationData::sample_from_animation(const Ref<Animation> &animation, const Skeleton3D *skeleton_3d, double p_time) {
const Vector<Animation::Track *> tracks = animation->get_tracks();
Animation::Track *const *tracks_ptr = tracks.ptr();
@ -20,7 +12,7 @@ void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const Vecto
for (int i = 0; i < count; i++) {
AnimationData::TrackValue *track_value = nullptr;
const Animation::Track *animation_track = tracks_ptr[i];
const NodePath& track_node_path = animation_track->path;
const NodePath &track_node_path = animation_track->path;
if (!animation_track->enabled) {
continue;
}
@ -31,11 +23,11 @@ void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const Vecto
AnimationData::PositionTrackValue *position_track_value = memnew(AnimationData::PositionTrackValue);
if (track_node_path.get_subname_count() == 1) {
int bone_idx = context.skeleton_3d->find_bone(track_node_path.get_subname(0));
int bone_idx = skeleton_3d->find_bone(track_node_path.get_subname(0));
if (bone_idx != -1) {
position_track_value->bone_idx = bone_idx;
}
animation->try_position_track_interpolate(i, node_time_info.position, &position_track_value->position);
animation->try_position_track_interpolate(i, p_time, &position_track_value->position);
} else {
// TODO
assert(false && !"Not yet implemented");
@ -48,11 +40,11 @@ void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const Vecto
AnimationData::RotationTrackValue *rotation_track_value = memnew(AnimationData::RotationTrackValue);
if (track_node_path.get_subname_count() == 1) {
int bone_idx = context.skeleton_3d->find_bone(track_node_path.get_subname(0));
int bone_idx = skeleton_3d->find_bone(track_node_path.get_subname(0));
if (bone_idx != -1) {
rotation_track_value->bone_idx = bone_idx;
}
animation->try_rotation_track_interpolate(i, node_time_info.position, &rotation_track_value->rotation);
animation->try_rotation_track_interpolate(i, p_time, &rotation_track_value->rotation);
} else {
// TODO
assert(false && !"Not yet implemented");
@ -69,6 +61,22 @@ void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const Vecto
}
track_value->track = tracks_ptr[i];
output.set_value(animation_track->thash, track_value);
set_value(animation_track->thash, track_value);
}
}
void AnimationSamplerNode::initialize(GraphEvaluationContext &context) {
animation = context.animation_player->get_animation(animation_name);
node_time_info.length = animation->get_length();
node_time_info.loop_mode = Animation::LOOP_LINEAR;
}
void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const Vector<AnimationData *> &inputs, AnimationData &output) {
assert(inputs.size() == 0);
output.clear();
output.sample_from_animation(animation, context.skeleton_3d, node_time_info.position);
}
void AnimationBlend2Node::evaluate(GraphEvaluationContext &context, const Vector<AnimationData *> &inputs, AnimationData &output) {
}

View File

@ -7,11 +7,12 @@
#include <cassert>
struct GraphEvaluationContext {
AnimationPlayer *animation_player = nullptr;
Skeleton3D *skeleton_3d = nullptr;
};
/**
* @class AnimationData
* Represents data that is transported via animation connections in the SyncedAnimationGraph.
*
* Essentially, it is a hash map for all Animation::Track values that can are sampled from an Animation.
*/
struct AnimationData {
enum TrackType : uint8_t {
TYPE_VALUE, // Set a value in a property, can be interpolated.
@ -28,18 +29,81 @@ struct AnimationData {
struct TrackValue {
Animation::Track *track = nullptr;
TrackType type = TYPE_ANIMATION;
virtual ~TrackValue() = default;
virtual void blend(const TrackValue &to_value, const float lambda) {
print_error(vformat("Blending of TrackValue of type %d with TrackValue of type %d not yet implemented.", type, to_value.type));
}
virtual bool operator==(const TrackValue &other_value) const {
print_error(vformat("Comparing TrackValue of type %d with TrackValue of type %d not yet implemented.", type, other_value.type));
return false;
}
bool operator!=(const TrackValue &other_value) const {
return !(*this == other_value);
}
virtual TrackValue *clone() const {
print_error(vformat("Cannot clone TrackValue of type %d: not yet implemented.", type));
return nullptr;
}
};
struct PositionTrackValue : public TrackValue {
int bone_idx = -1;
Vector3 position = Vector3(0, 0, 0);
PositionTrackValue() { type = TYPE_POSITION_3D; }
void blend(const TrackValue &to_value, const float lambda) override {
const PositionTrackValue *to_value_casted = &static_cast<const PositionTrackValue &>(to_value);
assert(bone_idx == to_value_casted->bone_idx);
position = (1. - lambda) * position + lambda * to_value_casted->position;
}
bool operator==(const TrackValue &other_value) const override {
if (type != other_value.type) {
return false;
}
const PositionTrackValue *other_value_casted = &static_cast<const PositionTrackValue &>(other_value);
return bone_idx == other_value_casted->bone_idx && position == other_value_casted->position;
}
TrackValue *clone() const override {
PositionTrackValue *result = memnew(PositionTrackValue);
result->bone_idx = bone_idx;
result->position = position;
return result;
}
};
struct RotationTrackValue : public TrackValue {
int bone_idx = -1;
Quaternion rotation = Quaternion(0, 0, 0, 1);
RotationTrackValue() { type = TYPE_ROTATION_3D; }
void blend(const TrackValue &to_value, const float lambda) override {
const RotationTrackValue *to_value_casted = &static_cast<const RotationTrackValue &>(to_value);
assert(bone_idx == to_value_casted->bone_idx);
rotation = rotation.slerp(to_value_casted->rotation, lambda);
}
bool operator==(const TrackValue &other_value) const override {
if (type != other_value.type) {
return false;
}
const RotationTrackValue *other_value_casted = &static_cast<const RotationTrackValue &>(other_value);
return bone_idx == other_value_casted->bone_idx && rotation == other_value_casted->rotation;
}
TrackValue *clone() const override {
RotationTrackValue *result = memnew(RotationTrackValue);
result->bone_idx = bone_idx;
result->rotation = rotation;
return result;
}
};
struct ScaleTrackValue : public TrackValue {
@ -52,8 +116,26 @@ struct AnimationData {
~AnimationData() {
_clear_values();
}
AnimationData(const AnimationData &other) {
for (const KeyValue<Animation::TypeHash, TrackValue *> &K : other.track_values) {
track_values.insert(K.key, K.value->clone());
}
}
AnimationData(AnimationData &&other) noexcept :
track_values(std::exchange(other.track_values, AHashMap<Animation::TypeHash, TrackValue *, HashHasher>())) {
}
AnimationData &operator=(const AnimationData &other) {
AnimationData temp(other);
std::swap(track_values, temp.track_values);
return *this;
}
AnimationData &operator=(AnimationData &&other) noexcept {
std::swap(track_values, other.track_values);
return *this;
}
void set_value(Animation::TypeHash thash, TrackValue *value) {
void
set_value(const Animation::TypeHash& thash, TrackValue *value) {
if (!track_values.has(thash)) {
track_values.insert(thash, value);
} else {
@ -65,6 +147,39 @@ struct AnimationData {
_clear_values();
}
bool has_same_tracks(const AnimationData &other) const {
HashSet<Animation::TypeHash> valid_track_hashes;
for (const KeyValue<Animation::TypeHash, TrackValue *> &K : track_values) {
valid_track_hashes.insert(K.key);
}
for (const KeyValue<Animation::TypeHash, TrackValue *> &K : other.track_values) {
if (HashSet<Animation::TypeHash>::Iterator entry = valid_track_hashes.find(K.key)) {
valid_track_hashes.remove(entry);
} else {
return false;
}
}
return valid_track_hashes.size() == 0;
}
void blend(const AnimationData &to_data, const float lambda) {
if (!has_same_tracks(to_data)) {
print_error("Cannot blend AnimationData: tracks do not match.");
return;
}
for (const KeyValue<Animation::TypeHash, TrackValue *> &K : track_values) {
TrackValue *track_value = K.value;
TrackValue *other_track_value = to_data.track_values[K.key];
track_value->blend(*other_track_value, lambda);
}
}
void sample_from_animation(const Ref<Animation> &animation, const Skeleton3D *skeleton_3d, double p_time);
AHashMap<Animation::TypeHash, TrackValue *, HashHasher> track_values; // Animation::Track to TrackValue
protected:
@ -78,6 +193,15 @@ protected:
struct SyncTrack {
};
struct GraphEvaluationContext {
AnimationPlayer *animation_player = nullptr;
Skeleton3D *skeleton_3d = nullptr;
};
/**
* @class SyncedAnimationNode
* Base class for all nodes in an SyncedAnimationGraph including BlendTree nodes and StateMachine states.
*/
class SyncedAnimationNode : public Resource {
GDCLASS(SyncedAnimationNode, Resource);
@ -90,26 +214,22 @@ public:
double sync_position = 0.0;
double delta = 0.0;
double sync_delta = 0.0;
bool is_synced = false;
Animation::LoopMode loop_mode = Animation::LOOP_NONE;
SyncTrack sync_track;
};
NodeTimeInfo node_time_info;
struct InputPort {
StringName name;
SyncedAnimationNode *node;
};
Vector<InputPort> input_port;
bool active = false;
StringName name;
virtual ~SyncedAnimationNode() = default;
virtual ~SyncedAnimationNode() override = default;
virtual void initialize(GraphEvaluationContext &context) {}
virtual void activate_inputs(Vector<Ref<SyncedAnimationNode>> input_nodes) {
// By default, all inputs nodes are activated.
for (Ref<SyncedAnimationNode> node: input_nodes) {
for (const Ref<SyncedAnimationNode> &node : input_nodes) {
node->active = true;
}
}
@ -149,7 +269,6 @@ public:
}
}
bool active = false;
bool set_input_node(const StringName &socket_name, SyncedAnimationNode *node);
virtual void get_input_names(Vector<StringName> &inputs) const {}
@ -187,10 +306,14 @@ public:
class AnimationBlend2Node : public SyncedAnimationNode {
public:
float blend_weight = 0.0f;
void get_input_names(Vector<StringName> &inputs) const override {
inputs.push_back("Input0");
inputs.push_back("Input1");
}
void evaluate(GraphEvaluationContext &context, const Vector<AnimationData *> &inputs, AnimationData &output) override;
};
struct BlendTreeConnection {
@ -232,11 +355,7 @@ struct BlendTreeBuilder {
print_line(result);
}
void apply_node_mapping(LocalVector<int> node_index_mapping) {
if (parent_node_index != -1) {
parent_node_index = node_index_mapping[parent_node_index];
}
void apply_node_mapping(const LocalVector<int> &node_index_mapping) {
// 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];
@ -246,7 +365,7 @@ struct BlendTreeBuilder {
// Map connected subtrees
HashSet<int> old_indices = input_subtree_node_indices;
input_subtree_node_indices.clear();
for (int old_index: old_indices) {
for (int old_index : old_indices) {
input_subtree_node_indices.insert(node_index_mapping.find(old_index));
}
}
@ -284,10 +403,6 @@ struct BlendTreeBuilder {
void sort_nodes_and_references() {
LocalVector<int> sorted_node_indices = get_sorted_node_indices();
LocalVector<int> index_mapping;
for (int i : sorted_node_indices) {
index_mapping.push_back(sorted_node_indices.find(i));
}
Vector<Ref<SyncedAnimationNode>> sorted_nodes;
Vector<NodeConnectionInfo> old_node_connection_info = node_connection_info;
@ -298,7 +413,10 @@ struct BlendTreeBuilder {
}
nodes = sorted_nodes;
for (NodeConnectionInfo& connection_info: node_connection_info) {
for (NodeConnectionInfo &connection_info : node_connection_info) {
if (connection_info.parent_node_index != -1) {
connection_info.parent_node_index = sorted_node_indices[connection_info.parent_node_index];
}
connection_info.apply_node_mapping(sorted_node_indices);
}
}
@ -314,7 +432,9 @@ struct BlendTreeBuilder {
void sort_nodes_recursive(int node_index, LocalVector<int> &result) {
for (int input_node_index : node_connection_info[node_index].connected_child_node_index_at_port) {
sort_nodes_recursive(input_node_index, result);
if (input_node_index >= 0) {
sort_nodes_recursive(input_node_index, result);
}
}
result.push_back(node_index);
}
@ -397,16 +517,11 @@ struct BlendTreeBuilder {
};
class SyncedBlendTree : public SyncedAnimationNode {
Vector<Ref<SyncedAnimationNode>> tree_nodes;
Vector<Vector<int>> tree_node_subgraph;
Vector<BlendTreeConnection> tree_connections;
Vector<Ref<SyncedAnimationNode>> nodes;
struct NodeRuntimeData {
Vector<Ref<SyncedAnimationNode>> input_nodes;
Vector<AnimationData*> input_data;
Vector<AnimationData *> input_data;
AnimationData *output_data = nullptr;
};
LocalVector<NodeRuntimeData> _node_runtime_data;
@ -422,7 +537,7 @@ class SyncedBlendTree : public SyncedAnimationNode {
// Add nodes and allocate runtime data
for (int i = 0; i < tree_builder.nodes.size(); i++) {
Ref<SyncedAnimationNode> node = tree_builder.nodes[i];
const Ref<SyncedAnimationNode> node = tree_builder.nodes[i];
nodes.push_back(node);
NodeRuntimeData node_runtime_data;
@ -437,10 +552,11 @@ class SyncedBlendTree : public SyncedAnimationNode {
// Populate runtime data (only now is this.nodes populated to retrieve the nodes)
for (int i = 0; i < nodes.size(); i++) {
Ref<SyncedAnimationNode> node = nodes[i];
NodeRuntimeData& node_runtime_data = _node_runtime_data[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]]);
const int connected_node_index = tree_builder.node_connection_info[i].connected_child_node_index_at_port[port_index];
node_runtime_data.input_nodes.push_back(nodes[connected_node_index]);
}
}
@ -448,18 +564,11 @@ class SyncedBlendTree : public SyncedAnimationNode {
}
public:
SyncedBlendTree() {
Ref<OutputNode> output_node;
output_node.instantiate();
output_node->name = "Output";
nodes.push_back(output_node);
Ref<SyncedAnimationNode> get_output_node() const {
return tree_builder.nodes[0];
}
Ref<SyncedAnimationNode> get_output_node() {
return nodes[0];
}
int get_node_index(const Ref<SyncedAnimationNode> node) {
int get_node_index(const Ref<SyncedAnimationNode> &node) const {
for (int i = 0; i < nodes.size(); i++) {
if (nodes[i] == node) {
return i;
@ -489,6 +598,8 @@ public:
// overrides from SyncedAnimationNode
void initialize(GraphEvaluationContext &context) override {
setup_tree();
for (Ref<SyncedAnimationNode> node : nodes) {
node->initialize(context);
}
@ -503,9 +614,7 @@ public:
continue;
}
NodeRuntimeData& node_runtime_data = _node_runtime_data[i];
node_runtime_data.output_data = memnew(AnimationData);
const NodeRuntimeData &node_runtime_data = _node_runtime_data[i];
node->activate_inputs(node_runtime_data.input_nodes);
}
}
@ -518,28 +627,53 @@ public:
continue;
}
NodeRuntimeData& node_runtime_data = _node_runtime_data[i];
node_runtime_data.output_data = memnew(AnimationData);
const NodeRuntimeData &node_runtime_data = _node_runtime_data[i];
node->calculate_sync_track(node_runtime_data.input_nodes);
}
}
void update_time(double p_delta) override {
for (int i = 0; i < nodes.size(); i++) {
nodes[0]->node_time_info.delta = p_delta;
nodes[0]->node_time_info.position += p_delta;
for (int i = 1; i < nodes.size(); i++) {
Ref<SyncedAnimationNode> node = nodes[i];
if (!node->active) {
continue;
}
NodeRuntimeData& node_runtime_data = _node_runtime_data[i];
node_runtime_data.output_data = memnew(AnimationData);
Ref<SyncedAnimationNode> node_parent = nodes[tree_builder.node_connection_info[i].parent_node_index];
node->update_time(node_runtime_data.input_nodes);
if (node->node_time_info.is_synced) {
node->update_time(node_parent->node_time_info.position);
} else {
node->update_time(node_parent->node_time_info.delta);
}
}
}
void evaluate(GraphEvaluationContext &context, const Vector<AnimationData *> &input_datas, AnimationData &output_data) override {
for (int i = nodes.size() - 1; i > 0; i--) {
const Ref<SyncedAnimationNode> &node = nodes[i];
if (!node->active) {
continue;
}
NodeRuntimeData &node_runtime_data = _node_runtime_data[i];
if (i == 1) {
node_runtime_data.output_data = &output_data;
} else {
node_runtime_data.output_data = memnew(AnimationData);
}
node->evaluate(context, node_runtime_data.input_data, *node_runtime_data.output_data);
for (int child_index : tree_builder.node_connection_info[i].connected_child_node_index_at_port) {
memfree(_node_runtime_data[child_index].output_data);
}
}
}
};

View File

@ -2,7 +2,6 @@
#include "../synced_animation_graph.h"
#include "scene/main/window.h"
#include "servers/rendering/rendering_server_default.h"
#include "tests/test_macros.h"
@ -55,7 +54,7 @@ struct SyncedAnimationGraphFixture {
namespace TestSyncedAnimationGraph {
TEST_CASE("[SyncedAnimationGraph] TestBlendTreeConstruction") {
TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") {
BlendTreeBuilder tree_constructor;
Ref<AnimationSamplerNode> animation_sampler_node0;
@ -85,8 +84,8 @@ TEST_CASE("[SyncedAnimationGraph] TestBlendTreeConstruction") {
// Tree
// Sampler0 -\
// Sampler1 -+- Blend0 -\
// Sampler2 ------------+ Blend1 - Output
// Sampler1 -+ Blend0 -\
// Sampler2 -----------+ Blend1 - Output
CHECK(tree_constructor.add_connection(animation_sampler_node0, node_blend0, "Input0"));
@ -124,36 +123,45 @@ TEST_CASE("[SyncedAnimationGraph] TestBlendTreeConstruction") {
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<int> 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) {
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)) {
TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] Test AnimationData blending") {
AnimationData data_t0;
data_t0.sample_from_animation(test_animation, skeleton_node, 0.0);
AnimationData data_t1;
data_t1.sample_from_animation(test_animation, skeleton_node, 1.0);
AnimationData data_t0_5;
data_t0_5.sample_from_animation(test_animation, skeleton_node, 0.5);
AnimationData data_blended = data_t0;
data_blended.blend(data_t1, 0.5);
REQUIRE(data_blended.has_same_tracks(data_t0_5));
for (const KeyValue<Animation::TypeHash, AnimationData::TrackValue *> &K : data_blended.track_values) {
CHECK(K.value->operator==(*data_t0_5.track_values.find(K.key)->value));
}
// And also check that values do not match
data_blended = data_t0;
data_blended.blend(data_t1, 0.3);
REQUIRE(data_blended.has_same_tracks(data_t0_5));
for (const KeyValue<Animation::TypeHash, AnimationData::TrackValue *> &K : data_blended.track_values) {
CHECK(K.value->operator!=(*data_t0_5.track_values.find(K.key)->value));
}
}
TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] SyncedAnimationGraph with an AnimationSampler as root node") {
Ref<AnimationSamplerNode> animation_sampler_node;
animation_sampler_node.instantiate();
animation_sampler_node->animation_name = "animation_library/TestAnimation";
@ -175,15 +183,18 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph
CHECK(hip_bone_position.z == doctest::Approx(0.03));
}
// Currently disabled!
TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] SimpleBlendTreeTest" * doctest::skip(true)) {
TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph][BlendTree] BlendTree with a AnimationSamplerNode connected to the output") {
Ref<SyncedBlendTree> synced_blend_tree_node;
synced_blend_tree_node.instantiate();
Ref<AnimationSamplerNode> animation_sampler_node;
animation_sampler_node.instantiate();
animation_sampler_node->animation_name = "animation_library/TestAnimation";
synced_blend_tree_node->add_node(animation_sampler_node);
REQUIRE(synced_blend_tree_node->add_connection(animation_sampler_node, synced_blend_tree_node->get_output_node(), "Input"));
synced_blend_tree_node->initialize(synced_animation_graph->get_context());
synced_animation_graph->set_graph_root_node(synced_blend_tree_node);