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
|
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.
|
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
|
A solution to this problem is presented by Bobby Anguelov at https://www.youtube.com/watch?v=Jkv0pbp0ckQ&t=7998s.
|
||||||
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
|
In short: animations are using a "SyncTrack" to annotate the periods (or phases) of an animation (e.g. `left foot down`,
|
||||||
plausible poses one has to blend poses from matching fractional positions within the phase spaces.
|
`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
|
#### Example
|
||||||
|
|
||||||
Given two animations `W` (a walking animation) and `R` (a running animation) annotated with SyncTracks on phases
|
Given two animations:
|
||||||
`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.
|
|
||||||
|
|
||||||
Both animations do not need to have matching durations and neither do each phases
|
* `Walk` with SyncTrack
|
||||||
have to be of the same duration. However, both have to have the same phases (number and order).
|
* `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
|
## Blend Trees
|
||||||
|
|
||||||
@ -80,23 +103,12 @@ Some nodes have special names in the Blend Tree:
|
|||||||
|
|
||||||
## Blend Tree Evaluation Process
|
## 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)
|
### 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)`
|
* all Blend Tree nodes only operate on properties they own and any other data (e.g. inputs and outputs) are specified
|
||||||
function of the node.
|
via arguments to `SyncedAnimationNode::evaluate(context, inputs, output)` function of the node.
|
||||||
*
|
|
||||||
|
|
||||||
Advantages:
|
Advantages:
|
||||||
|
|
||||||
@ -106,10 +118,17 @@ Advantages:
|
|||||||
|
|
||||||
Disadvantages:
|
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++
|
```c++
|
||||||
// BlendTree.h
|
// BlendTree.h
|
||||||
|
|||||||
@ -168,15 +168,15 @@ void SyncedAnimationGraph::_process_graph(double p_delta, bool p_update_only) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
graph_root_node->activate_inputs();
|
graph_root_node->activate_inputs(Vector<Ref<SyncedAnimationNode>>());
|
||||||
graph_root_node->calculate_sync_track();
|
graph_root_node->calculate_sync_track(Vector<Ref<SyncedAnimationNode>>());
|
||||||
graph_root_node->update_time(p_delta);
|
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);
|
_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) {
|
for (const KeyValue<Animation::TypeHash, AnimationData::TrackValue *> &K : output_data.track_values) {
|
||||||
const AnimationData::TrackValue *track_value = K.value;
|
const AnimationData::TrackValue *track_value = K.value;
|
||||||
switch (track_value->type) {
|
switch (track_value->type) {
|
||||||
|
|||||||
@ -32,7 +32,7 @@ protected:
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
void _process_graph(double p_delta, bool p_update_only = false);
|
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);
|
void set_active(bool p_active);
|
||||||
bool is_active() const;
|
bool is_active() const;
|
||||||
@ -55,6 +55,10 @@ public:
|
|||||||
void set_callback_mode_discrete(AnimationMixer::AnimationCallbackModeDiscrete p_mode);
|
void set_callback_mode_discrete(AnimationMixer::AnimationCallbackModeDiscrete p_mode);
|
||||||
AnimationMixer::AnimationCallbackModeDiscrete get_callback_mode_discrete() const;
|
AnimationMixer::AnimationCallbackModeDiscrete get_callback_mode_discrete() const;
|
||||||
|
|
||||||
|
GraphEvaluationContext &get_context() {
|
||||||
|
return graph_context;
|
||||||
|
}
|
||||||
|
|
||||||
SyncedAnimationGraph();
|
SyncedAnimationGraph();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@ -4,15 +4,7 @@
|
|||||||
|
|
||||||
#include "synced_animation_node.h"
|
#include "synced_animation_node.h"
|
||||||
|
|
||||||
void AnimationSamplerNode::initialize(GraphEvaluationContext &context) {
|
void AnimationData::sample_from_animation(const Ref<Animation> &animation, const Skeleton3D *skeleton_3d, double p_time) {
|
||||||
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);
|
|
||||||
|
|
||||||
const Vector<Animation::Track *> tracks = animation->get_tracks();
|
const Vector<Animation::Track *> tracks = animation->get_tracks();
|
||||||
Animation::Track *const *tracks_ptr = tracks.ptr();
|
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++) {
|
for (int i = 0; i < count; i++) {
|
||||||
AnimationData::TrackValue *track_value = nullptr;
|
AnimationData::TrackValue *track_value = nullptr;
|
||||||
const Animation::Track *animation_track = tracks_ptr[i];
|
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) {
|
if (!animation_track->enabled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -31,11 +23,11 @@ void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const Vecto
|
|||||||
AnimationData::PositionTrackValue *position_track_value = memnew(AnimationData::PositionTrackValue);
|
AnimationData::PositionTrackValue *position_track_value = memnew(AnimationData::PositionTrackValue);
|
||||||
|
|
||||||
if (track_node_path.get_subname_count() == 1) {
|
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) {
|
if (bone_idx != -1) {
|
||||||
position_track_value->bone_idx = bone_idx;
|
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 {
|
} else {
|
||||||
// TODO
|
// TODO
|
||||||
assert(false && !"Not yet implemented");
|
assert(false && !"Not yet implemented");
|
||||||
@ -48,11 +40,11 @@ void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const Vecto
|
|||||||
AnimationData::RotationTrackValue *rotation_track_value = memnew(AnimationData::RotationTrackValue);
|
AnimationData::RotationTrackValue *rotation_track_value = memnew(AnimationData::RotationTrackValue);
|
||||||
|
|
||||||
if (track_node_path.get_subname_count() == 1) {
|
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) {
|
if (bone_idx != -1) {
|
||||||
rotation_track_value->bone_idx = bone_idx;
|
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 {
|
} else {
|
||||||
// TODO
|
// TODO
|
||||||
assert(false && !"Not yet implemented");
|
assert(false && !"Not yet implemented");
|
||||||
@ -69,6 +61,22 @@ void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const Vecto
|
|||||||
}
|
}
|
||||||
|
|
||||||
track_value->track = tracks_ptr[i];
|
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>
|
#include <cassert>
|
||||||
|
|
||||||
struct GraphEvaluationContext {
|
/**
|
||||||
AnimationPlayer *animation_player = nullptr;
|
* @class AnimationData
|
||||||
Skeleton3D *skeleton_3d = nullptr;
|
* 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 {
|
struct AnimationData {
|
||||||
enum TrackType : uint8_t {
|
enum TrackType : uint8_t {
|
||||||
TYPE_VALUE, // Set a value in a property, can be interpolated.
|
TYPE_VALUE, // Set a value in a property, can be interpolated.
|
||||||
@ -28,18 +29,81 @@ struct AnimationData {
|
|||||||
struct TrackValue {
|
struct TrackValue {
|
||||||
Animation::Track *track = nullptr;
|
Animation::Track *track = nullptr;
|
||||||
TrackType type = TYPE_ANIMATION;
|
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 {
|
struct PositionTrackValue : public TrackValue {
|
||||||
int bone_idx = -1;
|
int bone_idx = -1;
|
||||||
Vector3 position = Vector3(0, 0, 0);
|
Vector3 position = Vector3(0, 0, 0);
|
||||||
PositionTrackValue() { type = TYPE_POSITION_3D; }
|
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 {
|
struct RotationTrackValue : public TrackValue {
|
||||||
int bone_idx = -1;
|
int bone_idx = -1;
|
||||||
Quaternion rotation = Quaternion(0, 0, 0, 1);
|
Quaternion rotation = Quaternion(0, 0, 0, 1);
|
||||||
RotationTrackValue() { type = TYPE_ROTATION_3D; }
|
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 {
|
struct ScaleTrackValue : public TrackValue {
|
||||||
@ -52,8 +116,26 @@ struct AnimationData {
|
|||||||
~AnimationData() {
|
~AnimationData() {
|
||||||
_clear_values();
|
_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)) {
|
if (!track_values.has(thash)) {
|
||||||
track_values.insert(thash, value);
|
track_values.insert(thash, value);
|
||||||
} else {
|
} else {
|
||||||
@ -65,6 +147,39 @@ struct AnimationData {
|
|||||||
_clear_values();
|
_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
|
AHashMap<Animation::TypeHash, TrackValue *, HashHasher> track_values; // Animation::Track to TrackValue
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@ -78,6 +193,15 @@ protected:
|
|||||||
struct SyncTrack {
|
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 {
|
class SyncedAnimationNode : public Resource {
|
||||||
GDCLASS(SyncedAnimationNode, Resource);
|
GDCLASS(SyncedAnimationNode, Resource);
|
||||||
|
|
||||||
@ -90,26 +214,22 @@ public:
|
|||||||
double sync_position = 0.0;
|
double sync_position = 0.0;
|
||||||
double delta = 0.0;
|
double delta = 0.0;
|
||||||
double sync_delta = 0.0;
|
double sync_delta = 0.0;
|
||||||
|
bool is_synced = false;
|
||||||
|
|
||||||
Animation::LoopMode loop_mode = Animation::LOOP_NONE;
|
Animation::LoopMode loop_mode = Animation::LOOP_NONE;
|
||||||
SyncTrack sync_track;
|
SyncTrack sync_track;
|
||||||
};
|
};
|
||||||
NodeTimeInfo node_time_info;
|
NodeTimeInfo node_time_info;
|
||||||
|
bool active = false;
|
||||||
struct InputPort {
|
|
||||||
StringName name;
|
|
||||||
SyncedAnimationNode *node;
|
|
||||||
};
|
|
||||||
|
|
||||||
Vector<InputPort> input_port;
|
|
||||||
|
|
||||||
StringName name;
|
StringName name;
|
||||||
|
|
||||||
virtual ~SyncedAnimationNode() = default;
|
virtual ~SyncedAnimationNode() override = default;
|
||||||
virtual void initialize(GraphEvaluationContext &context) {}
|
virtual void initialize(GraphEvaluationContext &context) {}
|
||||||
|
|
||||||
virtual void activate_inputs(Vector<Ref<SyncedAnimationNode>> input_nodes) {
|
virtual void activate_inputs(Vector<Ref<SyncedAnimationNode>> input_nodes) {
|
||||||
// By default, all inputs nodes are activated.
|
// By default, all inputs nodes are activated.
|
||||||
for (Ref<SyncedAnimationNode> node: input_nodes) {
|
for (const Ref<SyncedAnimationNode> &node : input_nodes) {
|
||||||
node->active = true;
|
node->active = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,7 +269,6 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool active = false;
|
|
||||||
bool set_input_node(const StringName &socket_name, SyncedAnimationNode *node);
|
bool set_input_node(const StringName &socket_name, SyncedAnimationNode *node);
|
||||||
virtual void get_input_names(Vector<StringName> &inputs) const {}
|
virtual void get_input_names(Vector<StringName> &inputs) const {}
|
||||||
|
|
||||||
@ -187,10 +306,14 @@ public:
|
|||||||
|
|
||||||
class AnimationBlend2Node : public SyncedAnimationNode {
|
class AnimationBlend2Node : public SyncedAnimationNode {
|
||||||
public:
|
public:
|
||||||
|
float blend_weight = 0.0f;
|
||||||
|
|
||||||
void get_input_names(Vector<StringName> &inputs) const override {
|
void get_input_names(Vector<StringName> &inputs) const override {
|
||||||
inputs.push_back("Input0");
|
inputs.push_back("Input0");
|
||||||
inputs.push_back("Input1");
|
inputs.push_back("Input1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void evaluate(GraphEvaluationContext &context, const Vector<AnimationData *> &inputs, AnimationData &output) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct BlendTreeConnection {
|
struct BlendTreeConnection {
|
||||||
@ -232,11 +355,7 @@ struct BlendTreeBuilder {
|
|||||||
print_line(result);
|
print_line(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
void apply_node_mapping(LocalVector<int> node_index_mapping) {
|
void apply_node_mapping(const LocalVector<int> &node_index_mapping) {
|
||||||
if (parent_node_index != -1) {
|
|
||||||
parent_node_index = node_index_mapping[parent_node_index];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map connected node indices
|
// Map connected node indices
|
||||||
for (unsigned int j = 0; j < connected_child_node_index_at_port.size(); j++) {
|
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];
|
int connected_node_index = connected_child_node_index_at_port[j];
|
||||||
@ -246,7 +365,7 @@ struct BlendTreeBuilder {
|
|||||||
// Map connected subtrees
|
// Map connected subtrees
|
||||||
HashSet<int> old_indices = input_subtree_node_indices;
|
HashSet<int> old_indices = input_subtree_node_indices;
|
||||||
input_subtree_node_indices.clear();
|
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));
|
input_subtree_node_indices.insert(node_index_mapping.find(old_index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -284,10 +403,6 @@ struct BlendTreeBuilder {
|
|||||||
|
|
||||||
void sort_nodes_and_references() {
|
void sort_nodes_and_references() {
|
||||||
LocalVector<int> sorted_node_indices = get_sorted_node_indices();
|
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<Ref<SyncedAnimationNode>> sorted_nodes;
|
||||||
Vector<NodeConnectionInfo> old_node_connection_info = node_connection_info;
|
Vector<NodeConnectionInfo> old_node_connection_info = node_connection_info;
|
||||||
@ -298,7 +413,10 @@ struct BlendTreeBuilder {
|
|||||||
}
|
}
|
||||||
nodes = sorted_nodes;
|
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);
|
connection_info.apply_node_mapping(sorted_node_indices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -314,7 +432,9 @@ struct BlendTreeBuilder {
|
|||||||
|
|
||||||
void sort_nodes_recursive(int node_index, LocalVector<int> &result) {
|
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) {
|
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);
|
result.push_back(node_index);
|
||||||
}
|
}
|
||||||
@ -397,16 +517,11 @@ struct BlendTreeBuilder {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class SyncedBlendTree : public SyncedAnimationNode {
|
class SyncedBlendTree : public SyncedAnimationNode {
|
||||||
Vector<Ref<SyncedAnimationNode>> tree_nodes;
|
|
||||||
Vector<Vector<int>> tree_node_subgraph;
|
|
||||||
|
|
||||||
Vector<BlendTreeConnection> tree_connections;
|
|
||||||
|
|
||||||
Vector<Ref<SyncedAnimationNode>> nodes;
|
Vector<Ref<SyncedAnimationNode>> nodes;
|
||||||
|
|
||||||
struct NodeRuntimeData {
|
struct NodeRuntimeData {
|
||||||
Vector<Ref<SyncedAnimationNode>> input_nodes;
|
Vector<Ref<SyncedAnimationNode>> input_nodes;
|
||||||
Vector<AnimationData*> input_data;
|
Vector<AnimationData *> input_data;
|
||||||
AnimationData *output_data = nullptr;
|
AnimationData *output_data = nullptr;
|
||||||
};
|
};
|
||||||
LocalVector<NodeRuntimeData> _node_runtime_data;
|
LocalVector<NodeRuntimeData> _node_runtime_data;
|
||||||
@ -422,7 +537,7 @@ class SyncedBlendTree : public SyncedAnimationNode {
|
|||||||
|
|
||||||
// Add nodes and allocate runtime data
|
// Add nodes and allocate runtime data
|
||||||
for (int i = 0; i < tree_builder.nodes.size(); i++) {
|
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);
|
nodes.push_back(node);
|
||||||
|
|
||||||
NodeRuntimeData node_runtime_data;
|
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)
|
// Populate runtime data (only now is this.nodes populated to retrieve the nodes)
|
||||||
for (int i = 0; i < nodes.size(); i++) {
|
for (int i = 0; i < nodes.size(); i++) {
|
||||||
Ref<SyncedAnimationNode> node = nodes[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++) {
|
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:
|
public:
|
||||||
SyncedBlendTree() {
|
Ref<SyncedAnimationNode> get_output_node() const {
|
||||||
Ref<OutputNode> output_node;
|
return tree_builder.nodes[0];
|
||||||
output_node.instantiate();
|
|
||||||
output_node->name = "Output";
|
|
||||||
nodes.push_back(output_node);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ref<SyncedAnimationNode> get_output_node() {
|
int get_node_index(const Ref<SyncedAnimationNode> &node) const {
|
||||||
return nodes[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
int get_node_index(const Ref<SyncedAnimationNode> node) {
|
|
||||||
for (int i = 0; i < nodes.size(); i++) {
|
for (int i = 0; i < nodes.size(); i++) {
|
||||||
if (nodes[i] == node) {
|
if (nodes[i] == node) {
|
||||||
return i;
|
return i;
|
||||||
@ -489,6 +598,8 @@ public:
|
|||||||
|
|
||||||
// overrides from SyncedAnimationNode
|
// overrides from SyncedAnimationNode
|
||||||
void initialize(GraphEvaluationContext &context) override {
|
void initialize(GraphEvaluationContext &context) override {
|
||||||
|
setup_tree();
|
||||||
|
|
||||||
for (Ref<SyncedAnimationNode> node : nodes) {
|
for (Ref<SyncedAnimationNode> node : nodes) {
|
||||||
node->initialize(context);
|
node->initialize(context);
|
||||||
}
|
}
|
||||||
@ -503,9 +614,7 @@ public:
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
NodeRuntimeData& node_runtime_data = _node_runtime_data[i];
|
const NodeRuntimeData &node_runtime_data = _node_runtime_data[i];
|
||||||
node_runtime_data.output_data = memnew(AnimationData);
|
|
||||||
|
|
||||||
node->activate_inputs(node_runtime_data.input_nodes);
|
node->activate_inputs(node_runtime_data.input_nodes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -518,28 +627,53 @@ public:
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
NodeRuntimeData& node_runtime_data = _node_runtime_data[i];
|
const NodeRuntimeData &node_runtime_data = _node_runtime_data[i];
|
||||||
node_runtime_data.output_data = memnew(AnimationData);
|
|
||||||
|
|
||||||
node->calculate_sync_track(node_runtime_data.input_nodes);
|
node->calculate_sync_track(node_runtime_data.input_nodes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void update_time(double p_delta) override {
|
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];
|
Ref<SyncedAnimationNode> node = nodes[i];
|
||||||
|
|
||||||
if (!node->active) {
|
if (!node->active) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
NodeRuntimeData& node_runtime_data = _node_runtime_data[i];
|
Ref<SyncedAnimationNode> node_parent = nodes[tree_builder.node_connection_info[i].parent_node_index];
|
||||||
node_runtime_data.output_data = memnew(AnimationData);
|
|
||||||
|
|
||||||
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 {
|
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 "../synced_animation_graph.h"
|
||||||
#include "scene/main/window.h"
|
#include "scene/main/window.h"
|
||||||
#include "servers/rendering/rendering_server_default.h"
|
|
||||||
|
|
||||||
#include "tests/test_macros.h"
|
#include "tests/test_macros.h"
|
||||||
|
|
||||||
@ -55,7 +54,7 @@ struct SyncedAnimationGraphFixture {
|
|||||||
|
|
||||||
namespace TestSyncedAnimationGraph {
|
namespace TestSyncedAnimationGraph {
|
||||||
|
|
||||||
TEST_CASE("[SyncedAnimationGraph] TestBlendTreeConstruction") {
|
TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") {
|
||||||
BlendTreeBuilder tree_constructor;
|
BlendTreeBuilder tree_constructor;
|
||||||
|
|
||||||
Ref<AnimationSamplerNode> animation_sampler_node0;
|
Ref<AnimationSamplerNode> animation_sampler_node0;
|
||||||
@ -85,8 +84,8 @@ TEST_CASE("[SyncedAnimationGraph] TestBlendTreeConstruction") {
|
|||||||
|
|
||||||
// Tree
|
// Tree
|
||||||
// Sampler0 -\
|
// Sampler0 -\
|
||||||
// Sampler1 -+- Blend0 -\
|
// Sampler1 -+ Blend0 -\
|
||||||
// Sampler2 ------------+ Blend1 - Output
|
// Sampler2 -----------+ Blend1 - Output
|
||||||
|
|
||||||
CHECK(tree_constructor.add_connection(animation_sampler_node0, node_blend0, "Input0"));
|
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(4));
|
||||||
CHECK(tree_constructor.node_connection_info[0].input_subtree_node_indices.has(5));
|
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();
|
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.
|
// 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 (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);
|
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;
|
Ref<AnimationSamplerNode> animation_sampler_node;
|
||||||
animation_sampler_node.instantiate();
|
animation_sampler_node.instantiate();
|
||||||
animation_sampler_node->animation_name = "animation_library/TestAnimation";
|
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));
|
CHECK(hip_bone_position.z == doctest::Approx(0.03));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently disabled!
|
TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph][BlendTree] BlendTree with a AnimationSamplerNode connected to the output") {
|
||||||
TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] SimpleBlendTreeTest" * doctest::skip(true)) {
|
|
||||||
Ref<SyncedBlendTree> synced_blend_tree_node;
|
Ref<SyncedBlendTree> synced_blend_tree_node;
|
||||||
synced_blend_tree_node.instantiate();
|
synced_blend_tree_node.instantiate();
|
||||||
|
|
||||||
Ref<AnimationSamplerNode> animation_sampler_node;
|
Ref<AnimationSamplerNode> animation_sampler_node;
|
||||||
animation_sampler_node.instantiate();
|
animation_sampler_node.instantiate();
|
||||||
animation_sampler_node->animation_name = "animation_library/TestAnimation";
|
animation_sampler_node->animation_name = "animation_library/TestAnimation";
|
||||||
|
|
||||||
synced_blend_tree_node->add_node(animation_sampler_node);
|
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);
|
synced_animation_graph->set_graph_root_node(synced_blend_tree_node);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user