Added saving and loading blend tree resources.

This commit is contained in:
Martin Felis 2025-12-29 15:25:10 +01:00
parent 46f940a67c
commit 537712c806
4 changed files with 267 additions and 28 deletions

View File

@ -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) {

View File

@ -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;
}

View File

@ -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) {

View File

@ -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