Compare commits
4 Commits
1384d4a156
...
56fde580c3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56fde580c3 | ||
|
|
f4eea6d2d4 | ||
|
|
e09995c3fa | ||
|
|
ea2cb6b8e8 |
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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) {
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user