WIP: refactored test to use a fixture, started working on Blend Tree evaluation.
This commit is contained in:
parent
33fae3458b
commit
5880dde6ec
@ -56,27 +56,14 @@ invalid.
|
||||
|
||||
### Example:
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
|
||||
left to right direction
|
||||
|
||||
abstract Output {}
|
||||
abstract Blend2 {
|
||||
bool is_synced
|
||||
|
||||
float weight()
|
||||
}
|
||||
abstract AnimationA {}
|
||||
abstract TimeScale {}
|
||||
abstract AnimationB {}
|
||||
|
||||
AnimationA --> Blend2
|
||||
AnimationB --> TimeScale
|
||||
TimeScale --> Blend2
|
||||
Blend2 --> Output
|
||||
|
||||
@enduml
|
||||
```mermaid
|
||||
flowchart LR
|
||||
AnimationB --> TimeScale("TimeScale
|
||||
----
|
||||
*scale*")
|
||||
AnimationA --> Blend2
|
||||
TimeScale --> Blend2
|
||||
Blend2 --> Output
|
||||
```
|
||||
|
||||
A Blend Tree always has a designated output node where the time delta is specified as an input and after the Blend Tree
|
||||
@ -110,6 +97,18 @@ that is only needed during evaluation to the Blend Tree.
|
||||
### Blend Tree Evaluation
|
||||
|
||||
```c++
|
||||
// BlendTree.h
|
||||
class BlendTree: public SyncedAnimationNode {
|
||||
private:
|
||||
Vector<SyncedAnimationNode*> nodes;
|
||||
Vector<int> node_parent; // node_parent[i] is the index of the parent of node i.
|
||||
Vector<AnimationData*> node_output; // output for each node
|
||||
Vector<Vector<SyncedAnimationNode*>> node_input_nodes;
|
||||
Vector<Vector<AnimationData*>> node_input_data; // list of inputs for all nodes.
|
||||
|
||||
int get_index_for_node(const SyncedAnimationNode& node);
|
||||
};
|
||||
|
||||
// BlendTree.cpp
|
||||
void BlendTree::initialize_tree() {
|
||||
for (int i = 0; ci < num_connections; i++) {
|
||||
@ -119,9 +118,11 @@ void BlendTree::initialize_tree() {
|
||||
}
|
||||
|
||||
void BlendTree::activate_inputs() {
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
if (nodes[i].is_active()) {
|
||||
nodes[i].activate_inputs()
|
||||
nodes[0]->activate_inputs();
|
||||
|
||||
for (int i = 1; i < nodes.size(); i++) {
|
||||
if (nodes[i]->is_active()) {
|
||||
nodes[i]->activate_inputs(node_input_nodes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -146,15 +147,15 @@ void BlendTree::update_time() {
|
||||
}
|
||||
}
|
||||
|
||||
void BlendTree::evaluate(AnimationData& output) {
|
||||
void BlendTree::evaluate(GraphEvaluationContext &context, const Vector<AnimationData*>& inputs, AnimationData &output) {
|
||||
for (int i = nodes.size() - 1; i > 0; i--) {
|
||||
if (nodes[i]->is_active()) {
|
||||
nodes[i]->output = AnimationDataPool::allocate();
|
||||
nodes[i]->evaluate();
|
||||
node_output[i] = AnimationDataPool::allocate();
|
||||
nodes[i]->evaluate(context, node_inputs[i], node_output[i]);
|
||||
|
||||
// node[i] is done, so we can deallocate the output handles of all input nodes of node[i].
|
||||
for (AnimationGraphnNode& input_node: input_nodes[i]) {
|
||||
AnimationDataPool::deallocate(input_node.output);
|
||||
AnimationDataPool::deallocate(node_output[input_node.index]);
|
||||
}
|
||||
|
||||
nodes[i]->set_active(false);
|
||||
@ -188,7 +189,8 @@ void Blend2Node::update_time(SyncedAnimationNode::NodeTimeInfo time_info) {
|
||||
}
|
||||
}
|
||||
|
||||
void Blend2Node::evaluate(const Array<const AnimationData>& inputs, AnimationData& output) {
|
||||
void Blend2Node::evaluate(GraphEvaluationContext &context, const Vector<AnimationData*>& inputs, AnimationData &output) {
|
||||
assert(inputs.size() == 2);
|
||||
output = lerp(inputs[0]->get_output(), inputs[1], blend_weight);
|
||||
}
|
||||
|
||||
@ -210,8 +212,9 @@ void TimeScaleNode::update_time(SyncedAnimationNode::NodeTimeInfo time_info) {
|
||||
}
|
||||
}
|
||||
|
||||
void TimeScaleNode::evaluate(const Array<const AnimationData>& inputs, AnimationData& output) {
|
||||
std::swap(output, input_node_0->output);
|
||||
void TimeScaleNode::evaluate(GraphEvaluationContext &context, const Vector<AnimationData*>& inputs, AnimationData &output) {
|
||||
assert(inputs.size() == 1);
|
||||
output = inputs[0]->duplicate();
|
||||
}
|
||||
|
||||
```
|
||||
@ -261,7 +264,7 @@ We use the term "value data" to distinguish from Animation Data.
|
||||
|
||||
### Effects on the graph topology
|
||||
|
||||
* Need to generalize Output Sockets to different types instead of only "Animation Data".
|
||||
* Need to generalize Input and Output Ports to different types instead of only "Animation Data".
|
||||
* How to evaluate? Two types of subgraphs:
|
||||
* a) Data/value inputs (e.g. for blend weights) that have to be evaluated before UpdateConnections. Maybe restrict
|
||||
to data that is not animation data dependent?
|
||||
@ -471,7 +474,7 @@ when a node becomes active/deactivated.
|
||||
|
||||
Re-use of animation data ports
|
||||
|
||||
## 5. Inputs into Subgraphs
|
||||
## 5. Inputs into embedded subgraphs
|
||||
|
||||
### Description
|
||||
|
||||
|
||||
@ -171,7 +171,7 @@ void SyncedAnimationGraph::_process_graph(double p_delta, bool p_update_only) {
|
||||
graph_root_node->activate_inputs();
|
||||
graph_root_node->calculate_sync_track();
|
||||
graph_root_node->update_time(p_delta);
|
||||
graph_root_node->evaluate(graph_context, graph_output);
|
||||
graph_root_node->evaluate(graph_context, Vector<AnimationData*>(), graph_output);
|
||||
|
||||
_apply_animation_data(graph_output);
|
||||
}
|
||||
|
||||
@ -10,7 +10,9 @@ void AnimationSamplerNode::initialize(GraphEvaluationContext &context) {
|
||||
node_time_info.loop_mode = Animation::LOOP_LINEAR;
|
||||
}
|
||||
|
||||
void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, AnimationData &output) {
|
||||
void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const Vector<AnimationData*>& inputs, AnimationData &output) {
|
||||
assert(inputs.size() == 0);
|
||||
|
||||
const Vector<Animation::Track *> tracks = animation->get_tracks();
|
||||
Animation::Track *const *tracks_ptr = tracks.ptr();
|
||||
|
||||
|
||||
@ -12,7 +12,6 @@ struct GraphEvaluationContext {
|
||||
Skeleton3D *skeleton_3d = nullptr;
|
||||
};
|
||||
|
||||
|
||||
struct AnimationData {
|
||||
enum TrackType : uint8_t {
|
||||
TYPE_VALUE, // Set a value in a property, can be interpolated.
|
||||
@ -105,6 +104,8 @@ public:
|
||||
|
||||
Vector<InputPort> input_port;
|
||||
|
||||
StringName name;
|
||||
|
||||
virtual ~SyncedAnimationNode() = default;
|
||||
virtual void initialize(GraphEvaluationContext &context) {}
|
||||
virtual void activate_inputs() {}
|
||||
@ -132,14 +133,13 @@ public:
|
||||
}
|
||||
}
|
||||
}
|
||||
virtual void evaluate(GraphEvaluationContext &context, AnimationData &output) {}
|
||||
virtual void evaluate(GraphEvaluationContext &context, const Vector<AnimationData*>& inputs, AnimationData &output) {}
|
||||
|
||||
bool is_active() const { return active; }
|
||||
bool set_input_node(const StringName &socket_name, SyncedAnimationNode *node);
|
||||
void get_input_names(Vector<StringName> &inputs);
|
||||
virtual void get_input_names(Vector<StringName> &inputs) {};
|
||||
|
||||
private:
|
||||
AnimationData *output = nullptr;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
@ -153,28 +153,113 @@ private:
|
||||
Ref<Animation> animation;
|
||||
|
||||
void initialize(GraphEvaluationContext &context) override;
|
||||
void evaluate(GraphEvaluationContext &context, AnimationData &output) override;
|
||||
void evaluate(GraphEvaluationContext &context, const Vector<AnimationData*>& inputs, AnimationData &output) override;
|
||||
};
|
||||
|
||||
class BlendTree : public SyncedAnimationNode {
|
||||
struct Connection {
|
||||
const SyncedAnimationNode* source_node = nullptr;
|
||||
const SyncedAnimationNode* target_node = nullptr;
|
||||
class OutputNode : public SyncedAnimationNode {
|
||||
public:
|
||||
void get_input_names(Vector<StringName> &inputs) override {
|
||||
inputs.push_back("Input");
|
||||
}
|
||||
};
|
||||
|
||||
class SyncedBlendTree : public SyncedAnimationNode {
|
||||
Vector<Ref<SyncedAnimationNode>> nodes;
|
||||
|
||||
struct Connection {
|
||||
const Ref<SyncedAnimationNode> source_node = nullptr;
|
||||
const Ref<SyncedAnimationNode> target_node = nullptr;
|
||||
const StringName target_socket_name = "";
|
||||
};
|
||||
|
||||
Vector<SyncedAnimationNode> nodes;
|
||||
Vector<int> node_parent;
|
||||
Vector<Connection> connections;
|
||||
|
||||
Vector<int> node_parent;
|
||||
Vector<int> node_eval_order;
|
||||
|
||||
void _update_eval_order() {
|
||||
// TODO: proof of concept code: currently assume nodes are properly added, though this is not guaranteed.
|
||||
node_eval_order.clear();
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
node_eval_order.push_back(i);
|
||||
}
|
||||
}
|
||||
public:
|
||||
void connect_nodes(const SyncedAnimationNode* source_node, const SyncedAnimationNode* target_node, StringName target_socket_name) {
|
||||
// TODO
|
||||
// connections.append(Connection{source_node, target_node, target_socket_name});
|
||||
// sort_nodes_by_evaluation_order();
|
||||
SyncedBlendTree() {
|
||||
Ref<OutputNode> output_node;
|
||||
output_node.instantiate();
|
||||
output_node->name = "Output";
|
||||
nodes.push_back(output_node);
|
||||
node_eval_order.push_back(0);
|
||||
}
|
||||
|
||||
Ref<SyncedAnimationNode> get_output_node() {
|
||||
return nodes[0];
|
||||
}
|
||||
|
||||
int get_node_index(const Ref<SyncedAnimationNode> node) {
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
if (nodes[i] == node) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int add_node(const Ref<SyncedAnimationNode>& node) {
|
||||
nodes.push_back(node);
|
||||
int node_index = nodes.size() - 1;
|
||||
return node_index;
|
||||
}
|
||||
|
||||
bool connect_nodes(const Ref<SyncedAnimationNode>& source_node, const Ref<SyncedAnimationNode>& target_node, StringName target_socket_name) {
|
||||
_update_eval_order();
|
||||
|
||||
if (get_node_index(source_node) == -1) {
|
||||
print_error("Cannot connect nodes: source node not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (get_node_index(target_node) == -1) {
|
||||
print_error("Cannot connect nodes: target node not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector<StringName> target_inputs;
|
||||
target_node->get_input_names(target_inputs);
|
||||
|
||||
if (!target_inputs.has(target_socket_name)) {
|
||||
print_error("Cannot connect nodes: target socket not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void sort_nodes_by_evaluation_order() {
|
||||
// TODO: sort nodes and node_parent s.t. for node i all children have index > i.
|
||||
}
|
||||
|
||||
// overrides from SyncedAnimationNode
|
||||
void initialize(GraphEvaluationContext &context) override {
|
||||
for (Ref<SyncedAnimationNode> node : nodes) {
|
||||
node->initialize(context);
|
||||
}
|
||||
}
|
||||
|
||||
void activate_inputs() override {
|
||||
|
||||
}
|
||||
|
||||
void calculate_sync_track() override {
|
||||
|
||||
}
|
||||
|
||||
void update_time(double p_delta) override {
|
||||
|
||||
}
|
||||
|
||||
void evaluate(GraphEvaluationContext &context, const Vector<AnimationData*>& inputs, AnimationData &output) override {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@ -6,42 +6,56 @@
|
||||
|
||||
#include "tests/test_macros.h"
|
||||
|
||||
namespace TestSyncedAnimationGraph {
|
||||
TEST_CASE("[SceneTree][SyncedAnimationGraph] Simple") {
|
||||
Node* character_node = memnew(Node);
|
||||
character_node->set_name("CharacterNode");
|
||||
SceneTree::get_singleton()->get_root()->add_child(character_node);
|
||||
struct SyncedAnimationGraphFixture {
|
||||
Node* character_node;
|
||||
Skeleton3D* skeleton_node;
|
||||
AnimationPlayer* player_node;
|
||||
|
||||
Skeleton3D *skeleton_node = memnew(Skeleton3D);
|
||||
skeleton_node->set_name("Skeleton");
|
||||
character_node->add_child(skeleton_node);
|
||||
|
||||
skeleton_node->add_bone("Root");
|
||||
int hip_bone_index = skeleton_node->add_bone("Hips");
|
||||
|
||||
AnimationPlayer *player_node = memnew(AnimationPlayer);
|
||||
player_node->set_name("AnimationPlayer");
|
||||
|
||||
Ref<Animation> animation = memnew(Animation);
|
||||
const int track_index = animation->add_track(Animation::TYPE_POSITION_3D);
|
||||
CHECK(track_index == 0);
|
||||
animation->track_insert_key(track_index, 0.0, Vector3(0., 0., 0.));
|
||||
animation->track_insert_key(track_index, 1.0, Vector3(1., 2., 3.));
|
||||
animation->track_set_path(track_index, NodePath(vformat("%s:%s", skeleton_node->get_path().get_concatenated_names(),"Hips")));
|
||||
int hip_bone_index = -1;
|
||||
|
||||
Ref<Animation> test_animation;
|
||||
Ref<AnimationLibrary> animation_library;
|
||||
animation_library.instantiate();
|
||||
animation_library->add_animation("TestAnimation", animation);
|
||||
|
||||
player_node->add_animation_library("animation_library", animation_library);
|
||||
SceneTree::get_singleton()->get_root()->add_child(player_node);
|
||||
SyncedAnimationGraph* synced_animation_graph;
|
||||
SyncedAnimationGraphFixture() {
|
||||
character_node = memnew(Node);
|
||||
character_node->set_name("CharacterNode");
|
||||
SceneTree::get_singleton()->get_root()->add_child(character_node);
|
||||
|
||||
SyncedAnimationGraph *synced_animation_graph = memnew(SyncedAnimationGraph);
|
||||
SceneTree::get_singleton()->get_root()->add_child(synced_animation_graph);
|
||||
skeleton_node = memnew(Skeleton3D);
|
||||
skeleton_node->set_name("Skeleton");
|
||||
character_node->add_child(skeleton_node);
|
||||
|
||||
synced_animation_graph->set_animation_player(player_node->get_path());
|
||||
synced_animation_graph->set_skeleton(skeleton_node->get_path());
|
||||
skeleton_node->add_bone("Root");
|
||||
hip_bone_index = skeleton_node->add_bone("Hips");
|
||||
|
||||
player_node = memnew(AnimationPlayer);
|
||||
player_node->set_name("AnimationPlayer");
|
||||
|
||||
test_animation = memnew(Animation);
|
||||
const int track_index = test_animation->add_track(Animation::TYPE_POSITION_3D);
|
||||
CHECK(track_index == 0);
|
||||
test_animation->track_insert_key(track_index, 0.0, Vector3(0., 0., 0.));
|
||||
test_animation->track_insert_key(track_index, 1.0, Vector3(1., 2., 3.));
|
||||
test_animation->track_set_path(track_index, NodePath(vformat("%s:%s", skeleton_node->get_path().get_concatenated_names(),"Hips")));
|
||||
|
||||
animation_library.instantiate();
|
||||
animation_library->add_animation("TestAnimation", test_animation);
|
||||
|
||||
player_node->add_animation_library("animation_library", animation_library);
|
||||
SceneTree::get_singleton()->get_root()->add_child(player_node);
|
||||
|
||||
synced_animation_graph = memnew(SyncedAnimationGraph);
|
||||
SceneTree::get_singleton()->get_root()->add_child(synced_animation_graph);
|
||||
|
||||
synced_animation_graph->set_animation_player(player_node->get_path());
|
||||
synced_animation_graph->set_skeleton(skeleton_node->get_path());
|
||||
}
|
||||
};
|
||||
|
||||
namespace TestSyncedAnimationGraph {
|
||||
|
||||
TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] SimpleAnimationSamplerTest") {
|
||||
Ref<AnimationSamplerNode> animation_sampler_node;
|
||||
animation_sampler_node.instantiate();
|
||||
animation_sampler_node->animation_name = "animation_library/TestAnimation";
|
||||
@ -62,4 +76,35 @@ TEST_CASE("[SceneTree][SyncedAnimationGraph] Simple") {
|
||||
CHECK(hip_bone_position.y == doctest::Approx(0.02));
|
||||
CHECK(hip_bone_position.z == doctest::Approx(0.03));
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] SimpleBlendTreeTest") {
|
||||
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);
|
||||
|
||||
|
||||
synced_blend_tree_node->connect_nodes(animation_sampler_node, synced_blend_tree_node->get_output_node(), "Input");
|
||||
|
||||
synced_animation_graph->set_graph_root_node(synced_blend_tree_node);
|
||||
|
||||
Vector3 hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin;
|
||||
|
||||
CHECK(hip_bone_position.x == doctest::Approx(0.0));
|
||||
CHECK(hip_bone_position.y == doctest::Approx(0.0));
|
||||
CHECK(hip_bone_position.z == doctest::Approx(0.0));
|
||||
|
||||
SceneTree::get_singleton()->process(0.01);
|
||||
|
||||
hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin;
|
||||
|
||||
CHECK(hip_bone_position.x == doctest::Approx(0.01));
|
||||
CHECK(hip_bone_position.y == doctest::Approx(0.02));
|
||||
CHECK(hip_bone_position.z == doctest::Approx(0.03));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user