Added saving and loading blend tree resources.
This commit is contained in:
parent
46f940a67c
commit
537712c806
@ -8,6 +8,10 @@ void initialize_synced_blend_tree_module(ModuleInitializationLevel p_level) {
|
||||
return;
|
||||
}
|
||||
ClassDB::register_class<SyncedAnimationGraph>();
|
||||
ClassDB::register_class<SyncedAnimationNode>();
|
||||
ClassDB::register_class<SyncedBlendTree>();
|
||||
ClassDB::register_class<AnimationSamplerNode>();
|
||||
ClassDB::register_class<AnimationBlend2Node>();
|
||||
}
|
||||
|
||||
void uninitialize_synced_blend_tree_module(ModuleInitializationLevel p_level) {
|
||||
|
||||
@ -4,6 +4,100 @@
|
||||
|
||||
#include "synced_animation_node.h"
|
||||
|
||||
void SyncedBlendTree::_get_property_list(List<PropertyInfo> *p_list) const {
|
||||
for (const Ref<SyncedAnimationNode> &node : nodes) {
|
||||
String prop_name = node->name;
|
||||
if (prop_name != "Output") {
|
||||
p_list->push_back(PropertyInfo(Variant::OBJECT, "nodes/" + prop_name + "/node", PROPERTY_HINT_RESOURCE_TYPE, "AnimationNode", PROPERTY_USAGE_NO_EDITOR));
|
||||
}
|
||||
p_list->push_back(PropertyInfo(Variant::VECTOR2, "nodes/" + prop_name + "/position", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR));
|
||||
}
|
||||
|
||||
p_list->push_back(PropertyInfo(Variant::ARRAY, "node_connections", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR));
|
||||
}
|
||||
|
||||
bool SyncedBlendTree::_get(const StringName &p_name, Variant &r_value) const {
|
||||
String prop_name = p_name;
|
||||
if (prop_name.begins_with("nodes/")) {
|
||||
String node_name = prop_name.get_slicec('/', 1);
|
||||
String what = prop_name.get_slicec('/', 2);
|
||||
int node_index = find_node_index_by_name(node_name);
|
||||
|
||||
if (what == "node") {
|
||||
if (node_index != -1) {
|
||||
r_value = nodes[node_index];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (what == "position") {
|
||||
if (node_index != -1) {
|
||||
r_value = nodes[node_index]->position;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (prop_name == "node_connections") {
|
||||
Array conns;
|
||||
conns.resize(tree_builder.connections.size() * 3);
|
||||
|
||||
int idx = 0;
|
||||
for (const BlendTreeConnection &connection : tree_builder.connections) {
|
||||
conns[idx * 3 + 0] = connection.target_node->name;
|
||||
conns[idx * 3 + 1] = connection.target_node->get_node_input_index(connection.target_port_name);
|
||||
conns[idx * 3 + 2] = connection.source_node->name;
|
||||
idx++;
|
||||
}
|
||||
|
||||
r_value = conns;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SyncedBlendTree::_set(const StringName &p_name, const Variant &p_value) {
|
||||
String prop_name = p_name;
|
||||
if (prop_name.begins_with("nodes/")) {
|
||||
String node_name = prop_name.get_slicec('/', 1);
|
||||
String what = prop_name.get_slicec('/', 2);
|
||||
|
||||
if (what == "node") {
|
||||
Ref<SyncedAnimationNode> anode = p_value;
|
||||
if (anode.is_valid()) {
|
||||
anode->name = node_name;
|
||||
add_node(anode);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (what == "position") {
|
||||
int node_index = find_node_index_by_name(node_name);
|
||||
if (node_index > -1) {
|
||||
tree_builder.nodes[node_index]->position = p_value;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} else if (prop_name == "node_connections") {
|
||||
Array conns = p_value;
|
||||
ERR_FAIL_COND_V(conns.size() % 3 != 0, false);
|
||||
|
||||
for (int i = 0; i < conns.size(); i += 3) {
|
||||
int target_node_index = find_node_index_by_name(conns[i]);
|
||||
int target_node_port_index = conns[i + 1];
|
||||
int source_node_index = find_node_index_by_name(conns[i + 2]);
|
||||
|
||||
Ref<SyncedAnimationNode> target_node = tree_builder.nodes[target_node_index];
|
||||
Vector<StringName> target_input_names;
|
||||
target_node->get_input_names(target_input_names);
|
||||
|
||||
add_connection(tree_builder.nodes[source_node_index], target_node, target_input_names[target_node_port_index]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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();
|
||||
@ -78,7 +172,59 @@ void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const Local
|
||||
output.sample_from_animation(animation, context.skeleton_3d, node_time_info.position);
|
||||
}
|
||||
|
||||
void AnimationSamplerNode::set_animation(const StringName &p_name) {
|
||||
animation_name = p_name;
|
||||
}
|
||||
|
||||
StringName AnimationSamplerNode::get_animation() const {
|
||||
return animation_name;
|
||||
}
|
||||
|
||||
void AnimationSamplerNode::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("set_animation", "name"), &AnimationSamplerNode::set_animation);
|
||||
ClassDB::bind_method(D_METHOD("get_animation"), &AnimationSamplerNode::get_animation);
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "animation"), "set_animation", "get_animation");
|
||||
}
|
||||
|
||||
void AnimationBlend2Node::evaluate(GraphEvaluationContext &context, const LocalVector<AnimationData *> &inputs, AnimationData &output) {
|
||||
output = *inputs[0];
|
||||
output.blend(*inputs[1], blend_weight);
|
||||
}
|
||||
|
||||
void AnimationBlend2Node::set_use_sync(bool p_sync) {
|
||||
sync = p_sync;
|
||||
}
|
||||
|
||||
bool AnimationBlend2Node::is_using_sync() const {
|
||||
return sync;
|
||||
}
|
||||
|
||||
void AnimationBlend2Node::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("set_use_sync", "enable"), &AnimationBlend2Node::set_use_sync);
|
||||
ClassDB::bind_method(D_METHOD("is_using_sync"), &AnimationBlend2Node::is_using_sync);
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "sync"), "set_use_sync", "is_using_sync");
|
||||
}
|
||||
|
||||
void AnimationBlend2Node::_get_property_list(List<PropertyInfo> *p_list) const {
|
||||
p_list->push_back(PropertyInfo(Variant::FLOAT, blend_amount, PROPERTY_HINT_RANGE, "0,1,0.01,or_less,or_greater"));
|
||||
}
|
||||
|
||||
bool AnimationBlend2Node::_get(const StringName &p_name, Variant &r_value) const {
|
||||
if (p_name == blend_amount) {
|
||||
r_value = blend_weight;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AnimationBlend2Node::_set(const StringName &p_name, const Variant &p_value) {
|
||||
if (p_name == blend_amount) {
|
||||
blend_weight = p_value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -225,6 +225,7 @@ public:
|
||||
bool active = false;
|
||||
|
||||
StringName name;
|
||||
Vector2 position;
|
||||
|
||||
virtual ~SyncedAnimationNode() override = default;
|
||||
virtual void initialize(GraphEvaluationContext &context) {}
|
||||
@ -284,6 +285,11 @@ public:
|
||||
get_input_names(inputs);
|
||||
return inputs.size();
|
||||
}
|
||||
|
||||
//protected:
|
||||
// void _get_property_list(List<PropertyInfo> *p_list) const;
|
||||
// bool _get(const StringName &p_name, Variant &r_value) const;
|
||||
// bool _set(const StringName &p_name, const Variant &p_value);
|
||||
};
|
||||
|
||||
class AnimationSamplerNode : public SyncedAnimationNode {
|
||||
@ -292,14 +298,22 @@ class AnimationSamplerNode : public SyncedAnimationNode {
|
||||
public:
|
||||
StringName animation_name;
|
||||
|
||||
void set_animation(const StringName &p_name);
|
||||
StringName get_animation() const;
|
||||
|
||||
private:
|
||||
Ref<Animation> animation;
|
||||
|
||||
void initialize(GraphEvaluationContext &context) override;
|
||||
void evaluate(GraphEvaluationContext &context, const LocalVector<AnimationData *> &inputs, AnimationData &output) override;
|
||||
|
||||
protected:
|
||||
static void _bind_methods();
|
||||
};
|
||||
|
||||
class OutputNode : public SyncedAnimationNode {
|
||||
GDCLASS(OutputNode, SyncedAnimationNode);
|
||||
|
||||
public:
|
||||
void get_input_names(Vector<StringName> &inputs) const override {
|
||||
inputs.push_back("Input");
|
||||
@ -307,8 +321,12 @@ public:
|
||||
};
|
||||
|
||||
class AnimationBlend2Node : public SyncedAnimationNode {
|
||||
GDCLASS(AnimationBlend2Node, SyncedAnimationNode);
|
||||
|
||||
public:
|
||||
StringName blend_amount = PNAME("blend_amount");
|
||||
float blend_weight = 0.0f;
|
||||
bool sync = false;
|
||||
|
||||
void get_input_names(Vector<StringName> &inputs) const override {
|
||||
inputs.push_back("Input0");
|
||||
@ -316,6 +334,16 @@ public:
|
||||
}
|
||||
|
||||
void evaluate(GraphEvaluationContext &context, const LocalVector<AnimationData *> &inputs, AnimationData &output) override;
|
||||
|
||||
void set_use_sync(bool p_sync);
|
||||
bool is_using_sync() const;
|
||||
|
||||
protected:
|
||||
static void _bind_methods();
|
||||
|
||||
void _get_property_list(List<PropertyInfo> *p_list) const;
|
||||
bool _get(const StringName &p_name, Variant &r_value) const;
|
||||
bool _set(const StringName &p_name, const Variant &p_value);
|
||||
};
|
||||
|
||||
struct BlendTreeConnection {
|
||||
@ -375,7 +403,7 @@ struct BlendTreeBuilder {
|
||||
|
||||
Vector<Ref<SyncedAnimationNode>> nodes; // All added nodes
|
||||
LocalVector<NodeConnectionInfo> node_connection_info;
|
||||
Vector<BlendTreeConnection> connections;
|
||||
LocalVector<BlendTreeConnection> connections;
|
||||
|
||||
BlendTreeBuilder() {
|
||||
Ref<OutputNode> output_node;
|
||||
@ -388,7 +416,7 @@ struct BlendTreeBuilder {
|
||||
return nodes[0];
|
||||
}
|
||||
|
||||
int get_node_index(const Ref<SyncedAnimationNode> &node) const {
|
||||
int find_node_index(const Ref<SyncedAnimationNode> &node) const {
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
if (nodes[i] == node) {
|
||||
return i;
|
||||
@ -398,7 +426,29 @@ struct BlendTreeBuilder {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int find_node_index_by_name(const StringName &name) const {
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
if (nodes[i]->name == name) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void add_node(const Ref<SyncedAnimationNode> &node) {
|
||||
StringName node_base_name = node->name;
|
||||
if (node_base_name.is_empty()) {
|
||||
node_base_name = node->get_class_name();
|
||||
}
|
||||
node->name = node_base_name;
|
||||
|
||||
int number_suffix = 1;
|
||||
while (find_node_index_by_name(node->name) != -1) {
|
||||
node->name = vformat("%s %d", node_base_name, number_suffix);
|
||||
number_suffix++;
|
||||
}
|
||||
|
||||
nodes.push_back(node);
|
||||
node_connection_info.push_back(NodeConnectionInfo(node.ptr()));
|
||||
}
|
||||
@ -460,12 +510,13 @@ struct BlendTreeBuilder {
|
||||
return false;
|
||||
}
|
||||
|
||||
int source_node_index = get_node_index(source_node);
|
||||
int target_node_index = get_node_index(target_node);
|
||||
int source_node_index = find_node_index(source_node);
|
||||
int target_node_index = find_node_index(target_node);
|
||||
int target_input_port_index = target_node->get_node_input_index(target_port_name);
|
||||
|
||||
node_connection_info[source_node_index].parent_node_index = target_node_index;
|
||||
node_connection_info[target_node_index].connected_child_node_index_at_port[target_input_port_index] = source_node_index;
|
||||
connections.push_back(BlendTreeConnection{ source_node, target_node, target_port_name });
|
||||
|
||||
add_index_and_update_subtrees_recursive(source_node_index, target_node_index);
|
||||
|
||||
@ -473,7 +524,7 @@ struct BlendTreeBuilder {
|
||||
}
|
||||
|
||||
bool is_connection_valid(const Ref<SyncedAnimationNode> &source_node, const Ref<SyncedAnimationNode> &target_node, StringName target_port_name) {
|
||||
int source_node_index = get_node_index(source_node);
|
||||
int source_node_index = find_node_index(source_node);
|
||||
if (source_node_index == -1) {
|
||||
print_error("Cannot connect nodes: source node not found.");
|
||||
return false;
|
||||
@ -484,17 +535,12 @@ struct BlendTreeBuilder {
|
||||
return false;
|
||||
}
|
||||
|
||||
int target_node_index = get_node_index(target_node);
|
||||
int target_node_index = find_node_index(target_node);
|
||||
if (target_node_index == -1) {
|
||||
print_error("Cannot connect nodes: target node not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target_node == get_output_node() && connections.size() > 0) {
|
||||
print_error("Cannot add connection to output node: output node is already connected");
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector<StringName> target_inputs;
|
||||
target_node->get_input_names(target_inputs);
|
||||
|
||||
@ -519,6 +565,8 @@ struct BlendTreeBuilder {
|
||||
};
|
||||
|
||||
class SyncedBlendTree : public SyncedAnimationNode {
|
||||
GDCLASS(SyncedBlendTree, SyncedAnimationNode);
|
||||
|
||||
Vector<Ref<SyncedAnimationNode>> nodes;
|
||||
|
||||
BlendTreeBuilder tree_builder;
|
||||
@ -557,6 +605,11 @@ class SyncedBlendTree : public SyncedAnimationNode {
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
void _get_property_list(List<PropertyInfo> *p_list) const;
|
||||
bool _get(const StringName &p_name, Variant &r_value) const;
|
||||
bool _set(const StringName &p_name, const Variant &p_value);
|
||||
|
||||
public:
|
||||
struct NodeRuntimeData {
|
||||
Vector<Ref<SyncedAnimationNode>> input_nodes;
|
||||
@ -569,14 +622,12 @@ public:
|
||||
return tree_builder.nodes[0];
|
||||
}
|
||||
|
||||
int get_node_index(const Ref<SyncedAnimationNode> &node) const {
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
if (nodes[i] == node) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
int find_node_index(const Ref<SyncedAnimationNode> &node) const {
|
||||
return tree_builder.find_node_index(node);
|
||||
}
|
||||
|
||||
return -1;
|
||||
int find_node_index_by_name(const StringName &name) const {
|
||||
return tree_builder.find_node_index_by_name(name);
|
||||
}
|
||||
|
||||
void add_node(const Ref<SyncedAnimationNode> &node) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "../synced_animation_graph.h"
|
||||
#include "scene/animation/animation_tree.h"
|
||||
#include "scene/main/window.h"
|
||||
|
||||
#include "tests/test_macros.h"
|
||||
@ -105,8 +106,8 @@ TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") {
|
||||
CHECK(tree_constructor.add_connection(animation_sampler_node0, node_blend0, "Input0"));
|
||||
|
||||
// Ensure that subtree is properly updated
|
||||
int sampler0_index = tree_constructor.get_node_index(animation_sampler_node0);
|
||||
int blend0_index = tree_constructor.get_node_index(node_blend0);
|
||||
int sampler0_index = tree_constructor.find_node_index(animation_sampler_node0);
|
||||
int blend0_index = tree_constructor.find_node_index(node_blend0);
|
||||
CHECK(tree_constructor.node_connection_info[blend0_index].input_subtree_node_indices.has(sampler0_index));
|
||||
|
||||
// Connect blend0 to blend1
|
||||
@ -118,8 +119,8 @@ TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") {
|
||||
CHECK(tree_constructor.add_connection(animation_sampler_node1, node_blend0, "Input1"));
|
||||
|
||||
// Ensure that subtree is properly updated
|
||||
int sampler1_index = tree_constructor.get_node_index(animation_sampler_node0);
|
||||
int blend1_index = tree_constructor.get_node_index(node_blend1);
|
||||
int sampler1_index = tree_constructor.find_node_index(animation_sampler_node0);
|
||||
int blend1_index = tree_constructor.find_node_index(node_blend1);
|
||||
CHECK(tree_constructor.node_connection_info[blend1_index].input_subtree_node_indices.has(sampler1_index));
|
||||
CHECK(tree_constructor.node_connection_info[blend1_index].input_subtree_node_indices.has(sampler0_index));
|
||||
CHECK(tree_constructor.node_connection_info[blend1_index].input_subtree_node_indices.has(blend0_index));
|
||||
@ -262,12 +263,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph
|
||||
|
||||
synced_blend_tree_node->initialize(synced_animation_graph->get_context());
|
||||
|
||||
// int sampler_node_1_index = synced_blend_tree_node->get_node_index(animation_sampler_node_1);
|
||||
// const SyncedBlendTree::NodeRuntimeData &sampler_node_1_runtime_data = synced_blend_tree_node->_node_runtime_data[sampler_node_1_index];
|
||||
|
||||
// int sampler_node_2_index = synced_blend_tree_node->get_node_index(animation_sampler_node_2);
|
||||
// const SyncedBlendTree::NodeRuntimeData &sampler_node_2_runtime_data = synced_blend_tree_node->_node_runtime_data[sampler_node_2_index];
|
||||
int blend2_node_index = synced_blend_tree_node->get_node_index(blend2_node);
|
||||
int blend2_node_index = synced_blend_tree_node->find_node_index(blend2_node);
|
||||
const SyncedBlendTree::NodeRuntimeData &blend2_runtime_data = synced_blend_tree_node->_node_runtime_data[blend2_node_index];
|
||||
|
||||
CHECK(blend2_runtime_data.input_nodes[0] == animation_sampler_node_a);
|
||||
@ -288,6 +284,48 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph
|
||||
CHECK(hip_bone_position.x == doctest::Approx(0.75));
|
||||
CHECK(hip_bone_position.y == doctest::Approx(1.5));
|
||||
CHECK(hip_bone_position.z == doctest::Approx(2.25));
|
||||
|
||||
// Test saving and loading of the blend tree to a resource
|
||||
ResourceSaver::save(synced_blend_tree_node, "synced_blend_tree_node.tres");
|
||||
|
||||
REQUIRE(ClassDB::class_exists("AnimationSamplerNode"));
|
||||
|
||||
// Load blend tree
|
||||
Ref<SyncedBlendTree> loaded_synced_blend_tree = ResourceLoader::load("synced_blend_tree_node.tres");
|
||||
REQUIRE(loaded_synced_blend_tree.is_valid());
|
||||
|
||||
loaded_synced_blend_tree->initialize(synced_animation_graph->get_context());
|
||||
synced_animation_graph->set_graph_root_node(loaded_synced_blend_tree);
|
||||
|
||||
// Re-evaluate using a different time. All animation samplers will start again from 0.
|
||||
SceneTree::get_singleton()->process(0.2);
|
||||
|
||||
hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin;
|
||||
|
||||
CHECK(hip_bone_position.x == doctest::Approx(0.3));
|
||||
CHECK(hip_bone_position.y == doctest::Approx(0.6));
|
||||
CHECK(hip_bone_position.z == doctest::Approx(0.9));
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph][BlendTree][Blend2Node] Serialize AnimationTree" * doctest::skip(true)) {
|
||||
AnimationTree *animation_tree = memnew(AnimationTree);
|
||||
|
||||
character_node->add_child(animation_tree);
|
||||
animation_tree->set_animation_player(player_node->get_path());
|
||||
animation_tree->set_root_node(character_node->get_path());
|
||||
Ref<AnimationNodeAnimation> animation_node_animation;
|
||||
animation_node_animation.instantiate();
|
||||
animation_node_animation->set_animation("TestAnimationA");
|
||||
|
||||
Ref<AnimationNodeBlendTree> animation_node_blend_tree;
|
||||
animation_node_blend_tree.instantiate();
|
||||
animation_node_blend_tree->add_node("SamplerTestAnimationA", animation_node_animation, Vector2(0, 0));
|
||||
animation_node_blend_tree->connect_node("output", 0, "SamplerTestAnimationA");
|
||||
animation_node_blend_tree->setup_local_to_scene();
|
||||
|
||||
animation_tree->set_root_animation_node(animation_node_blend_tree);
|
||||
|
||||
ResourceSaver::save(animation_node_blend_tree, "animation_tree.tres");
|
||||
}
|
||||
|
||||
} //namespace TestSyncedAnimationGraph
|
||||
Loading…
x
Reference in New Issue
Block a user