From 5d0bf10ce7dadeedef48ac5e4fd843ec88c872dc Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Fri, 23 Jan 2026 12:05:39 +0100 Subject: [PATCH 01/29] Prevent crash when trying to evaluate without an existing Skeleton3D. --- blendalot_animation_graph.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/blendalot_animation_graph.cpp b/blendalot_animation_graph.cpp index 7ca1a81..976db5f 100644 --- a/blendalot_animation_graph.cpp +++ b/blendalot_animation_graph.cpp @@ -295,6 +295,10 @@ void BLTAnimationGraph::_process_graph(double p_delta, bool p_update_only) { return; } + if (graph_context.skeleton_3d == nullptr) { + return; + } + GodotProfileZone("SyncedAnimationGraph::_process_graph"); _update_properties(); -- 2.47.2 From fd13c53e52c63a53ad9a989b1d6a9c175f461769 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Sat, 24 Jan 2026 15:38:27 +0100 Subject: [PATCH 02/29] Exposed additional functions to GDScript to implement a GDScript based EditorPlugin. --- blendalot_animation_graph.cpp | 2 +- blendalot_animation_node.cpp | 45 +++++++---- blendalot_animation_node.h | 116 ++++++++++++++++++++++------ tests/test_synced_animation_graph.h | 23 +++--- 4 files changed, 132 insertions(+), 54 deletions(-) diff --git a/blendalot_animation_graph.cpp b/blendalot_animation_graph.cpp index 976db5f..53a013c 100644 --- a/blendalot_animation_graph.cpp +++ b/blendalot_animation_graph.cpp @@ -51,7 +51,7 @@ void BLTAnimationGraph::_update_properties_for_node(const String &p_base_path, R p_node->get_child_nodes(&children); for (const Ref &child_node : children) { - _update_properties_for_node(p_base_path + child_node->name + "/", child_node); + _update_properties_for_node(p_base_path + child_node->get_name() + "/", child_node); } } diff --git a/blendalot_animation_node.cpp b/blendalot_animation_node.cpp index 1418d25..936eb83 100644 --- a/blendalot_animation_node.cpp +++ b/blendalot_animation_node.cpp @@ -5,9 +5,17 @@ #include "blendalot_animation_node.h" void BLTAnimationNode::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_position", "position"), &BLTAnimationNode::set_position); + ClassDB::bind_method(D_METHOD("get_position"), &BLTAnimationNode::get_position); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "position"), "set_position", "get_position"); + ADD_SIGNAL(MethodInfo("tree_changed")); ADD_SIGNAL(MethodInfo("animation_node_renamed", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "old_name"), PropertyInfo(Variant::STRING, "new_name"))); ADD_SIGNAL(MethodInfo("animation_node_removed", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "name"))); + + ClassDB::bind_method(D_METHOD("get_input_names"), &BLTAnimationNode::get_input_names_as_typed_array); + ClassDB::bind_method(D_METHOD("get_input_count"), &BLTAnimationNode::get_input_count); + ClassDB::bind_method(D_METHOD("get_input_index"), &BLTAnimationNode::get_input_index); } void BLTAnimationNode::get_parameter_list(List *r_list) const { @@ -42,8 +50,13 @@ void BLTAnimationNode::_animation_node_removed(const ObjectID &p_oid, const Stri void BLTAnimationNodeBlendTree::_bind_methods() { ClassDB::bind_method(D_METHOD("add_node", "animation_node"), &BLTAnimationNodeBlendTree::add_node); + ClassDB::bind_method(D_METHOD("get_node", "node_name"), &BLTAnimationNodeBlendTree::get_node); ClassDB::bind_method(D_METHOD("get_output_node"), &BLTAnimationNodeBlendTree::get_output_node); + ClassDB::bind_method(D_METHOD("get_node_names"), &BLTAnimationNodeBlendTree::get_node_names_as_typed_array); + + ClassDB::bind_method(D_METHOD("is_connection_valid", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::is_connection_valid); ClassDB::bind_method(D_METHOD("add_connection", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::add_connection); + ClassDB::bind_method(D_METHOD("get_connections"), &BLTAnimationNodeBlendTree::get_connections_as_array); BIND_CONSTANT(CONNECTION_OK); BIND_CONSTANT(CONNECTION_ERROR_GRAPH_ALREADY_INITIALIZED); @@ -57,7 +70,7 @@ void BLTAnimationNodeBlendTree::_bind_methods() { void BLTAnimationNodeBlendTree::_get_property_list(List *p_list) const { for (const Ref &node : tree_graph.nodes) { - String prop_name = node->name; + String prop_name = node->get_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)); } @@ -93,9 +106,9 @@ bool BLTAnimationNodeBlendTree::_get(const StringName &p_name, Variant &r_value) int idx = 0; for (const BLTBlendTreeConnection &connection : tree_graph.connections) { - conns[idx * 3 + 0] = connection.target_node->name; + conns[idx * 3 + 0] = connection.target_node->get_name(); conns[idx * 3 + 1] = connection.target_node->get_input_index(connection.target_port_name); - conns[idx * 3 + 2] = connection.source_node->name; + conns[idx * 3 + 2] = connection.source_node->get_name(); idx++; } @@ -115,7 +128,7 @@ bool BLTAnimationNodeBlendTree::_set(const StringName &p_name, const Variant &p_ if (what == "node") { Ref anode = p_value; if (anode.is_valid()) { - anode->name = node_name; + anode->set_name(node_name); add_node(anode); } return true; @@ -138,8 +151,7 @@ bool BLTAnimationNodeBlendTree::_set(const StringName &p_name, const Variant &p_ int source_node_index = find_node_index_by_name(conns[i + 2]); Ref target_node = tree_graph.nodes[target_node_index]; - Vector target_input_names; - target_node->get_input_names(target_input_names); + Vector target_input_names = target_node->get_input_names(); add_connection(tree_graph.nodes[source_node_index], target_node, target_input_names[target_node_port_index]); } @@ -255,11 +267,13 @@ void AnimationDataAllocator::register_track_values(const Ref &animati } bool BLTAnimationNodeSampler::initialize(GraphEvaluationContext &context) { - BLTAnimationNode::initialize(context); + if (!BLTAnimationNode::initialize(context)) { + return false; + } animation = context.animation_player->get_animation(animation_name); if (!animation.is_valid()) { - print_error(vformat("Cannot initialize node %s: animation '%s' not found in animation player.", name, animation_name)); + print_error(vformat("Cannot initialize node %s: animation '%s' not found in animation player.", get_name(), animation_name)); return false; } @@ -412,7 +426,7 @@ bool BLTAnimationNodeBlend2::_set(const StringName &p_name, const Variant &p_val BLTAnimationNodeBlendTree::BLTBlendTreeGraph::BLTBlendTreeGraph() { Ref output_node; output_node.instantiate(); - output_node->name = "Output"; + output_node->set_name("Output"); add_node(output_node); } @@ -432,7 +446,7 @@ int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_node_index(const Refname == name) { + if (nodes[i]->get_name() == name) { return i; } } @@ -441,15 +455,15 @@ int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_node_index_by_name(const } void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::add_node(const Ref &node) { - StringName node_base_name = node->name; + StringName node_base_name = node->get_name(); if (node_base_name.is_empty()) { node_base_name = node->get_class_name(); } - node->name = node_base_name; + node->set_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); + while (find_node_index_by_name(node->get_name()) != -1) { + node->set_name(vformat("%s %d", node_base_name, number_suffix)); number_suffix++; } @@ -546,8 +560,7 @@ BLTAnimationNodeBlendTree::ConnectionError BLTAnimationNodeBlendTree::BLTBlendTr return CONNECTION_ERROR_NO_TARGET_NODE; } - Vector target_inputs; - target_node->get_input_names(target_inputs); + Vector target_inputs = target_node->get_input_names(); if (!target_inputs.has(target_port_name)) { print_error("Cannot connect nodes: target port not found."); diff --git a/blendalot_animation_node.h b/blendalot_animation_node.h index a3f4ee7..3bea3a7 100644 --- a/blendalot_animation_node.h +++ b/blendalot_animation_node.h @@ -262,7 +262,6 @@ public: NodeTimeInfo node_time_info; bool active = false; - StringName name; Vector2 position; virtual ~BLTAnimationNode() override = default; @@ -285,6 +284,7 @@ public: node_time_info.loop_mode = input_nodes[0]->node_time_info.loop_mode; } } + virtual void update_time(double p_time) { if (node_time_info.is_synced) { node_time_info.sync_position = p_time; @@ -293,6 +293,7 @@ public: node_time_info.position += p_time; } } + virtual void evaluate(GraphEvaluationContext &context, const LocalVector &input_datas, AnimationData &output_data) { // By default, use the AnimationData of the first input. if (input_datas.size() > 0) { @@ -300,16 +301,32 @@ public: } } - virtual void get_input_names(Vector &inputs) const {} + void set_position(const Vector2 &p_position) { + position = p_position; + } + + Vector2 get_position() const { + return position; + } + + virtual Vector get_input_names() const { return {}; } + + TypedArray get_input_names_as_typed_array() const { + TypedArray typed_arr; + Vector vec = get_input_names(); + typed_arr.resize(vec.size()); + for (uint32_t i = 0; i < vec.size(); i++) { + typed_arr[i] = vec[i]; + } + return typed_arr; + } int get_input_index(const StringName &port_name) const { - Vector inputs; - get_input_names(inputs); + Vector inputs = get_input_names(); return inputs.find(port_name); } int get_input_count() const { - Vector inputs; - get_input_names(inputs); + Vector inputs = get_input_names(); return inputs.size(); } @@ -341,8 +358,8 @@ class BLTAnimationNodeOutput : public BLTAnimationNode { GDCLASS(BLTAnimationNodeOutput, BLTAnimationNode); public: - void get_input_names(Vector &inputs) const override { - inputs.push_back("Input"); + Vector get_input_names() const override { + return { "Output" }; } }; @@ -353,20 +370,21 @@ public: float blend_weight = 0.0f; bool sync = true; - void get_input_names(Vector &inputs) const override { - inputs.push_back("Input0"); - inputs.push_back("Input1"); + Vector get_input_names() const override { + return { "Input0", "Input1" }; } bool initialize(GraphEvaluationContext &context) override { - bool result = BLTAnimationNode::initialize(context); + if (!BLTAnimationNode::initialize(context)) { + return false; + } if (sync) { // TODO: do we always want looping in this case or do we traverse the graph to check what's reasonable? node_time_info.loop_mode = Animation::LOOP_LINEAR; } - return result; + return true; } void activate_inputs(const Vector> &input_nodes) override { input_nodes[0]->active = true; @@ -549,10 +567,6 @@ public: }; LocalVector _node_runtime_data; - Ref get_output_node() const { - return tree_graph.nodes[0]; - } - int find_node_index(const Ref &node) const { return tree_graph.find_node_index(node); } @@ -561,14 +575,6 @@ public: return tree_graph.find_node_index_by_name(name); } - Ref get_node(int node_index) { - if (node_index < 0 || node_index > tree_graph.nodes.size()) { - return nullptr; - } - - return tree_graph.nodes[node_index]; - } - void add_node(const Ref &node) { if (tree_initialized) { print_error("Cannot add node to BlendTree: BlendTree already initialized."); @@ -578,6 +584,51 @@ public: tree_graph.add_node(node); } + TypedArray get_node_names_as_typed_array() const { + Vector vec; + for (const Ref &node : tree_graph.nodes) { + vec.push_back(node->get_name()); + } + + TypedArray typed_arr; + typed_arr.resize(vec.size()); + for (uint32_t i = 0; i < vec.size(); i++) { + typed_arr[i] = vec[i]; + } + return typed_arr; + } + + Ref get_node(const StringName &node_name) const { + int node_index = tree_graph.find_node_index_by_name(node_name); + + if (node_index >= 0) { + return tree_graph.nodes[node_index]; + } + + return nullptr; + } + + Ref get_node_by_index(int node_index) const { + if (node_index < 0 || node_index > tree_graph.nodes.size()) { + return nullptr; + } + + return tree_graph.nodes[node_index]; + } + + Ref get_output_node() const { + return tree_graph.nodes[0]; + } + + ConnectionError is_connection_valid(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) { + if (tree_initialized) { + print_error("Cannot add connection to BlendTree: BlendTree already initialized."); + return CONNECTION_ERROR_GRAPH_ALREADY_INITIALIZED; + } + + return tree_graph.is_connection_valid(source_node, target_node, target_port_name); + } + ConnectionError add_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) { if (tree_initialized) { print_error("Cannot add connection to BlendTree: BlendTree already initialized."); @@ -587,8 +638,23 @@ public: return tree_graph.add_connection(source_node, target_node, target_port_name); } + Array get_connections_as_array() const { + Array result; + for (const BLTBlendTreeConnection &connection : tree_graph.connections) { + result.push_back(connection.source_node); + result.push_back(connection.target_node); + result.push_back(connection.target_port_name); + } + + return result; + } + // overrides from SyncedAnimationNode bool initialize(GraphEvaluationContext &context) override { + if (!BLTAnimationNode::initialize(context)) { + return false; + } + sort_nodes(); setup_runtime_data(); diff --git a/tests/test_synced_animation_graph.h b/tests/test_synced_animation_graph.h index f3923a9..94ef779 100644 --- a/tests/test_synced_animation_graph.h +++ b/tests/test_synced_animation_graph.h @@ -146,27 +146,27 @@ TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") { Ref animation_sampler_node0; animation_sampler_node0.instantiate(); - animation_sampler_node0->name = "Sampler0"; + animation_sampler_node0->set_name("Sampler0"); tree_constructor.add_node(animation_sampler_node0); Ref animation_sampler_node1; animation_sampler_node1.instantiate(); - animation_sampler_node1->name = "Sampler1"; + animation_sampler_node1->set_name("Sampler1"); tree_constructor.add_node(animation_sampler_node1); Ref animation_sampler_node2; animation_sampler_node2.instantiate(); - animation_sampler_node2->name = "Sampler2"; + animation_sampler_node2->set_name("Sampler2"); tree_constructor.add_node(animation_sampler_node2); Ref node_blend0; node_blend0.instantiate(); - node_blend0->name = "Blend0"; + node_blend0->set_name("Blend0"); tree_constructor.add_node(node_blend0); Ref node_blend1; node_blend1.instantiate(); - node_blend1->name = "Blend1"; + node_blend1->set_name("Blend1"); tree_constructor.add_node(node_blend1); // Tree @@ -201,7 +201,7 @@ TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") { CHECK(tree_constructor.node_connection_info[blend1_index].input_subtree_node_indices.has(blend0_index)); // Perform remaining connections - CHECK(BLTAnimationNodeBlendTree::CONNECTION_OK == tree_constructor.add_connection(node_blend1, tree_constructor.get_output_node(), "Input")); + CHECK(BLTAnimationNodeBlendTree::CONNECTION_OK == tree_constructor.add_connection(node_blend1, tree_constructor.get_output_node(), "Output")); CHECK(BLTAnimationNodeBlendTree::CONNECTION_OK == tree_constructor.add_connection(animation_sampler_node2, node_blend1, "Input1")); // Output node must have all nodes in its subtree: @@ -287,7 +287,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph animation_sampler_node->animation_name = "animation_library/TestAnimationA"; synced_blend_tree_node->add_node(animation_sampler_node); - REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == synced_blend_tree_node->add_connection(animation_sampler_node, synced_blend_tree_node->get_output_node(), "Input")); + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == synced_blend_tree_node->add_connection(animation_sampler_node, synced_blend_tree_node->get_output_node(), "Output")); synced_blend_tree_node->initialize(synced_animation_graph->get_context()); @@ -329,18 +329,17 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph // Blend2 Ref blend2_node; blend2_node.instantiate(); - blend2_node->name = "Blend2"; + blend2_node->set_name("Blend2"); blend2_node->blend_weight = 0.5; blend2_node->sync = false; synced_blend_tree_node->add_node(blend2_node); // Connect nodes - Vector blend2_inputs; - blend2_node->get_input_names(blend2_inputs); + Vector blend2_inputs = blend2_node->get_input_names(); REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == synced_blend_tree_node->add_connection(animation_sampler_node_a, blend2_node, blend2_inputs[0])); REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == synced_blend_tree_node->add_connection(animation_sampler_node_b, blend2_node, blend2_inputs[1])); - REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == synced_blend_tree_node->add_connection(blend2_node, synced_blend_tree_node->get_output_node(), "Input")); + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == synced_blend_tree_node->add_connection(blend2_node, synced_blend_tree_node->get_output_node(), "Output")); synced_blend_tree_node->initialize(synced_animation_graph->get_context()); @@ -437,7 +436,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph Ref loaded_synced_blend_tree = ResourceLoader::load("synced_blend_tree_node.tres"); REQUIRE(loaded_synced_blend_tree.is_valid()); - Ref loaded_blend2_node = loaded_synced_blend_tree->get_node(loaded_synced_blend_tree->find_node_index_by_name("Blend2")); + Ref loaded_blend2_node = loaded_synced_blend_tree->get_node_by_index(loaded_synced_blend_tree->find_node_index_by_name("Blend2")); REQUIRE(loaded_blend2_node.is_valid()); CHECK(loaded_blend2_node->sync == false); CHECK(loaded_blend2_node->blend_weight == blend2_node->blend_weight); -- 2.47.2 From d3fe4afc57f55a55b7f770f55fa307f3c33da8aa Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Sat, 24 Jan 2026 15:39:12 +0100 Subject: [PATCH 03/29] WIP: BlendTree editor. --- demo/addons/blendalot/blendalot_main_panel.gd | 212 +++++++++++++++++- .../blendalot/blendalot_main_panel.tscn | 75 ++++++- demo/addons/blendalot/blendalot_plugin.gd | 4 + demo/project.godot | 4 + 4 files changed, 287 insertions(+), 8 deletions(-) diff --git a/demo/addons/blendalot/blendalot_main_panel.gd b/demo/addons/blendalot/blendalot_main_panel.gd index 46e7bdf..929733d 100644 --- a/demo/addons/blendalot/blendalot_main_panel.gd +++ b/demo/addons/blendalot/blendalot_main_panel.gd @@ -1,16 +1,222 @@ @tool extends Control +@onready var blend_tree_graph_edit: GraphEdit = %BlendTreeGraphEdit +@onready var file_name_line_edit: LineEdit = %FileNameLineEdit +@onready var tree_option_button: OptionButton = %TreeOptionButton +@onready var add_node_popup_menu: PopupMenu = %AddNodePopupMenu + +var blend_tree:BLTAnimationNodeBlendTree = BLTAnimationNodeBlendTree.new() +var blend_tree_node_to_graph_node = {} +var graph_node_to_blend_tree_node = {} +var selected_nodes = {} +var new_node_position = Vector2.INF + +var registered_nodes = [ + "BLTAnimationNodeSampler", + "BLTAnimationNodeBlend2", + "BLTAnimationNodeBlendTree", + ] # Called when the node enters the scene tree for the first time. func _ready() -> void: - pass # Replace with function body. + for node_name in registered_nodes: + add_node_popup_menu.add_item(node_name) # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta: float) -> void: pass +func create_node_for_blt_node(blt_node: BLTAnimationNode) -> GraphNode: + var result_graph_node:GraphNode = GraphNode.new() + result_graph_node.name = blt_node.resource_name + result_graph_node.title = blt_node.resource_name + result_graph_node.position_offset = blt_node.position + + var result_slot_offset = 0 + + if (blt_node.get_class() != "BLTAnimationNodeOutput"): + result_slot_offset = 1 + var output_slot_label:Label = Label.new() + output_slot_label.text = "Result" + result_graph_node.add_child(output_slot_label) + result_graph_node.set_slot(0, false, 1, Color.WHITE, true, 1, Color.WHITE) + + var inputs = blt_node.get_input_names() + for i in range(len(inputs)): + var slot_label:Label = Label.new() + slot_label.text = inputs[i] + result_graph_node.add_child(slot_label) + result_graph_node.set_slot(i + result_slot_offset, true, 1, Color.WHITE, false, 1, Color.BLACK) + + return result_graph_node -func _on_hit_me_button_pressed() -> void: - print("Hello from the main screen plugin!") + +func _on_add_node_button_pressed() -> void: + blend_tree_graph_edit.add_child(create_node_for_blt_node(BLTAnimationNodeOutput.new())) + blend_tree_graph_edit.add_child(create_node_for_blt_node(BLTAnimationNodeBlend2.new())) + + +func _on_reset_graph_button_pressed() -> void: + _reset_editor() + _update_editor_from_blend_tree() + + +func _reset_editor(): + for child in blend_tree_graph_edit.get_children(): + if child.name == "_connection_layer": + continue + + child.get_parent().remove_child(child) + child.queue_free() + + blend_tree_graph_edit.clear_connections() + + blend_tree = BLTAnimationNodeBlendTree.new() + blend_tree_node_to_graph_node = {} + graph_node_to_blend_tree_node = {} + selected_nodes = {} + + +func _update_editor_nodes_from_blend_tree(): + for node_name in blend_tree.get_node_names(): + var blend_tree_node:BLTAnimationNode = blend_tree.get_node(node_name) + var graph_node:GraphNode = create_node_for_blt_node(blend_tree_node) + blend_tree_graph_edit.add_child(graph_node) + + blend_tree_node_to_graph_node[blend_tree_node] = graph_node + graph_node_to_blend_tree_node[graph_node] = blend_tree_node + + +func _update_editor_connections_from_blend_tree(): + var connection_array = blend_tree.get_connections() + + for i in range(len(connection_array) / 3): + var source_node:BLTAnimationNode = connection_array[i * 3] + var target_node:BLTAnimationNode = connection_array[i * 3 + 1] + var target_port = connection_array[i * 3 + 2] + + var source_graph_node = blend_tree_node_to_graph_node[source_node] + + var connect_result = blend_tree_graph_edit.connect_node(source_node.resource_name, 0, target_node.resource_name, target_node.get_input_index(target_port), true) + + +func _update_editor_from_blend_tree(): + _update_editor_nodes_from_blend_tree() + _update_editor_connections_from_blend_tree() + + +func _on_save_button_pressed() -> void: + ResourceSaver.save(blend_tree, "res://" + file_name_line_edit.text) + + +func _on_load_button_pressed() -> void: + var loaded_blend_tree:BLTAnimationNodeBlendTree = ResourceLoader.load("res://" + file_name_line_edit.text, "BLTAnimationNodeBlendTree", 0) + + if loaded_blend_tree: + _reset_editor() + blend_tree = loaded_blend_tree + _update_editor_from_blend_tree() + + +func _on_instantiate_tree_button_pressed() -> void: + var graph_name = tree_option_button.get_item_text(tree_option_button.selected) + + if graph_name == "AnimationSampler": + _reset_editor() + + var sampler_node:BLTAnimationNodeSampler = BLTAnimationNodeSampler.new() + sampler_node.animation = "SampleLibrary/Idle" + + blend_tree.add_node(sampler_node) + blend_tree.add_connection(sampler_node, blend_tree.get_output_node(), "Output") + + _update_editor_from_blend_tree() + elif graph_name == "Blend2": + _reset_editor() + + var sampler_node_walk:BLTAnimationNodeSampler = BLTAnimationNodeSampler.new() + sampler_node_walk.animation = "SampleLibrary/Walk" + + var sampler_node_run:BLTAnimationNodeSampler = BLTAnimationNodeSampler.new() + sampler_node_run.animation = "SampleLibrary/Run" + + var blend2_node:BLTAnimationNodeBlend2 = BLTAnimationNodeBlend2.new() + + blend_tree.add_node(sampler_node_walk) + blend_tree.add_node(sampler_node_run) + blend_tree.add_node(blend2_node) + + blend_tree.add_connection(blend2_node, blend_tree.get_output_node(), "Output") + + blend_tree.add_connection(sampler_node_walk, blend2_node, "Input0") + blend_tree.add_connection(sampler_node_run, blend2_node, "Input1") + + _update_editor_from_blend_tree() + + +func _on_blend_tree_graph_edit_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void: + print("Trying to connect '%s' port %d to node '%s' port %d" % [from_node, from_port, to_node, to_port]) + + var source_node:BLTAnimationNode = blend_tree.get_node(from_node) + var target_node:BLTAnimationNode = blend_tree.get_node(to_node) + + if target_node == null: + push_error("Invalid connection, target node %s not found." % to_node) + return + + var target_node_port_name = target_node.get_input_names()[to_port] + + var connection_result = blend_tree.is_connection_valid(source_node, target_node, target_node_port_name) + if connection_result != blend_tree.CONNECTION_OK: + push_error("Could not add connection (error %d)" % connection_result) + return + + blend_tree.add_connection(source_node, target_node, target_node_port_name) + + var connect_result = blend_tree_graph_edit.connect_node(from_node, from_port, to_node, to_port, true) + print("graph connect result: " + str(connect_result)) + + print("Success!") + + +func _on_blend_tree_graph_edit_end_node_move() -> void: + for graph_node:GraphNode in selected_nodes.keys(): + graph_node_to_blend_tree_node[graph_node].position = graph_node.position + + +func _on_blend_tree_graph_edit_begin_node_move() -> void: + pass # Replace with function body. + + +func _on_blend_tree_graph_edit_node_selected(node: Node) -> void: + selected_nodes[node] = node + + +func _on_blend_tree_graph_edit_node_deselected(node: Node) -> void: + if selected_nodes.has(node): + selected_nodes.erase(node) + + +func _on_blend_tree_graph_edit_popup_request(at_position: Vector2) -> void: + add_node_popup_menu.position = get_screen_position() + get_local_mouse_position() + add_node_popup_menu.reset_size() + add_node_popup_menu.popup() + new_node_position = at_position + + +func _on_add_node_popup_menu_index_pressed(index: int) -> void: + var new_blend_tree_node: BLTAnimationNode = ClassDB.instantiate(registered_nodes[index]) + blend_tree.add_node(new_blend_tree_node) + var graph_node:GraphNode = create_node_for_blt_node(new_blend_tree_node) + + graph_node_to_blend_tree_node[graph_node] = new_blend_tree_node + blend_tree_node_to_graph_node[new_blend_tree_node] = graph_node + + blend_tree_graph_edit.add_child(graph_node) + + if new_node_position != Vector2.INF: + graph_node.position_offset = new_node_position + + new_node_position = Vector2.INF diff --git a/demo/addons/blendalot/blendalot_main_panel.tscn b/demo/addons/blendalot/blendalot_main_panel.tscn index 796e64c..9ed5e99 100644 --- a/demo/addons/blendalot/blendalot_main_panel.tscn +++ b/demo/addons/blendalot/blendalot_main_panel.tscn @@ -13,7 +13,7 @@ size_flags_horizontal = 3 size_flags_vertical = 3 script = ExtResource("1_427jg") -[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=2044593527] +[node name="BlendTreeEditorContainer" type="VBoxContainer" parent="." unique_id=2044593527] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -21,13 +21,78 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -[node name="HitMeButton" type="Button" parent="VBoxContainer" unique_id=1060776498] +[node name="HBoxContainer" type="HBoxContainer" parent="BlendTreeEditorContainer" unique_id=1742071203] +layout_mode = 2 + +[node name="ResetGraphButton" type="Button" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=1060776498] layout_mode = 2 size_flags_horizontal = 0 -text = "Hit me!" +text = "Reset" -[node name="GraphFrame" type="GraphFrame" parent="VBoxContainer" unique_id=184673233] +[node name="FileNameLineEdit" type="LineEdit" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=2033619565] +unique_name_in_owner = true +custom_minimum_size = Vector2(250, 0) +layout_mode = 2 +text = "editor_test_tree.tres" + +[node name="SaveButton" type="Button" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=597228263] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Save +" + +[node name="LoadButton" type="Button" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=559146765] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Load +" + +[node name="AddNodeButton" type="Button" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=985452697] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Add Node" + +[node name="TreeOptionButton" type="OptionButton" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=760974122] +unique_name_in_owner = true +layout_mode = 2 +selected = 0 +item_count = 2 +popup/item_0/text = "AnimationSampler" +popup/item_0/id = 1 +popup/item_1/text = "Blend2" +popup/item_1/id = 2 + +[node name="InstantiateTreeButton" type="Button" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=127759440] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Instantiate" + +[node name="Panel" type="Panel" parent="BlendTreeEditorContainer" unique_id=424652158] layout_mode = 2 size_flags_vertical = 3 -[connection signal="pressed" from="VBoxContainer/HitMeButton" to="." method="_on_hit_me_button_pressed"] +[node name="AddNodePopupMenu" type="PopupMenu" parent="BlendTreeEditorContainer/Panel" unique_id=2020489213] +unique_name_in_owner = true +oversampling_override = 1.0 + +[node name="BlendTreeGraphEdit" type="GraphEdit" parent="BlendTreeEditorContainer/Panel" unique_id=387715755] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/ResetGraphButton" to="." method="_on_reset_graph_button_pressed"] +[connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/SaveButton" to="." method="_on_save_button_pressed"] +[connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/LoadButton" to="." method="_on_load_button_pressed"] +[connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/AddNodeButton" to="." method="_on_add_node_button_pressed"] +[connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/InstantiateTreeButton" to="." method="_on_instantiate_tree_button_pressed"] +[connection signal="index_pressed" from="BlendTreeEditorContainer/Panel/AddNodePopupMenu" to="." method="_on_add_node_popup_menu_index_pressed"] +[connection signal="begin_node_move" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_begin_node_move"] +[connection signal="connection_request" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_connection_request"] +[connection signal="end_node_move" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_end_node_move"] +[connection signal="node_deselected" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_node_deselected"] +[connection signal="node_selected" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_node_selected"] +[connection signal="popup_request" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_popup_request"] diff --git a/demo/addons/blendalot/blendalot_plugin.gd b/demo/addons/blendalot/blendalot_plugin.gd index 47c4336..f794e23 100644 --- a/demo/addons/blendalot/blendalot_plugin.gd +++ b/demo/addons/blendalot/blendalot_plugin.gd @@ -43,3 +43,7 @@ func _get_plugin_name(): func _get_plugin_icon(): return EditorInterface.get_editor_theme().get_icon("Node", "EditorIcons") + + +func _handles(obj: Object) -> bool: + return obj is BLTAnimationNodeBlendTree diff --git a/demo/project.godot b/demo/project.godot index 69fbb8d..a54ee6f 100644 --- a/demo/project.godot +++ b/demo/project.godot @@ -23,3 +23,7 @@ window/size/viewport_height=1024 [dotnet] project/assembly_name="Synced Blend Tree Test" + +[editor_plugins] + +enabled=PackedStringArray("res://addons/blendalot/plugin.cfg") -- 2.47.2 From 1e7dd4ba45acfccecfdd711650b8a85141979fc7 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Sat, 24 Jan 2026 23:10:40 +0100 Subject: [PATCH 04/29] BlendTree connection can now be removed. --- blendalot_animation_node.cpp | 90 ++++++++++++++++++++++------- blendalot_animation_node.h | 38 +++++++++--- tests/test_synced_animation_graph.h | 75 +++++++++++++++++++++++- 3 files changed, 172 insertions(+), 31 deletions(-) diff --git a/blendalot_animation_node.cpp b/blendalot_animation_node.cpp index 936eb83..fc3d97b 100644 --- a/blendalot_animation_node.cpp +++ b/blendalot_animation_node.cpp @@ -468,7 +468,10 @@ void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::add_node(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) { - ConnectionError result = is_connection_valid(source_node, target_node, target_port_name); - if (result != CONNECTION_OK) { - return result; +void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::remove_subtree_and_update_subtrees_recursive(int node_index, const HashSet &removed_subtree_indices) { + NodeConnectionInfo &connection_info = node_connection_info[node_index]; + + for (int subtree_node_index : removed_subtree_indices) { + connection_info.input_subtree_node_indices.erase(subtree_node_index); } - 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_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(BLTBlendTreeConnection{ source_node, target_node, target_port_name }); - - add_index_and_update_subtrees_recursive(source_node_index, target_node_index); - - return CONNECTION_OK; + if (connection_info.parent_node_index != -1) { + remove_subtree_and_update_subtrees_recursive(connection_info.parent_node_index, removed_subtree_indices); + } } BLTAnimationNodeBlendTree::ConnectionError BLTAnimationNodeBlendTree::BLTBlendTreeGraph::is_connection_valid(const Ref &source_node, const Ref &target_node, StringName target_port_name) const { @@ -580,3 +576,53 @@ BLTAnimationNodeBlendTree::ConnectionError BLTAnimationNodeBlendTree::BLTBlendTr return CONNECTION_OK; } + +BLTAnimationNodeBlendTree::ConnectionError BLTAnimationNodeBlendTree::BLTBlendTreeGraph::add_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) { + ConnectionError result = is_connection_valid(source_node, target_node, target_port_name); + if (result != CONNECTION_OK) { + return result; + } + + 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_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(BLTBlendTreeConnection{ source_node, target_node, target_port_name }); + + add_index_and_update_subtrees_recursive(source_node_index, target_node_index); + + return CONNECTION_OK; +} + +int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_connection_index(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) const { + for (uint32_t i = 0; i < connections.size(); i++) { + if (connections[i].source_node == source_node && connections[i].target_node == target_node && connections[i].target_port_name == target_port_name) { + return i; + } + } + + return -1; +} + +BLTAnimationNodeBlendTree::ConnectionError BLTAnimationNodeBlendTree::BLTBlendTreeGraph::remove_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) { + int source_node_index = find_node_index(source_node); + NodeConnectionInfo &connection_info = node_connection_info[source_node_index]; + + if (connection_info.parent_node_index != -1) { + NodeConnectionInfo &parent_connection_info = node_connection_info[connection_info.parent_node_index]; + parent_connection_info.input_subtree_node_indices.erase(source_node_index); + parent_connection_info.connected_child_node_index_at_port[target_node->get_input_index(target_port_name)] = -1; + + remove_subtree_and_update_subtrees_recursive(connection_info.parent_node_index, connection_info.input_subtree_node_indices); + + connection_info.parent_node_index = -1; + + uint32_t connection_index = find_connection_index(source_node, target_node, target_port_name); + assert(connection_index >= 0); + connections.remove_at(connection_index); + } + + return CONNECTION_OK; +} diff --git a/blendalot_animation_node.h b/blendalot_animation_node.h index 3bea3a7..d9f24c5 100644 --- a/blendalot_animation_node.h +++ b/blendalot_animation_node.h @@ -441,9 +441,9 @@ private: }; struct BLTBlendTreeConnection { - const Ref source_node = nullptr; - const Ref target_node = nullptr; - const StringName target_port_name = ""; + Ref source_node = nullptr; + Ref target_node = nullptr; + StringName target_port_name = ""; }; class BLTAnimationNodeBlendTree : public BLTAnimationNode { @@ -495,7 +495,19 @@ public: } } - void _print_subtree() const; + void _print_subtree() const { + String result = vformat("subtree node indices (%d): ", input_subtree_node_indices.size()); + bool is_first = true; + for (int index : input_subtree_node_indices) { + if (is_first) { + result += vformat("%d", index); + is_first = false; + } else { + result += vformat(", %d", index); + } + } + print_line(result); + } }; Vector> nodes; // All added nodes @@ -505,17 +517,20 @@ public: BLTBlendTreeGraph(); Ref get_output_node(); - int find_node_index(const Ref &node) const; int find_node_index_by_name(const StringName &name) const; void sort_nodes_and_references(); LocalVector get_sorted_node_indices(); void sort_nodes_recursive(int node_index, LocalVector &result); - void add_index_and_update_subtrees_recursive(int node, int node_parent); - ConnectionError is_connection_valid(const Ref &source_node, const Ref &target_node, StringName target_port_name) const; + void add_index_and_update_subtrees_recursive(int node_index, int node_parent_index); + void remove_subtree_and_update_subtrees_recursive(int node, const HashSet &removed_subtree_indices); void add_node(const Ref &node); + + ConnectionError is_connection_valid(const Ref &source_node, const Ref &target_node, StringName target_port_name) const; ConnectionError add_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name); + int find_connection_index(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) const; + ConnectionError remove_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name); }; private: @@ -638,6 +653,15 @@ public: return tree_graph.add_connection(source_node, target_node, target_port_name); } + ConnectionError remove_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) { + if (tree_initialized) { + print_error("Cannot remove connection to BlendTree: BlendTree already initialized."); + return CONNECTION_ERROR_GRAPH_ALREADY_INITIALIZED; + } + + return tree_graph.remove_connection(source_node, target_node, target_port_name); + } + Array get_connections_as_array() const { Array result; for (const BLTBlendTreeConnection &connection : tree_graph.connections) { diff --git a/tests/test_synced_animation_graph.h b/tests/test_synced_animation_graph.h index 94ef779..1b272ff 100644 --- a/tests/test_synced_animation_graph.h +++ b/tests/test_synced_animation_graph.h @@ -213,10 +213,10 @@ TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") { tree_constructor.sort_nodes_and_references(); - // 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 (i is part of the subtree) 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) { - CHECK(input_index > i); + CHECK(input_index >= i); } } } @@ -455,4 +455,75 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph } } +TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph][BlendTreeGraph][ChangeConnectivity] BlendTreeGraph with various nodes and connections that are removed") { + BLTAnimationNodeBlendTree::BLTBlendTreeGraph blend_tree_graph; + + // TestAnimationA + Ref animation_sampler_node_a; + animation_sampler_node_a.instantiate(); + animation_sampler_node_a->animation_name = "animation_library/TestAnimationA"; + + blend_tree_graph.add_node(animation_sampler_node_a); + + // TestAnimationB + Ref animation_sampler_node_b; + animation_sampler_node_b.instantiate(); + animation_sampler_node_b->animation_name = "animation_library/TestAnimationB"; + + blend_tree_graph.add_node(animation_sampler_node_b); + + // TestAnimationB + Ref animation_sampler_node_c; + animation_sampler_node_c.instantiate(); + animation_sampler_node_c->animation_name = "animation_library/TestAnimationC"; + + blend_tree_graph.add_node(animation_sampler_node_c); + + // Blend2A + Ref blend2_node_a; + blend2_node_a.instantiate(); + blend2_node_a->set_name("Blend2A"); + blend2_node_a->blend_weight = 0.5; + blend2_node_a->sync = false; + + blend_tree_graph.add_node(blend2_node_a); + + // Blend2B + Ref blend2_node_b; + blend2_node_b.instantiate(); + blend2_node_b->set_name("Blend2A"); + blend2_node_b->blend_weight = 0.5; + blend2_node_b->sync = false; + + blend_tree_graph.add_node(blend2_node_b); + + // Connect nodes: Subgraph Output, Blend2A, SamplerA + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(blend2_node_a, blend_tree_graph.get_output_node(), "Output")); + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(animation_sampler_node_a, blend2_node_a, "Input0")); + + // Connect nodes: Subgraph Blend2A, SamplerB, SamplerB + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(animation_sampler_node_b, blend2_node_b, "Input0")); + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(animation_sampler_node_c, blend2_node_b, "Input1")); + + HashSet subgraph_output_initial = blend_tree_graph.node_connection_info[0].input_subtree_node_indices; + HashSet subgraph_blend2a_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_a)].input_subtree_node_indices; + HashSet subgraph_blend2b_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_b)].input_subtree_node_indices; + + // Add and remove connection + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(blend2_node_b, blend2_node_a, "Input1")); + blend_tree_graph.remove_connection(blend2_node_b, blend2_node_a, "Input1"); + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.is_connection_valid(blend2_node_b, blend2_node_a, "Input1")); + + // Check that we have the same subgraphs as before the connection + CHECK(subgraph_output_initial == blend_tree_graph.node_connection_info[0].input_subtree_node_indices); + CHECK(subgraph_blend2a_initial == blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_a)].input_subtree_node_indices); + CHECK(subgraph_blend2b_initial == blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_b)].input_subtree_node_indices); + + // Check that we also do not + for (const BLTBlendTreeConnection &connection : blend_tree_graph.connections) { + bool connection_equals_removed_connection = connection.source_node == blend2_node_b && connection.target_node == blend2_node_a && connection.target_port_name == "Input1"; + CHECK(connection_equals_removed_connection == false); + } +} + } //namespace TestSyncedAnimationGraph \ No newline at end of file -- 2.47.2 From 4c428a865acdf2b21a392fedb7308aaf8728bef0 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Sun, 25 Jan 2026 01:31:02 +0100 Subject: [PATCH 05/29] BlendTree nodes can now be removed. --- blendalot_animation_graph.cpp | 2 +- blendalot_animation_node.cpp | 42 ++++++++++++++++++- blendalot_animation_node.h | 28 +++++++++---- tests/test_synced_animation_graph.h | 65 ++++++++++++++++++++++------- 4 files changed, 110 insertions(+), 27 deletions(-) diff --git a/blendalot_animation_graph.cpp b/blendalot_animation_graph.cpp index 53a013c..ee3921b 100644 --- a/blendalot_animation_graph.cpp +++ b/blendalot_animation_graph.cpp @@ -23,7 +23,7 @@ void BLTAnimationGraph::_bind_methods() { ClassDB::bind_method(D_METHOD("set_tree_root", "animation_node"), &BLTAnimationGraph::set_root_animation_node); ClassDB::bind_method(D_METHOD("get_tree_root"), &BLTAnimationGraph::get_root_animation_node); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "tree_root", PROPERTY_HINT_RESOURCE_TYPE, "SyncedAnimationNode"), "set_tree_root", "get_tree_root"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "tree_root", PROPERTY_HINT_RESOURCE_TYPE, "BLTAnimationNode"), "set_tree_root", "get_tree_root"); ClassDB::bind_method(D_METHOD("set_skeleton", "skeleton"), &BLTAnimationGraph::set_skeleton); ClassDB::bind_method(D_METHOD("get_skeleton"), &BLTAnimationGraph::get_skeleton); diff --git a/blendalot_animation_node.cpp b/blendalot_animation_node.cpp index fc3d97b..05cc9b3 100644 --- a/blendalot_animation_node.cpp +++ b/blendalot_animation_node.cpp @@ -50,12 +50,14 @@ void BLTAnimationNode::_animation_node_removed(const ObjectID &p_oid, const Stri void BLTAnimationNodeBlendTree::_bind_methods() { ClassDB::bind_method(D_METHOD("add_node", "animation_node"), &BLTAnimationNodeBlendTree::add_node); + ClassDB::bind_method(D_METHOD("remove_node", "animation_node"), &BLTAnimationNodeBlendTree::remove_node); ClassDB::bind_method(D_METHOD("get_node", "node_name"), &BLTAnimationNodeBlendTree::get_node); ClassDB::bind_method(D_METHOD("get_output_node"), &BLTAnimationNodeBlendTree::get_output_node); ClassDB::bind_method(D_METHOD("get_node_names"), &BLTAnimationNodeBlendTree::get_node_names_as_typed_array); ClassDB::bind_method(D_METHOD("is_connection_valid", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::is_connection_valid); ClassDB::bind_method(D_METHOD("add_connection", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::add_connection); + ClassDB::bind_method(D_METHOD("remove_connection", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::remove_connection); ClassDB::bind_method(D_METHOD("get_connections"), &BLTAnimationNodeBlendTree::get_connections_as_array); BIND_CONSTANT(CONNECTION_OK); @@ -435,7 +437,7 @@ Ref BLTAnimationNodeBlendTree::BLTBlendTreeGraph::get_output_n } int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_node_index(const Ref &node) const { - for (int i = 0; i < nodes.size(); i++) { + for (uint32_t i = 0; i < nodes.size(); i++) { if (nodes[i] == node) { return i; } @@ -445,7 +447,7 @@ int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_node_index(const Refget_name() == name) { return i; } @@ -474,6 +476,42 @@ void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::add_node(const Ref &node) { + int removed_node_index = find_node_index(node); + assert(removed_node_index >= 0); + + // Remove all connections to and from this node + for (uint32_t i = connections.size() - 1; i > 0; i--) { + if (connections[i].source_node == node || connections[i].target_node == node) { + remove_connection(connections[i].source_node, connections[i].target_node, connections[i].target_port_name); + } + } + + // Remove the data directly related to this node + node_connection_info.remove_at(removed_node_index); + nodes.remove_at(removed_node_index); + + // Ensure all indices are cleaned up. + for (NodeConnectionInfo &connection_info : node_connection_info) { + for (unsigned int j = 0; j < connection_info.connected_child_node_index_at_port.size(); j++) { + if (connection_info.connected_child_node_index_at_port[j] > removed_node_index) { + connection_info.connected_child_node_index_at_port[j] = connection_info.connected_child_node_index_at_port[j] - 1; + } + } + + // Map connected subtrees + HashSet old_indices = connection_info.input_subtree_node_indices; + connection_info.input_subtree_node_indices.clear(); + for (int old_index : old_indices) { + if (old_index > removed_node_index) { + connection_info.input_subtree_node_indices.insert(old_index - 1); + } else { + connection_info.input_subtree_node_indices.insert(old_index); + } + } + } +} + void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::sort_nodes_and_references() { LocalVector sorted_node_indices = get_sorted_node_indices(); diff --git a/blendalot_animation_node.h b/blendalot_animation_node.h index d9f24c5..5c6cb2a 100644 --- a/blendalot_animation_node.h +++ b/blendalot_animation_node.h @@ -510,7 +510,7 @@ public: } }; - Vector> nodes; // All added nodes + LocalVector> nodes; // All added nodes LocalVector node_connection_info; LocalVector connections; @@ -526,6 +526,7 @@ public: void remove_subtree_and_update_subtrees_recursive(int node, const HashSet &removed_subtree_indices); void add_node(const Ref &node); + void remove_node(const Ref &node); ConnectionError is_connection_valid(const Ref &source_node, const Ref &target_node, StringName target_port_name) const; ConnectionError add_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name); @@ -544,7 +545,7 @@ private: void setup_runtime_data() { // Add nodes and allocate runtime data - for (int i = 0; i < tree_graph.nodes.size(); i++) { + for (uint32_t i = 0; i < tree_graph.nodes.size(); i++) { const Ref node = tree_graph.nodes[i]; NodeRuntimeData node_runtime_data; @@ -557,7 +558,7 @@ private: } // Populate runtime data (only now is this.nodes populated to retrieve the nodes) - for (int i = 0; i < tree_graph.nodes.size(); i++) { + for (uint32_t i = 0; i < tree_graph.nodes.size(); i++) { Ref node = tree_graph.nodes[i]; NodeRuntimeData &node_runtime_data = _node_runtime_data[i]; @@ -599,6 +600,15 @@ public: tree_graph.add_node(node); } + void remove_node(const Ref &node) { + if (tree_initialized) { + print_error("Cannot remove node from BlendTree: BlendTree already initialized."); + return; + } + + tree_graph.remove_node(node); + } + TypedArray get_node_names_as_typed_array() const { Vector vec; for (const Ref &node : tree_graph.nodes) { @@ -624,7 +634,7 @@ public: } Ref get_node_by_index(int node_index) const { - if (node_index < 0 || node_index > tree_graph.nodes.size()) { + if (node_index < 0 || node_index > static_cast(tree_graph.nodes.size())) { return nullptr; } @@ -673,7 +683,7 @@ public: return result; } - // overrides from SyncedAnimationNode + // overrides from BLTAnimationNode bool initialize(GraphEvaluationContext &context) override { if (!BLTAnimationNode::initialize(context)) { return false; @@ -698,7 +708,7 @@ public: GodotProfileZone("SyncedBlendTree::activate_inputs"); tree_graph.nodes[0]->active = true; - for (int i = 0; i < tree_graph.nodes.size(); i++) { + for (uint32_t i = 0; i < tree_graph.nodes.size(); i++) { const Ref &node = tree_graph.nodes[i]; if (!node->active) { @@ -712,7 +722,7 @@ public: void calculate_sync_track(const Vector> &input_nodes) override { GodotProfileZone("SyncedBlendTree::calculate_sync_track"); - for (int i = tree_graph.nodes.size() - 1; i > 0; i--) { + for (uint32_t i = tree_graph.nodes.size() - 1; i > 0; i--) { const Ref &node = tree_graph.nodes[i]; if (!node->active) { @@ -731,7 +741,7 @@ public: tree_graph.nodes[0]->node_time_info.delta = p_delta; tree_graph.nodes[0]->node_time_info.position += p_delta; - for (int i = 1; i < tree_graph.nodes.size(); i++) { + for (uint32_t i = 1; i < tree_graph.nodes.size(); i++) { const Ref &node = tree_graph.nodes[i]; if (!node->active) { @@ -751,7 +761,7 @@ public: void evaluate(GraphEvaluationContext &context, const LocalVector &input_datas, AnimationData &output_data) override { ZoneScopedN("SyncedBlendTree::evaluate"); - for (int i = tree_graph.nodes.size() - 1; i > 0; i--) { + for (uint32_t i = tree_graph.nodes.size() - 1; i > 0; i--) { const Ref &node = tree_graph.nodes[i]; if (!node->active) { diff --git a/tests/test_synced_animation_graph.h b/tests/test_synced_animation_graph.h index 1b272ff..ac97d2a 100644 --- a/tests/test_synced_animation_graph.h +++ b/tests/test_synced_animation_graph.h @@ -505,24 +505,59 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(animation_sampler_node_b, blend2_node_b, "Input0")); REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(animation_sampler_node_c, blend2_node_b, "Input1")); - HashSet subgraph_output_initial = blend_tree_graph.node_connection_info[0].input_subtree_node_indices; - HashSet subgraph_blend2a_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_a)].input_subtree_node_indices; - HashSet subgraph_blend2b_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_b)].input_subtree_node_indices; + SUBCASE("Add and remove a connection") { + HashSet subgraph_output_initial = blend_tree_graph.node_connection_info[0].input_subtree_node_indices; + HashSet subgraph_blend2a_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_a)].input_subtree_node_indices; + HashSet subgraph_blend2b_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_b)].input_subtree_node_indices; - // Add and remove connection - REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(blend2_node_b, blend2_node_a, "Input1")); - blend_tree_graph.remove_connection(blend2_node_b, blend2_node_a, "Input1"); - REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.is_connection_valid(blend2_node_b, blend2_node_a, "Input1")); + // Add and remove connection + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(blend2_node_b, blend2_node_a, "Input1")); + blend_tree_graph.remove_connection(blend2_node_b, blend2_node_a, "Input1"); + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.is_connection_valid(blend2_node_b, blend2_node_a, "Input1")); - // Check that we have the same subgraphs as before the connection - CHECK(subgraph_output_initial == blend_tree_graph.node_connection_info[0].input_subtree_node_indices); - CHECK(subgraph_blend2a_initial == blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_a)].input_subtree_node_indices); - CHECK(subgraph_blend2b_initial == blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_b)].input_subtree_node_indices); + // Check that we have the same subgraphs as before the connection + CHECK(subgraph_output_initial == blend_tree_graph.node_connection_info[0].input_subtree_node_indices); + CHECK(subgraph_blend2a_initial == blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_a)].input_subtree_node_indices); + CHECK(subgraph_blend2b_initial == blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_b)].input_subtree_node_indices); - // Check that we also do not - for (const BLTBlendTreeConnection &connection : blend_tree_graph.connections) { - bool connection_equals_removed_connection = connection.source_node == blend2_node_b && connection.target_node == blend2_node_a && connection.target_port_name == "Input1"; - CHECK(connection_equals_removed_connection == false); + // Check that we also do not + for (const BLTBlendTreeConnection &connection : blend_tree_graph.connections) { + bool connection_equals_removed_connection = connection.source_node == blend2_node_b && connection.target_node == blend2_node_a && connection.target_port_name == "Input1"; + CHECK(connection_equals_removed_connection == false); + } + } + + SUBCASE("Remove a node") { + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(blend2_node_b, blend2_node_a, "Input1")); + + int animation_sampler_node_b_index_pre_remove = blend_tree_graph.find_node_index(animation_sampler_node_b); + int blend2_node_a_index_pre_remove = blend_tree_graph.find_node_index(blend2_node_a); + int blend2_node_b_index_pre_remove = blend_tree_graph.find_node_index(blend2_node_b); + + CHECK(blend_tree_graph.node_connection_info[0].input_subtree_node_indices.size() == 6); + CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_pre_remove].input_subtree_node_indices.size() == 5); + + blend_tree_graph.remove_node(animation_sampler_node_a); + + for (const BLTBlendTreeConnection &connection : blend_tree_graph.connections) { + bool is_connection_with_removed_node = connection.source_node == animation_sampler_node_a || connection.target_node == animation_sampler_node_a; + CHECK(is_connection_with_removed_node == false); + } + + int animation_sampler_node_b_index_post_remove = blend_tree_graph.find_node_index(animation_sampler_node_b); + int blend2_node_a_index_post_remove = blend_tree_graph.find_node_index(blend2_node_a); + int blend2_node_b_index_post_remove = blend_tree_graph.find_node_index(blend2_node_b); + + CHECK(blend_tree_graph.find_node_index(animation_sampler_node_a) == -1); + CHECK(blend2_node_b_index_post_remove == blend2_node_b_index_pre_remove - 1); + CHECK(animation_sampler_node_b_index_post_remove == animation_sampler_node_b_index_pre_remove - 1); + + CHECK(blend_tree_graph.node_connection_info[0].input_subtree_node_indices.size() == 5); + CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_post_remove].input_subtree_node_indices.size() == 4); + CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_post_remove].connected_child_node_index_at_port[0] == -1); + CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_post_remove].connected_child_node_index_at_port[1] == blend2_node_b_index_post_remove); + CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(blend2_node_b_index_post_remove)); + CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(animation_sampler_node_b_index_post_remove)); } } -- 2.47.2 From 50243eafbab9fd680976ce072aacbfcfa67f1fe9 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Wed, 28 Jan 2026 21:05:16 +0100 Subject: [PATCH 06/29] Minor additional work on the BlendTreeEditor. --- demo/addons/blendalot/blendalot_main_panel.gd | 55 ++++++++++++++++++- .../blendalot/blendalot_main_panel.tscn | 12 +++- demo/addons/blendalot/blendalot_plugin.gd | 9 ++- demo/main.gd | 13 ----- demo/main.tscn | 18 ++++-- demo/project.godot | 2 +- 6 files changed, 86 insertions(+), 23 deletions(-) diff --git a/demo/addons/blendalot/blendalot_main_panel.gd b/demo/addons/blendalot/blendalot_main_panel.gd index 929733d..627e92e 100644 --- a/demo/addons/blendalot/blendalot_main_panel.gd +++ b/demo/addons/blendalot/blendalot_main_panel.gd @@ -1,4 +1,5 @@ @tool +class_name BlendalotMainPanel extends Control @onready var blend_tree_graph_edit: GraphEdit = %BlendTreeGraphEdit @@ -20,6 +21,8 @@ var registered_nodes = [ # Called when the node enters the scene tree for the first time. func _ready() -> void: + add_node_popup_menu.clear(true) + for node_name in registered_nodes: add_node_popup_menu.add_item(node_name) @@ -61,6 +64,9 @@ func _on_add_node_button_pressed() -> void: func _on_reset_graph_button_pressed() -> void: _reset_editor() _update_editor_from_blend_tree() + + var graph_rect:Rect2 = blend_tree_graph_edit.get_rect() + blend_tree_graph_edit.scroll_offset = graph_rect.size * -0.5 - Vector2(200,0) func _reset_editor(): @@ -79,6 +85,15 @@ func _reset_editor(): selected_nodes = {} +func edit_blend_tree(blend_tree_animation_node:BLTAnimationNode): + print("Starting to edit blend_tree_animation_node " + str(blend_tree_animation_node)) + print("Owner: %s" % blend_tree_animation_node) + _reset_editor() + blend_tree = blend_tree_animation_node + + _update_editor_nodes_from_blend_tree() + _update_editor_connections_from_blend_tree() + func _update_editor_nodes_from_blend_tree(): for node_name in blend_tree.get_node_names(): var blend_tree_node:BLTAnimationNode = blend_tree.get_node(node_name) @@ -190,8 +205,9 @@ func _on_blend_tree_graph_edit_begin_node_move() -> void: pass # Replace with function body. -func _on_blend_tree_graph_edit_node_selected(node: Node) -> void: - selected_nodes[node] = node +func _on_blend_tree_graph_edit_node_selected(graph_node: Node) -> void: + selected_nodes[graph_node] = graph_node + EditorInterface.get_inspector().edit(graph_node_to_blend_tree_node[graph_node]) func _on_blend_tree_graph_edit_node_deselected(node: Node) -> void: @@ -203,7 +219,7 @@ func _on_blend_tree_graph_edit_popup_request(at_position: Vector2) -> void: add_node_popup_menu.position = get_screen_position() + get_local_mouse_position() add_node_popup_menu.reset_size() add_node_popup_menu.popup() - new_node_position = at_position + new_node_position = blend_tree_graph_edit.scroll_offset + at_position func _on_add_node_popup_menu_index_pressed(index: int) -> void: @@ -220,3 +236,36 @@ func _on_add_node_popup_menu_index_pressed(index: int) -> void: graph_node.position_offset = new_node_position new_node_position = Vector2.INF + + +func _on_blend_tree_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void: + for node_name:StringName in nodes: + print("remove node '%s'" % node_name) + var blend_tree_node:BLTAnimationNode = blend_tree.get_node(node_name) + if blend_tree_node == null: + push_error("Cannot delete node '%s': node not found." % node_name) + continue + + var graph_node:GraphNode = blend_tree_node_to_graph_node[blend_tree_node] + blend_tree.remove_node(blend_tree_node) + blend_tree_node_to_graph_node.erase(blend_tree_node) + + graph_node_to_blend_tree_node.erase(graph_node) + blend_tree_graph_edit.remove_child(graph_node) + graph_node.queue_free() + + blend_tree_graph_edit.clear_connections() + + if node_name in selected_nodes.keys(): + selected_nodes.erase(node_name) + + +func _on_blend_tree_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void: + print("removing connection") + + var blend_tree_source_node = blend_tree.get_node(from_node) + var blend_tree_target_node = blend_tree.get_node(to_node) + var target_port_name = blend_tree_target_node.get_input_names()[to_port] + blend_tree.remove_connection(blend_tree_source_node, blend_tree_target_node, target_port_name) + + blend_tree_graph_edit.disconnect_node(from_node, from_port, to_node, to_port) diff --git a/demo/addons/blendalot/blendalot_main_panel.tscn b/demo/addons/blendalot/blendalot_main_panel.tscn index 9ed5e99..78b70ad 100644 --- a/demo/addons/blendalot/blendalot_main_panel.tscn +++ b/demo/addons/blendalot/blendalot_main_panel.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=2 format=3 uid="uid://31c6depvs0y1"] +[gd_scene format=3 uid="uid://31c6depvs0y1"] [ext_resource type="Script" uid="uid://dvulvuytt81lw" path="res://addons/blendalot/blendalot_main_panel.gd" id="1_427jg"] @@ -74,6 +74,13 @@ size_flags_vertical = 3 [node name="AddNodePopupMenu" type="PopupMenu" parent="BlendTreeEditorContainer/Panel" unique_id=2020489213] unique_name_in_owner = true oversampling_override = 1.0 +item_count = 3 +item_0/text = "BLTAnimationNodeSampler" +item_0/id = 0 +item_1/text = "BLTAnimationNodeBlend2" +item_1/id = 1 +item_2/text = "BLTAnimationNodeBlendTree" +item_2/id = 2 [node name="BlendTreeGraphEdit" type="GraphEdit" parent="BlendTreeEditorContainer/Panel" unique_id=387715755] unique_name_in_owner = true @@ -83,6 +90,7 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +right_disconnects = true [connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/ResetGraphButton" to="." method="_on_reset_graph_button_pressed"] [connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/SaveButton" to="." method="_on_save_button_pressed"] @@ -92,6 +100,8 @@ grow_vertical = 2 [connection signal="index_pressed" from="BlendTreeEditorContainer/Panel/AddNodePopupMenu" to="." method="_on_add_node_popup_menu_index_pressed"] [connection signal="begin_node_move" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_begin_node_move"] [connection signal="connection_request" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_connection_request"] +[connection signal="delete_nodes_request" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_delete_nodes_request"] +[connection signal="disconnection_request" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_disconnection_request"] [connection signal="end_node_move" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_end_node_move"] [connection signal="node_deselected" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_node_deselected"] [connection signal="node_selected" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_node_selected"] diff --git a/demo/addons/blendalot/blendalot_plugin.gd b/demo/addons/blendalot/blendalot_plugin.gd index f794e23..24572d3 100644 --- a/demo/addons/blendalot/blendalot_plugin.gd +++ b/demo/addons/blendalot/blendalot_plugin.gd @@ -3,7 +3,7 @@ extends EditorPlugin const MainPanel = preload("res://addons/blendalot/blendalot_main_panel.tscn") -var main_panel_instance +var main_panel_instance:BlendalotMainPanel func _enable_plugin() -> void: # Add autoloads here. @@ -47,3 +47,10 @@ func _get_plugin_icon(): func _handles(obj: Object) -> bool: return obj is BLTAnimationNodeBlendTree + +func _edit(object: Object): + if object is BLTAnimationNodeBlendTree: + main_panel_instance.edit_blend_tree(object) + return + + print("Cannot (yet) edit object " + str(object)) diff --git a/demo/main.gd b/demo/main.gd index 9934242..d4963cc 100644 --- a/demo/main.gd +++ b/demo/main.gd @@ -12,19 +12,6 @@ extends Node3D func _ready() -> void: blend_weight_slider.value = 0.5 - var blend_tree: BLTAnimationNodeBlendTree = BLTAnimationNodeBlendTree.new() - var output_node: BLTAnimationNodeOutput = blend_tree.get_output_node() - var sampler_node_1: BLTAnimationNodeSampler = BLTAnimationNodeSampler.new() - - sampler_node_1.animation = "animation_library/Walk-InPlace" - - blend_tree.add_node(sampler_node_1) - var result = blend_tree.add_connection(sampler_node_1, output_node, "Input") - var anim_graph: BLTAnimationGraph = mixamo_amy_walk_run_synced.get_node("SyncedAnimationGraph") - - anim_graph.tree_root = blend_tree - - # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta: float) -> void: pass diff --git a/demo/main.tscn b/demo/main.tscn index 12222c3..1d1932f 100644 --- a/demo/main.tscn +++ b/demo/main.tscn @@ -1,11 +1,10 @@ -[gd_scene load_steps=14 format=3 uid="uid://svj53e2xoio"] +[gd_scene format=3 uid="uid://svj53e2xoio"] [ext_resource type="PackedScene" uid="uid://d1xcqdqr1qeu6" path="res://assets/MixamoAmy.glb" id="1_0xm2m"] [ext_resource type="Script" uid="uid://bjvgqujpqumj7" path="res://main.gd" id="1_1bvp3"] [ext_resource type="AnimationLibrary" uid="uid://dwubn740aqx51" path="res://animation_library.res" id="3_1bvp3"] [ext_resource type="AnimationNodeBlendTree" uid="uid://dqy0dgwsm8t46" path="res://animation_tree_walk_limp.tres" id="3_272bh"] [ext_resource type="BLTAnimationNodeBlendTree" uid="uid://2qfwr1xkiw0s" path="res://synced_blend_tree_walk_limp.tres" id="4_lquwl"] -[ext_resource type="BLTAnimationNodeBlendTree" uid="uid://qsk64ax2o47f" path="res://synced_blend_tree_walk_run.tres" id="5_7mycd"] [ext_resource type="AnimationNodeBlendTree" uid="uid://vsf71o82lkld" path="res://animation_tree_walk_run.tres" id="6_5vw27"] [sub_resource type="Theme" id="Theme_272bh"] @@ -32,6 +31,17 @@ sky = SubResource("Sky_1bvp3") tonemap_mode = 2 glow_enabled = true +[sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_7mycd"] +resource_name = "BLTAnimationNodeSampler" +position = Vector2(149.47485, 361.29053) +animation = &"animation_library/Walk-InPlace" + +[sub_resource type="BLTAnimationNodeBlendTree" id="BLTAnimationNodeBlendTree_272bh"] +nodes/Output/position = Vector2(602.0149, 281.4305) +nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_7mycd") +nodes/BLTAnimationNodeSampler/position = Vector2(149.47485, 361.29053) +node_connections = ["Output", 0, "BLTAnimationNodeSampler"] + [node name="Main" type="Node3D" unique_id=933302313] script = ExtResource("1_1bvp3") @@ -156,13 +166,13 @@ unique_name_in_owner = true transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4, 0, 0) [node name="AnimationPlayer2" type="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" unique_id=1255239074] +active = false libraries/animation_library = ExtResource("3_1bvp3") [node name="SyncedAnimationGraph" type="BLTAnimationGraph" parent="Characters/MixamoAmyWalkRunSynced" unique_id=1602406394] animation_player = NodePath("../AnimationPlayer2") -tree_root = ExtResource("5_7mycd") +tree_root = SubResource("BLTAnimationNodeBlendTree_272bh") skeleton = NodePath("../Armature/Skeleton3D") -parameters/BLTAnimationNodeBlend2/blend_amount = 0.4 [connection signal="value_changed" from="UI/MarginContainer/HBoxContainer/BlendWeightSlider" to="." method="_on_blend_weight_slider_value_changed"] diff --git a/demo/project.godot b/demo/project.godot index a54ee6f..6e39ec1 100644 --- a/demo/project.godot +++ b/demo/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="Synced Blend Tree Test" run/main_scene="uid://svj53e2xoio" -config/features=PackedStringArray("4.5", "Forward Plus") +config/features=PackedStringArray("4.6", "Forward Plus") config/icon="res://icon.svg" [display] -- 2.47.2 From 89c3c387571a0d4f7942cf2176a97684fa952efc Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Thu, 29 Jan 2026 23:21:38 +0100 Subject: [PATCH 07/29] Made BlendTree Editor more robust. --- blendalot_animation_node.cpp | 7 ++- demo/addons/blendalot/blendalot_main_panel.gd | 31 +++++++--- doc/design.md | 2 +- tests/test_synced_animation_graph.h | 60 +++++++++++++------ 4 files changed, 73 insertions(+), 27 deletions(-) diff --git a/blendalot_animation_node.cpp b/blendalot_animation_node.cpp index 05cc9b3..5afb8a3 100644 --- a/blendalot_animation_node.cpp +++ b/blendalot_animation_node.cpp @@ -477,11 +477,16 @@ void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::add_node(const Ref &node) { + if (node == get_output_node()) { + // Output node not allowed to be removed + return; + } + int removed_node_index = find_node_index(node); assert(removed_node_index >= 0); // Remove all connections to and from this node - for (uint32_t i = connections.size() - 1; i > 0; i--) { + for (int i = static_cast(connections.size()) - 1; i >= 0; i--) { if (connections[i].source_node == node || connections[i].target_node == node) { remove_connection(connections[i].source_node, connections[i].target_node, connections[i].target_port_name); } diff --git a/demo/addons/blendalot/blendalot_main_panel.gd b/demo/addons/blendalot/blendalot_main_panel.gd index 627e92e..b6e54b7 100644 --- a/demo/addons/blendalot/blendalot_main_panel.gd +++ b/demo/addons/blendalot/blendalot_main_panel.gd @@ -210,9 +210,9 @@ func _on_blend_tree_graph_edit_node_selected(graph_node: Node) -> void: EditorInterface.get_inspector().edit(graph_node_to_blend_tree_node[graph_node]) -func _on_blend_tree_graph_edit_node_deselected(node: Node) -> void: - if selected_nodes.has(node): - selected_nodes.erase(node) +func _on_blend_tree_graph_edit_node_deselected(graph_node: Node) -> void: + if selected_nodes.has(graph_node): + selected_nodes.erase(graph_node) func _on_blend_tree_graph_edit_popup_request(at_position: Vector2) -> void: @@ -238,26 +238,41 @@ func _on_add_node_popup_menu_index_pressed(index: int) -> void: new_node_position = Vector2.INF +func _blend_tree_graph_edit_remove_node_connections(graph_node:GraphNode): + var node_connections:Array = [] + + for connection:Dictionary in blend_tree_graph_edit.connections: + if connection["from_node"] == graph_node.name or connection["to_node"] == graph_node.name: + node_connections.append(connection) + + for node_connection:Dictionary in node_connections: + print("Removing connection %s" % str(node_connection)) + blend_tree_graph_edit.disconnect_node(node_connection["from_node"], node_connection["from_port"], node_connection["to_node"], node_connection["to_port"]) + + func _on_blend_tree_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void: for node_name:StringName in nodes: print("remove node '%s'" % node_name) var blend_tree_node:BLTAnimationNode = blend_tree.get_node(node_name) + if blend_tree_node == null: push_error("Cannot delete node '%s': node not found." % node_name) continue + if blend_tree_node == blend_tree.get_output_node(): + push_warning("Output node not allowed to be removed.") + continue + var graph_node:GraphNode = blend_tree_node_to_graph_node[blend_tree_node] blend_tree.remove_node(blend_tree_node) blend_tree_node_to_graph_node.erase(blend_tree_node) + _blend_tree_graph_edit_remove_node_connections(graph_node) graph_node_to_blend_tree_node.erase(graph_node) blend_tree_graph_edit.remove_child(graph_node) - graph_node.queue_free() + _on_blend_tree_graph_edit_node_deselected(graph_node) - blend_tree_graph_edit.clear_connections() - - if node_name in selected_nodes.keys(): - selected_nodes.erase(node_name) + EditorInterface.get_inspector().edit(null) func _on_blend_tree_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void: diff --git a/doc/design.md b/doc/design.md index 4ae8c5f..7d84fb3 100644 --- a/doc/design.md +++ b/doc/design.md @@ -111,7 +111,7 @@ Some nodes have special names in the Blend Tree: Except for the output node of a Blend Tree the following properties hold: * all Blend Tree nodes only operate on properties they own and any other data (e.g. inputs and outputs) are specified - via arguments to `SyncedAnimationNode::evaluate(context, inputs, output)` function of the node. + via arguments to `BLTAnimationNode::evaluate(context, inputs, output)` function of the node. Advantages: diff --git a/tests/test_synced_animation_graph.h b/tests/test_synced_animation_graph.h index ac97d2a..3720023 100644 --- a/tests/test_synced_animation_graph.h +++ b/tests/test_synced_animation_graph.h @@ -501,7 +501,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(blend2_node_a, blend_tree_graph.get_output_node(), "Output")); REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(animation_sampler_node_a, blend2_node_a, "Input0")); - // Connect nodes: Subgraph Blend2A, SamplerB, SamplerB + // Connect nodes: Subgraph Blend2A, SamplerB, SamplerC REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(animation_sampler_node_b, blend2_node_b, "Input0")); REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(animation_sampler_node_c, blend2_node_b, "Input1")); @@ -537,27 +537,53 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph CHECK(blend_tree_graph.node_connection_info[0].input_subtree_node_indices.size() == 6); CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_pre_remove].input_subtree_node_indices.size() == 5); - blend_tree_graph.remove_node(animation_sampler_node_a); + SUBCASE("Remove animation_sampler_node_a") { + blend_tree_graph.remove_node(animation_sampler_node_a); - for (const BLTBlendTreeConnection &connection : blend_tree_graph.connections) { - bool is_connection_with_removed_node = connection.source_node == animation_sampler_node_a || connection.target_node == animation_sampler_node_a; - CHECK(is_connection_with_removed_node == false); + for (const BLTBlendTreeConnection &connection : blend_tree_graph.connections) { + bool is_connection_with_removed_node = connection.source_node == animation_sampler_node_a || connection.target_node == animation_sampler_node_a; + CHECK(is_connection_with_removed_node == false); + } + + int animation_sampler_node_b_index_post_remove = blend_tree_graph.find_node_index(animation_sampler_node_b); + int blend2_node_a_index_post_remove = blend_tree_graph.find_node_index(blend2_node_a); + int blend2_node_b_index_post_remove = blend_tree_graph.find_node_index(blend2_node_b); + + CHECK(blend_tree_graph.find_node_index(animation_sampler_node_a) == -1); + CHECK(blend2_node_b_index_post_remove == blend2_node_b_index_pre_remove - 1); + CHECK(animation_sampler_node_b_index_post_remove == animation_sampler_node_b_index_pre_remove - 1); + + CHECK(blend_tree_graph.node_connection_info[0].input_subtree_node_indices.size() == 5); + CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_post_remove].input_subtree_node_indices.size() == 4); + CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_post_remove].connected_child_node_index_at_port[0] == -1); + CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_post_remove].connected_child_node_index_at_port[1] == blend2_node_b_index_post_remove); + CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(blend2_node_b_index_post_remove)); + CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(animation_sampler_node_b_index_post_remove)); } - int animation_sampler_node_b_index_post_remove = blend_tree_graph.find_node_index(animation_sampler_node_b); - int blend2_node_a_index_post_remove = blend_tree_graph.find_node_index(blend2_node_a); - int blend2_node_b_index_post_remove = blend_tree_graph.find_node_index(blend2_node_b); + SUBCASE("Remove blend2_node_a") { + blend_tree_graph.remove_node(blend2_node_a); - CHECK(blend_tree_graph.find_node_index(animation_sampler_node_a) == -1); - CHECK(blend2_node_b_index_post_remove == blend2_node_b_index_pre_remove - 1); - CHECK(animation_sampler_node_b_index_post_remove == animation_sampler_node_b_index_pre_remove - 1); + for (const BLTBlendTreeConnection &connection : blend_tree_graph.connections) { + bool is_connection_with_removed_node = connection.source_node == blend2_node_a || connection.target_node == blend2_node_a; + CHECK(is_connection_with_removed_node == false); + } - CHECK(blend_tree_graph.node_connection_info[0].input_subtree_node_indices.size() == 5); - CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_post_remove].input_subtree_node_indices.size() == 4); - CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_post_remove].connected_child_node_index_at_port[0] == -1); - CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_post_remove].connected_child_node_index_at_port[1] == blend2_node_b_index_post_remove); - CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(blend2_node_b_index_post_remove)); - CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(animation_sampler_node_b_index_post_remove)); + int animation_sampler_node_b_index_post_remove = blend_tree_graph.find_node_index(animation_sampler_node_b); + int animation_sampler_node_c_index_post_remove = blend_tree_graph.find_node_index(animation_sampler_node_c); + int blend2_node_b_index_post_remove = blend_tree_graph.find_node_index(blend2_node_b); + + CHECK(blend_tree_graph.find_node_index(blend2_node_a) == -1); + CHECK(blend2_node_b_index_post_remove == blend2_node_b_index_pre_remove - 1); + CHECK(animation_sampler_node_b_index_post_remove == animation_sampler_node_b_index_pre_remove); + + CHECK(blend_tree_graph.node_connection_info[0].input_subtree_node_indices.size() == 1); + CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.size() == 3); + blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove]._print_subtree(); + CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(blend2_node_b_index_post_remove)); + CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(animation_sampler_node_b_index_post_remove)); + CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(animation_sampler_node_c_index_post_remove)); + } } } -- 2.47.2 From 6330e34ea55438ad03be16ffcb9fc0d423b89fd0 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Thu, 29 Jan 2026 23:27:36 +0100 Subject: [PATCH 08/29] Renaming SyncedAnimationGraph -> Blendalot --- ...ion_graph.h => test_blendalot_animgraph.h} | 54 +++++++++---------- tests/test_sync_track.h | 2 +- 2 files changed, 28 insertions(+), 28 deletions(-) rename tests/{test_synced_animation_graph.h => test_blendalot_animgraph.h} (91%) diff --git a/tests/test_synced_animation_graph.h b/tests/test_blendalot_animgraph.h similarity index 91% rename from tests/test_synced_animation_graph.h rename to tests/test_blendalot_animgraph.h index 3720023..af028e6 100644 --- a/tests/test_synced_animation_graph.h +++ b/tests/test_blendalot_animgraph.h @@ -6,7 +6,7 @@ #include "tests/test_macros.h" -struct SyncedAnimationGraphFixture { +struct BlendTreeFixture { Node *character_node; Skeleton3D *skeleton_node; AnimationPlayer *player_node; @@ -20,8 +20,8 @@ struct SyncedAnimationGraphFixture { Ref animation_library; - BLTAnimationGraph *synced_animation_graph; - SyncedAnimationGraphFixture() { + BLTAnimationGraph *animation_graph; + BlendTreeFixture() { BLTAnimationGraph *scene_animation_graph = dynamic_cast(SceneTree::get_singleton()->get_root()->find_child("SyncedAnimationGraphFixtureTestNode", true, false)); if (scene_animation_graph == nullptr) { @@ -50,12 +50,12 @@ struct SyncedAnimationGraphFixture { SceneTree::get_singleton()->get_root()->add_child(player_node); - synced_animation_graph = memnew(BLTAnimationGraph); - synced_animation_graph->set_name("SyncedAnimationGraphFixtureTestNode"); - SceneTree::get_singleton()->get_root()->add_child(synced_animation_graph); + animation_graph = memnew(BLTAnimationGraph); + animation_graph->set_name("SyncedAnimationGraphFixtureTestNode"); + SceneTree::get_singleton()->get_root()->add_child(animation_graph); - synced_animation_graph->set_animation_player(player_node->get_path()); - synced_animation_graph->set_skeleton(skeleton_node->get_path()); + animation_graph->set_animation_player(player_node->get_path()); + animation_graph->set_skeleton(skeleton_node->get_path()); } void setup_animations() { @@ -112,8 +112,8 @@ struct SyncedAnimationGraphFixture { } void assign_scene_variables() { - synced_animation_graph = dynamic_cast(SceneTree::get_singleton()->get_root()->find_child("SyncedAnimationGraphFixtureTestNode", true, false)); - REQUIRE(synced_animation_graph); + animation_graph = dynamic_cast(SceneTree::get_singleton()->get_root()->find_child("SyncedAnimationGraphFixtureTestNode", true, false)); + REQUIRE(animation_graph); character_node = (SceneTree::get_singleton()->get_root()->find_child("CharacterNode", true, false)); REQUIRE(character_node != nullptr); skeleton_node = dynamic_cast((SceneTree::get_singleton()->get_root()->find_child("Skeleton", true, false))); @@ -139,9 +139,9 @@ struct SyncedAnimationGraphFixture { } }; -namespace TestSyncedAnimationGraph { +namespace TestBlendalotAnimationGraph { -TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") { +TEST_CASE("[Blendalot][BlendTree] Test BlendTree construction") { BLTAnimationNodeBlendTree::BLTBlendTreeGraph tree_constructor; Ref animation_sampler_node0; @@ -221,7 +221,7 @@ TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") { } } -TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] Test AnimationData blending") { +TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot] Test AnimationData blending") { AnimationData data_t0; data_t0.allocate_track_values(test_animation_a, skeleton_node); data_t0.sample_from_animation(test_animation_a, skeleton_node, 0.0); @@ -256,12 +256,12 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph } } -TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] SyncedAnimationGraph evaluation with an AnimationSampler as root node") { +TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot] SyncedAnimationGraph evaluation with an AnimationSampler as root node") { Ref animation_sampler_node; animation_sampler_node.instantiate(); animation_sampler_node->animation_name = "animation_library/TestAnimationA"; - synced_animation_graph->set_root_animation_node(animation_sampler_node); + animation_graph->set_root_animation_node(animation_sampler_node); Vector3 hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; @@ -278,7 +278,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph CHECK(hip_bone_position.z == doctest::Approx(0.03)); } -TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph][BlendTree] BlendTree evaluation with a AnimationSamplerNode connected to the output") { +TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTree] BlendTree evaluation with a AnimationSamplerNode connected to the output") { Ref synced_blend_tree_node; synced_blend_tree_node.instantiate(); @@ -289,9 +289,9 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph synced_blend_tree_node->add_node(animation_sampler_node); REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == synced_blend_tree_node->add_connection(animation_sampler_node, synced_blend_tree_node->get_output_node(), "Output")); - synced_blend_tree_node->initialize(synced_animation_graph->get_context()); + synced_blend_tree_node->initialize(animation_graph->get_context()); - synced_animation_graph->set_root_animation_node(synced_blend_tree_node); + animation_graph->set_root_animation_node(synced_blend_tree_node); Vector3 hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; @@ -308,7 +308,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph CHECK(hip_bone_position.z == doctest::Approx(0.03)); } -TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph][BlendTree][Blend2Node] BlendTree evaluation with a Blend2Node connected to the output") { +TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTree][Blend2Node] BlendTree evaluation with a Blend2Node connected to the output") { Ref synced_blend_tree_node; synced_blend_tree_node.instantiate(); @@ -341,7 +341,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == synced_blend_tree_node->add_connection(animation_sampler_node_b, blend2_node, blend2_inputs[1])); REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == synced_blend_tree_node->add_connection(blend2_node, synced_blend_tree_node->get_output_node(), "Output")); - synced_blend_tree_node->initialize(synced_animation_graph->get_context()); + synced_blend_tree_node->initialize(animation_graph->get_context()); int blend2_node_index = synced_blend_tree_node->find_node_index(blend2_node); const BLTAnimationNodeBlendTree::NodeRuntimeData &blend2_runtime_data = synced_blend_tree_node->_node_runtime_data[blend2_node_index]; @@ -349,7 +349,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph CHECK(blend2_runtime_data.input_nodes[0] == animation_sampler_node_a); CHECK(blend2_runtime_data.input_nodes[1] == animation_sampler_node_b); - synced_animation_graph->set_root_animation_node(synced_blend_tree_node); + animation_graph->set_root_animation_node(synced_blend_tree_node); SUBCASE("Perform default evaluation") { Vector3 hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; @@ -387,9 +387,9 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph animation_sampler_node_a->animation_name = "animation_library/TestAnimationSyncA"; animation_sampler_node_b->animation_name = "animation_library/TestAnimationSyncB"; blend2_node->sync = true; - synced_blend_tree_node->initialize(synced_animation_graph->get_context()); + synced_blend_tree_node->initialize(animation_graph->get_context()); - REQUIRE(synced_animation_graph->get_root_animation_node().ptr() == synced_blend_tree_node.ptr()); + REQUIRE(animation_graph->get_root_animation_node().ptr() == synced_blend_tree_node.ptr()); // By blending both animations we get a SyncTrack of duration 1.5s with the following // intervals: @@ -441,8 +441,8 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph CHECK(loaded_blend2_node->sync == false); CHECK(loaded_blend2_node->blend_weight == blend2_node->blend_weight); - loaded_synced_blend_tree->initialize(synced_animation_graph->get_context()); - synced_animation_graph->set_root_animation_node(loaded_synced_blend_tree); + loaded_synced_blend_tree->initialize(animation_graph->get_context()); + animation_graph->set_root_animation_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); @@ -455,7 +455,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph } } -TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph][BlendTreeGraph][ChangeConnectivity] BlendTreeGraph with various nodes and connections that are removed") { +TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTreeGraph][ChangeConnectivity] BlendTreeGraph with various nodes and connections that are removed") { BLTAnimationNodeBlendTree::BLTBlendTreeGraph blend_tree_graph; // TestAnimationA @@ -587,4 +587,4 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph } } -} //namespace TestSyncedAnimationGraph \ No newline at end of file +} //namespace TestBlendalotAnimationGraph \ No newline at end of file diff --git a/tests/test_sync_track.h b/tests/test_sync_track.h index 83fa35a..a2fd1f7 100644 --- a/tests/test_sync_track.h +++ b/tests/test_sync_track.h @@ -4,7 +4,7 @@ #include "tests/test_macros.h" -namespace TestSyncedAnimationGraph { +namespace TestBlendalotAnimationGraph { TEST_CASE("[SyncedAnimationGraph][SyncTrack] Basic") { SyncTrack track_a; -- 2.47.2 From 0554691e46bdd10962ef7d6f7098004697f20a15 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Fri, 30 Jan 2026 15:33:27 +0100 Subject: [PATCH 09/29] WIP: making BlendTree Editor usable. --- blendalot_animation_graph.cpp | 18 +++++++-- blendalot_animation_graph.h | 2 +- blendalot_animation_node.cpp | 59 ++++++++++++++++++++++++---- blendalot_animation_node.h | 72 +++++++++++++++++++++-------------- 4 files changed, 110 insertions(+), 41 deletions(-) diff --git a/blendalot_animation_graph.cpp b/blendalot_animation_graph.cpp index ee3921b..079dc04 100644 --- a/blendalot_animation_graph.cpp +++ b/blendalot_animation_graph.cpp @@ -132,12 +132,16 @@ void BLTAnimationGraph::_get_property_list(List *p_list) const { } } -void BLTAnimationGraph::_tree_changed() { +void BLTAnimationGraph::_graph_changed(const StringName &node_name) { + print_line(vformat("Graph changed %x", (uintptr_t)this)); + if (properties_dirty) { return; } callable_mp(this, &BLTAnimationGraph::_update_properties).call_deferred(); + callable_mp(this, &BLTAnimationGraph::_setup_graph).call_deferred(); + properties_dirty = true; } @@ -239,6 +243,8 @@ void BLTAnimationGraph::set_animation_player(const NodePath &p_path) { } graph_context.animation_player = Object::cast_to(get_node_or_null(animation_player_path)); + print_line(vformat("Setting animation player of graph %x to %x", (uintptr_t)(this), (uintptr_t)graph_context.animation_player)); + _setup_evaluation_context(); _setup_graph(); @@ -251,14 +257,15 @@ NodePath BLTAnimationGraph::get_animation_player() const { void BLTAnimationGraph::set_root_animation_node(const Ref &p_animation_node) { if (root_animation_node.is_valid()) { - root_animation_node->disconnect(SNAME("tree_changed"), callable_mp(this, &BLTAnimationGraph::_tree_changed)); + root_animation_node->disconnect(SNAME("node_changed"), callable_mp(this, &BLTAnimationGraph::_graph_changed)); } root_animation_node = p_animation_node; if (root_animation_node.is_valid()) { _setup_graph(); - root_animation_node->connect(SNAME("tree_changed"), callable_mp(this, &BLTAnimationGraph::_tree_changed)); + root_animation_node->connect(SNAME("node_changed"), callable_mp(this, &BLTAnimationGraph::_graph_changed)); + print_line(vformat("connected node_changed event to graph %x", (uintptr_t)this)); } properties_dirty = true; @@ -295,7 +302,7 @@ void BLTAnimationGraph::_process_graph(double p_delta, bool p_update_only) { return; } - if (graph_context.skeleton_3d == nullptr) { + if (graph_context.skeleton_3d == nullptr || graph_context.animation_player == nullptr) { return; } @@ -358,6 +365,7 @@ void BLTAnimationGraph::_set_process(bool p_process, bool p_force) { } void BLTAnimationGraph::_setup_evaluation_context() { + print_line("_setup_evaluation_context()"); _cleanup_evaluation_context(); graph_context.animation_player = Object::cast_to(get_node_or_null(animation_player_path)); @@ -374,6 +382,8 @@ void BLTAnimationGraph::_setup_graph() { return; } + print_line(vformat("_setup_graph() on graph %x and root node %x", (uintptr_t)(void *)(this), (uintptr_t)(root_animation_node.ptr()))); + root_animation_node->initialize(graph_context); } diff --git a/blendalot_animation_graph.h b/blendalot_animation_graph.h index a532997..560ba2c 100644 --- a/blendalot_animation_graph.h +++ b/blendalot_animation_graph.h @@ -23,7 +23,7 @@ private: void _update_properties() const; void _update_properties_for_node(const String &p_base_path, Ref p_node) const; - void _tree_changed(); + void _graph_changed(const StringName &node_name); protected: void _notification(int p_what); diff --git a/blendalot_animation_node.cpp b/blendalot_animation_node.cpp index 5afb8a3..da5c6f7 100644 --- a/blendalot_animation_node.cpp +++ b/blendalot_animation_node.cpp @@ -9,10 +9,11 @@ void BLTAnimationNode::_bind_methods() { ClassDB::bind_method(D_METHOD("get_position"), &BLTAnimationNode::get_position); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "position"), "set_position", "get_position"); - ADD_SIGNAL(MethodInfo("tree_changed")); ADD_SIGNAL(MethodInfo("animation_node_renamed", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "old_name"), PropertyInfo(Variant::STRING, "new_name"))); ADD_SIGNAL(MethodInfo("animation_node_removed", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "name"))); + ADD_SIGNAL(MethodInfo(SNAME("node_changed"), PropertyInfo(Variant::STRING_NAME, "node_name"))); + ClassDB::bind_method(D_METHOD("get_input_names"), &BLTAnimationNode::get_input_names_as_typed_array); ClassDB::bind_method(D_METHOD("get_input_count"), &BLTAnimationNode::get_input_count); ClassDB::bind_method(D_METHOD("get_input_index"), &BLTAnimationNode::get_input_index); @@ -36,8 +37,8 @@ Variant BLTAnimationNode::get_parameter(const StringName &p_name) const { return Variant(); } -void BLTAnimationNode::_tree_changed() { - emit_signal(SNAME("tree_changed")); +void BLTAnimationNode::_node_changed() { + emit_signal(SNAME("node_changed"), get_name()); } void BLTAnimationNode::_animation_node_renamed(const ObjectID &p_oid, const String &p_old_name, const String &p_new_name) { @@ -59,7 +60,6 @@ void BLTAnimationNodeBlendTree::_bind_methods() { ClassDB::bind_method(D_METHOD("add_connection", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::add_connection); ClassDB::bind_method(D_METHOD("remove_connection", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::remove_connection); ClassDB::bind_method(D_METHOD("get_connections"), &BLTAnimationNodeBlendTree::get_connections_as_array); - BIND_CONSTANT(CONNECTION_OK); BIND_CONSTANT(CONNECTION_ERROR_GRAPH_ALREADY_INITIALIZED); BIND_CONSTANT(CONNECTION_ERROR_NO_SOURCE_NODE); @@ -273,7 +273,12 @@ bool BLTAnimationNodeSampler::initialize(GraphEvaluationContext &context) { return false; } - animation = context.animation_player->get_animation(animation_name); + animation_player = context.animation_player; + if (animation_name.is_empty()) { + return true; + } + + animation = animation_player->get_animation(animation_name); if (!animation.is_valid()) { print_error(vformat("Cannot initialize node %s: animation '%s' not found in animation player.", get_name(), animation_name)); return false; @@ -333,6 +338,11 @@ void BLTAnimationNodeSampler::evaluate(GraphEvaluationContext &context, const Lo output.sample_from_animation(animation, context.skeleton_3d, node_time_info.position); } +void BLTAnimationNodeSampler::set_animation_player(AnimationPlayer *p_player) { + animation_player = p_player; + _node_changed(); +} + void BLTAnimationNodeSampler::set_animation(const StringName &p_name) { animation_name = p_name; } @@ -341,11 +351,42 @@ StringName BLTAnimationNodeSampler::get_animation() const { return animation_name; } +TypedArray BLTAnimationNodeSampler::get_animations_as_typed_array() const { + TypedArray typed_arr; + + if (animation_player == nullptr) { + print_error(vformat("BLTAnimationNodeSampler '%s' not yet initialized", get_name())); + return typed_arr; + } + + Vector vec; + + List animation_libraries; + animation_player->get_animation_library_list(&animation_libraries); + + for (const StringName &library_name : animation_libraries) { + Ref library = animation_player->get_animation_library(library_name); + List animation_list; + library->get_animation_list(&animation_list); + for (const StringName &library_animation : animation_list) { + vec.push_back(library_animation); + } + } + + typed_arr.resize(vec.size()); + for (uint32_t i = 0; i < vec.size(); i++) { + typed_arr[i] = vec[i]; + } + return typed_arr; +} + void BLTAnimationNodeSampler::_bind_methods() { ClassDB::bind_method(D_METHOD("set_animation", "name"), &BLTAnimationNodeSampler::set_animation); ClassDB::bind_method(D_METHOD("get_animation"), &BLTAnimationNodeSampler::get_animation); ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "animation"), "set_animation", "get_animation"); + + ClassDB::bind_method(D_METHOD("get_animations"), &BLTAnimationNodeSampler::get_animations_as_typed_array); } void BLTAnimationNodeBlend2::evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) { @@ -476,10 +517,10 @@ void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::add_node(const Ref &node) { +bool BLTAnimationNodeBlendTree::BLTBlendTreeGraph::remove_node(const Ref &node) { if (node == get_output_node()) { // Output node not allowed to be removed - return; + return false; } int removed_node_index = find_node_index(node); @@ -515,6 +556,8 @@ void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::remove_node(const Ref= 0); connections.remove_at(connection_index); + } else { + return CONNECTION_ERROR_CONNECTION_NOT_FOUND; } return CONNECTION_OK; diff --git a/blendalot_animation_node.h b/blendalot_animation_node.h index 5c6cb2a..b187890 100644 --- a/blendalot_animation_node.h +++ b/blendalot_animation_node.h @@ -5,6 +5,7 @@ #include "scene/3d/skeleton_3d.h" #include "scene/animation/animation_player.h" +#include "scene/resources/animation_library.h" #include "sync_track.h" #include @@ -245,7 +246,7 @@ protected: virtual void set_parameter(const StringName &p_name, const Variant &p_value); virtual Variant get_parameter(const StringName &p_name) const; - virtual void _tree_changed(); + virtual void _node_changed(); virtual void _animation_node_renamed(const ObjectID &p_oid, const String &p_old_name, const String &p_new_name); virtual void _animation_node_removed(const ObjectID &p_oid, const StringName &p_node); @@ -273,6 +274,11 @@ public: virtual void activate_inputs(const Vector> &input_nodes) { // By default, all inputs nodes are activated. for (const Ref &node : input_nodes) { + if (node.ptr() == nullptr) { + // TODO: add checking whether tree can be evaluated, i.e. whether all inputs are properly connected. + continue; + } + node->active = true; node->node_time_info.is_synced = node_time_info.is_synced; } @@ -339,10 +345,14 @@ class BLTAnimationNodeSampler : public BLTAnimationNode { public: StringName animation_name; + AnimationPlayer *animation_player = nullptr; + void set_animation_player(AnimationPlayer *p_player); void set_animation(const StringName &p_name); StringName get_animation() const; + TypedArray get_animations_as_typed_array() const; + private: Ref animation; @@ -459,6 +469,7 @@ public: CONNECTION_ERROR_TARGET_PORT_NOT_FOUND, CONNECTION_ERROR_TARGET_PORT_ALREADY_CONNECTED, CONNECTION_ERROR_CONNECTION_CREATES_LOOP, + CONNECTION_ERROR_CONNECTION_NOT_FOUND }; /** @@ -526,7 +537,7 @@ public: void remove_subtree_and_update_subtrees_recursive(int node, const HashSet &removed_subtree_indices); void add_node(const Ref &node); - void remove_node(const Ref &node); + bool remove_node(const Ref &node); ConnectionError is_connection_valid(const Ref &source_node, const Ref &target_node, StringName target_port_name) const; ConnectionError add_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name); @@ -537,6 +548,7 @@ public: private: BLTBlendTreeGraph tree_graph; bool tree_initialized = false; + GraphEvaluationContext *_graph_evaluation_context = nullptr; void sort_nodes() { _node_runtime_data.clear(); @@ -564,7 +576,11 @@ private: for (int port_index = 0; port_index < node->get_input_count(); port_index++) { const int connected_node_index = tree_graph.node_connection_info[i].connected_child_node_index_at_port[port_index]; - node_runtime_data.input_nodes.push_back(tree_graph.nodes[connected_node_index]); + if (connected_node_index == -1) { + node_runtime_data.input_nodes.push_back(nullptr); + } else { + node_runtime_data.input_nodes.push_back(tree_graph.nodes[connected_node_index]); + } } } } @@ -587,26 +603,22 @@ public: return tree_graph.find_node_index(node); } - int find_node_index_by_name(const StringName &name) const { - return tree_graph.find_node_index_by_name(name); + int find_node_index_by_name(const StringName &p_name) const { + return tree_graph.find_node_index_by_name(p_name); } void add_node(const Ref &node) { - if (tree_initialized) { - print_error("Cannot add node to BlendTree: BlendTree already initialized."); - return; - } - tree_graph.add_node(node); + + if (_graph_evaluation_context != nullptr) { + node->initialize(*_graph_evaluation_context); + } } void remove_node(const Ref &node) { - if (tree_initialized) { - print_error("Cannot remove node from BlendTree: BlendTree already initialized."); - return; + if (tree_graph.remove_node(node)) { + _node_changed(); } - - tree_graph.remove_node(node); } TypedArray get_node_names_as_typed_array() const { @@ -646,30 +658,25 @@ public: } ConnectionError is_connection_valid(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) { - if (tree_initialized) { - print_error("Cannot add connection to BlendTree: BlendTree already initialized."); - return CONNECTION_ERROR_GRAPH_ALREADY_INITIALIZED; - } - return tree_graph.is_connection_valid(source_node, target_node, target_port_name); } ConnectionError add_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) { - if (tree_initialized) { - print_error("Cannot add connection to BlendTree: BlendTree already initialized."); - return CONNECTION_ERROR_GRAPH_ALREADY_INITIALIZED; + ConnectionError result = tree_graph.add_connection(source_node, target_node, target_port_name); + if (result == CONNECTION_OK) { + _node_changed(); } - return tree_graph.add_connection(source_node, target_node, target_port_name); + return result; } ConnectionError remove_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) { - if (tree_initialized) { - print_error("Cannot remove connection to BlendTree: BlendTree already initialized."); - return CONNECTION_ERROR_GRAPH_ALREADY_INITIALIZED; + ConnectionError result = tree_graph.remove_connection(source_node, target_node, target_port_name); + if (result == CONNECTION_OK) { + _node_changed(); } - return tree_graph.remove_connection(source_node, target_node, target_port_name); + return result; } Array get_connections_as_array() const { @@ -689,6 +696,8 @@ public: return false; } + _graph_evaluation_context = &context; + sort_nodes(); setup_runtime_data(); @@ -707,6 +716,11 @@ public: activate_inputs(const Vector> &input_nodes) override { GodotProfileZone("SyncedBlendTree::activate_inputs"); + // TODO: add checking whether tree can be evaluated, i.e. whether all inputs are properly connected. + if (tree_graph.nodes.size() == 1) { + return; + } + tree_graph.nodes[0]->active = true; for (uint32_t i = 0; i < tree_graph.nodes.size(); i++) { const Ref &node = tree_graph.nodes[i]; @@ -759,7 +773,7 @@ public: } void evaluate(GraphEvaluationContext &context, const LocalVector &input_datas, AnimationData &output_data) override { - ZoneScopedN("SyncedBlendTree::evaluate"); + GodotProfileZone("SyncedBlendTree::evaluate"); for (uint32_t i = tree_graph.nodes.size() - 1; i > 0; i--) { const Ref &node = tree_graph.nodes[i]; -- 2.47.2 From a2295680d18b38e27884587a211501a4e5c34779 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Sun, 1 Feb 2026 10:15:48 +0100 Subject: [PATCH 10/29] Ensure nodes that are added to the tree but not within the subtree of the root are still referenced after sorting. --- blendalot_animation_node.cpp | 21 ++++++++++++++++++++- tests/test_blendalot_animgraph.h | 15 +++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/blendalot_animation_node.cpp b/blendalot_animation_node.cpp index da5c6f7..fb20c0c 100644 --- a/blendalot_animation_node.cpp +++ b/blendalot_animation_node.cpp @@ -545,6 +545,10 @@ bool BLTAnimationNodeBlendTree::BLTBlendTreeGraph::remove_node(const Ref removed_node_index) { + connection_info.parent_node_index--; + } + // Map connected subtrees HashSet old_indices = connection_info.input_subtree_node_indices; connection_info.input_subtree_node_indices.clear(); @@ -563,13 +567,14 @@ bool BLTAnimationNodeBlendTree::BLTBlendTreeGraph::remove_node(const Ref sorted_node_indices = get_sorted_node_indices(); - Vector> sorted_nodes; + LocalVector> sorted_nodes; LocalVector old_node_connection_info = node_connection_info; for (unsigned int i = 0; i < sorted_node_indices.size(); i++) { int node_index = sorted_node_indices[i]; sorted_nodes.push_back(nodes[node_index]); node_connection_info[i] = old_node_connection_info[node_index]; } + nodes = sorted_nodes; for (NodeConnectionInfo &connection_info : node_connection_info) { @@ -586,6 +591,20 @@ LocalVector BLTAnimationNodeBlendTree::BLTBlendTreeGraph::get_sorted_node_i sort_nodes_recursive(0, result); result.reverse(); + HashSet connected_node_indices; + for (int node_index : result) { + connected_node_indices.insert(node_index); + } + + // Ensure that nodes that are not reachable from the root node are still added to + // the sorted nodes indices. + for (Ref &node : nodes) { + int node_index = find_node_index(node); + if (!connected_node_indices.has(node_index)) { + result.push_back(node_index); + } + } + return result; } diff --git a/tests/test_blendalot_animgraph.h b/tests/test_blendalot_animgraph.h index af028e6..4bed159 100644 --- a/tests/test_blendalot_animgraph.h +++ b/tests/test_blendalot_animgraph.h @@ -537,7 +537,14 @@ TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTreeGraph][Chan CHECK(blend_tree_graph.node_connection_info[0].input_subtree_node_indices.size() == 6); CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_pre_remove].input_subtree_node_indices.size() == 5); - SUBCASE("Remove animation_sampler_node_a") { + SUBCASE("Removing the output node does nothing") { + int num_nodes = blend_tree_graph.nodes.size(); + int num_connections = blend_tree_graph.connections.size(); + CHECK(blend_tree_graph.remove_node(blend_tree_graph.get_output_node()) == false); + CHECK(blend_tree_graph.connections.size() == num_connections); + } + + SUBCASE("Remove a node with no children") { blend_tree_graph.remove_node(animation_sampler_node_a); for (const BLTBlendTreeConnection &connection : blend_tree_graph.connections) { @@ -561,8 +568,12 @@ TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTreeGraph][Chan CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(animation_sampler_node_b_index_post_remove)); } - SUBCASE("Remove blend2_node_a") { + SUBCASE("Remove a node with parent and children") { + int num_nodes = blend_tree_graph.nodes.size(); blend_tree_graph.remove_node(blend2_node_a); + blend_tree_graph.sort_nodes_and_references(); + + CHECK(blend_tree_graph.nodes.size() == num_nodes - 1); for (const BLTBlendTreeConnection &connection : blend_tree_graph.connections) { bool is_connection_with_removed_node = connection.source_node == blend2_node_a || connection.target_node == blend2_node_a; -- 2.47.2 From a764222c02ecf82e6cd903290fea52e4b9ff4417 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Sun, 1 Feb 2026 10:16:18 +0100 Subject: [PATCH 11/29] Added missing parameter to BLTAnimationNode::get_input_index binding. --- blendalot_animation_node.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blendalot_animation_node.cpp b/blendalot_animation_node.cpp index fb20c0c..a1d55ea 100644 --- a/blendalot_animation_node.cpp +++ b/blendalot_animation_node.cpp @@ -16,7 +16,7 @@ void BLTAnimationNode::_bind_methods() { ClassDB::bind_method(D_METHOD("get_input_names"), &BLTAnimationNode::get_input_names_as_typed_array); ClassDB::bind_method(D_METHOD("get_input_count"), &BLTAnimationNode::get_input_count); - ClassDB::bind_method(D_METHOD("get_input_index"), &BLTAnimationNode::get_input_index); + ClassDB::bind_method(D_METHOD("get_input_index", "node"), &BLTAnimationNode::get_input_index); } void BLTAnimationNode::get_parameter_list(List *r_list) const { -- 2.47.2 From f1a42302a6c19d174177b1580af0b534ac28ce60 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Sun, 1 Feb 2026 10:39:37 +0100 Subject: [PATCH 12/29] Nodes now properly keep their positions in the blend tree. --- demo/addons/blendalot/blendalot_main_panel.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/addons/blendalot/blendalot_main_panel.gd b/demo/addons/blendalot/blendalot_main_panel.gd index b6e54b7..b8e943e 100644 --- a/demo/addons/blendalot/blendalot_main_panel.gd +++ b/demo/addons/blendalot/blendalot_main_panel.gd @@ -198,7 +198,7 @@ func _on_blend_tree_graph_edit_connection_request(from_node: StringName, from_po func _on_blend_tree_graph_edit_end_node_move() -> void: for graph_node:GraphNode in selected_nodes.keys(): - graph_node_to_blend_tree_node[graph_node].position = graph_node.position + graph_node_to_blend_tree_node[graph_node].position = graph_node.position_offset func _on_blend_tree_graph_edit_begin_node_move() -> void: -- 2.47.2 From 00ea4b8b7e3fadf54cdb7ab66b5326eae37c2a66 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Mon, 2 Feb 2026 16:17:33 +0100 Subject: [PATCH 13/29] BlendTreeEditor starts to be usable. --- blendalot_animation_graph.cpp | 9 +- blendalot_animation_graph.h | 1 + blendalot_animation_node.cpp | 77 +++++++--- blendalot_animation_node.h | 15 +- demo/addons/blendalot/blendalot_main_panel.gd | 78 ++++++---- .../blendalot/blendalot_main_panel.tscn | 7 +- demo/main.tscn | 145 ++++++++++++++++-- demo/synced_blend_tree_walk_limp.tres | 19 ++- demo/synced_blend_tree_walk_run.tres | 18 ++- tests/test_blendalot_animgraph.h | 1 + 10 files changed, 277 insertions(+), 93 deletions(-) diff --git a/blendalot_animation_graph.cpp b/blendalot_animation_graph.cpp index 079dc04..38d07aa 100644 --- a/blendalot_animation_graph.cpp +++ b/blendalot_animation_graph.cpp @@ -256,6 +256,8 @@ NodePath BLTAnimationGraph::get_animation_player() const { } void BLTAnimationGraph::set_root_animation_node(const Ref &p_animation_node) { + print_line(vformat("setting root node to node %s", p_animation_node->get_name())); + if (root_animation_node.is_valid()) { root_animation_node->disconnect(SNAME("node_changed"), callable_mp(this, &BLTAnimationGraph::_graph_changed)); } @@ -265,7 +267,6 @@ void BLTAnimationGraph::set_root_animation_node(const Ref &p_a if (root_animation_node.is_valid()) { _setup_graph(); root_animation_node->connect(SNAME("node_changed"), callable_mp(this, &BLTAnimationGraph::_graph_changed)); - print_line(vformat("connected node_changed event to graph %x", (uintptr_t)this)); } properties_dirty = true; @@ -298,7 +299,7 @@ NodePath BLTAnimationGraph::get_skeleton() const { } void BLTAnimationGraph::_process_graph(double p_delta, bool p_update_only) { - if (!root_animation_node.is_valid()) { + if (!root_animation_node.is_valid() || is_graph_initialization_valid == false) { return; } @@ -383,8 +384,8 @@ void BLTAnimationGraph::_setup_graph() { } print_line(vformat("_setup_graph() on graph %x and root node %x", (uintptr_t)(void *)(this), (uintptr_t)(root_animation_node.ptr()))); - - root_animation_node->initialize(graph_context); + is_graph_initialization_valid = root_animation_node->initialize(graph_context); + print_line(vformat("is_graph_initialization_valid = %s", is_graph_initialization_valid ? "true" : "false")); } BLTAnimationGraph::BLTAnimationGraph() { diff --git a/blendalot_animation_graph.h b/blendalot_animation_graph.h index 560ba2c..807d2c0 100644 --- a/blendalot_animation_graph.h +++ b/blendalot_animation_graph.h @@ -19,6 +19,7 @@ private: mutable AHashMap, StringName>> parameter_to_node_parameter_map; mutable bool properties_dirty = true; + bool is_graph_initialization_valid = false; void _update_properties() const; void _update_properties_for_node(const String &p_base_path, Ref p_node) const; diff --git a/blendalot_animation_node.cpp b/blendalot_animation_node.cpp index a1d55ea..6b7cc7e 100644 --- a/blendalot_animation_node.cpp +++ b/blendalot_animation_node.cpp @@ -274,36 +274,21 @@ bool BLTAnimationNodeSampler::initialize(GraphEvaluationContext &context) { } animation_player = context.animation_player; - if (animation_name.is_empty()) { - return true; + + if (animation_player == nullptr) { + return false; } - animation = animation_player->get_animation(animation_name); - if (!animation.is_valid()) { - print_error(vformat("Cannot initialize node %s: animation '%s' not found in animation player.", get_name(), animation_name)); + if (animation_name.is_empty()) { + return false; + } + + if (!set_animation(animation_name)) { return false; } context.animation_data_allocator.register_track_values(animation, context.skeleton_3d); - node_time_info.loop_mode = animation->get_loop_mode(); - - // Initialize Sync Track from marker - LocalVector sync_markers; - int marker_index = 0; - StringName marker_name = itos(marker_index); - while (animation->has_marker(marker_name)) { - sync_markers.push_back(animation->get_marker_time(marker_name)); - marker_index++; - marker_name = itos(marker_index); - } - - if (sync_markers.size() > 0) { - node_time_info.sync_track = SyncTrack::create_from_markers(animation->get_length(), sync_markers); - } else { - node_time_info.sync_track = SyncTrack::create_from_markers(animation->get_length(), { 0 }); - } - return true; } @@ -343,8 +328,52 @@ void BLTAnimationNodeSampler::set_animation_player(AnimationPlayer *p_player) { _node_changed(); } -void BLTAnimationNodeSampler::set_animation(const StringName &p_name) { +bool BLTAnimationNodeSampler::set_animation(const StringName &p_name) { + bool has_animation_name_changed = p_name != animation_name; animation_name = p_name; + + if (animation_player == nullptr) { + return false; + } + + if (!animation_player->has_animation(p_name)) { + if (has_animation_name_changed) { + _node_changed(); + } + return false; + } + + animation = animation_player->get_animation(p_name); + if (!animation.is_valid()) { + print_error(vformat("Cannot initialize node %s: animation '%s' not found in animation player.", get_name(), animation_name)); + + _node_changed(); + return false; + } + + node_time_info.loop_mode = animation->get_loop_mode(); + + // Initialize Sync Track from marker + LocalVector sync_markers; + int marker_index = 0; + StringName marker_name = itos(marker_index); + while (animation->has_marker(marker_name)) { + sync_markers.push_back(animation->get_marker_time(marker_name)); + marker_index++; + marker_name = itos(marker_index); + } + + if (sync_markers.size() > 0) { + node_time_info.sync_track = SyncTrack::create_from_markers(animation->get_length(), sync_markers); + } else { + node_time_info.sync_track = SyncTrack::create_from_markers(animation->get_length(), { 0 }); + } + + if (has_animation_name_changed) { + _node_changed(); + } + + return true; } StringName BLTAnimationNodeSampler::get_animation() const { diff --git a/blendalot_animation_node.h b/blendalot_animation_node.h index b187890..41e6922 100644 --- a/blendalot_animation_node.h +++ b/blendalot_animation_node.h @@ -348,7 +348,7 @@ public: AnimationPlayer *animation_player = nullptr; void set_animation_player(AnimationPlayer *p_player); - void set_animation(const StringName &p_name); + bool set_animation(const StringName &p_name); StringName get_animation() const; TypedArray get_animations_as_typed_array() const; @@ -394,6 +394,11 @@ public: node_time_info.loop_mode = Animation::LOOP_LINEAR; } + if (node_time_info.loop_mode != Animation::LOOP_LINEAR) { + print_line(vformat("Forcing loop mode to linear on nonde %s", get_name())); + node_time_info.loop_mode = Animation::LOOP_LINEAR; + } + return true; } void activate_inputs(const Vector> &input_nodes) override { @@ -421,9 +426,9 @@ public: if (!Math::is_zero_approx(node_time_info.sync_track.duration)) { node_time_info.position = Math::fposmod(static_cast(node_time_info.position), node_time_info.sync_track.duration); node_time_info.sync_position = node_time_info.sync_track.calc_sync_from_abs_time(node_time_info.position); - } else { - assert(false && !"Loop mode ping-pong not yet supported"); } + } else { + assert(false && !"Loop mode ping-pong not yet supported"); } } } @@ -690,6 +695,10 @@ public: return result; } + void _tree_node_changed(const StringName &node_name) { + _node_changed(); + } + // overrides from BLTAnimationNode bool initialize(GraphEvaluationContext &context) override { if (!BLTAnimationNode::initialize(context)) { diff --git a/demo/addons/blendalot/blendalot_main_panel.gd b/demo/addons/blendalot/blendalot_main_panel.gd index b8e943e..b25746d 100644 --- a/demo/addons/blendalot/blendalot_main_panel.gd +++ b/demo/addons/blendalot/blendalot_main_panel.gd @@ -7,6 +7,8 @@ extends Control @onready var tree_option_button: OptionButton = %TreeOptionButton @onready var add_node_popup_menu: PopupMenu = %AddNodePopupMenu +var root_animation_node:BLTAnimationNode = null +var last_edited_graph_node:GraphNode = null var blend_tree:BLTAnimationNodeBlendTree = BLTAnimationNodeBlendTree.new() var blend_tree_node_to_graph_node = {} var graph_node_to_blend_tree_node = {} @@ -31,6 +33,7 @@ func _ready() -> void: func _process(delta: float) -> void: pass + func create_node_for_blt_node(blt_node: BLTAnimationNode) -> GraphNode: var result_graph_node:GraphNode = GraphNode.new() result_graph_node.name = blt_node.resource_name @@ -53,15 +56,41 @@ func create_node_for_blt_node(blt_node: BLTAnimationNode) -> GraphNode: result_graph_node.add_child(slot_label) result_graph_node.set_slot(i + result_slot_offset, true, 1, Color.WHITE, false, 1, Color.BLACK) + blt_node.node_changed.connect(_triggrer_animation_graph_initialize) + return result_graph_node -func _on_add_node_button_pressed() -> void: - blend_tree_graph_edit.add_child(create_node_for_blt_node(BLTAnimationNodeOutput.new())) - blend_tree_graph_edit.add_child(create_node_for_blt_node(BLTAnimationNodeBlend2.new())) +func _on_blend_tree_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void: + for node_name:StringName in nodes: + print("remove node '%s'" % node_name) + var blend_tree_node:BLTAnimationNode = blend_tree.get_node(node_name) + + if blend_tree_node == null: + push_error("Cannot delete node '%s': node not found." % node_name) + continue + + if blend_tree_node == blend_tree.get_output_node(): + push_warning("Output node not allowed to be removed.") + continue + + blend_tree_node.node_changed.disconnect(_triggrer_animation_graph_initialize) + + var graph_node:GraphNode = blend_tree_node_to_graph_node[blend_tree_node] + blend_tree.remove_node(blend_tree_node) + blend_tree_node_to_graph_node.erase(blend_tree_node) + + _blend_tree_graph_edit_remove_node_connections(graph_node) + graph_node_to_blend_tree_node.erase(graph_node) + blend_tree_graph_edit.remove_child(graph_node) + _on_blend_tree_graph_edit_node_deselected(graph_node) + + EditorInterface.get_inspector().edit(null) func _on_reset_graph_button_pressed() -> void: + EditorInterface.get_inspector().edit(null) + _reset_editor() _update_editor_from_blend_tree() @@ -89,11 +118,13 @@ func edit_blend_tree(blend_tree_animation_node:BLTAnimationNode): print("Starting to edit blend_tree_animation_node " + str(blend_tree_animation_node)) print("Owner: %s" % blend_tree_animation_node) _reset_editor() + root_animation_node = blend_tree_animation_node blend_tree = blend_tree_animation_node _update_editor_nodes_from_blend_tree() _update_editor_connections_from_blend_tree() + func _update_editor_nodes_from_blend_tree(): for node_name in blend_tree.get_node_names(): var blend_tree_node:BLTAnimationNode = blend_tree.get_node(node_name) @@ -207,6 +238,7 @@ func _on_blend_tree_graph_edit_begin_node_move() -> void: func _on_blend_tree_graph_edit_node_selected(graph_node: Node) -> void: selected_nodes[graph_node] = graph_node + last_edited_graph_node = graph_node EditorInterface.get_inspector().edit(graph_node_to_blend_tree_node[graph_node]) @@ -224,14 +256,14 @@ func _on_blend_tree_graph_edit_popup_request(at_position: Vector2) -> void: func _on_add_node_popup_menu_index_pressed(index: int) -> void: var new_blend_tree_node: BLTAnimationNode = ClassDB.instantiate(registered_nodes[index]) - blend_tree.add_node(new_blend_tree_node) + blend_tree.add_node(new_blend_tree_node) + var graph_node:GraphNode = create_node_for_blt_node(new_blend_tree_node) + blend_tree_graph_edit.add_child(graph_node) graph_node_to_blend_tree_node[graph_node] = new_blend_tree_node blend_tree_node_to_graph_node[new_blend_tree_node] = graph_node - blend_tree_graph_edit.add_child(graph_node) - if new_node_position != Vector2.INF: graph_node.position_offset = new_node_position @@ -250,31 +282,6 @@ func _blend_tree_graph_edit_remove_node_connections(graph_node:GraphNode): blend_tree_graph_edit.disconnect_node(node_connection["from_node"], node_connection["from_port"], node_connection["to_node"], node_connection["to_port"]) -func _on_blend_tree_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void: - for node_name:StringName in nodes: - print("remove node '%s'" % node_name) - var blend_tree_node:BLTAnimationNode = blend_tree.get_node(node_name) - - if blend_tree_node == null: - push_error("Cannot delete node '%s': node not found." % node_name) - continue - - if blend_tree_node == blend_tree.get_output_node(): - push_warning("Output node not allowed to be removed.") - continue - - var graph_node:GraphNode = blend_tree_node_to_graph_node[blend_tree_node] - blend_tree.remove_node(blend_tree_node) - blend_tree_node_to_graph_node.erase(blend_tree_node) - - _blend_tree_graph_edit_remove_node_connections(graph_node) - graph_node_to_blend_tree_node.erase(graph_node) - blend_tree_graph_edit.remove_child(graph_node) - _on_blend_tree_graph_edit_node_deselected(graph_node) - - EditorInterface.get_inspector().edit(null) - - func _on_blend_tree_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void: print("removing connection") @@ -284,3 +291,12 @@ func _on_blend_tree_graph_edit_disconnection_request(from_node: StringName, from blend_tree.remove_connection(blend_tree_source_node, blend_tree_target_node, target_port_name) blend_tree_graph_edit.disconnect_node(from_node, from_port, to_node, to_port) + + +func _trigger_animation_graph_initialize(node_name:StringName) -> void: + root_animation_node.node_changed.emit("root") + + +func _on_visibility_changed() -> void: + if visible and is_instance_valid(last_edited_graph_node): + _on_blend_tree_graph_edit_node_selected(last_edited_graph_node) diff --git a/demo/addons/blendalot/blendalot_main_panel.tscn b/demo/addons/blendalot/blendalot_main_panel.tscn index 78b70ad..bc0b98e 100644 --- a/demo/addons/blendalot/blendalot_main_panel.tscn +++ b/demo/addons/blendalot/blendalot_main_panel.tscn @@ -47,10 +47,10 @@ size_flags_horizontal = 0 text = "Load " -[node name="AddNodeButton" type="Button" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=985452697] +[node name="ReinitializeButton" type="Button" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=719949205] layout_mode = 2 size_flags_horizontal = 0 -text = "Add Node" +text = "Reinitialize" [node name="TreeOptionButton" type="OptionButton" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=760974122] unique_name_in_owner = true @@ -92,10 +92,11 @@ grow_horizontal = 2 grow_vertical = 2 right_disconnects = true +[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"] [connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/ResetGraphButton" to="." method="_on_reset_graph_button_pressed"] [connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/SaveButton" to="." method="_on_save_button_pressed"] [connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/LoadButton" to="." method="_on_load_button_pressed"] -[connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/AddNodeButton" to="." method="_on_add_node_button_pressed"] +[connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/ReinitializeButton" to="." method="_trigger_animation_graph_initialize"] [connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/InstantiateTreeButton" to="." method="_on_instantiate_tree_button_pressed"] [connection signal="index_pressed" from="BlendTreeEditorContainer/Panel/AddNodePopupMenu" to="." method="_on_add_node_popup_menu_index_pressed"] [connection signal="begin_node_move" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_begin_node_move"] diff --git a/demo/main.tscn b/demo/main.tscn index 1d1932f..e766978 100644 --- a/demo/main.tscn +++ b/demo/main.tscn @@ -4,8 +4,9 @@ [ext_resource type="Script" uid="uid://bjvgqujpqumj7" path="res://main.gd" id="1_1bvp3"] [ext_resource type="AnimationLibrary" uid="uid://dwubn740aqx51" path="res://animation_library.res" id="3_1bvp3"] [ext_resource type="AnimationNodeBlendTree" uid="uid://dqy0dgwsm8t46" path="res://animation_tree_walk_limp.tres" id="3_272bh"] -[ext_resource type="BLTAnimationNodeBlendTree" uid="uid://2qfwr1xkiw0s" path="res://synced_blend_tree_walk_limp.tres" id="4_lquwl"] [ext_resource type="AnimationNodeBlendTree" uid="uid://vsf71o82lkld" path="res://animation_tree_walk_run.tres" id="6_5vw27"] +[ext_resource type="BLTAnimationNodeBlendTree" uid="uid://2qfwr1xkiw0s" path="res://synced_blend_tree_walk_limp.tres" id="6_272bh"] +[ext_resource type="BLTAnimationNodeBlendTree" uid="uid://qsk64ax2o47f" path="res://synced_blend_tree_walk_run.tres" id="7_272bh"] [sub_resource type="Theme" id="Theme_272bh"] default_font_size = 30 @@ -31,17 +32,6 @@ sky = SubResource("Sky_1bvp3") tonemap_mode = 2 glow_enabled = true -[sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_7mycd"] -resource_name = "BLTAnimationNodeSampler" -position = Vector2(149.47485, 361.29053) -animation = &"animation_library/Walk-InPlace" - -[sub_resource type="BLTAnimationNodeBlendTree" id="BLTAnimationNodeBlendTree_272bh"] -nodes/Output/position = Vector2(602.0149, 281.4305) -nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_7mycd") -nodes/BLTAnimationNodeSampler/position = Vector2(149.47485, 361.29053) -node_connections = ["Output", 0, "BLTAnimationNodeSampler"] - [node name="Main" type="Node3D" unique_id=933302313] script = ExtResource("1_1bvp3") @@ -157,22 +147,145 @@ libraries/animation_library = ExtResource("3_1bvp3") [node name="SyncedAnimationGraph" type="BLTAnimationGraph" parent="Characters/MixamoAmyWalkLimpSynced" unique_id=1866796918] animation_player = NodePath("../AnimationPlayer2") -tree_root = ExtResource("4_lquwl") +tree_root = ExtResource("6_272bh") skeleton = NodePath("../Armature/Skeleton3D") -parameters/BLTAnimationNodeBlend2/blend_amount = 0.4 +parameters/BLTAnimationNodeBlend2/blend_amount = 1.0 [node name="MixamoAmyWalkRunSynced" parent="Characters" unique_id=2088190993 instance=ExtResource("1_0xm2m")] unique_name_in_owner = true transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4, 0, 0) -[node name="AnimationPlayer2" type="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" unique_id=1255239074] +[node name="Skeleton3D" parent="Characters/MixamoAmyWalkRunSynced/Armature" parent_id_path=PackedInt32Array(2088190993, 52334938) index="0" unique_id=2014991417] +bones/0/position = Vector3(2.3944292, 58.292915, 0) +bones/2/position = Vector3(2.3954058, -1.2283401, -67.867485) +bones/2/rotation = Quaternion(-0.7015852, -0.016084917, 0.002894751, 0.7123981) +bones/3/position = Vector3(1.8626451e-08, 3.9624305, -0.274297) +bones/3/rotation = Quaternion(-0.019988505, 0.0068857046, -0.0031864278, 0.9997697) +bones/4/position = Vector3(5.9604645e-08, 14.123348, 0.2644268) +bones/4/rotation = Quaternion(0.0014081367, 0.015367857, -0.008578158, 0.9998441) +bones/5/position = Vector3(3.7252903e-08, 11.404536, 0.106110305) +bones/5/rotation = Quaternion(0.0014081355, 0.0153678525, -0.0085781645, 0.9998441) +bones/6/position = Vector3(2.9802322e-08, 9.946667, 1.6090326e-08) +bones/6/rotation = Quaternion(0.039844688, -0.0022483557, 0.009721856, 0.9991561) +bones/7/position = Vector3(3.1292439e-07, 8.579114, 0.11416777) +bones/7/rotation = Quaternion(0.018197661, -0.0028079022, 0.025565712, 0.99950355) +bones/8/position = Vector3(1.8626451e-09, 25.089184, 0.33387405) +bones/9/position = Vector3(4.22409, 8.898366, -0.056614783) +bones/9/rotation = Quaternion(0.69302666, 0.2985537, -0.5364231, 0.3779267) +bones/10/position = Vector3(7.763405e-07, 8.86101, -1.885943e-05) +bones/10/rotation = Quaternion(0.36105165, -0.046350844, -0.1645193, 0.91674787) +bones/11/position = Vector3(-6.736064e-08, 18.975187, -5.359281e-06) +bones/11/rotation = Quaternion(0.03416674, 0.15374418, 0.242136, 0.9573729) +bones/12/position = Vector3(2.0505329e-06, 19.896036, 4.198435e-06) +bones/12/rotation = Quaternion(-0.02613752, 0.22233193, -0.068033114, 0.97224313) +bones/13/position = Vector3(-2.0888743, 2.1412597, 1.2094326) +bones/13/rotation = Quaternion(0.25766087, -0.0057431525, 0.24790713, 0.93387365) +bones/14/position = Vector3(-0.3297696, 2.5601492, 5.6111962e-06) +bones/14/rotation = Quaternion(-0.02088847, -0.0012659901, -0.06479406, 0.9976792) +bones/15/position = Vector3(0.07692051, 2.7271638, -7.698024e-07) +bones/15/rotation = Quaternion(0.041175473, -5.169663e-07, -3.3207877e-07, 0.99915195) +bones/16/position = Vector3(0.25285125, 2.2376482, -2.5033949e-06) +bones/17/position = Vector3(-2.5860305, 6.9422946, 0.029643927) +bones/17/rotation = Quaternion(0.3595198, 0.019201318, 0.093114324, 0.9282804) +bones/18/position = Vector3(0.0010065138, 2.844871, 2.6222544e-05) +bones/18/rotation = Quaternion(0.5266805, -0.009538538, 0.050116353, 0.8485306) +bones/19/position = Vector3(-0.001300633, 2.6654923, -3.370296e-06) +bones/19/rotation = Quaternion(0.0067229928, -1.8172469e-07, -1.12402185e-08, 0.9999774) +bones/20/position = Vector3(0.00029665232, 2.2974284, 2.6011842e-06) +bones/21/position = Vector3(-0.81500757, 6.8271494, -0.11008961) +bones/21/rotation = Quaternion(0.4838051, -0.02263074, 0.097104214, 0.86947656) +bones/22/position = Vector3(-0.011725575, 3.256784, 9.871595e-06) +bones/22/rotation = Quaternion(0.70388824, -0.007956969, 0.07827657, 0.70593894) +bones/23/position = Vector3(-0.00070961565, 3.1778917, -4.440292e-06) +bones/23/rotation = Quaternion(0.05807299, -2.1569534e-08, -4.8931874e-08, 0.99831235) +bones/24/position = Vector3(0.012437165, 2.6951318, 2.2368273e-05) +bones/25/position = Vector3(0.89882326, 6.7787366, -0.023027958) +bones/25/rotation = Quaternion(0.47609472, -0.10504542, 0.00788891, 0.87306076) +bones/26/position = Vector3(-0.0040293336, 2.958041, 1.3685599e-05) +bones/26/rotation = Quaternion(0.71283627, 0.0028766363, 0.06198941, 0.69857895) +bones/27/position = Vector3(0.0024927258, 2.756927, -1.8636636e-05) +bones/27/rotation = Quaternion(-0.0033205294, 3.9059813e-08, 6.5682637e-09, 0.9999945) +bones/28/position = Vector3(0.0015397072, 2.3345788, 1.1771219e-05) +bones/29/position = Vector3(2.5022223, 6.3907866, 0.08744741) +bones/29/rotation = Quaternion(0.5714825, -0.1510222, 0.0009240997, 0.8065963) +bones/30/position = Vector3(0.0032016933, 2.6237173, 5.5511973e-06) +bones/30/rotation = Quaternion(0.638921, 0.023284703, 0.05857911, 0.7666847) +bones/31/position = Vector3(0.003878951, 2.1320877, -1.2886679e-06) +bones/31/rotation = Quaternion(-0.06925962, -1.0360488e-07, -7.0190005e-08, 0.9975987) +bones/32/position = Vector3(-0.0070759356, 1.7932303, -8.8289596e-07) +bones/33/position = Vector3(-4.2240915, 8.897269, -0.030230172) +bones/33/rotation = Quaternion(0.65103245, -0.34380102, 0.56754214, 0.36858305) +bones/34/position = Vector3(9.327891e-07, 8.8610115, -9.847447e-06) +bones/34/rotation = Quaternion(0.4207574, -0.005679023, 0.07089772, 0.9043808) +bones/35/position = Vector3(6.476337e-07, 18.973557, 5.8403616e-06) +bones/35/rotation = Quaternion(-0.0409054, -0.11762145, -0.36220855, 0.92374086) +bones/36/position = Vector3(-7.630494e-08, 19.89834, -1.8180976e-06) +bones/36/rotation = Quaternion(-0.18468891, -0.12580799, -0.09516018, 0.97005504) +bones/37/position = Vector3(2.0620086, 2.1477072, 1.2410417) +bones/37/rotation = Quaternion(0.1984708, 0.031765047, -0.28800797, 0.9362969) +bones/38/position = Vector3(0.2744484, 2.5658755, 3.7584634e-06) +bones/38/rotation = Quaternion(0.081494935, -0.19653738, 0.11717984, 0.9700518) +bones/39/position = Vector3(-0.076725245, 2.730221, -7.234994e-06) +bones/39/rotation = Quaternion(0.037325032, -2.1781963e-08, 1.19075e-07, 0.99930316) +bones/40/position = Vector3(-0.19772077, 2.2442596, 9.685756e-06) +bones/41/position = Vector3(2.5699375, 7.0537868, 0.05093465) +bones/41/rotation = Quaternion(0.48021728, 0.0082633095, -0.045813248, 0.87591136) +bones/42/position = Vector3(-0.0015108623, 2.816315, -9.529522e-06) +bones/42/rotation = Quaternion(0.6120701, 0.02828741, -0.048200864, 0.78882384) +bones/43/position = Vector3(0.0011758618, 2.6396425, 2.472788e-06) +bones/43/rotation = Quaternion(0.0026853334, 3.311302e-08, -5.8677845e-08, 0.9999965) +bones/44/position = Vector3(0.00033191964, 2.2415001, 1.3194513e-06) +bones/45/position = Vector3(0.76549155, 7.018211, -0.09038047) +bones/45/rotation = Quaternion(0.49598414, 0.036653362, -0.038273394, 0.8667126) +bones/46/position = Vector3(0.008488461, 3.3401706, -6.017696e-06) +bones/46/rotation = Quaternion(0.70486575, 0.040216748, -0.058438864, 0.70578283) +bones/47/position = Vector3(-0.007900611, 3.0566626, 5.4270527e-06) +bones/47/rotation = Quaternion(-0.027461762, -2.0760188e-08, 5.6872928e-08, 0.9996229) +bones/48/position = Vector3(-0.00058989227, 2.63951, 9.457464e-06) +bones/49/position = Vector3(-0.8923034, 6.9110794, -0.12160769) +bones/49/rotation = Quaternion(0.49361324, 0.033564895, -0.017181082, 0.86886364) +bones/50/position = Vector3(-0.00297606, 2.923956, 1.7109673e-05) +bones/50/rotation = Quaternion(0.69540185, -0.01958265, -0.069078155, 0.7150251) +bones/51/position = Vector3(0.0026345253, 2.717951, -5.427028e-06) +bones/51/rotation = Quaternion(0.009096339, 7.025756e-08, 2.6579988e-08, 0.99995863) +bones/52/position = Vector3(0.0003426671, 2.379634, 1.4747493e-06) +bones/53/position = Vector3(-2.4431183, 6.453577, 0.12551774) +bones/53/rotation = Quaternion(0.45701033, 0.026051568, -0.02899594, 0.8886062) +bones/54/position = Vector3(-0.0026362836, 2.6278691, 4.2827646e-06) +bones/54/rotation = Quaternion(0.7443899, -0.044672288, -0.09278318, 0.6597555) +bones/55/position = Vector3(-0.001860708, 2.1224687, 9.940322e-07) +bones/55/rotation = Quaternion(-0.03619139, -1.1229469e-07, 2.121775e-08, 0.9993449) +bones/56/position = Vector3(0.004502952, 1.7919822, 1.2924895e-05) +bones/57/position = Vector3(7.557004, -4.826265, 0.30053553) +bones/57/rotation = Quaternion(-0.063634895, -0.18599084, -0.9804429, 0.009476706) +bones/58/position = Vector3(-0.124621816, 24.06574, 0.10470339) +bones/58/rotation = Quaternion(-0.25773793, 0.06259592, -0.01817601, 0.9640138) +bones/59/position = Vector3(-0.0060171685, 33.063114, 0.00394835) +bones/59/rotation = Quaternion(0.5448946, 0.0072068824, 0.026147991, 0.83806586) +bones/60/position = Vector3(-0.0052775713, 12.893309, -0.5155027) +bones/60/rotation = Quaternion(0.3145146, -0.058151364, 0.021719113, 0.9472208) +bones/61/position = Vector3(0.014227723, 6.420835, 0.007887725) +bones/62/position = Vector3(-7.557004, -4.826261, 0.30053535) +bones/62/rotation = Quaternion(-0.072349995, 0.14396061, 0.98645425, 0.030805206) +bones/63/position = Vector3(0.12476807, 24.070103, -0.14491707) +bones/63/rotation = Quaternion(-0.49734375, 0.011248301, -0.008667116, 0.86743724) +bones/64/position = Vector3(0.0060205855, 33.063133, 0.004499323) +bones/64/rotation = Quaternion(0.33301854, -0.06086632, -0.07533557, 0.9379331) +bones/65/position = Vector3(0.005378528, 12.718271, -0.52236915) +bones/65/rotation = Quaternion(0.49081448, 0.17055358, 0.0037184241, 0.8543996) +bones/66/position = Vector3(-0.014239848, 6.412128, 0.0079107955) + +[node name="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" index="1" unique_id=945472897] active = false + +[node name="AnimationPlayer2" type="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" unique_id=1255239074] libraries/animation_library = ExtResource("3_1bvp3") [node name="SyncedAnimationGraph" type="BLTAnimationGraph" parent="Characters/MixamoAmyWalkRunSynced" unique_id=1602406394] animation_player = NodePath("../AnimationPlayer2") -tree_root = SubResource("BLTAnimationNodeBlendTree_272bh") +tree_root = ExtResource("7_272bh") skeleton = NodePath("../Armature/Skeleton3D") +parameters/BLTAnimationNodeBlend2/blend_amount = 0.0 [connection signal="value_changed" from="UI/MarginContainer/HBoxContainer/BlendWeightSlider" to="." method="_on_blend_weight_slider_value_changed"] diff --git a/demo/synced_blend_tree_walk_limp.tres b/demo/synced_blend_tree_walk_limp.tres index fd2c576..20ac3b2 100644 --- a/demo/synced_blend_tree_walk_limp.tres +++ b/demo/synced_blend_tree_walk_limp.tres @@ -1,19 +1,26 @@ -[gd_resource type="BLTAnimationNodeBlendTree" load_steps=4 format=3 uid="uid://2qfwr1xkiw0s"] +[gd_resource type="BLTAnimationNodeBlendTree" format=3 uid="uid://2qfwr1xkiw0s"] [sub_resource type="BLTAnimationNodeBlend2" id="BLTAnimationNodeBlend2_bvt3d"] -blend_amount = 0.4 +resource_name = "BLTAnimationNodeBlend2" +position = Vector2(950.50195, 749.8301) +blend_amount = 1.0 [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_sntl5"] +resource_name = "BLTAnimationNodeSampler 1" +position = Vector2(101.323975, 1247.624) animation = &"animation_library/Limping-InPlace" [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_n4m28"] +resource_name = "BLTAnimationNodeSampler" +position = Vector2(101.323975, 398.44608) animation = &"animation_library/Walk-InPlace" [resource] +nodes/Output/position = Vector2(1741.1158, 925.52203) nodes/BLTAnimationNodeBlend2/node = SubResource("BLTAnimationNodeBlend2_bvt3d") -nodes/BLTAnimationNodeBlend2/position = Vector2(0, 0) +nodes/BLTAnimationNodeBlend2/position = Vector2(950.50195, 749.8301) "nodes/BLTAnimationNodeSampler 1/node" = SubResource("BLTAnimationNodeSampler_sntl5") -"nodes/BLTAnimationNodeSampler 1/position" = Vector2(0, 0) +"nodes/BLTAnimationNodeSampler 1/position" = Vector2(101.323975, 1247.624) nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_n4m28") -nodes/BLTAnimationNodeSampler/position = Vector2(0, 0) -node_connections = [&"BLTAnimationNodeBlend2", 0, &"BLTAnimationNodeSampler", &"BLTAnimationNodeBlend2", 1, &"BLTAnimationNodeSampler 1", &"Output", 0, &"BLTAnimationNodeBlend2"] +nodes/BLTAnimationNodeSampler/position = Vector2(101.323975, 398.44608) +node_connections = ["BLTAnimationNodeBlend2", 0, "BLTAnimationNodeSampler", "BLTAnimationNodeBlend2", 1, "BLTAnimationNodeSampler 1", "Output", 0, "BLTAnimationNodeBlend2"] diff --git a/demo/synced_blend_tree_walk_run.tres b/demo/synced_blend_tree_walk_run.tres index 10d6c6e..5728f70 100644 --- a/demo/synced_blend_tree_walk_run.tres +++ b/demo/synced_blend_tree_walk_run.tres @@ -1,19 +1,25 @@ -[gd_resource type="BLTAnimationNodeBlendTree" load_steps=4 format=3 uid="uid://qsk64ax2o47f"] +[gd_resource type="BLTAnimationNodeBlendTree" format=3 uid="uid://qsk64ax2o47f"] [sub_resource type="BLTAnimationNodeBlend2" id="BLTAnimationNodeBlend2_bvt3d"] -blend_amount = 0.4 +resource_name = "BLTAnimationNodeBlend2" +position = Vector2(-420, 220) [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_sntl5"] +resource_name = "BLTAnimationNodeSampler 1" +position = Vector2(-1020, 380) animation = &"animation_library/Run-InPlace" [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_n4m28"] +resource_name = "BLTAnimationNodeSampler" +position = Vector2(-880, 0) animation = &"animation_library/Walk-InPlace" [resource] +nodes/Output/position = Vector2(180, 80) nodes/BLTAnimationNodeBlend2/node = SubResource("BLTAnimationNodeBlend2_bvt3d") -nodes/BLTAnimationNodeBlend2/position = Vector2(0, 0) +nodes/BLTAnimationNodeBlend2/position = Vector2(-420, 220) "nodes/BLTAnimationNodeSampler 1/node" = SubResource("BLTAnimationNodeSampler_sntl5") -"nodes/BLTAnimationNodeSampler 1/position" = Vector2(0, 0) +"nodes/BLTAnimationNodeSampler 1/position" = Vector2(-1020, 380) nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_n4m28") -nodes/BLTAnimationNodeSampler/position = Vector2(0, 0) -node_connections = [&"BLTAnimationNodeBlend2", 0, &"BLTAnimationNodeSampler", &"BLTAnimationNodeBlend2", 1, &"BLTAnimationNodeSampler 1", &"Output", 0, &"BLTAnimationNodeBlend2"] +nodes/BLTAnimationNodeSampler/position = Vector2(-880, 0) +node_connections = ["BLTAnimationNodeBlend2", 0, "BLTAnimationNodeSampler", "BLTAnimationNodeBlend2", 1, "BLTAnimationNodeSampler 1", "Output", 0, "BLTAnimationNodeBlend2"] diff --git a/tests/test_blendalot_animgraph.h b/tests/test_blendalot_animgraph.h index 4bed159..bc6c5e6 100644 --- a/tests/test_blendalot_animgraph.h +++ b/tests/test_blendalot_animgraph.h @@ -542,6 +542,7 @@ TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTreeGraph][Chan int num_connections = blend_tree_graph.connections.size(); CHECK(blend_tree_graph.remove_node(blend_tree_graph.get_output_node()) == false); CHECK(blend_tree_graph.connections.size() == num_connections); + CHECK(blend_tree_graph.nodes.size() == num_nodes); } SUBCASE("Remove a node with no children") { -- 2.47.2 From f07b54e42b1bcf8a8bd95925e50b1c86ad523222 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Mon, 2 Feb 2026 18:40:32 +0100 Subject: [PATCH 14/29] Minor fixes and typos. --- demo/addons/blendalot/blendalot_main_panel.gd | 4 +- demo/animation_tree_walk_limp.tres | 2 +- demo/animation_tree_walk_run.tres | 2 +- demo/main.tscn | 88 +++++++++---------- 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/demo/addons/blendalot/blendalot_main_panel.gd b/demo/addons/blendalot/blendalot_main_panel.gd index b25746d..5d9334c 100644 --- a/demo/addons/blendalot/blendalot_main_panel.gd +++ b/demo/addons/blendalot/blendalot_main_panel.gd @@ -56,7 +56,7 @@ func create_node_for_blt_node(blt_node: BLTAnimationNode) -> GraphNode: result_graph_node.add_child(slot_label) result_graph_node.set_slot(i + result_slot_offset, true, 1, Color.WHITE, false, 1, Color.BLACK) - blt_node.node_changed.connect(_triggrer_animation_graph_initialize) + blt_node.node_changed.connect(_trigger_animation_graph_initialize) return result_graph_node @@ -74,7 +74,7 @@ func _on_blend_tree_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> push_warning("Output node not allowed to be removed.") continue - blend_tree_node.node_changed.disconnect(_triggrer_animation_graph_initialize) + blend_tree_node.node_changed.disconnect(_trigger_animation_graph_initialize) var graph_node:GraphNode = blend_tree_node_to_graph_node[blend_tree_node] blend_tree.remove_node(blend_tree_node) diff --git a/demo/animation_tree_walk_limp.tres b/demo/animation_tree_walk_limp.tres index a41f2f6..82e9dd0 100644 --- a/demo/animation_tree_walk_limp.tres +++ b/demo/animation_tree_walk_limp.tres @@ -1,4 +1,4 @@ -[gd_resource type="AnimationNodeBlendTree" load_steps=4 format=3 uid="uid://dqy0dgwsm8t46"] +[gd_resource type="AnimationNodeBlendTree" format=3 uid="uid://dqy0dgwsm8t46"] [sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_h2yge"] animation = &"Limping-InPlace" diff --git a/demo/animation_tree_walk_run.tres b/demo/animation_tree_walk_run.tres index f8e0b34..fd1de9c 100644 --- a/demo/animation_tree_walk_run.tres +++ b/demo/animation_tree_walk_run.tres @@ -1,4 +1,4 @@ -[gd_resource type="AnimationNodeBlendTree" load_steps=4 format=3 uid="uid://vsf71o82lkld"] +[gd_resource type="AnimationNodeBlendTree" format=3 uid="uid://vsf71o82lkld"] [sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_h2yge"] animation = &"Run-InPlace" diff --git a/demo/main.tscn b/demo/main.tscn index e766978..22da37c 100644 --- a/demo/main.tscn +++ b/demo/main.tscn @@ -155,29 +155,29 @@ parameters/BLTAnimationNodeBlend2/blend_amount = 1.0 unique_name_in_owner = true transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4, 0, 0) -[node name="Skeleton3D" parent="Characters/MixamoAmyWalkRunSynced/Armature" parent_id_path=PackedInt32Array(2088190993, 52334938) index="0" unique_id=2014991417] -bones/0/position = Vector3(2.3944292, 58.292915, 0) -bones/2/position = Vector3(2.3954058, -1.2283401, -67.867485) -bones/2/rotation = Quaternion(-0.7015852, -0.016084917, 0.002894751, 0.7123981) +[node name="Skeleton3D" parent="Characters/MixamoAmyWalkRunSynced/Armature" parent_id_path=PackedInt32Array(2088190993, 259532241) index="0" unique_id=1597913436] +bones/0/position = Vector3(0.03366956, 22.30488, 0) +bones/2/position = Vector3(0.021933563, -1.2283401, -66.54255) +bones/2/rotation = Quaternion(-0.7020165, -0.025985688, 0.0034645642, 0.7116779) bones/3/position = Vector3(1.8626451e-08, 3.9624305, -0.274297) -bones/3/rotation = Quaternion(-0.019988505, 0.0068857046, -0.0031864278, 0.9997697) +bones/3/rotation = Quaternion(-0.016682021, 0.014965671, -0.0026113286, 0.9997449) bones/4/position = Vector3(5.9604645e-08, 14.123348, 0.2644268) -bones/4/rotation = Quaternion(0.0014081367, 0.015367857, -0.008578158, 0.9998441) +bones/4/rotation = Quaternion(0.008006268, 0.031130861, -0.0048357854, 0.9994714) bones/5/position = Vector3(3.7252903e-08, 11.404536, 0.106110305) -bones/5/rotation = Quaternion(0.0014081355, 0.0153678525, -0.0085781645, 0.9998441) +bones/5/rotation = Quaternion(0.008006268, 0.03113088, -0.0048357914, 0.9994714) bones/6/position = Vector3(2.9802322e-08, 9.946667, 1.6090326e-08) -bones/6/rotation = Quaternion(0.039844688, -0.0022483557, 0.009721856, 0.9991561) +bones/6/rotation = Quaternion(0.043000314, -0.01605664, 0.0336432, 0.9983794) bones/7/position = Vector3(3.1292439e-07, 8.579114, 0.11416777) -bones/7/rotation = Quaternion(0.018197661, -0.0028079022, 0.025565712, 0.99950355) +bones/7/rotation = Quaternion(0.0067235962, -0.019416407, -0.004696545, 0.9997777) bones/8/position = Vector3(1.8626451e-09, 25.089184, 0.33387405) bones/9/position = Vector3(4.22409, 8.898366, -0.056614783) -bones/9/rotation = Quaternion(0.69302666, 0.2985537, -0.5364231, 0.3779267) +bones/9/rotation = Quaternion(0.6655204, 0.33482903, -0.54497546, 0.38467056) bones/10/position = Vector3(7.763405e-07, 8.86101, -1.885943e-05) -bones/10/rotation = Quaternion(0.36105165, -0.046350844, -0.1645193, 0.91674787) +bones/10/rotation = Quaternion(0.3856572, -0.029256662, -0.20285419, 0.8995903) bones/11/position = Vector3(-6.736064e-08, 18.975187, -5.359281e-06) -bones/11/rotation = Quaternion(0.03416674, 0.15374418, 0.242136, 0.9573729) +bones/11/rotation = Quaternion(0.0491358, 0.18193112, 0.21587172, 0.95806384) bones/12/position = Vector3(2.0505329e-06, 19.896036, 4.198435e-06) -bones/12/rotation = Quaternion(-0.02613752, 0.22233193, -0.068033114, 0.97224313) +bones/12/rotation = Quaternion(-0.10542738, 0.21719287, 0.097704284, 0.9654874) bones/13/position = Vector3(-2.0888743, 2.1412597, 1.2094326) bones/13/rotation = Quaternion(0.25766087, -0.0057431525, 0.24790713, 0.93387365) bones/14/position = Vector3(-0.3297696, 2.5601492, 5.6111962e-06) @@ -186,96 +186,96 @@ bones/15/position = Vector3(0.07692051, 2.7271638, -7.698024e-07) bones/15/rotation = Quaternion(0.041175473, -5.169663e-07, -3.3207877e-07, 0.99915195) bones/16/position = Vector3(0.25285125, 2.2376482, -2.5033949e-06) bones/17/position = Vector3(-2.5860305, 6.9422946, 0.029643927) -bones/17/rotation = Quaternion(0.3595198, 0.019201318, 0.093114324, 0.9282804) +bones/17/rotation = Quaternion(0.32230198, 0.013257779, 0.0790869, 0.9432342) bones/18/position = Vector3(0.0010065138, 2.844871, 2.6222544e-05) -bones/18/rotation = Quaternion(0.5266805, -0.009538538, 0.050116353, 0.8485306) +bones/18/rotation = Quaternion(0.51890975, -0.009350935, 0.048978366, 0.8533736) bones/19/position = Vector3(-0.001300633, 2.6654923, -3.370296e-06) bones/19/rotation = Quaternion(0.0067229928, -1.8172469e-07, -1.12402185e-08, 0.9999774) bones/20/position = Vector3(0.00029665232, 2.2974284, 2.6011842e-06) bones/21/position = Vector3(-0.81500757, 6.8271494, -0.11008961) -bones/21/rotation = Quaternion(0.4838051, -0.02263074, 0.097104214, 0.86947656) +bones/21/rotation = Quaternion(0.44832447, -0.029388895, 0.08389875, 0.8894394) bones/22/position = Vector3(-0.011725575, 3.256784, 9.871595e-06) -bones/22/rotation = Quaternion(0.70388824, -0.007956969, 0.07827657, 0.70593894) +bones/22/rotation = Quaternion(0.6974019, -0.007876825, 0.0774008, 0.71244484) bones/23/position = Vector3(-0.00070961565, 3.1778917, -4.440292e-06) bones/23/rotation = Quaternion(0.05807299, -2.1569534e-08, -4.8931874e-08, 0.99831235) bones/24/position = Vector3(0.012437165, 2.6951318, 2.2368273e-05) bones/25/position = Vector3(0.89882326, 6.7787366, -0.023027958) -bones/25/rotation = Quaternion(0.47609472, -0.10504542, 0.00788891, 0.87306076) +bones/25/rotation = Quaternion(0.43942147, -0.11050543, -0.00536839, 0.8914418) bones/26/position = Vector3(-0.0040293336, 2.958041, 1.3685599e-05) -bones/26/rotation = Quaternion(0.71283627, 0.0028766363, 0.06198941, 0.69857895) +bones/26/rotation = Quaternion(0.70637834, 0.0028474892, 0.061491687, 0.70515275) bones/27/position = Vector3(0.0024927258, 2.756927, -1.8636636e-05) bones/27/rotation = Quaternion(-0.0033205294, 3.9059813e-08, 6.5682637e-09, 0.9999945) bones/28/position = Vector3(0.0015397072, 2.3345788, 1.1771219e-05) bones/29/position = Vector3(2.5022223, 6.3907866, 0.08744741) -bones/29/rotation = Quaternion(0.5714825, -0.1510222, 0.0009240997, 0.8065963) +bones/29/rotation = Quaternion(0.5366728, -0.15708813, -0.01123394, 0.8289629) bones/30/position = Vector3(0.0032016933, 2.6237173, 5.5511973e-06) -bones/30/rotation = Quaternion(0.638921, 0.023284703, 0.05857911, 0.7666847) +bones/30/rotation = Quaternion(0.6318143, 0.022965502, 0.05855106, 0.7725639) bones/31/position = Vector3(0.003878951, 2.1320877, -1.2886679e-06) bones/31/rotation = Quaternion(-0.06925962, -1.0360488e-07, -7.0190005e-08, 0.9975987) bones/32/position = Vector3(-0.0070759356, 1.7932303, -8.8289596e-07) bones/33/position = Vector3(-4.2240915, 8.897269, -0.030230172) -bones/33/rotation = Quaternion(0.65103245, -0.34380102, 0.56754214, 0.36858305) +bones/33/rotation = Quaternion(0.65423024, -0.3460148, 0.5481125, 0.38965285) bones/34/position = Vector3(9.327891e-07, 8.8610115, -9.847447e-06) -bones/34/rotation = Quaternion(0.4207574, -0.005679023, 0.07089772, 0.9043808) +bones/34/rotation = Quaternion(0.33522785, 0.13258426, -0.06878553, 0.93022156) bones/35/position = Vector3(6.476337e-07, 18.973557, 5.8403616e-06) -bones/35/rotation = Quaternion(-0.0409054, -0.11762145, -0.36220855, 0.92374086) +bones/35/rotation = Quaternion(0.06291787, -0.06795199, -0.34171283, 0.9352305) bones/36/position = Vector3(-7.630494e-08, 19.89834, -1.8180976e-06) -bones/36/rotation = Quaternion(-0.18468891, -0.12580799, -0.09516018, 0.97005504) +bones/36/rotation = Quaternion(-0.13934937, -0.17929602, 0.059740018, 0.972042) bones/37/position = Vector3(2.0620086, 2.1477072, 1.2410417) -bones/37/rotation = Quaternion(0.1984708, 0.031765047, -0.28800797, 0.9362969) +bones/37/rotation = Quaternion(0.20313361, 0.020001005, -0.30569944, 0.9299916) bones/38/position = Vector3(0.2744484, 2.5658755, 3.7584634e-06) -bones/38/rotation = Quaternion(0.081494935, -0.19653738, 0.11717984, 0.9700518) +bones/38/rotation = Quaternion(0.07182112, -0.15283245, 0.11806564, 0.97854203) bones/39/position = Vector3(-0.076725245, 2.730221, -7.234994e-06) bones/39/rotation = Quaternion(0.037325032, -2.1781963e-08, 1.19075e-07, 0.99930316) bones/40/position = Vector3(-0.19772077, 2.2442596, 9.685756e-06) bones/41/position = Vector3(2.5699375, 7.0537868, 0.05093465) -bones/41/rotation = Quaternion(0.48021728, 0.0082633095, -0.045813248, 0.87591136) +bones/41/rotation = Quaternion(0.44285733, 0.008293978, -0.03807221, 0.89574504) bones/42/position = Vector3(-0.0015108623, 2.816315, -9.529522e-06) -bones/42/rotation = Quaternion(0.6120701, 0.02828741, -0.048200864, 0.78882384) +bones/42/rotation = Quaternion(0.5924978, 0.028048418, -0.045164384, 0.80381584) bones/43/position = Vector3(0.0011758618, 2.6396425, 2.472788e-06) bones/43/rotation = Quaternion(0.0026853334, 3.311302e-08, -5.8677845e-08, 0.9999965) bones/44/position = Vector3(0.00033191964, 2.2415001, 1.3194513e-06) bones/45/position = Vector3(0.76549155, 7.018211, -0.09038047) -bones/45/rotation = Quaternion(0.49598414, 0.036653362, -0.038273394, 0.8667126) +bones/45/rotation = Quaternion(0.46793976, 0.036938474, -0.033631414, 0.88234746) bones/46/position = Vector3(0.008488461, 3.3401706, -6.017696e-06) -bones/46/rotation = Quaternion(0.70486575, 0.040216748, -0.058438864, 0.70578283) +bones/46/rotation = Quaternion(0.6849328, 0.039953418, -0.055356182, 0.72540087) bones/47/position = Vector3(-0.007900611, 3.0566626, 5.4270527e-06) bones/47/rotation = Quaternion(-0.027461762, -2.0760188e-08, 5.6872928e-08, 0.9996229) bones/48/position = Vector3(-0.00058989227, 2.63951, 9.457464e-06) bones/49/position = Vector3(-0.8923034, 6.9110794, -0.12160769) -bones/49/rotation = Quaternion(0.49361324, 0.033564895, -0.017181082, 0.86886364) +bones/49/rotation = Quaternion(0.47066858, 0.033067867, -0.0154665, 0.8815544) bones/50/position = Vector3(-0.00297606, 2.923956, 1.7109673e-05) -bones/50/rotation = Quaternion(0.69540185, -0.01958265, -0.069078155, 0.7150251) +bones/50/rotation = Quaternion(0.6740056, -0.019516876, -0.06773796, 0.7353549) bones/51/position = Vector3(0.0026345253, 2.717951, -5.427028e-06) bones/51/rotation = Quaternion(0.009096339, 7.025756e-08, 2.6579988e-08, 0.99995863) bones/52/position = Vector3(0.0003426671, 2.379634, 1.4747493e-06) bones/53/position = Vector3(-2.4431183, 6.453577, 0.12551774) -bones/53/rotation = Quaternion(0.45701033, 0.026051568, -0.02899594, 0.8886062) +bones/53/rotation = Quaternion(0.4273077, 0.025455933, -0.032704208, 0.9031559) bones/54/position = Vector3(-0.0026362836, 2.6278691, 4.2827646e-06) -bones/54/rotation = Quaternion(0.7443899, -0.044672288, -0.09278318, 0.6597555) +bones/54/rotation = Quaternion(0.73083663, -0.044815022, -0.09265602, 0.6747476) bones/55/position = Vector3(-0.001860708, 2.1224687, 9.940322e-07) bones/55/rotation = Quaternion(-0.03619139, -1.1229469e-07, 2.121775e-08, 0.9993449) bones/56/position = Vector3(0.004502952, 1.7919822, 1.2924895e-05) bones/57/position = Vector3(7.557004, -4.826265, 0.30053553) -bones/57/rotation = Quaternion(-0.063634895, -0.18599084, -0.9804429, 0.009476706) +bones/57/rotation = Quaternion(-0.036588665, -0.34452513, -0.9375553, 0.030883614) bones/58/position = Vector3(-0.124621816, 24.06574, 0.10470339) -bones/58/rotation = Quaternion(-0.25773793, 0.06259592, -0.01817601, 0.9640138) +bones/58/rotation = Quaternion(-0.17203768, -0.022209316, 0.008335125, 0.98480475) bones/59/position = Vector3(-0.0060171685, 33.063114, 0.00394835) -bones/59/rotation = Quaternion(0.5448946, 0.0072068824, 0.026147991, 0.83806586) +bones/59/rotation = Quaternion(0.500414, -0.0045057763, 0.0039036346, 0.86576587) bones/60/position = Vector3(-0.0052775713, 12.893309, -0.5155027) -bones/60/rotation = Quaternion(0.3145146, -0.058151364, 0.021719113, 0.9472208) +bones/60/rotation = Quaternion(0.34159487, -0.05822111, 0.025601145, 0.9376929) bones/61/position = Vector3(0.014227723, 6.420835, 0.007887725) bones/62/position = Vector3(-7.557004, -4.826261, 0.30053535) -bones/62/rotation = Quaternion(-0.072349995, 0.14396061, 0.98645425, 0.030805206) +bones/62/rotation = Quaternion(-0.105550915, -0.06569394, 0.9920036, 0.021728868) bones/63/position = Vector3(0.12476807, 24.070103, -0.14491707) -bones/63/rotation = Quaternion(-0.49734375, 0.011248301, -0.008667116, 0.86743724) +bones/63/rotation = Quaternion(-0.21048684, -0.113292366, 0.05454326, 0.96947676) bones/64/position = Vector3(0.0060205855, 33.063133, 0.004499323) -bones/64/rotation = Quaternion(0.33301854, -0.06086632, -0.07533557, 0.9379331) +bones/64/rotation = Quaternion(0.5910368, -0.030297726, -0.04612658, 0.8047532) bones/65/position = Vector3(0.005378528, 12.718271, -0.52236915) -bones/65/rotation = Quaternion(0.49081448, 0.17055358, 0.0037184241, 0.8543996) +bones/65/rotation = Quaternion(0.4105115, 0.015659228, -0.01512902, 0.91159546) bones/66/position = Vector3(-0.014239848, 6.412128, 0.0079107955) -[node name="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" index="1" unique_id=945472897] +[node name="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" index="1" unique_id=99701443] active = false [node name="AnimationPlayer2" type="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" unique_id=1255239074] -- 2.47.2 From 14329e606ecf35fd66c537024fbd79caf91c3ade Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Mon, 2 Feb 2026 20:38:55 +0100 Subject: [PATCH 15/29] Properly initialize animation player when loading a scene. --- blendalot_animation_graph.cpp | 33 +++++++++++++++------------------ blendalot_animation_graph.h | 1 + demo/main.tscn | 1 + 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/blendalot_animation_graph.cpp b/blendalot_animation_graph.cpp index 38d07aa..56d6109 100644 --- a/blendalot_animation_graph.cpp +++ b/blendalot_animation_graph.cpp @@ -149,7 +149,7 @@ void BLTAnimationGraph::_notification(int p_what) { GodotProfileZone("SyncedAnimationGraph::_notification"); switch (p_what) { - case Node::NOTIFICATION_READY: { + case NOTIFICATION_ENTER_TREE: { _setup_evaluation_context(); _setup_graph(); @@ -234,16 +234,9 @@ AnimationMixer::AnimationCallbackModeDiscrete BLTAnimationGraph::get_callback_mo } void BLTAnimationGraph::set_animation_player(const NodePath &p_path) { - animation_player_path = p_path; - if (p_path.is_empty()) { - // set_root_node(SceneStringName(path_pp)); - // while (animation_libraries.size()) { - // remove_animation_library(animation_libraries[0].name); - // } - } - graph_context.animation_player = Object::cast_to(get_node_or_null(animation_player_path)); + print_line(vformat("set_animation_player(%s) ", p_path)); - print_line(vformat("Setting animation player of graph %x to %x", (uintptr_t)(this), (uintptr_t)graph_context.animation_player)); + animation_player_path = p_path; _setup_evaluation_context(); _setup_graph(); @@ -279,14 +272,9 @@ Ref BLTAnimationGraph::get_root_animation_node() const { } void BLTAnimationGraph::set_skeleton(const NodePath &p_path) { + print_line(vformat("set_skeleton(%s) ", p_path)); + skeleton_path = p_path; - if (p_path.is_empty()) { - // set_root_node(SceneStringName(path_pp)); - // while (animation_libraries.size()) { - // remove_animation_library(animation_libraries[0].name); - // } - } - graph_context.skeleton_3d = Object::cast_to(get_node_or_null(skeleton_path)); _setup_evaluation_context(); _setup_graph(); @@ -365,11 +353,20 @@ void BLTAnimationGraph::_set_process(bool p_process, bool p_force) { processing = p_process; } +void BLTAnimationGraph::_setup_animation_player() { + if (!is_inside_tree()) { + return; + } + + graph_context.animation_player = Object::cast_to(get_node_or_null(animation_player_path)); + print_line(vformat("AnimationPlayer of graph %x is now %x", (uintptr_t)(this), (uintptr_t)graph_context.animation_player)); +} + void BLTAnimationGraph::_setup_evaluation_context() { print_line("_setup_evaluation_context()"); _cleanup_evaluation_context(); - graph_context.animation_player = Object::cast_to(get_node_or_null(animation_player_path)); + _setup_animation_player(); graph_context.skeleton_3d = Object::cast_to(get_node_or_null(skeleton_path)); } diff --git a/blendalot_animation_graph.h b/blendalot_animation_graph.h index 807d2c0..3298979 100644 --- a/blendalot_animation_graph.h +++ b/blendalot_animation_graph.h @@ -76,6 +76,7 @@ public: private: void _set_process(bool p_process, bool p_force = false); + void _setup_animation_player(); void _setup_evaluation_context(); void _cleanup_evaluation_context(); diff --git a/demo/main.tscn b/demo/main.tscn index 22da37c..3385af8 100644 --- a/demo/main.tscn +++ b/demo/main.tscn @@ -279,6 +279,7 @@ bones/66/position = Vector3(-0.014239848, 6.412128, 0.0079107955) active = false [node name="AnimationPlayer2" type="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" unique_id=1255239074] +active = false libraries/animation_library = ExtResource("3_1bvp3") [node name="SyncedAnimationGraph" type="BLTAnimationGraph" parent="Characters/MixamoAmyWalkRunSynced" unique_id=1602406394] -- 2.47.2 From f3db8b67d0403dccc6e0c92a3f76efb01033dba5 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Thu, 5 Feb 2026 21:55:18 +0100 Subject: [PATCH 16/29] Fixed compilation with latest master. --- blendalot_animation_node.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blendalot_animation_node.cpp b/blendalot_animation_node.cpp index 6b7cc7e..de867df 100644 --- a/blendalot_animation_node.cpp +++ b/blendalot_animation_node.cpp @@ -166,7 +166,7 @@ bool BLTAnimationNodeBlendTree::_set(const StringName &p_name, const Variant &p_ void AnimationData::sample_from_animation(const Ref &animation, const Skeleton3D *skeleton_3d, double p_time) { GodotProfileZone("AnimationData::sample_from_animation"); - const LocalVector tracks = animation->get_tracks(); + const LocalVector &tracks = animation->get_tracks(); Animation::Track *const *tracks_ptr = tracks.ptr(); int count = tracks.size(); @@ -250,7 +250,7 @@ void AnimationData::allocate_track_value(const Animation::Track *animation_track void AnimationData::allocate_track_values(const Ref &animation, const Skeleton3D *skeleton_3d) { GodotProfileZone("AnimationData::allocate_track_values"); - const LocalVector tracks = animation->get_tracks(); + const LocalVector &tracks = animation->get_tracks(); Animation::Track *const *tracks_ptr = tracks.ptr(); int count = tracks.size(); @@ -597,7 +597,7 @@ void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::sort_nodes_and_references() { LocalVector sorted_node_indices = get_sorted_node_indices(); LocalVector> sorted_nodes; - LocalVector old_node_connection_info = node_connection_info; + LocalVector old_node_connection_info(node_connection_info); for (unsigned int i = 0; i < sorted_node_indices.size(); i++) { int node_index = sorted_node_indices[i]; sorted_nodes.push_back(nodes[node_index]); -- 2.47.2 From 2cee55037be7dddf50cec4b385ad3f93e7c77b5d Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Thu, 5 Feb 2026 22:13:45 +0100 Subject: [PATCH 17/29] Renamed BLTAnimationNode::position to BLTAnimationNode::graph_offset and made it hidden in the editor by default. --- blendalot_animation_node.cpp | 16 +-- blendalot_animation_node.h | 10 +- demo/addons/blendalot/blendalot_main_panel.gd | 5 +- demo/main.tscn | 115 +++++++++++------- demo/synced_blend_tree_node.tres | 7 +- demo/synced_blend_tree_walk_limp.tres | 14 +-- demo/synced_blend_tree_walk_run.tres | 12 +- 7 files changed, 103 insertions(+), 76 deletions(-) diff --git a/blendalot_animation_node.cpp b/blendalot_animation_node.cpp index de867df..a0a53de 100644 --- a/blendalot_animation_node.cpp +++ b/blendalot_animation_node.cpp @@ -5,9 +5,9 @@ #include "blendalot_animation_node.h" void BLTAnimationNode::_bind_methods() { - ClassDB::bind_method(D_METHOD("set_position", "position"), &BLTAnimationNode::set_position); - ClassDB::bind_method(D_METHOD("get_position"), &BLTAnimationNode::get_position); - ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "position"), "set_position", "get_position"); + ClassDB::bind_method(D_METHOD("set_graph_offset", "offset"), &BLTAnimationNode::set_graph_offset); + ClassDB::bind_method(D_METHOD("get_graph_offset"), &BLTAnimationNode::get_graph_offset); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "graph_offset", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_graph_offset", "get_graph_offset"); ADD_SIGNAL(MethodInfo("animation_node_renamed", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "old_name"), PropertyInfo(Variant::STRING, "new_name"))); ADD_SIGNAL(MethodInfo("animation_node_removed", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "name"))); @@ -76,7 +76,7 @@ void BLTAnimationNodeBlendTree::_get_property_list(List *p_list) c 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::VECTOR2, "nodes/" + prop_name + "/graph_offset", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); } p_list->push_back(PropertyInfo(Variant::ARRAY, "node_connections", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); @@ -96,9 +96,9 @@ bool BLTAnimationNodeBlendTree::_get(const StringName &p_name, Variant &r_value) } } - if (what == "position") { + if (what == "graph_offset") { if (node_index != -1) { - r_value = tree_graph.nodes[node_index]->position; + r_value = tree_graph.nodes[node_index]->graph_offset; return true; } } @@ -136,10 +136,10 @@ bool BLTAnimationNodeBlendTree::_set(const StringName &p_name, const Variant &p_ return true; } - if (what == "position") { + if (what == "graph_offset") { int node_index = find_node_index_by_name(node_name); if (node_index > -1) { - tree_graph.nodes[node_index]->position = p_value; + tree_graph.nodes[node_index]->graph_offset = p_value; } return true; } diff --git a/blendalot_animation_node.h b/blendalot_animation_node.h index 41e6922..0263bcb 100644 --- a/blendalot_animation_node.h +++ b/blendalot_animation_node.h @@ -263,7 +263,7 @@ public: NodeTimeInfo node_time_info; bool active = false; - Vector2 position; + Vector2 graph_offset; virtual ~BLTAnimationNode() override = default; virtual bool initialize(GraphEvaluationContext &context) { @@ -307,12 +307,12 @@ public: } } - void set_position(const Vector2 &p_position) { - position = p_position; + void set_graph_offset(const Vector2 &p_position) { + graph_offset = p_position; } - Vector2 get_position() const { - return position; + Vector2 get_graph_offset() const { + return graph_offset; } virtual Vector get_input_names() const { return {}; } diff --git a/demo/addons/blendalot/blendalot_main_panel.gd b/demo/addons/blendalot/blendalot_main_panel.gd index 5d9334c..a82f38b 100644 --- a/demo/addons/blendalot/blendalot_main_panel.gd +++ b/demo/addons/blendalot/blendalot_main_panel.gd @@ -38,7 +38,7 @@ func create_node_for_blt_node(blt_node: BLTAnimationNode) -> GraphNode: var result_graph_node:GraphNode = GraphNode.new() result_graph_node.name = blt_node.resource_name result_graph_node.title = blt_node.resource_name - result_graph_node.position_offset = blt_node.position + result_graph_node.position_offset = blt_node.graph_offset var result_slot_offset = 0 @@ -229,7 +229,7 @@ func _on_blend_tree_graph_edit_connection_request(from_node: StringName, from_po func _on_blend_tree_graph_edit_end_node_move() -> void: for graph_node:GraphNode in selected_nodes.keys(): - graph_node_to_blend_tree_node[graph_node].position = graph_node.position_offset + graph_node_to_blend_tree_node[graph_node].graph_offset = graph_node.position_offset func _on_blend_tree_graph_edit_begin_node_move() -> void: @@ -266,6 +266,7 @@ func _on_add_node_popup_menu_index_pressed(index: int) -> void: if new_node_position != Vector2.INF: graph_node.position_offset = new_node_position + new_blend_tree_node.graph_offset = new_node_position new_node_position = Vector2.INF diff --git a/demo/main.tscn b/demo/main.tscn index 3385af8..411fc5a 100644 --- a/demo/main.tscn +++ b/demo/main.tscn @@ -6,7 +6,6 @@ [ext_resource type="AnimationNodeBlendTree" uid="uid://dqy0dgwsm8t46" path="res://animation_tree_walk_limp.tres" id="3_272bh"] [ext_resource type="AnimationNodeBlendTree" uid="uid://vsf71o82lkld" path="res://animation_tree_walk_run.tres" id="6_5vw27"] [ext_resource type="BLTAnimationNodeBlendTree" uid="uid://2qfwr1xkiw0s" path="res://synced_blend_tree_walk_limp.tres" id="6_272bh"] -[ext_resource type="BLTAnimationNodeBlendTree" uid="uid://qsk64ax2o47f" path="res://synced_blend_tree_walk_run.tres" id="7_272bh"] [sub_resource type="Theme" id="Theme_272bh"] default_font_size = 30 @@ -32,6 +31,30 @@ sky = SubResource("Sky_1bvp3") tonemap_mode = 2 glow_enabled = true +[sub_resource type="BLTAnimationNodeBlend2" id="BLTAnimationNodeBlend2_bvt3d"] +resource_name = "BLTAnimationNodeBlend2" +graph_offset = Vector2(-540, -120) + +[sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_sntl5"] +resource_name = "BLTAnimationNodeSampler 1" +graph_offset = Vector2(-1120, 180) +animation = &"animation_library/Run-InPlace" + +[sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_n4m28"] +resource_name = "BLTAnimationNodeSampler" +graph_offset = Vector2(-1120, -280) +animation = &"animation_library/Walk-InPlace" + +[sub_resource type="BLTAnimationNodeBlendTree" id="BLTAnimationNodeBlendTree_7mycd"] +nodes/Output/position = null +nodes/BLTAnimationNodeBlend2/node = SubResource("BLTAnimationNodeBlend2_bvt3d") +nodes/BLTAnimationNodeBlend2/position = null +"nodes/BLTAnimationNodeSampler 1/node" = SubResource("BLTAnimationNodeSampler_sntl5") +"nodes/BLTAnimationNodeSampler 1/position" = null +nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_n4m28") +nodes/BLTAnimationNodeSampler/position = null +node_connections = ["BLTAnimationNodeBlend2", 0, "BLTAnimationNodeSampler", "BLTAnimationNodeBlend2", 1, "BLTAnimationNodeSampler 1", "Output", 0, "BLTAnimationNodeBlend2"] + [node name="Main" type="Node3D" unique_id=933302313] script = ExtResource("1_1bvp3") @@ -155,29 +178,29 @@ parameters/BLTAnimationNodeBlend2/blend_amount = 1.0 unique_name_in_owner = true transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4, 0, 0) -[node name="Skeleton3D" parent="Characters/MixamoAmyWalkRunSynced/Armature" parent_id_path=PackedInt32Array(2088190993, 259532241) index="0" unique_id=1597913436] -bones/0/position = Vector3(0.03366956, 22.30488, 0) -bones/2/position = Vector3(0.021933563, -1.2283401, -66.54255) -bones/2/rotation = Quaternion(-0.7020165, -0.025985688, 0.0034645642, 0.7116779) +[node name="Skeleton3D" parent="Characters/MixamoAmyWalkRunSynced/Armature" parent_id_path=PackedInt32Array(2088190993, 52334938) index="0" unique_id=2014991417] +bones/0/position = Vector3(1.3737135, 42.922382, 0) +bones/2/position = Vector3(1.37469, -1.2283401, -63.996525) +bones/2/rotation = Quaternion(-0.69760156, -0.026379798, 0.019266207, 0.7157409) bones/3/position = Vector3(1.8626451e-08, 3.9624305, -0.274297) -bones/3/rotation = Quaternion(-0.016682021, 0.014965671, -0.0026113286, 0.9997449) +bones/3/rotation = Quaternion(-0.022506908, 0.017493227, -0.0074128713, 0.99956536) bones/4/position = Vector3(5.9604645e-08, 14.123348, 0.2644268) -bones/4/rotation = Quaternion(0.008006268, 0.031130861, -0.0048357854, 0.9994714) +bones/4/rotation = Quaternion(-0.00365946, 0.03815137, -0.014585497, 0.99915886) bones/5/position = Vector3(3.7252903e-08, 11.404536, 0.106110305) -bones/5/rotation = Quaternion(0.008006268, 0.03113088, -0.0048357914, 0.9994714) +bones/5/rotation = Quaternion(-0.0036594612, 0.038151365, -0.014585488, 0.99915886) bones/6/position = Vector3(2.9802322e-08, 9.946667, 1.6090326e-08) -bones/6/rotation = Quaternion(0.043000314, -0.01605664, 0.0336432, 0.9983794) +bones/6/rotation = Quaternion(0.031101733, -0.016018406, 0.03743902, 0.9986864) bones/7/position = Vector3(3.1292439e-07, 8.579114, 0.11416777) -bones/7/rotation = Quaternion(0.0067235962, -0.019416407, -0.004696545, 0.9997777) +bones/7/rotation = Quaternion(0.007968462, -0.023749013, 0.017739752, 0.9995287) bones/8/position = Vector3(1.8626451e-09, 25.089184, 0.33387405) bones/9/position = Vector3(4.22409, 8.898366, -0.056614783) -bones/9/rotation = Quaternion(0.6655204, 0.33482903, -0.54497546, 0.38467056) +bones/9/rotation = Quaternion(0.67403185, 0.32335418, -0.54488075, 0.3797739) bones/10/position = Vector3(7.763405e-07, 8.86101, -1.885943e-05) -bones/10/rotation = Quaternion(0.3856572, -0.029256662, -0.20285419, 0.8995903) +bones/10/rotation = Quaternion(0.3653002, -0.028586289, -0.27500978, 0.8888802) bones/11/position = Vector3(-6.736064e-08, 18.975187, -5.359281e-06) -bones/11/rotation = Quaternion(0.0491358, 0.18193112, 0.21587172, 0.95806384) +bones/11/rotation = Quaternion(0.053082827, 0.17740138, 0.31139585, 0.9320641) bones/12/position = Vector3(2.0505329e-06, 19.896036, 4.198435e-06) -bones/12/rotation = Quaternion(-0.10542738, 0.21719287, 0.097704284, 0.9654874) +bones/12/rotation = Quaternion(-0.02275733, 0.23029867, -0.11381663, 0.96617305) bones/13/position = Vector3(-2.0888743, 2.1412597, 1.2094326) bones/13/rotation = Quaternion(0.25766087, -0.0057431525, 0.24790713, 0.93387365) bones/14/position = Vector3(-0.3297696, 2.5601492, 5.6111962e-06) @@ -186,96 +209,96 @@ bones/15/position = Vector3(0.07692051, 2.7271638, -7.698024e-07) bones/15/rotation = Quaternion(0.041175473, -5.169663e-07, -3.3207877e-07, 0.99915195) bones/16/position = Vector3(0.25285125, 2.2376482, -2.5033949e-06) bones/17/position = Vector3(-2.5860305, 6.9422946, 0.029643927) -bones/17/rotation = Quaternion(0.32230198, 0.013257779, 0.0790869, 0.9432342) +bones/17/rotation = Quaternion(0.35217047, 0.017903585, 0.089692846, 0.9314562) bones/18/position = Vector3(0.0010065138, 2.844871, 2.6222544e-05) -bones/18/rotation = Quaternion(0.51890975, -0.009350935, 0.048978366, 0.8533736) +bones/18/rotation = Quaternion(0.5128366, -0.009204624, 0.048090808, 0.8570888) bones/19/position = Vector3(-0.001300633, 2.6654923, -3.370296e-06) bones/19/rotation = Quaternion(0.0067229928, -1.8172469e-07, -1.12402185e-08, 0.9999774) bones/20/position = Vector3(0.00029665232, 2.2974284, 2.6011842e-06) bones/21/position = Vector3(-0.81500757, 6.8271494, -0.11008961) -bones/21/rotation = Quaternion(0.44832447, -0.029388895, 0.08389875, 0.8894394) +bones/21/rotation = Quaternion(0.47678298, -0.024179399, 0.09396766, 0.8736494) bones/22/position = Vector3(-0.011725575, 3.256784, 9.871595e-06) -bones/22/rotation = Quaternion(0.6974019, -0.007876825, 0.0774008, 0.71244484) +bones/22/rotation = Quaternion(0.6923176, -0.007814516, 0.076714024, 0.7174611) bones/23/position = Vector3(-0.00070961565, 3.1778917, -4.440292e-06) bones/23/rotation = Quaternion(0.05807299, -2.1569534e-08, -4.8931874e-08, 0.99831235) bones/24/position = Vector3(0.012437165, 2.6951318, 2.2368273e-05) bones/25/position = Vector3(0.89882326, 6.7787366, -0.023027958) -bones/25/rotation = Quaternion(0.43942147, -0.11050543, -0.00536839, 0.8914418) +bones/25/rotation = Quaternion(0.46876705, -0.10649501, 0.0048512574, 0.8768654) bones/26/position = Vector3(-0.0040293336, 2.958041, 1.3685599e-05) -bones/26/rotation = Quaternion(0.70637834, 0.0028474892, 0.061491687, 0.70515275) +bones/26/rotation = Quaternion(0.7013153, 0.002825051, 0.061101884, 0.7102221) bones/27/position = Vector3(0.0024927258, 2.756927, -1.8636636e-05) bones/27/rotation = Quaternion(-0.0033205294, 3.9059813e-08, 6.5682637e-09, 0.9999945) bones/28/position = Vector3(0.0015397072, 2.3345788, 1.1771219e-05) bones/29/position = Vector3(2.5022223, 6.3907866, 0.08744741) -bones/29/rotation = Quaternion(0.5366728, -0.15708813, -0.01123394, 0.8289629) +bones/29/rotation = Quaternion(0.56450063, -0.15266693, -0.0018035976, 0.8111898) bones/30/position = Vector3(0.0032016933, 2.6237173, 5.5511973e-06) -bones/30/rotation = Quaternion(0.6318143, 0.022965502, 0.05855106, 0.7725639) +bones/30/rotation = Quaternion(0.6262513, 0.022716032, 0.058525927, 0.7770894) bones/31/position = Vector3(0.003878951, 2.1320877, -1.2886679e-06) bones/31/rotation = Quaternion(-0.06925962, -1.0360488e-07, -7.0190005e-08, 0.9975987) bones/32/position = Vector3(-0.0070759356, 1.7932303, -8.8289596e-07) bones/33/position = Vector3(-4.2240915, 8.897269, -0.030230172) -bones/33/rotation = Quaternion(0.65423024, -0.3460148, 0.5481125, 0.38965285) +bones/33/rotation = Quaternion(0.64940715, -0.35115576, 0.552351, 0.38712853) bones/34/position = Vector3(9.327891e-07, 8.8610115, -9.847447e-06) -bones/34/rotation = Quaternion(0.33522785, 0.13258426, -0.06878553, 0.93022156) +bones/34/rotation = Quaternion(0.37322992, 0.051175877, -0.014639804, 0.92621064) bones/35/position = Vector3(6.476337e-07, 18.973557, 5.8403616e-06) -bones/35/rotation = Quaternion(0.06291787, -0.06795199, -0.34171283, 0.9352305) +bones/35/rotation = Quaternion(-2.38223e-05, -0.0878173, -0.45667896, 0.8852867) bones/36/position = Vector3(-7.630494e-08, 19.89834, -1.8180976e-06) -bones/36/rotation = Quaternion(-0.13934937, -0.17929602, 0.059740018, 0.972042) +bones/36/rotation = Quaternion(-0.22069603, -0.15216982, -0.028448751, 0.9629787) bones/37/position = Vector3(2.0620086, 2.1477072, 1.2410417) -bones/37/rotation = Quaternion(0.20313361, 0.020001005, -0.30569944, 0.9299916) +bones/37/rotation = Quaternion(0.21391132, 0.020952638, -0.2679689, 0.93914616) bones/38/position = Vector3(0.2744484, 2.5658755, 3.7584634e-06) -bones/38/rotation = Quaternion(0.07182112, -0.15283245, 0.11806564, 0.97854203) +bones/38/rotation = Quaternion(0.064052425, -0.18935232, 0.09533193, 0.9751691) bones/39/position = Vector3(-0.076725245, 2.730221, -7.234994e-06) bones/39/rotation = Quaternion(0.037325032, -2.1781963e-08, 1.19075e-07, 0.99930316) bones/40/position = Vector3(-0.19772077, 2.2442596, 9.685756e-06) bones/41/position = Vector3(2.5699375, 7.0537868, 0.05093465) -bones/41/rotation = Quaternion(0.44285733, 0.008293978, -0.03807221, 0.89574504) +bones/41/rotation = Quaternion(0.45433038, 0.007974177, -0.040324483, 0.8898841) bones/42/position = Vector3(-0.0015108623, 2.816315, -9.529522e-06) -bones/42/rotation = Quaternion(0.5924978, 0.028048418, -0.045164384, 0.80381584) +bones/42/rotation = Quaternion(0.6120771, 0.028411472, -0.047977913, 0.78882915) bones/43/position = Vector3(0.0011758618, 2.6396425, 2.472788e-06) bones/43/rotation = Quaternion(0.0026853334, 3.311302e-08, -5.8677845e-08, 0.9999965) bones/44/position = Vector3(0.00033191964, 2.2415001, 1.3194513e-06) bones/45/position = Vector3(0.76549155, 7.018211, -0.09038047) -bones/45/rotation = Quaternion(0.46793976, 0.036938474, -0.033631414, 0.88234746) +bones/45/rotation = Quaternion(0.483478, 0.036460996, -0.036219023, 0.87384653) bones/46/position = Vector3(0.008488461, 3.3401706, -6.017696e-06) -bones/46/rotation = Quaternion(0.6849328, 0.039953418, -0.055356182, 0.72540087) +bones/46/rotation = Quaternion(0.701871, 0.04011571, -0.05796006, 0.7088077) bones/47/position = Vector3(-0.007900611, 3.0566626, 5.4270527e-06) bones/47/rotation = Quaternion(-0.027461762, -2.0760188e-08, 5.6872928e-08, 0.9996229) bones/48/position = Vector3(-0.00058989227, 2.63951, 9.457464e-06) bones/49/position = Vector3(-0.8923034, 6.9110794, -0.12160769) -bones/49/rotation = Quaternion(0.47066858, 0.033067867, -0.0154665, 0.8815544) +bones/49/rotation = Quaternion(0.48708817, 0.03278609, -0.015544657, 0.8725985) bones/50/position = Vector3(-0.00297606, 2.923956, 1.7109673e-05) -bones/50/rotation = Quaternion(0.6740056, -0.019516876, -0.06773796, 0.7353549) +bones/50/rotation = Quaternion(0.6931497, -0.019576456, -0.06891241, 0.71722454) bones/51/position = Vector3(0.0026345253, 2.717951, -5.427028e-06) bones/51/rotation = Quaternion(0.009096339, 7.025756e-08, 2.6579988e-08, 0.99995863) bones/52/position = Vector3(0.0003426671, 2.379634, 1.4747493e-06) bones/53/position = Vector3(-2.4431183, 6.453577, 0.12551774) -bones/53/rotation = Quaternion(0.4273077, 0.025455933, -0.032704208, 0.9031559) +bones/53/rotation = Quaternion(0.44882435, 0.025137398, -0.033032093, 0.89265347) bones/54/position = Vector3(-0.0026362836, 2.6278691, 4.2827646e-06) -bones/54/rotation = Quaternion(0.73083663, -0.044815022, -0.09265602, 0.6747476) +bones/54/rotation = Quaternion(0.7472647, -0.045266245, -0.09292602, 0.65643823) bones/55/position = Vector3(-0.001860708, 2.1224687, 9.940322e-07) bones/55/rotation = Quaternion(-0.03619139, -1.1229469e-07, 2.121775e-08, 0.9993449) bones/56/position = Vector3(0.004502952, 1.7919822, 1.2924895e-05) bones/57/position = Vector3(7.557004, -4.826265, 0.30053553) -bones/57/rotation = Quaternion(-0.036588665, -0.34452513, -0.9375553, 0.030883614) +bones/57/rotation = Quaternion(-0.04979198, -0.3562142, -0.93288946, 0.018696664) bones/58/position = Vector3(-0.124621816, 24.06574, 0.10470339) -bones/58/rotation = Quaternion(-0.17203768, -0.022209316, 0.008335125, 0.98480475) +bones/58/rotation = Quaternion(-0.30548102, 0.032796834, -0.01660958, 0.9514882) bones/59/position = Vector3(-0.0060171685, 33.063114, 0.00394835) -bones/59/rotation = Quaternion(0.500414, -0.0045057763, 0.0039036346, 0.86576587) +bones/59/rotation = Quaternion(0.44427872, 0.000708564, 0.0098030325, 0.8958346) bones/60/position = Vector3(-0.0052775713, 12.893309, -0.5155027) -bones/60/rotation = Quaternion(0.34159487, -0.05822111, 0.025601145, 0.9376929) +bones/60/rotation = Quaternion(0.31359768, -0.055260643, 0.025034118, 0.94761604) bones/61/position = Vector3(0.014227723, 6.420835, 0.007887725) bones/62/position = Vector3(-7.557004, -4.826261, 0.30053535) -bones/62/rotation = Quaternion(-0.105550915, -0.06569394, 0.9920036, 0.021728868) +bones/62/rotation = Quaternion(-0.10350199, -0.08600675, 0.989418, 0.0542423) bones/63/position = Vector3(0.12476807, 24.070103, -0.14491707) -bones/63/rotation = Quaternion(-0.21048684, -0.113292366, 0.05454326, 0.96947676) +bones/63/rotation = Quaternion(-0.32747734, -0.0674623, 0.0130414665, 0.9423574) bones/64/position = Vector3(0.0060205855, 33.063133, 0.004499323) -bones/64/rotation = Quaternion(0.5910368, -0.030297726, -0.04612658, 0.8047532) +bones/64/rotation = Quaternion(0.26777777, -0.01583679, -0.0035161127, 0.9633441) bones/65/position = Vector3(0.005378528, 12.718271, -0.52236915) -bones/65/rotation = Quaternion(0.4105115, 0.015659228, -0.01512902, 0.91159546) +bones/65/rotation = Quaternion(0.6911166, 0.033012606, 0.0070720776, 0.72195446) bones/66/position = Vector3(-0.014239848, 6.412128, 0.0079107955) -[node name="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" index="1" unique_id=99701443] +[node name="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" index="1" unique_id=945472897] active = false [node name="AnimationPlayer2" type="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" unique_id=1255239074] @@ -284,7 +307,7 @@ libraries/animation_library = ExtResource("3_1bvp3") [node name="SyncedAnimationGraph" type="BLTAnimationGraph" parent="Characters/MixamoAmyWalkRunSynced" unique_id=1602406394] animation_player = NodePath("../AnimationPlayer2") -tree_root = ExtResource("7_272bh") +tree_root = SubResource("BLTAnimationNodeBlendTree_7mycd") skeleton = NodePath("../Armature/Skeleton3D") parameters/BLTAnimationNodeBlend2/blend_amount = 0.0 diff --git a/demo/synced_blend_tree_node.tres b/demo/synced_blend_tree_node.tres index e3124c5..3dacf6f 100644 --- a/demo/synced_blend_tree_node.tres +++ b/demo/synced_blend_tree_node.tres @@ -1,12 +1,15 @@ -[gd_resource type="BLTAnimationNodeBlendTree" load_steps=4 format=3] +[gd_resource type="BLTAnimationNodeBlendTree" format=3] [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_bvt3d"] +resource_name = "BLTAnimationNodeSampler 1" animation = &"animation_library/TestAnimationB" [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_sntl5"] +resource_name = "BLTAnimationNodeSampler" animation = &"animation_library/TestAnimationA" [sub_resource type="BLTAnimationNodeBlend2" id="BLTAnimationNodeBlend2_n4m28"] +resource_name = "Blend2" sync = false blend_amount = 0.5 sync = false @@ -18,4 +21,4 @@ nodes/Blend2/position = Vector2(0, 0) "nodes/BLTAnimationNodeSampler 1/position" = Vector2(0, 0) nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_sntl5") nodes/BLTAnimationNodeSampler/position = Vector2(0, 0) -node_connections = [&"Blend2", 0, &"BLTAnimationNodeSampler", &"Blend2", 1, &"BLTAnimationNodeSampler 1", &"Output", 0, &"Blend2"] +node_connections = ["Blend2", 0, "BLTAnimationNodeSampler", "Blend2", 1, "BLTAnimationNodeSampler 1", "Output", 0, "Blend2"] diff --git a/demo/synced_blend_tree_walk_limp.tres b/demo/synced_blend_tree_walk_limp.tres index 20ac3b2..7cbdee2 100644 --- a/demo/synced_blend_tree_walk_limp.tres +++ b/demo/synced_blend_tree_walk_limp.tres @@ -2,25 +2,25 @@ [sub_resource type="BLTAnimationNodeBlend2" id="BLTAnimationNodeBlend2_bvt3d"] resource_name = "BLTAnimationNodeBlend2" -position = Vector2(950.50195, 749.8301) +graph_offset = Vector2(-600, 180) blend_amount = 1.0 [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_sntl5"] resource_name = "BLTAnimationNodeSampler 1" -position = Vector2(101.323975, 1247.624) +graph_offset = Vector2(-1200, 560) animation = &"animation_library/Limping-InPlace" [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_n4m28"] resource_name = "BLTAnimationNodeSampler" -position = Vector2(101.323975, 398.44608) +graph_offset = Vector2(-1300, -40) animation = &"animation_library/Walk-InPlace" [resource] -nodes/Output/position = Vector2(1741.1158, 925.52203) +nodes/Output/position = null nodes/BLTAnimationNodeBlend2/node = SubResource("BLTAnimationNodeBlend2_bvt3d") -nodes/BLTAnimationNodeBlend2/position = Vector2(950.50195, 749.8301) +nodes/BLTAnimationNodeBlend2/position = null "nodes/BLTAnimationNodeSampler 1/node" = SubResource("BLTAnimationNodeSampler_sntl5") -"nodes/BLTAnimationNodeSampler 1/position" = Vector2(101.323975, 1247.624) +"nodes/BLTAnimationNodeSampler 1/position" = null nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_n4m28") -nodes/BLTAnimationNodeSampler/position = Vector2(101.323975, 398.44608) +nodes/BLTAnimationNodeSampler/position = null node_connections = ["BLTAnimationNodeBlend2", 0, "BLTAnimationNodeSampler", "BLTAnimationNodeBlend2", 1, "BLTAnimationNodeSampler 1", "Output", 0, "BLTAnimationNodeBlend2"] diff --git a/demo/synced_blend_tree_walk_run.tres b/demo/synced_blend_tree_walk_run.tres index 5728f70..7171fb9 100644 --- a/demo/synced_blend_tree_walk_run.tres +++ b/demo/synced_blend_tree_walk_run.tres @@ -2,24 +2,24 @@ [sub_resource type="BLTAnimationNodeBlend2" id="BLTAnimationNodeBlend2_bvt3d"] resource_name = "BLTAnimationNodeBlend2" -position = Vector2(-420, 220) +graph_offset = Vector2(-360, 140) [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_sntl5"] resource_name = "BLTAnimationNodeSampler 1" -position = Vector2(-1020, 380) +graph_offset = Vector2(-1140, 440) animation = &"animation_library/Run-InPlace" [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_n4m28"] resource_name = "BLTAnimationNodeSampler" -position = Vector2(-880, 0) +graph_offset = Vector2(-1080, -40) animation = &"animation_library/Walk-InPlace" [resource] nodes/Output/position = Vector2(180, 80) nodes/BLTAnimationNodeBlend2/node = SubResource("BLTAnimationNodeBlend2_bvt3d") -nodes/BLTAnimationNodeBlend2/position = Vector2(-420, 220) +nodes/BLTAnimationNodeBlend2/position = Vector2(-360, 140) "nodes/BLTAnimationNodeSampler 1/node" = SubResource("BLTAnimationNodeSampler_sntl5") -"nodes/BLTAnimationNodeSampler 1/position" = Vector2(-1020, 380) +"nodes/BLTAnimationNodeSampler 1/position" = Vector2(-1140, 440) nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_n4m28") -nodes/BLTAnimationNodeSampler/position = Vector2(-880, 0) +nodes/BLTAnimationNodeSampler/position = Vector2(-1080, -40) node_connections = ["BLTAnimationNodeBlend2", 0, "BLTAnimationNodeSampler", "BLTAnimationNodeBlend2", 1, "BLTAnimationNodeSampler 1", "Output", 0, "BLTAnimationNodeBlend2"] -- 2.47.2 From 7255c99ef76eebad80444cc0580c26b753958ab6 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Thu, 5 Feb 2026 22:29:00 +0100 Subject: [PATCH 18/29] BLTAnimationNodeBlend2.sync is no more exposed as parameter, only as property. --- blendalot_animation_node.cpp | 11 ---- demo/main.tscn | 91 +++++++++++++-------------- demo/synced_blend_tree_walk_limp.tres | 7 +-- 3 files changed, 48 insertions(+), 61 deletions(-) diff --git a/blendalot_animation_node.cpp b/blendalot_animation_node.cpp index a0a53de..4399e0f 100644 --- a/blendalot_animation_node.cpp +++ b/blendalot_animation_node.cpp @@ -464,7 +464,6 @@ Variant BLTAnimationNodeBlend2::get_parameter_default_value(const StringName &p_ void BLTAnimationNodeBlend2::_get_property_list(List *p_list) const { p_list->push_back(PropertyInfo(Variant::FLOAT, blend_weight_pname, PROPERTY_HINT_RANGE, "0,1,0.01,or_less,or_greater")); - p_list->push_back(PropertyInfo(Variant::BOOL, sync_pname)); } bool BLTAnimationNodeBlend2::_get(const StringName &p_name, Variant &r_value) const { @@ -473,11 +472,6 @@ bool BLTAnimationNodeBlend2::_get(const StringName &p_name, Variant &r_value) co return true; } - if (p_name == sync_pname) { - r_value = sync; - return true; - } - return false; } @@ -487,11 +481,6 @@ bool BLTAnimationNodeBlend2::_set(const StringName &p_name, const Variant &p_val return true; } - if (p_name == sync_pname) { - sync = p_value; - return true; - } - return false; } diff --git a/demo/main.tscn b/demo/main.tscn index 411fc5a..c7c61c5 100644 --- a/demo/main.tscn +++ b/demo/main.tscn @@ -46,13 +46,12 @@ graph_offset = Vector2(-1120, -280) animation = &"animation_library/Walk-InPlace" [sub_resource type="BLTAnimationNodeBlendTree" id="BLTAnimationNodeBlendTree_7mycd"] -nodes/Output/position = null nodes/BLTAnimationNodeBlend2/node = SubResource("BLTAnimationNodeBlend2_bvt3d") -nodes/BLTAnimationNodeBlend2/position = null +nodes/BLTAnimationNodeBlend2/graph_offset = Vector2(-540, -120) "nodes/BLTAnimationNodeSampler 1/node" = SubResource("BLTAnimationNodeSampler_sntl5") -"nodes/BLTAnimationNodeSampler 1/position" = null +"nodes/BLTAnimationNodeSampler 1/graph_offset" = Vector2(-1120, 180) nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_n4m28") -nodes/BLTAnimationNodeSampler/position = null +nodes/BLTAnimationNodeSampler/graph_offset = Vector2(-1120, -280) node_connections = ["BLTAnimationNodeBlend2", 0, "BLTAnimationNodeSampler", "BLTAnimationNodeBlend2", 1, "BLTAnimationNodeSampler 1", "Output", 0, "BLTAnimationNodeBlend2"] [node name="Main" type="Node3D" unique_id=933302313] @@ -179,28 +178,28 @@ unique_name_in_owner = true transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4, 0, 0) [node name="Skeleton3D" parent="Characters/MixamoAmyWalkRunSynced/Armature" parent_id_path=PackedInt32Array(2088190993, 52334938) index="0" unique_id=2014991417] -bones/0/position = Vector3(1.3737135, 42.922382, 0) -bones/2/position = Vector3(1.37469, -1.2283401, -63.996525) -bones/2/rotation = Quaternion(-0.69760156, -0.026379798, 0.019266207, 0.7157409) +bones/0/position = Vector3(1.2535723, 103.23205, 0) +bones/2/position = Vector3(1.2545489, -1.2283401, -63.87065) +bones/2/rotation = Quaternion(-0.68592405, 0.04377775, -0.051641874, 0.724517) bones/3/position = Vector3(1.8626451e-08, 3.9624305, -0.274297) -bones/3/rotation = Quaternion(-0.022506908, 0.017493227, -0.0074128713, 0.99956536) +bones/3/rotation = Quaternion(-0.022852536, -0.012121664, 0.0041041635, 0.99965584) bones/4/position = Vector3(5.9604645e-08, 14.123348, 0.2644268) -bones/4/rotation = Quaternion(-0.00365946, 0.03815137, -0.014585497, 0.99915886) +bones/4/rotation = Quaternion(-0.004333934, -0.027048457, 0.008698374, 0.99958694) bones/5/position = Vector3(3.7252903e-08, 11.404536, 0.106110305) -bones/5/rotation = Quaternion(-0.0036594612, 0.038151365, -0.014585488, 0.99915886) +bones/5/rotation = Quaternion(-0.0043339357, -0.027048472, 0.008698381, 0.99958694) bones/6/position = Vector3(2.9802322e-08, 9.946667, 1.6090326e-08) -bones/6/rotation = Quaternion(0.031101733, -0.016018406, 0.03743902, 0.9986864) +bones/6/rotation = Quaternion(0.01833006, 0.009223348, -0.022252144, 0.99954194) bones/7/position = Vector3(3.1292439e-07, 8.579114, 0.11416777) -bones/7/rotation = Quaternion(0.007968462, -0.023749013, 0.017739752, 0.9995287) +bones/7/rotation = Quaternion(0.012213917, 0.0008427718, 0.018853437, 0.99974734) bones/8/position = Vector3(1.8626451e-09, 25.089184, 0.33387405) bones/9/position = Vector3(4.22409, 8.898366, -0.056614783) -bones/9/rotation = Quaternion(0.67403185, 0.32335418, -0.54488075, 0.3797739) +bones/9/rotation = Quaternion(0.6974001, 0.2956288, -0.5221589, 0.39190146) bones/10/position = Vector3(7.763405e-07, 8.86101, -1.885943e-05) -bones/10/rotation = Quaternion(0.3653002, -0.028586289, -0.27500978, 0.8888802) +bones/10/rotation = Quaternion(0.26628703, -0.06642532, 0.008976073, 0.9615604) bones/11/position = Vector3(-6.736064e-08, 18.975187, -5.359281e-06) -bones/11/rotation = Quaternion(0.053082827, 0.17740138, 0.31139585, 0.9320641) +bones/11/rotation = Quaternion(0.07033256, 0.12871316, 0.46235967, 0.8744769) bones/12/position = Vector3(2.0505329e-06, 19.896036, 4.198435e-06) -bones/12/rotation = Quaternion(-0.02275733, 0.23029867, -0.11381663, 0.96617305) +bones/12/rotation = Quaternion(-0.16744098, 0.1057004, 0.06551589, 0.97800744) bones/13/position = Vector3(-2.0888743, 2.1412597, 1.2094326) bones/13/rotation = Quaternion(0.25766087, -0.0057431525, 0.24790713, 0.93387365) bones/14/position = Vector3(-0.3297696, 2.5601492, 5.6111962e-06) @@ -209,93 +208,93 @@ bones/15/position = Vector3(0.07692051, 2.7271638, -7.698024e-07) bones/15/rotation = Quaternion(0.041175473, -5.169663e-07, -3.3207877e-07, 0.99915195) bones/16/position = Vector3(0.25285125, 2.2376482, -2.5033949e-06) bones/17/position = Vector3(-2.5860305, 6.9422946, 0.029643927) -bones/17/rotation = Quaternion(0.35217047, 0.017903585, 0.089692846, 0.9314562) +bones/17/rotation = Quaternion(0.34374678, 0.015888449, 0.085352995, 0.9350405) bones/18/position = Vector3(0.0010065138, 2.844871, 2.6222544e-05) -bones/18/rotation = Quaternion(0.5128366, -0.009204624, 0.048090808, 0.8570888) +bones/18/rotation = Quaternion(0.57630825, -0.010742382, 0.057439517, 0.8151404) bones/19/position = Vector3(-0.001300633, 2.6654923, -3.370296e-06) bones/19/rotation = Quaternion(0.0067229928, -1.8172469e-07, -1.12402185e-08, 0.9999774) bones/20/position = Vector3(0.00029665232, 2.2974284, 2.6011842e-06) bones/21/position = Vector3(-0.81500757, 6.8271494, -0.11008961) -bones/21/rotation = Quaternion(0.47678298, -0.024179399, 0.09396766, 0.8736494) +bones/21/rotation = Quaternion(0.4686701, -0.026470345, 0.09011306, 0.87836635) bones/22/position = Vector3(-0.011725575, 3.256784, 9.871595e-06) -bones/22/rotation = Quaternion(0.6923176, -0.007814516, 0.076714024, 0.7174611) +bones/22/rotation = Quaternion(0.7447584, -0.008458963, 0.08384082, 0.66199255) bones/23/position = Vector3(-0.00070961565, 3.1778917, -4.440292e-06) bones/23/rotation = Quaternion(0.05807299, -2.1569534e-08, -4.8931874e-08, 0.99831235) bones/24/position = Vector3(0.012437165, 2.6951318, 2.2368273e-05) bones/25/position = Vector3(0.89882326, 6.7787366, -0.023027958) -bones/25/rotation = Quaternion(0.46876705, -0.10649501, 0.0048512574, 0.8768654) +bones/25/rotation = Quaternion(0.46021903, -0.10865909, 0.0012826431, 0.88113) bones/26/position = Vector3(-0.0040293336, 2.958041, 1.3685599e-05) -bones/26/rotation = Quaternion(0.7013153, 0.002825051, 0.061101884, 0.7102221) +bones/26/rotation = Quaternion(0.75350595, 0.003054577, 0.06509644, 0.65420324) bones/27/position = Vector3(0.0024927258, 2.756927, -1.8636636e-05) bones/27/rotation = Quaternion(-0.0033205294, 3.9059813e-08, 6.5682637e-09, 0.9999945) bones/28/position = Vector3(0.0015397072, 2.3345788, 1.1771219e-05) bones/29/position = Vector3(2.5022223, 6.3907866, 0.08744741) -bones/29/rotation = Quaternion(0.56450063, -0.15266693, -0.0018035976, 0.8111898) +bones/29/rotation = Quaternion(0.5563202, -0.15497696, -0.0049168295, 0.81637365) bones/30/position = Vector3(0.0032016933, 2.6237173, 5.5511973e-06) -bones/30/rotation = Quaternion(0.6262513, 0.022716032, 0.058525927, 0.7770894) +bones/30/rotation = Quaternion(0.6839778, 0.025319714, 0.05863921, 0.72670126) bones/31/position = Vector3(0.003878951, 2.1320877, -1.2886679e-06) bones/31/rotation = Quaternion(-0.06925962, -1.0360488e-07, -7.0190005e-08, 0.9975987) bones/32/position = Vector3(-0.0070759356, 1.7932303, -8.8289596e-07) bones/33/position = Vector3(-4.2240915, 8.897269, -0.030230172) -bones/33/rotation = Quaternion(0.64940715, -0.35115576, 0.552351, 0.38712853) +bones/33/rotation = Quaternion(0.6396871, -0.34111822, 0.60846674, 0.32281098) bones/34/position = Vector3(9.327891e-07, 8.8610115, -9.847447e-06) -bones/34/rotation = Quaternion(0.37322992, 0.051175877, -0.014639804, 0.92621064) +bones/34/rotation = Quaternion(0.3631728, -0.04426816, 0.26242447, 0.892905) bones/35/position = Vector3(6.476337e-07, 18.973557, 5.8403616e-06) -bones/35/rotation = Quaternion(-2.38223e-05, -0.0878173, -0.45667896, 0.8852867) +bones/35/rotation = Quaternion(0.020210672, -0.1976225, -0.21386963, 0.95645) bones/36/position = Vector3(-7.630494e-08, 19.89834, -1.8180976e-06) -bones/36/rotation = Quaternion(-0.22069603, -0.15216982, -0.028448751, 0.9629787) +bones/36/rotation = Quaternion(-0.0013729627, -0.10906849, 0.07843006, 0.9909344) bones/37/position = Vector3(2.0620086, 2.1477072, 1.2410417) -bones/37/rotation = Quaternion(0.21391132, 0.020952638, -0.2679689, 0.93914616) +bones/37/rotation = Quaternion(0.2022776, 0.03663972, -0.31487894, 0.9266028) bones/38/position = Vector3(0.2744484, 2.5658755, 3.7584634e-06) -bones/38/rotation = Quaternion(0.064052425, -0.18935232, 0.09533193, 0.9751691) +bones/38/rotation = Quaternion(0.10343577, -0.19018154, 0.15068975, 0.9645852) bones/39/position = Vector3(-0.076725245, 2.730221, -7.234994e-06) bones/39/rotation = Quaternion(0.037325032, -2.1781963e-08, 1.19075e-07, 0.99930316) bones/40/position = Vector3(-0.19772077, 2.2442596, 9.685756e-06) bones/41/position = Vector3(2.5699375, 7.0537868, 0.05093465) -bones/41/rotation = Quaternion(0.45433038, 0.007974177, -0.040324483, 0.8898841) +bones/41/rotation = Quaternion(0.33546335, 0.008294326, -0.03227234, 0.9414638) bones/42/position = Vector3(-0.0015108623, 2.816315, -9.529522e-06) -bones/42/rotation = Quaternion(0.6120771, 0.028411472, -0.047977913, 0.78882915) +bones/42/rotation = Quaternion(0.5038074, 0.025462631, -0.03289526, 0.86281383) bones/43/position = Vector3(0.0011758618, 2.6396425, 2.472788e-06) bones/43/rotation = Quaternion(0.0026853334, 3.311302e-08, -5.8677845e-08, 0.9999965) bones/44/position = Vector3(0.00033191964, 2.2415001, 1.3194513e-06) bones/45/position = Vector3(0.76549155, 7.018211, -0.09038047) -bones/45/rotation = Quaternion(0.483478, 0.036460996, -0.036219023, 0.87384653) +bones/45/rotation = Quaternion(0.365633, 0.035192948, -0.020985998, 0.92985666) bones/46/position = Vector3(0.008488461, 3.3401706, -6.017696e-06) -bones/46/rotation = Quaternion(0.701871, 0.04011571, -0.05796006, 0.7088077) +bones/46/rotation = Quaternion(0.60296637, 0.037562832, -0.043432355, 0.79569733) bones/47/position = Vector3(-0.007900611, 3.0566626, 5.4270527e-06) bones/47/rotation = Quaternion(-0.027461762, -2.0760188e-08, 5.6872928e-08, 0.9996229) bones/48/position = Vector3(-0.00058989227, 2.63951, 9.457464e-06) bones/49/position = Vector3(-0.8923034, 6.9110794, -0.12160769) -bones/49/rotation = Quaternion(0.48708817, 0.03278609, -0.015544657, 0.8725985) +bones/49/rotation = Quaternion(0.373129, 0.027513513, 0.0016994111, 0.92736983) bones/50/position = Vector3(-0.00297606, 2.923956, 1.7109673e-05) -bones/50/rotation = Quaternion(0.6931497, -0.019576456, -0.06891241, 0.71722454) +bones/50/rotation = Quaternion(0.5844741, -0.018550195, -0.061117128, 0.80889475) bones/51/position = Vector3(0.0026345253, 2.717951, -5.427028e-06) bones/51/rotation = Quaternion(0.009096339, 7.025756e-08, 2.6579988e-08, 0.99995863) bones/52/position = Vector3(0.0003426671, 2.379634, 1.4747493e-06) bones/53/position = Vector3(-2.4431183, 6.453577, 0.12551774) -bones/53/rotation = Quaternion(0.44882435, 0.025137398, -0.033032093, 0.89265347) +bones/53/rotation = Quaternion(0.33751208, 0.021386249, -0.009726644, 0.941028) bones/54/position = Vector3(-0.0026362836, 2.6278691, 4.2827646e-06) -bones/54/rotation = Quaternion(0.7472647, -0.045266245, -0.09292602, 0.65643823) +bones/54/rotation = Quaternion(0.64713013, -0.039810352, -0.090106204, 0.75598854) bones/55/position = Vector3(-0.001860708, 2.1224687, 9.940322e-07) bones/55/rotation = Quaternion(-0.03619139, -1.1229469e-07, 2.121775e-08, 0.9993449) bones/56/position = Vector3(0.004502952, 1.7919822, 1.2924895e-05) bones/57/position = Vector3(7.557004, -4.826265, 0.30053553) -bones/57/rotation = Quaternion(-0.04979198, -0.3562142, -0.93288946, 0.018696664) +bones/57/rotation = Quaternion(-0.08986163, 0.021308435, -0.99325967, 0.070043266) bones/58/position = Vector3(-0.124621816, 24.06574, 0.10470339) -bones/58/rotation = Quaternion(-0.30548102, 0.032796834, -0.01660958, 0.9514882) +bones/58/rotation = Quaternion(-0.4013793, 0.12247081, -0.016882984, 0.9075299) bones/59/position = Vector3(-0.0060171685, 33.063114, 0.00394835) -bones/59/rotation = Quaternion(0.44427872, 0.000708564, 0.0098030325, 0.8958346) +bones/59/rotation = Quaternion(0.43592802, 0.023901237, 0.044110287, 0.89858204) bones/60/position = Vector3(-0.0052775713, 12.893309, -0.5155027) -bones/60/rotation = Quaternion(0.31359768, -0.055260643, 0.025034118, 0.94761604) +bones/60/rotation = Quaternion(0.5547358, -0.05625008, 0.006090132, 0.83010066) bones/61/position = Vector3(0.014227723, 6.420835, 0.007887725) bones/62/position = Vector3(-7.557004, -4.826261, 0.30053535) -bones/62/rotation = Quaternion(-0.10350199, -0.08600675, 0.989418, 0.0542423) +bones/62/rotation = Quaternion(-0.0453176, 0.37578616, 0.92547935, 0.014799355) bones/63/position = Vector3(0.12476807, 24.070103, -0.14491707) -bones/63/rotation = Quaternion(-0.32747734, -0.0674623, 0.0130414665, 0.9423574) +bones/63/rotation = Quaternion(-0.27188188, -0.00034630176, 0.0028924432, 0.96232617) bones/64/position = Vector3(0.0060205855, 33.063133, 0.004499323) -bones/64/rotation = Quaternion(0.26777777, -0.01583679, -0.0035161127, 0.9633441) +bones/64/rotation = Quaternion(0.4376588, -0.0021246495, 3.749458e-05, 0.8991387) bones/65/position = Vector3(0.005378528, 12.718271, -0.52236915) -bones/65/rotation = Quaternion(0.6911166, 0.033012606, 0.0070720776, 0.72195446) +bones/65/rotation = Quaternion(0.34835225, 0.017514596, -0.0044682953, 0.9371894) bones/66/position = Vector3(-0.014239848, 6.412128, 0.0079107955) [node name="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" index="1" unique_id=945472897] diff --git a/demo/synced_blend_tree_walk_limp.tres b/demo/synced_blend_tree_walk_limp.tres index 7cbdee2..b23c127 100644 --- a/demo/synced_blend_tree_walk_limp.tres +++ b/demo/synced_blend_tree_walk_limp.tres @@ -16,11 +16,10 @@ graph_offset = Vector2(-1300, -40) animation = &"animation_library/Walk-InPlace" [resource] -nodes/Output/position = null nodes/BLTAnimationNodeBlend2/node = SubResource("BLTAnimationNodeBlend2_bvt3d") -nodes/BLTAnimationNodeBlend2/position = null +nodes/BLTAnimationNodeBlend2/graph_offset = Vector2(-600, 180) "nodes/BLTAnimationNodeSampler 1/node" = SubResource("BLTAnimationNodeSampler_sntl5") -"nodes/BLTAnimationNodeSampler 1/position" = null +"nodes/BLTAnimationNodeSampler 1/graph_offset" = Vector2(-1200, 560) nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_n4m28") -nodes/BLTAnimationNodeSampler/position = null +nodes/BLTAnimationNodeSampler/graph_offset = Vector2(-1300, -40) node_connections = ["BLTAnimationNodeBlend2", 0, "BLTAnimationNodeSampler", "BLTAnimationNodeBlend2", 1, "BLTAnimationNodeSampler 1", "Output", 0, "BLTAnimationNodeBlend2"] -- 2.47.2 From be6e021198d7ab8406bc63405dd8898573ff22bc Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Tue, 17 Feb 2026 23:08:36 +0100 Subject: [PATCH 19/29] Restructured Animation Graph Editor to facilitate hierarchical graphs (i.e. embedded sub graphs). --- blendalot_animation_node.cpp | 15 +- blendalot_animation_node.h | 34 +- .../blendalot/animation_graph_editor.gd | 105 +++++++ .../blendalot/animation_graph_editor.gd.uid | 1 + .../blendalot/animation_graph_editor.tscn | 83 +++++ ...lot_main_panel.gd => blend_tree_editor.gd} | 296 +++++++----------- .../addons/blendalot/blend_tree_editor.gd.uid | 1 + demo/addons/blendalot/blend_tree_editor.tscn | 51 +++ .../blendalot/blendalot_main_panel.gd.uid | 1 - .../blendalot/blendalot_main_panel.tscn | 109 ------- demo/addons/blendalot/blendalot_plugin.gd | 38 ++- demo/main.tscn | 282 +++++++++-------- 12 files changed, 567 insertions(+), 449 deletions(-) create mode 100644 demo/addons/blendalot/animation_graph_editor.gd create mode 100644 demo/addons/blendalot/animation_graph_editor.gd.uid create mode 100644 demo/addons/blendalot/animation_graph_editor.tscn rename demo/addons/blendalot/{blendalot_main_panel.gd => blend_tree_editor.gd} (57%) create mode 100644 demo/addons/blendalot/blend_tree_editor.gd.uid create mode 100644 demo/addons/blendalot/blend_tree_editor.tscn delete mode 100644 demo/addons/blendalot/blendalot_main_panel.gd.uid delete mode 100644 demo/addons/blendalot/blendalot_main_panel.tscn diff --git a/blendalot_animation_node.cpp b/blendalot_animation_node.cpp index 4399e0f..f3751c0 100644 --- a/blendalot_animation_node.cpp +++ b/blendalot_animation_node.cpp @@ -5,9 +5,9 @@ #include "blendalot_animation_node.h" void BLTAnimationNode::_bind_methods() { - ClassDB::bind_method(D_METHOD("set_graph_offset", "offset"), &BLTAnimationNode::set_graph_offset); - ClassDB::bind_method(D_METHOD("get_graph_offset"), &BLTAnimationNode::get_graph_offset); - ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "graph_offset", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_graph_offset", "get_graph_offset"); + ClassDB::bind_method(D_METHOD("set_position", "position"), &BLTAnimationNode::set_position); + ClassDB::bind_method(D_METHOD("get_position"), &BLTAnimationNode::get_position); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "position", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_position", "get_position"); ADD_SIGNAL(MethodInfo("animation_node_renamed", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "old_name"), PropertyInfo(Variant::STRING, "new_name"))); ADD_SIGNAL(MethodInfo("animation_node_removed", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "name"))); @@ -60,6 +60,11 @@ void BLTAnimationNodeBlendTree::_bind_methods() { ClassDB::bind_method(D_METHOD("add_connection", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::add_connection); ClassDB::bind_method(D_METHOD("remove_connection", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::remove_connection); ClassDB::bind_method(D_METHOD("get_connections"), &BLTAnimationNodeBlendTree::get_connections_as_array); + + ClassDB::bind_method(D_METHOD("set_graph_offset", "graph_offset"), &BLTAnimationNodeBlendTree::set_graph_offset); + ClassDB::bind_method(D_METHOD("get_graph_offset"), &BLTAnimationNodeBlendTree::get_graph_offset); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "graph_offset", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_graph_offset", "get_graph_offset"); + BIND_CONSTANT(CONNECTION_OK); BIND_CONSTANT(CONNECTION_ERROR_GRAPH_ALREADY_INITIALIZED); BIND_CONSTANT(CONNECTION_ERROR_NO_SOURCE_NODE); @@ -98,7 +103,7 @@ bool BLTAnimationNodeBlendTree::_get(const StringName &p_name, Variant &r_value) if (what == "graph_offset") { if (node_index != -1) { - r_value = tree_graph.nodes[node_index]->graph_offset; + r_value = tree_graph.nodes[node_index]->position; return true; } } @@ -139,7 +144,7 @@ bool BLTAnimationNodeBlendTree::_set(const StringName &p_name, const Variant &p_ if (what == "graph_offset") { int node_index = find_node_index_by_name(node_name); if (node_index > -1) { - tree_graph.nodes[node_index]->graph_offset = p_value; + tree_graph.nodes[node_index]->position = p_value; } return true; } diff --git a/blendalot_animation_node.h b/blendalot_animation_node.h index 0263bcb..f7a6ef3 100644 --- a/blendalot_animation_node.h +++ b/blendalot_animation_node.h @@ -257,13 +257,14 @@ public: double sync_position = 0.0; bool is_synced = false; - Animation::LoopMode loop_mode = Animation::LOOP_NONE; + // TODO: 2026-02-17: how to initialize loop_mode e.g. for a BlendTree or a StateMachine? + Animation::LoopMode loop_mode = Animation::LOOP_LINEAR; SyncTrack sync_track; }; NodeTimeInfo node_time_info; bool active = false; - Vector2 graph_offset; + Vector2 position; virtual ~BLTAnimationNode() override = default; virtual bool initialize(GraphEvaluationContext &context) { @@ -307,12 +308,12 @@ public: } } - void set_graph_offset(const Vector2 &p_position) { - graph_offset = p_position; + void set_position(const Vector2 &p_position) { + position = p_position; } - Vector2 get_graph_offset() const { - return graph_offset; + Vector2 get_position() const { + return position; } virtual Vector get_input_names() const { return {}; } @@ -597,6 +598,8 @@ protected: bool _set(const StringName &p_name, const Variant &p_value); public: + Vector2 graph_offset; + struct NodeRuntimeData { Vector> input_nodes; LocalVector input_data; @@ -604,6 +607,14 @@ public: }; LocalVector _node_runtime_data; + void set_graph_offset(const Vector2 &p_graph_offset) { + graph_offset = p_graph_offset; + } + + Vector2 get_graph_offset() const { + return graph_offset; + } + int find_node_index(const Ref &node) const { return tree_graph.find_node_index(node); } @@ -701,6 +712,8 @@ public: // overrides from BLTAnimationNode bool initialize(GraphEvaluationContext &context) override { + tree_initialized = false; + if (!BLTAnimationNode::initialize(context)) { return false; } @@ -716,6 +729,15 @@ public: } } + // All inputs must have a connected node. + for (const NodeRuntimeData &node_runtime_data : _node_runtime_data) { + for (const Ref &input_node : node_runtime_data.input_nodes) { + if (!input_node.is_valid()) { + return false; + } + } + } + tree_initialized = true; return true; diff --git a/demo/addons/blendalot/animation_graph_editor.gd b/demo/addons/blendalot/animation_graph_editor.gd new file mode 100644 index 0000000..fdef1f6 --- /dev/null +++ b/demo/addons/blendalot/animation_graph_editor.gd @@ -0,0 +1,105 @@ +@tool + +extends Control +class_name AnimationGraphEditor + +@onready var breadcrumb_button_container: HBoxContainer = %BreadcrumbButtons +@onready var active_graph_control: Control = %ActiveGraphControl + +var active_animation_graph_node:BLTAnimationGraph = null +var animation_graph_root_node:BLTAnimationNode = null +var graph_node_stack:Array[BLTAnimationNode] = [] +var active_graph_edit:Control = null +var active_graph_edit_index = -1 + +func reset_graph_control(): + for child in active_graph_control.get_children(): + active_graph_control.remove_child(child) + child.queue_free() + + +func edit_animation_root_node(blt_node:BLTAnimationNode): + graph_node_stack = [] + active_graph_edit_index = -1 + truncate_graph_stack(0) + + blt_node.resource_name = "Root" + + if blt_node is BLTAnimationNodeBlendTree: + animation_graph_root_node = blt_node + push_graph_stack(blt_node) + edit_graph(blt_node) + + push_warning("Cannot edit node %s. Graph type %s not yet supported." % [blt_node.resource_name, blt_node.get_class()]) + + +func push_graph_stack(blt_node:BLTAnimationNode): + graph_node_stack.append(blt_node) + active_graph_edit_index = graph_node_stack.size() - 1 + + var breadcrumb_button:Button = Button.new() + breadcrumb_button_container.add_child(breadcrumb_button) + breadcrumb_button.text = blt_node.resource_name + breadcrumb_button.toggle_mode = true + breadcrumb_button.set_meta("BLTAnimationNode", blt_node) + breadcrumb_button.set_meta("graph_edit_index", active_graph_edit_index) + breadcrumb_button.pressed.connect(on_breadcrumb_button_pressed.bind(active_graph_edit_index)) + + +func truncate_graph_stack(level:int): + graph_node_stack.resize(level) + + var is_above_stack_size = false + for child in breadcrumb_button_container.get_children(): + if is_above_stack_size: + breadcrumb_button_container.remove_child(child) + child.queue_free() + continue + + var button:Button = child as Button + if not is_instance_valid(button): + continue + + if button.get_meta("graph_edit_index") >= graph_node_stack.size(): + is_above_stack_size = true + breadcrumb_button_container.remove_child(child) + child.queue_free() + + +func on_breadcrumb_button_pressed(graph_edit_index:int): + print("on_breadcrumb_button_pressed(%d)" % graph_edit_index) + active_graph_edit_index = graph_edit_index + update_breadcrumb_button_container() + + edit_graph(graph_node_stack[graph_edit_index]) + + +func update_breadcrumb_button_container(): + for child in breadcrumb_button_container.get_children(): + var button:Button = child as Button + if not is_instance_valid(button): + continue + + if button.get_meta("graph_edit_index") == active_graph_edit_index: + button.set_pressed_no_signal(true) + else: + button.set_pressed_no_signal(false) + + +func edit_graph(blt_node:BLTAnimationNode): + if blt_node is BLTAnimationNodeBlendTree: + reset_graph_control() + var blend_tree_graph_edit:BltBlendTreeEditor = preload ("res://addons/blendalot/blend_tree_editor.tscn").instantiate() + active_graph_control.add_child(blend_tree_graph_edit) + blend_tree_graph_edit.edit_blend_tree(blt_node) + blend_tree_graph_edit.edit_subgraph.connect(handle_subgraph_edit) + active_graph_edit = blend_tree_graph_edit + else: + push_error("Cannot edit graph of node type %s" % blt_node.get_class()) + + +func handle_subgraph_edit(blt_node:BLTAnimationNode): + print("handling subgraph edit of node %s" % blt_node.resource_name) + truncate_graph_stack(active_graph_edit_index + 1) + push_graph_stack(blt_node) + edit_graph(blt_node) diff --git a/demo/addons/blendalot/animation_graph_editor.gd.uid b/demo/addons/blendalot/animation_graph_editor.gd.uid new file mode 100644 index 0000000..027e630 --- /dev/null +++ b/demo/addons/blendalot/animation_graph_editor.gd.uid @@ -0,0 +1 @@ +uid://bxxipuj2s5gxu diff --git a/demo/addons/blendalot/animation_graph_editor.tscn b/demo/addons/blendalot/animation_graph_editor.tscn new file mode 100644 index 0000000..13ab95f --- /dev/null +++ b/demo/addons/blendalot/animation_graph_editor.tscn @@ -0,0 +1,83 @@ +[gd_scene format=3 uid="uid://bk5mssvanwjnh"] + +[ext_resource type="Script" uid="uid://bxxipuj2s5gxu" path="res://addons/blendalot/animation_graph_editor.gd" id="1_un1ur"] +[ext_resource type="PackedScene" uid="uid://cptd46rpm0gl3" path="res://addons/blendalot/blend_tree_editor.tscn" id="2_utax0"] + +[node name="AnimationGraphEditor" type="VBoxContainer" unique_id=768619585] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_un1ur") + +[node name="DebugContainer" type="HBoxContainer" parent="." unique_id=1984631897] +visible = false +layout_mode = 2 + +[node name="ResetGraphButton" type="Button" parent="DebugContainer" unique_id=670074781] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Reset" + +[node name="FileNameLineEdit" type="LineEdit" parent="DebugContainer" unique_id=724929522] +unique_name_in_owner = true +custom_minimum_size = Vector2(250, 0) +layout_mode = 2 +text = "editor_test_tree.tres" + +[node name="SaveButton" type="Button" parent="DebugContainer" unique_id=706843675] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Save +" + +[node name="LoadButton" type="Button" parent="DebugContainer" unique_id=1467831200] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Load +" + +[node name="ReinitializeButton" type="Button" parent="DebugContainer" unique_id=281924859] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Reinitialize" + +[node name="TreeOptionButton" type="OptionButton" parent="DebugContainer" unique_id=2103827540] +unique_name_in_owner = true +layout_mode = 2 +selected = 0 +item_count = 2 +popup/item_0/text = "AnimationSampler" +popup/item_0/id = 1 +popup/item_1/text = "Blend2" +popup/item_1/id = 2 + +[node name="InstantiateTreeButton" type="Button" parent="DebugContainer" unique_id=735069321] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Instantiate" + +[node name="NavigationBar" type="MarginContainer" parent="." unique_id=815609909] +layout_mode = 2 + +[node name="Panel" type="Panel" parent="NavigationBar" unique_id=996737763] +layout_mode = 2 + +[node name="BreadcrumbButtons" type="HBoxContainer" parent="NavigationBar" unique_id=1375619232] +unique_name_in_owner = true +layout_mode = 2 + +[node name="PathLabel" type="Label" parent="NavigationBar/BreadcrumbButtons" unique_id=1544570774] +layout_mode = 2 +text = "Path:" + +[node name="ActiveGraphControl" type="Control" parent="." unique_id=1769528277] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 + +[node name="BlendTreeEditor" parent="ActiveGraphControl" unique_id=1313738200 instance=ExtResource("2_utax0")] +layout_mode = 1 diff --git a/demo/addons/blendalot/blendalot_main_panel.gd b/demo/addons/blendalot/blend_tree_editor.gd similarity index 57% rename from demo/addons/blendalot/blendalot_main_panel.gd rename to demo/addons/blendalot/blend_tree_editor.gd index a82f38b..187762a 100644 --- a/demo/addons/blendalot/blendalot_main_panel.gd +++ b/demo/addons/blendalot/blend_tree_editor.gd @@ -1,19 +1,22 @@ @tool -class_name BlendalotMainPanel + extends Control +class_name BltBlendTreeEditor @onready var blend_tree_graph_edit: GraphEdit = %BlendTreeGraphEdit -@onready var file_name_line_edit: LineEdit = %FileNameLineEdit -@onready var tree_option_button: OptionButton = %TreeOptionButton @onready var add_node_popup_menu: PopupMenu = %AddNodePopupMenu -var root_animation_node:BLTAnimationNode = null -var last_edited_graph_node:GraphNode = null -var blend_tree:BLTAnimationNodeBlendTree = BLTAnimationNodeBlendTree.new() +signal edit_subgraph(blt_node:BLTAnimationNode) +signal graph_changed() + +var blend_tree:BLTAnimationNodeBlendTree + var blend_tree_node_to_graph_node = {} var graph_node_to_blend_tree_node = {} + var selected_nodes = {} -var new_node_position = Vector2.INF +var last_selected_graph_node:GraphNode = null +var new_node_position:Vector2 = Vector2.ZERO var registered_nodes = [ "BLTAnimationNodeSampler", @@ -21,7 +24,7 @@ var registered_nodes = [ "BLTAnimationNodeBlendTree", ] -# Called when the node enters the scene tree for the first time. + func _ready() -> void: add_node_popup_menu.clear(true) @@ -29,75 +32,6 @@ func _ready() -> void: add_node_popup_menu.add_item(node_name) -# Called every frame. 'delta' is the elapsed time since the previous frame. -func _process(delta: float) -> void: - pass - - -func create_node_for_blt_node(blt_node: BLTAnimationNode) -> GraphNode: - var result_graph_node:GraphNode = GraphNode.new() - result_graph_node.name = blt_node.resource_name - result_graph_node.title = blt_node.resource_name - result_graph_node.position_offset = blt_node.graph_offset - - var result_slot_offset = 0 - - if (blt_node.get_class() != "BLTAnimationNodeOutput"): - result_slot_offset = 1 - var output_slot_label:Label = Label.new() - output_slot_label.text = "Result" - result_graph_node.add_child(output_slot_label) - result_graph_node.set_slot(0, false, 1, Color.WHITE, true, 1, Color.WHITE) - - var inputs = blt_node.get_input_names() - for i in range(len(inputs)): - var slot_label:Label = Label.new() - slot_label.text = inputs[i] - result_graph_node.add_child(slot_label) - result_graph_node.set_slot(i + result_slot_offset, true, 1, Color.WHITE, false, 1, Color.BLACK) - - blt_node.node_changed.connect(_trigger_animation_graph_initialize) - - return result_graph_node - - -func _on_blend_tree_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void: - for node_name:StringName in nodes: - print("remove node '%s'" % node_name) - var blend_tree_node:BLTAnimationNode = blend_tree.get_node(node_name) - - if blend_tree_node == null: - push_error("Cannot delete node '%s': node not found." % node_name) - continue - - if blend_tree_node == blend_tree.get_output_node(): - push_warning("Output node not allowed to be removed.") - continue - - blend_tree_node.node_changed.disconnect(_trigger_animation_graph_initialize) - - var graph_node:GraphNode = blend_tree_node_to_graph_node[blend_tree_node] - blend_tree.remove_node(blend_tree_node) - blend_tree_node_to_graph_node.erase(blend_tree_node) - - _blend_tree_graph_edit_remove_node_connections(graph_node) - graph_node_to_blend_tree_node.erase(graph_node) - blend_tree_graph_edit.remove_child(graph_node) - _on_blend_tree_graph_edit_node_deselected(graph_node) - - EditorInterface.get_inspector().edit(null) - - -func _on_reset_graph_button_pressed() -> void: - EditorInterface.get_inspector().edit(null) - - _reset_editor() - _update_editor_from_blend_tree() - - var graph_rect:Rect2 = blend_tree_graph_edit.get_rect() - blend_tree_graph_edit.scroll_offset = graph_rect.size * -0.5 - Vector2(200,0) - - func _reset_editor(): for child in blend_tree_graph_edit.get_children(): if child.name == "_connection_layer": @@ -108,18 +42,16 @@ func _reset_editor(): blend_tree_graph_edit.clear_connections() - blend_tree = BLTAnimationNodeBlendTree.new() + blend_tree = null blend_tree_node_to_graph_node = {} graph_node_to_blend_tree_node = {} selected_nodes = {} + - -func edit_blend_tree(blend_tree_animation_node:BLTAnimationNode): - print("Starting to edit blend_tree_animation_node " + str(blend_tree_animation_node)) - print("Owner: %s" % blend_tree_animation_node) +func edit_blend_tree(blt_blend_tree:BLTAnimationNodeBlendTree): _reset_editor() - root_animation_node = blend_tree_animation_node - blend_tree = blend_tree_animation_node + blend_tree = blt_blend_tree + blend_tree_graph_edit.scroll_offset = blend_tree.graph_offset _update_editor_nodes_from_blend_tree() _update_editor_connections_from_blend_tree() @@ -128,7 +60,7 @@ func edit_blend_tree(blend_tree_animation_node:BLTAnimationNode): func _update_editor_nodes_from_blend_tree(): for node_name in blend_tree.get_node_names(): var blend_tree_node:BLTAnimationNode = blend_tree.get_node(node_name) - var graph_node:GraphNode = create_node_for_blt_node(blend_tree_node) + var graph_node:GraphNode = create_graph_node_for_blt_node(blend_tree_node) blend_tree_graph_edit.add_child(graph_node) blend_tree_node_to_graph_node[blend_tree_node] = graph_node @@ -148,60 +80,55 @@ func _update_editor_connections_from_blend_tree(): var connect_result = blend_tree_graph_edit.connect_node(source_node.resource_name, 0, target_node.resource_name, target_node.get_input_index(target_port), true) -func _update_editor_from_blend_tree(): - _update_editor_nodes_from_blend_tree() - _update_editor_connections_from_blend_tree() - - -func _on_save_button_pressed() -> void: - ResourceSaver.save(blend_tree, "res://" + file_name_line_edit.text) - - -func _on_load_button_pressed() -> void: - var loaded_blend_tree:BLTAnimationNodeBlendTree = ResourceLoader.load("res://" + file_name_line_edit.text, "BLTAnimationNodeBlendTree", 0) - - if loaded_blend_tree: - _reset_editor() - blend_tree = loaded_blend_tree - _update_editor_from_blend_tree() - - -func _on_instantiate_tree_button_pressed() -> void: - var graph_name = tree_option_button.get_item_text(tree_option_button.selected) +func create_graph_node_for_blt_node(blt_node: BLTAnimationNode) -> GraphNode: + var result_graph_node:GraphNode = GraphNode.new() + result_graph_node.name = blt_node.resource_name + result_graph_node.title = blt_node.resource_name + result_graph_node.position_offset = blt_node.position - if graph_name == "AnimationSampler": - _reset_editor() - - var sampler_node:BLTAnimationNodeSampler = BLTAnimationNodeSampler.new() - sampler_node.animation = "SampleLibrary/Idle" - - blend_tree.add_node(sampler_node) - blend_tree.add_connection(sampler_node, blend_tree.get_output_node(), "Output") - - _update_editor_from_blend_tree() - elif graph_name == "Blend2": - _reset_editor() - - var sampler_node_walk:BLTAnimationNodeSampler = BLTAnimationNodeSampler.new() - sampler_node_walk.animation = "SampleLibrary/Walk" - - var sampler_node_run:BLTAnimationNodeSampler = BLTAnimationNodeSampler.new() - sampler_node_run.animation = "SampleLibrary/Run" - - var blend2_node:BLTAnimationNodeBlend2 = BLTAnimationNodeBlend2.new() - - blend_tree.add_node(sampler_node_walk) - blend_tree.add_node(sampler_node_run) - blend_tree.add_node(blend2_node) - - blend_tree.add_connection(blend2_node, blend_tree.get_output_node(), "Output") - - blend_tree.add_connection(sampler_node_walk, blend2_node, "Input0") - blend_tree.add_connection(sampler_node_run, blend2_node, "Input1") - - _update_editor_from_blend_tree() + var result_slot_offset = 0 + + if (blt_node.get_class() != "BLTAnimationNodeOutput"): + result_slot_offset = 1 + var output_slot_label:Label = Label.new() + output_slot_label.text = "Result" + result_graph_node.add_child(output_slot_label) + result_graph_node.set_slot(0, false, 1, Color.WHITE, true, 1, Color.WHITE) + + if blt_node.get_class() == "BLTAnimationNodeBlendTree": + result_graph_node.gui_input.connect(_on_node_gui_input.bind(result_graph_node)) + + var inputs = blt_node.get_input_names() + for i in range(len(inputs)): + var slot_label:Label = Label.new() + slot_label.text = inputs[i] + result_graph_node.add_child(slot_label) + result_graph_node.set_slot(i + result_slot_offset, true, 1, Color.WHITE, false, 1, Color.BLACK) + + blt_node.node_changed.connect(_trigger_graph_changed) + + return result_graph_node +func _trigger_graph_changed(): + graph_changed.emit() + + +func _remove_node_connections(graph_node:GraphNode): + var node_connections:Array = [] + + for connection:Dictionary in blend_tree_graph_edit.connections: + if connection["from_node"] == graph_node.name or connection["to_node"] == graph_node.name: + node_connections.append(connection) + + for node_connection:Dictionary in node_connections: + print("Removing connection %s" % str(node_connection)) + blend_tree_graph_edit.disconnect_node(node_connection["from_node"], node_connection["from_port"], node_connection["to_node"], node_connection["to_port"]) + + +# +# GraphEdit signal handling +# func _on_blend_tree_graph_edit_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void: print("Trying to connect '%s' port %d to node '%s' port %d" % [from_node, from_port, to_node, to_port]) @@ -227,19 +154,36 @@ func _on_blend_tree_graph_edit_connection_request(from_node: StringName, from_po print("Success!") +func _on_blend_tree_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void: + for node_name:StringName in nodes: + print("remove node '%s'" % node_name) + var blend_tree_node:BLTAnimationNode = blend_tree.get_node(node_name) + + if blend_tree_node == null: + push_error("Cannot delete node '%s': node not found." % node_name) + continue + + if blend_tree_node == blend_tree.get_output_node(): + push_warning("Output node not allowed to be removed.") + continue + + blend_tree_node.node_changed.disconnect(_trigger_graph_changed) + + var graph_node:GraphNode = blend_tree_node_to_graph_node[blend_tree_node] + blend_tree.remove_node(blend_tree_node) + blend_tree_node_to_graph_node.erase(blend_tree_node) + + _remove_node_connections(graph_node) + graph_node_to_blend_tree_node.erase(graph_node) + blend_tree_graph_edit.remove_child(graph_node) + _on_blend_tree_graph_edit_node_deselected(graph_node) + + EditorInterface.get_inspector().edit(null) + + func _on_blend_tree_graph_edit_end_node_move() -> void: for graph_node:GraphNode in selected_nodes.keys(): - graph_node_to_blend_tree_node[graph_node].graph_offset = graph_node.position_offset - - -func _on_blend_tree_graph_edit_begin_node_move() -> void: - pass # Replace with function body. - - -func _on_blend_tree_graph_edit_node_selected(graph_node: Node) -> void: - selected_nodes[graph_node] = graph_node - last_edited_graph_node = graph_node - EditorInterface.get_inspector().edit(graph_node_to_blend_tree_node[graph_node]) + graph_node_to_blend_tree_node[graph_node].position = graph_node.position_offset func _on_blend_tree_graph_edit_node_deselected(graph_node: Node) -> void: @@ -247,6 +191,19 @@ func _on_blend_tree_graph_edit_node_deselected(graph_node: Node) -> void: selected_nodes.erase(graph_node) +func _on_blend_tree_graph_edit_node_selected(graph_node: Node) -> void: + selected_nodes[graph_node] = graph_node + last_selected_graph_node = graph_node + EditorInterface.get_inspector().edit(graph_node_to_blend_tree_node[graph_node]) + + +func _on_blend_tree_graph_edit_scroll_offset_changed(offset: Vector2) -> void: + blend_tree.graph_offset = offset + + +# +# AddNodePopupMenu +# func _on_blend_tree_graph_edit_popup_request(at_position: Vector2) -> void: add_node_popup_menu.position = get_screen_position() + get_local_mouse_position() add_node_popup_menu.reset_size() @@ -258,7 +215,7 @@ func _on_add_node_popup_menu_index_pressed(index: int) -> void: var new_blend_tree_node: BLTAnimationNode = ClassDB.instantiate(registered_nodes[index]) blend_tree.add_node(new_blend_tree_node) - var graph_node:GraphNode = create_node_for_blt_node(new_blend_tree_node) + var graph_node:GraphNode = create_graph_node_for_blt_node(new_blend_tree_node) blend_tree_graph_edit.add_child(graph_node) graph_node_to_blend_tree_node[graph_node] = new_blend_tree_node @@ -266,38 +223,23 @@ func _on_add_node_popup_menu_index_pressed(index: int) -> void: if new_node_position != Vector2.INF: graph_node.position_offset = new_node_position - new_blend_tree_node.graph_offset = new_node_position + new_blend_tree_node.position = new_node_position new_node_position = Vector2.INF -func _blend_tree_graph_edit_remove_node_connections(graph_node:GraphNode): - var node_connections:Array = [] +# +# Handle Node double click +# +func _on_node_gui_input(input_event:InputEvent, graph_node:GraphNode): + # print("Got input event on graph node %s!" % graph_node.name) - for connection:Dictionary in blend_tree_graph_edit.connections: - if connection["from_node"] == graph_node.name or connection["to_node"] == graph_node.name: - node_connections.append(connection) + var mouse_button_event:InputEventMouseButton = input_event as InputEventMouseButton + if mouse_button_event and mouse_button_event.double_click: + _on_node_double_click(graph_node) + +func _on_node_double_click(graph_node:GraphNode): + var blend_tree_node:BLTAnimationNode = graph_node_to_blend_tree_node[graph_node] - for node_connection:Dictionary in node_connections: - print("Removing connection %s" % str(node_connection)) - blend_tree_graph_edit.disconnect_node(node_connection["from_node"], node_connection["from_port"], node_connection["to_node"], node_connection["to_port"]) - - -func _on_blend_tree_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void: - print("removing connection") - - var blend_tree_source_node = blend_tree.get_node(from_node) - var blend_tree_target_node = blend_tree.get_node(to_node) - var target_port_name = blend_tree_target_node.get_input_names()[to_port] - blend_tree.remove_connection(blend_tree_source_node, blend_tree_target_node, target_port_name) - - blend_tree_graph_edit.disconnect_node(from_node, from_port, to_node, to_port) - - -func _trigger_animation_graph_initialize(node_name:StringName) -> void: - root_animation_node.node_changed.emit("root") - - -func _on_visibility_changed() -> void: - if visible and is_instance_valid(last_edited_graph_node): - _on_blend_tree_graph_edit_node_selected(last_edited_graph_node) + if blend_tree_node is BLTAnimationNodeBlendTree: + edit_subgraph.emit(blend_tree_node) diff --git a/demo/addons/blendalot/blend_tree_editor.gd.uid b/demo/addons/blendalot/blend_tree_editor.gd.uid new file mode 100644 index 0000000..c2a2a36 --- /dev/null +++ b/demo/addons/blendalot/blend_tree_editor.gd.uid @@ -0,0 +1 @@ +uid://dr0ndqekm21gy diff --git a/demo/addons/blendalot/blend_tree_editor.tscn b/demo/addons/blendalot/blend_tree_editor.tscn new file mode 100644 index 0000000..57e51a7 --- /dev/null +++ b/demo/addons/blendalot/blend_tree_editor.tscn @@ -0,0 +1,51 @@ +[gd_scene format=3 uid="uid://cptd46rpm0gl3"] + +[ext_resource type="Script" uid="uid://dr0ndqekm21gy" path="res://addons/blendalot/blend_tree_editor.gd" id="1_0srhh"] + +[node name="BlendTreeEditor" type="Control" unique_id=1313738200] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_0srhh") + +[node name="Panel" type="Panel" parent="." unique_id=758924321] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 + +[node name="AddNodePopupMenu" type="PopupMenu" parent="Panel" unique_id=108570539] +unique_name_in_owner = true +oversampling_override = 1.0 +item_count = 3 +item_0/text = "BLTAnimationNodeSampler" +item_0/id = 0 +item_1/text = "BLTAnimationNodeBlend2" +item_1/id = 1 +item_2/text = "BLTAnimationNodeBlendTree" +item_2/id = 2 + +[node name="BlendTreeGraphEdit" type="GraphEdit" parent="Panel" unique_id=391120290] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +right_disconnects = true + +[connection signal="index_pressed" from="Panel/AddNodePopupMenu" to="." method="_on_add_node_popup_menu_index_pressed"] +[connection signal="connection_request" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_connection_request"] +[connection signal="delete_nodes_request" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_delete_nodes_request"] +[connection signal="end_node_move" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_end_node_move"] +[connection signal="node_deselected" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_node_deselected"] +[connection signal="node_selected" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_node_selected"] +[connection signal="popup_request" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_popup_request"] +[connection signal="scroll_offset_changed" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_scroll_offset_changed"] diff --git a/demo/addons/blendalot/blendalot_main_panel.gd.uid b/demo/addons/blendalot/blendalot_main_panel.gd.uid deleted file mode 100644 index 40b1784..0000000 --- a/demo/addons/blendalot/blendalot_main_panel.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dvulvuytt81lw diff --git a/demo/addons/blendalot/blendalot_main_panel.tscn b/demo/addons/blendalot/blendalot_main_panel.tscn deleted file mode 100644 index bc0b98e..0000000 --- a/demo/addons/blendalot/blendalot_main_panel.tscn +++ /dev/null @@ -1,109 +0,0 @@ -[gd_scene format=3 uid="uid://31c6depvs0y1"] - -[ext_resource type="Script" uid="uid://dvulvuytt81lw" path="res://addons/blendalot/blendalot_main_panel.gd" id="1_427jg"] - -[node name="BlendalotMainPanel" type="Control" unique_id=1259518158] -layout_mode = 3 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -size_flags_horizontal = 3 -size_flags_vertical = 3 -script = ExtResource("1_427jg") - -[node name="BlendTreeEditorContainer" type="VBoxContainer" parent="." unique_id=2044593527] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="HBoxContainer" type="HBoxContainer" parent="BlendTreeEditorContainer" unique_id=1742071203] -layout_mode = 2 - -[node name="ResetGraphButton" type="Button" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=1060776498] -layout_mode = 2 -size_flags_horizontal = 0 -text = "Reset" - -[node name="FileNameLineEdit" type="LineEdit" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=2033619565] -unique_name_in_owner = true -custom_minimum_size = Vector2(250, 0) -layout_mode = 2 -text = "editor_test_tree.tres" - -[node name="SaveButton" type="Button" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=597228263] -layout_mode = 2 -size_flags_horizontal = 0 -text = "Save -" - -[node name="LoadButton" type="Button" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=559146765] -layout_mode = 2 -size_flags_horizontal = 0 -text = "Load -" - -[node name="ReinitializeButton" type="Button" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=719949205] -layout_mode = 2 -size_flags_horizontal = 0 -text = "Reinitialize" - -[node name="TreeOptionButton" type="OptionButton" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=760974122] -unique_name_in_owner = true -layout_mode = 2 -selected = 0 -item_count = 2 -popup/item_0/text = "AnimationSampler" -popup/item_0/id = 1 -popup/item_1/text = "Blend2" -popup/item_1/id = 2 - -[node name="InstantiateTreeButton" type="Button" parent="BlendTreeEditorContainer/HBoxContainer" unique_id=127759440] -layout_mode = 2 -size_flags_horizontal = 0 -text = "Instantiate" - -[node name="Panel" type="Panel" parent="BlendTreeEditorContainer" unique_id=424652158] -layout_mode = 2 -size_flags_vertical = 3 - -[node name="AddNodePopupMenu" type="PopupMenu" parent="BlendTreeEditorContainer/Panel" unique_id=2020489213] -unique_name_in_owner = true -oversampling_override = 1.0 -item_count = 3 -item_0/text = "BLTAnimationNodeSampler" -item_0/id = 0 -item_1/text = "BLTAnimationNodeBlend2" -item_1/id = 1 -item_2/text = "BLTAnimationNodeBlendTree" -item_2/id = 2 - -[node name="BlendTreeGraphEdit" type="GraphEdit" parent="BlendTreeEditorContainer/Panel" unique_id=387715755] -unique_name_in_owner = true -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -right_disconnects = true - -[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"] -[connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/ResetGraphButton" to="." method="_on_reset_graph_button_pressed"] -[connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/SaveButton" to="." method="_on_save_button_pressed"] -[connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/LoadButton" to="." method="_on_load_button_pressed"] -[connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/ReinitializeButton" to="." method="_trigger_animation_graph_initialize"] -[connection signal="pressed" from="BlendTreeEditorContainer/HBoxContainer/InstantiateTreeButton" to="." method="_on_instantiate_tree_button_pressed"] -[connection signal="index_pressed" from="BlendTreeEditorContainer/Panel/AddNodePopupMenu" to="." method="_on_add_node_popup_menu_index_pressed"] -[connection signal="begin_node_move" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_begin_node_move"] -[connection signal="connection_request" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_connection_request"] -[connection signal="delete_nodes_request" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_delete_nodes_request"] -[connection signal="disconnection_request" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_disconnection_request"] -[connection signal="end_node_move" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_end_node_move"] -[connection signal="node_deselected" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_node_deselected"] -[connection signal="node_selected" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_node_selected"] -[connection signal="popup_request" from="BlendTreeEditorContainer/Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_popup_request"] diff --git a/demo/addons/blendalot/blendalot_plugin.gd b/demo/addons/blendalot/blendalot_plugin.gd index 24572d3..b58122f 100644 --- a/demo/addons/blendalot/blendalot_plugin.gd +++ b/demo/addons/blendalot/blendalot_plugin.gd @@ -1,9 +1,8 @@ @tool extends EditorPlugin -const MainPanel = preload("res://addons/blendalot/blendalot_main_panel.tscn") - -var main_panel_instance:BlendalotMainPanel +var editor_dock:EditorDock = null +var animation_graph_editor:AnimationGraphEditor = null func _enable_plugin() -> void: # Add autoloads here. @@ -16,25 +15,29 @@ func _disable_plugin() -> void: func _enter_tree() -> void: - main_panel_instance = MainPanel.instantiate() - # Add the main panel to the editor's main viewport. - EditorInterface.get_editor_main_screen().add_child(main_panel_instance) - # Hide the main panel. Very much required. - _make_visible(false) + editor_dock = EditorDock.new() + editor_dock.title = "Animation Graph" + editor_dock.default_slot = EditorDock.DOCK_SLOT_BOTTOM + animation_graph_editor = preload ("res://addons/blendalot/animation_graph_editor.tscn").instantiate() + editor_dock.add_child(animation_graph_editor) + add_dock(editor_dock) func _exit_tree() -> void: - if main_panel_instance: - main_panel_instance.queue_free() + remove_dock(editor_dock) + editor_dock.queue_free() + editor_dock = null + + animation_graph_editor.queue_free() + animation_graph_editor = null -func _has_main_screen(): - return true +func _has_main_screen() -> bool: + return false func _make_visible(visible): - if main_panel_instance: - main_panel_instance.visible = visible + pass func _get_plugin_name(): @@ -48,9 +51,14 @@ func _get_plugin_icon(): func _handles(obj: Object) -> bool: return obj is BLTAnimationNodeBlendTree + func _edit(object: Object): + if not is_instance_valid(animation_graph_editor): + push_error("Cannot edit object as AnimationGraphEditor is not initialized") + return + if object is BLTAnimationNodeBlendTree: - main_panel_instance.edit_blend_tree(object) + animation_graph_editor.edit_animation_root_node(object) return print("Cannot (yet) edit object " + str(object)) diff --git a/demo/main.tscn b/demo/main.tscn index c7c61c5..036b08e 100644 --- a/demo/main.tscn +++ b/demo/main.tscn @@ -31,28 +31,38 @@ sky = SubResource("Sky_1bvp3") tonemap_mode = 2 glow_enabled = true -[sub_resource type="BLTAnimationNodeBlend2" id="BLTAnimationNodeBlend2_bvt3d"] +[sub_resource type="BLTAnimationNodeBlend2" id="BLTAnimationNodeBlend2_7mycd"] resource_name = "BLTAnimationNodeBlend2" -graph_offset = Vector2(-540, -120) +position = Vector2(-320, -40) -[sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_sntl5"] -resource_name = "BLTAnimationNodeSampler 1" -graph_offset = Vector2(-1120, 180) -animation = &"animation_library/Run-InPlace" - -[sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_n4m28"] +[sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_272bh"] resource_name = "BLTAnimationNodeSampler" -graph_offset = Vector2(-1120, -280) +position = Vector2(-490, 7) +animation = &"animation_library/Walk-InPlace" + +[sub_resource type="BLTAnimationNodeBlendTree" id="BLTAnimationNodeBlendTree_5vw27"] +resource_name = "BLTAnimationNodeBlendTree" +position = Vector2(-640, -20) +graph_offset = Vector2(-766.67163, -102.823944) +nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_272bh") +nodes/BLTAnimationNodeSampler/graph_offset = Vector2(-490, 7) +node_connections = ["Output", 0, "BLTAnimationNodeSampler"] + +[sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_kek77"] +resource_name = "BLTAnimationNodeSampler" +position = Vector2(-620, 140) animation = &"animation_library/Walk-InPlace" [sub_resource type="BLTAnimationNodeBlendTree" id="BLTAnimationNodeBlendTree_7mycd"] -nodes/BLTAnimationNodeBlend2/node = SubResource("BLTAnimationNodeBlend2_bvt3d") -nodes/BLTAnimationNodeBlend2/graph_offset = Vector2(-540, -120) -"nodes/BLTAnimationNodeSampler 1/node" = SubResource("BLTAnimationNodeSampler_sntl5") -"nodes/BLTAnimationNodeSampler 1/graph_offset" = Vector2(-1120, 180) -nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_n4m28") -nodes/BLTAnimationNodeSampler/graph_offset = Vector2(-1120, -280) -node_connections = ["BLTAnimationNodeBlend2", 0, "BLTAnimationNodeSampler", "BLTAnimationNodeBlend2", 1, "BLTAnimationNodeSampler 1", "Output", 0, "BLTAnimationNodeBlend2"] +resource_name = "Root" +graph_offset = Vector2(-1054.4585, -50.771484) +nodes/BLTAnimationNodeBlend2/node = SubResource("BLTAnimationNodeBlend2_7mycd") +nodes/BLTAnimationNodeBlend2/graph_offset = Vector2(-320, -40) +nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_kek77") +nodes/BLTAnimationNodeSampler/graph_offset = Vector2(-620, 140) +nodes/BLTAnimationNodeBlendTree/node = SubResource("BLTAnimationNodeBlendTree_5vw27") +nodes/BLTAnimationNodeBlendTree/graph_offset = Vector2(-640, -20) +node_connections = ["Output", 0, "BLTAnimationNodeBlend2", "BLTAnimationNodeBlend2", 0, "BLTAnimationNodeBlendTree", "BLTAnimationNodeBlend2", 1, "BLTAnimationNodeSampler"] [node name="Main" type="Node3D" unique_id=933302313] script = ExtResource("1_1bvp3") @@ -159,6 +169,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.6, 0, 0) tree_root = ExtResource("6_5vw27") anim_player = NodePath("../AnimationPlayer") parameters/Blend2/blend_amount = 0.0 +parameters/BlendTree/Blend2/blend_amount = 0.0 [node name="MixamoAmyWalkLimpSynced" parent="Characters" unique_id=1018815116 instance=ExtResource("1_0xm2m")] unique_name_in_owner = true @@ -177,127 +188,126 @@ parameters/BLTAnimationNodeBlend2/blend_amount = 1.0 unique_name_in_owner = true transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4, 0, 0) -[node name="Skeleton3D" parent="Characters/MixamoAmyWalkRunSynced/Armature" parent_id_path=PackedInt32Array(2088190993, 52334938) index="0" unique_id=2014991417] -bones/0/position = Vector3(1.2535723, 103.23205, 0) -bones/2/position = Vector3(1.2545489, -1.2283401, -63.87065) -bones/2/rotation = Quaternion(-0.68592405, 0.04377775, -0.051641874, 0.724517) -bones/3/position = Vector3(1.8626451e-08, 3.9624305, -0.274297) -bones/3/rotation = Quaternion(-0.022852536, -0.012121664, 0.0041041635, 0.99965584) -bones/4/position = Vector3(5.9604645e-08, 14.123348, 0.2644268) -bones/4/rotation = Quaternion(-0.004333934, -0.027048457, 0.008698374, 0.99958694) -bones/5/position = Vector3(3.7252903e-08, 11.404536, 0.106110305) -bones/5/rotation = Quaternion(-0.0043339357, -0.027048472, 0.008698381, 0.99958694) -bones/6/position = Vector3(2.9802322e-08, 9.946667, 1.6090326e-08) -bones/6/rotation = Quaternion(0.01833006, 0.009223348, -0.022252144, 0.99954194) -bones/7/position = Vector3(3.1292439e-07, 8.579114, 0.11416777) -bones/7/rotation = Quaternion(0.012213917, 0.0008427718, 0.018853437, 0.99974734) -bones/8/position = Vector3(1.8626451e-09, 25.089184, 0.33387405) -bones/9/position = Vector3(4.22409, 8.898366, -0.056614783) -bones/9/rotation = Quaternion(0.6974001, 0.2956288, -0.5221589, 0.39190146) -bones/10/position = Vector3(7.763405e-07, 8.86101, -1.885943e-05) -bones/10/rotation = Quaternion(0.26628703, -0.06642532, 0.008976073, 0.9615604) -bones/11/position = Vector3(-6.736064e-08, 18.975187, -5.359281e-06) -bones/11/rotation = Quaternion(0.07033256, 0.12871316, 0.46235967, 0.8744769) -bones/12/position = Vector3(2.0505329e-06, 19.896036, 4.198435e-06) -bones/12/rotation = Quaternion(-0.16744098, 0.1057004, 0.06551589, 0.97800744) -bones/13/position = Vector3(-2.0888743, 2.1412597, 1.2094326) -bones/13/rotation = Quaternion(0.25766087, -0.0057431525, 0.24790713, 0.93387365) -bones/14/position = Vector3(-0.3297696, 2.5601492, 5.6111962e-06) -bones/14/rotation = Quaternion(-0.02088847, -0.0012659901, -0.06479406, 0.9976792) -bones/15/position = Vector3(0.07692051, 2.7271638, -7.698024e-07) -bones/15/rotation = Quaternion(0.041175473, -5.169663e-07, -3.3207877e-07, 0.99915195) -bones/16/position = Vector3(0.25285125, 2.2376482, -2.5033949e-06) -bones/17/position = Vector3(-2.5860305, 6.9422946, 0.029643927) -bones/17/rotation = Quaternion(0.34374678, 0.015888449, 0.085352995, 0.9350405) -bones/18/position = Vector3(0.0010065138, 2.844871, 2.6222544e-05) -bones/18/rotation = Quaternion(0.57630825, -0.010742382, 0.057439517, 0.8151404) -bones/19/position = Vector3(-0.001300633, 2.6654923, -3.370296e-06) -bones/19/rotation = Quaternion(0.0067229928, -1.8172469e-07, -1.12402185e-08, 0.9999774) -bones/20/position = Vector3(0.00029665232, 2.2974284, 2.6011842e-06) -bones/21/position = Vector3(-0.81500757, 6.8271494, -0.11008961) -bones/21/rotation = Quaternion(0.4686701, -0.026470345, 0.09011306, 0.87836635) -bones/22/position = Vector3(-0.011725575, 3.256784, 9.871595e-06) -bones/22/rotation = Quaternion(0.7447584, -0.008458963, 0.08384082, 0.66199255) -bones/23/position = Vector3(-0.00070961565, 3.1778917, -4.440292e-06) -bones/23/rotation = Quaternion(0.05807299, -2.1569534e-08, -4.8931874e-08, 0.99831235) -bones/24/position = Vector3(0.012437165, 2.6951318, 2.2368273e-05) -bones/25/position = Vector3(0.89882326, 6.7787366, -0.023027958) -bones/25/rotation = Quaternion(0.46021903, -0.10865909, 0.0012826431, 0.88113) -bones/26/position = Vector3(-0.0040293336, 2.958041, 1.3685599e-05) -bones/26/rotation = Quaternion(0.75350595, 0.003054577, 0.06509644, 0.65420324) -bones/27/position = Vector3(0.0024927258, 2.756927, -1.8636636e-05) -bones/27/rotation = Quaternion(-0.0033205294, 3.9059813e-08, 6.5682637e-09, 0.9999945) -bones/28/position = Vector3(0.0015397072, 2.3345788, 1.1771219e-05) -bones/29/position = Vector3(2.5022223, 6.3907866, 0.08744741) -bones/29/rotation = Quaternion(0.5563202, -0.15497696, -0.0049168295, 0.81637365) -bones/30/position = Vector3(0.0032016933, 2.6237173, 5.5511973e-06) -bones/30/rotation = Quaternion(0.6839778, 0.025319714, 0.05863921, 0.72670126) -bones/31/position = Vector3(0.003878951, 2.1320877, -1.2886679e-06) -bones/31/rotation = Quaternion(-0.06925962, -1.0360488e-07, -7.0190005e-08, 0.9975987) -bones/32/position = Vector3(-0.0070759356, 1.7932303, -8.8289596e-07) -bones/33/position = Vector3(-4.2240915, 8.897269, -0.030230172) -bones/33/rotation = Quaternion(0.6396871, -0.34111822, 0.60846674, 0.32281098) -bones/34/position = Vector3(9.327891e-07, 8.8610115, -9.847447e-06) -bones/34/rotation = Quaternion(0.3631728, -0.04426816, 0.26242447, 0.892905) -bones/35/position = Vector3(6.476337e-07, 18.973557, 5.8403616e-06) -bones/35/rotation = Quaternion(0.020210672, -0.1976225, -0.21386963, 0.95645) -bones/36/position = Vector3(-7.630494e-08, 19.89834, -1.8180976e-06) -bones/36/rotation = Quaternion(-0.0013729627, -0.10906849, 0.07843006, 0.9909344) -bones/37/position = Vector3(2.0620086, 2.1477072, 1.2410417) -bones/37/rotation = Quaternion(0.2022776, 0.03663972, -0.31487894, 0.9266028) -bones/38/position = Vector3(0.2744484, 2.5658755, 3.7584634e-06) -bones/38/rotation = Quaternion(0.10343577, -0.19018154, 0.15068975, 0.9645852) -bones/39/position = Vector3(-0.076725245, 2.730221, -7.234994e-06) -bones/39/rotation = Quaternion(0.037325032, -2.1781963e-08, 1.19075e-07, 0.99930316) -bones/40/position = Vector3(-0.19772077, 2.2442596, 9.685756e-06) -bones/41/position = Vector3(2.5699375, 7.0537868, 0.05093465) -bones/41/rotation = Quaternion(0.33546335, 0.008294326, -0.03227234, 0.9414638) -bones/42/position = Vector3(-0.0015108623, 2.816315, -9.529522e-06) -bones/42/rotation = Quaternion(0.5038074, 0.025462631, -0.03289526, 0.86281383) -bones/43/position = Vector3(0.0011758618, 2.6396425, 2.472788e-06) -bones/43/rotation = Quaternion(0.0026853334, 3.311302e-08, -5.8677845e-08, 0.9999965) -bones/44/position = Vector3(0.00033191964, 2.2415001, 1.3194513e-06) -bones/45/position = Vector3(0.76549155, 7.018211, -0.09038047) -bones/45/rotation = Quaternion(0.365633, 0.035192948, -0.020985998, 0.92985666) -bones/46/position = Vector3(0.008488461, 3.3401706, -6.017696e-06) -bones/46/rotation = Quaternion(0.60296637, 0.037562832, -0.043432355, 0.79569733) -bones/47/position = Vector3(-0.007900611, 3.0566626, 5.4270527e-06) -bones/47/rotation = Quaternion(-0.027461762, -2.0760188e-08, 5.6872928e-08, 0.9996229) -bones/48/position = Vector3(-0.00058989227, 2.63951, 9.457464e-06) -bones/49/position = Vector3(-0.8923034, 6.9110794, -0.12160769) -bones/49/rotation = Quaternion(0.373129, 0.027513513, 0.0016994111, 0.92736983) -bones/50/position = Vector3(-0.00297606, 2.923956, 1.7109673e-05) -bones/50/rotation = Quaternion(0.5844741, -0.018550195, -0.061117128, 0.80889475) -bones/51/position = Vector3(0.0026345253, 2.717951, -5.427028e-06) -bones/51/rotation = Quaternion(0.009096339, 7.025756e-08, 2.6579988e-08, 0.99995863) -bones/52/position = Vector3(0.0003426671, 2.379634, 1.4747493e-06) -bones/53/position = Vector3(-2.4431183, 6.453577, 0.12551774) -bones/53/rotation = Quaternion(0.33751208, 0.021386249, -0.009726644, 0.941028) -bones/54/position = Vector3(-0.0026362836, 2.6278691, 4.2827646e-06) -bones/54/rotation = Quaternion(0.64713013, -0.039810352, -0.090106204, 0.75598854) -bones/55/position = Vector3(-0.001860708, 2.1224687, 9.940322e-07) -bones/55/rotation = Quaternion(-0.03619139, -1.1229469e-07, 2.121775e-08, 0.9993449) -bones/56/position = Vector3(0.004502952, 1.7919822, 1.2924895e-05) -bones/57/position = Vector3(7.557004, -4.826265, 0.30053553) -bones/57/rotation = Quaternion(-0.08986163, 0.021308435, -0.99325967, 0.070043266) -bones/58/position = Vector3(-0.124621816, 24.06574, 0.10470339) -bones/58/rotation = Quaternion(-0.4013793, 0.12247081, -0.016882984, 0.9075299) -bones/59/position = Vector3(-0.0060171685, 33.063114, 0.00394835) -bones/59/rotation = Quaternion(0.43592802, 0.023901237, 0.044110287, 0.89858204) -bones/60/position = Vector3(-0.0052775713, 12.893309, -0.5155027) -bones/60/rotation = Quaternion(0.5547358, -0.05625008, 0.006090132, 0.83010066) -bones/61/position = Vector3(0.014227723, 6.420835, 0.007887725) -bones/62/position = Vector3(-7.557004, -4.826261, 0.30053535) -bones/62/rotation = Quaternion(-0.0453176, 0.37578616, 0.92547935, 0.014799355) -bones/63/position = Vector3(0.12476807, 24.070103, -0.14491707) -bones/63/rotation = Quaternion(-0.27188188, -0.00034630176, 0.0028924432, 0.96232617) -bones/64/position = Vector3(0.0060205855, 33.063133, 0.004499323) -bones/64/rotation = Quaternion(0.4376588, -0.0021246495, 3.749458e-05, 0.8991387) -bones/65/position = Vector3(0.005378528, 12.718271, -0.52236915) -bones/65/rotation = Quaternion(0.34835225, 0.017514596, -0.0044682953, 0.9371894) -bones/66/position = Vector3(-0.014239848, 6.412128, 0.0079107955) +[node name="Skeleton3D" parent="Characters/MixamoAmyWalkRunSynced/Armature" parent_id_path=PackedInt32Array(2088190993, 1791722621) index="0" unique_id=1831928682] +bones/2/position = Vector3(0, 0, 0) +bones/2/rotation = Quaternion(0, 0, 0, 1) +bones/3/position = Vector3(0, 0, 0) +bones/3/rotation = Quaternion(0, 0, 0, 1) +bones/4/position = Vector3(0, 0, 0) +bones/4/rotation = Quaternion(0, 0, 0, 1) +bones/5/position = Vector3(0, 0, 0) +bones/5/rotation = Quaternion(0, 0, 0, 1) +bones/6/position = Vector3(0, 0, 0) +bones/6/rotation = Quaternion(0, 0, 0, 1) +bones/7/position = Vector3(0, 0, 0) +bones/7/rotation = Quaternion(0, 0, 0, 1) +bones/8/position = Vector3(0, 0, 0) +bones/9/position = Vector3(0, 0, 0) +bones/9/rotation = Quaternion(0, 0, 0, 1) +bones/10/position = Vector3(0, 0, 0) +bones/10/rotation = Quaternion(0, 0, 0, 1) +bones/11/position = Vector3(0, 0, 0) +bones/11/rotation = Quaternion(0, 0, 0, 1) +bones/12/position = Vector3(0, 0, 0) +bones/12/rotation = Quaternion(0, 0, 0, 1) +bones/13/position = Vector3(0, 0, 0) +bones/13/rotation = Quaternion(0, 0, 0, 1) +bones/14/position = Vector3(0, 0, 0) +bones/14/rotation = Quaternion(0, 0, 0, 1) +bones/15/position = Vector3(0, 0, 0) +bones/15/rotation = Quaternion(0, 0, 0, 1) +bones/16/position = Vector3(0, 0, 0) +bones/17/position = Vector3(0, 0, 0) +bones/17/rotation = Quaternion(0, 0, 0, 1) +bones/18/position = Vector3(0, 0, 0) +bones/18/rotation = Quaternion(0, 0, 0, 1) +bones/19/position = Vector3(0, 0, 0) +bones/19/rotation = Quaternion(0, 0, 0, 1) +bones/20/position = Vector3(0, 0, 0) +bones/21/position = Vector3(0, 0, 0) +bones/21/rotation = Quaternion(0, 0, 0, 1) +bones/22/position = Vector3(0, 0, 0) +bones/22/rotation = Quaternion(0, 0, 0, 1) +bones/23/position = Vector3(0, 0, 0) +bones/23/rotation = Quaternion(0, 0, 0, 1) +bones/24/position = Vector3(0, 0, 0) +bones/25/position = Vector3(0, 0, 0) +bones/25/rotation = Quaternion(0, 0, 0, 1) +bones/26/position = Vector3(0, 0, 0) +bones/26/rotation = Quaternion(0, 0, 0, 1) +bones/27/position = Vector3(0, 0, 0) +bones/27/rotation = Quaternion(0, 0, 0, 1) +bones/28/position = Vector3(0, 0, 0) +bones/29/position = Vector3(0, 0, 0) +bones/29/rotation = Quaternion(0, 0, 0, 1) +bones/30/position = Vector3(0, 0, 0) +bones/30/rotation = Quaternion(0, 0, 0, 1) +bones/31/position = Vector3(0, 0, 0) +bones/31/rotation = Quaternion(0, 0, 0, 1) +bones/32/position = Vector3(0, 0, 0) +bones/33/position = Vector3(0, 0, 0) +bones/33/rotation = Quaternion(0, 0, 0, 1) +bones/34/position = Vector3(0, 0, 0) +bones/34/rotation = Quaternion(0, 0, 0, 1) +bones/35/position = Vector3(0, 0, 0) +bones/35/rotation = Quaternion(0, 0, 0, 1) +bones/36/position = Vector3(0, 0, 0) +bones/36/rotation = Quaternion(0, 0, 0, 1) +bones/37/position = Vector3(0, 0, 0) +bones/37/rotation = Quaternion(0, 0, 0, 1) +bones/38/position = Vector3(0, 0, 0) +bones/38/rotation = Quaternion(0, 0, 0, 1) +bones/39/position = Vector3(0, 0, 0) +bones/39/rotation = Quaternion(0, 0, 0, 1) +bones/40/position = Vector3(0, 0, 0) +bones/41/position = Vector3(0, 0, 0) +bones/41/rotation = Quaternion(0, 0, 0, 1) +bones/42/position = Vector3(0, 0, 0) +bones/42/rotation = Quaternion(0, 0, 0, 1) +bones/43/position = Vector3(0, 0, 0) +bones/43/rotation = Quaternion(0, 0, 0, 1) +bones/44/position = Vector3(0, 0, 0) +bones/45/position = Vector3(0, 0, 0) +bones/45/rotation = Quaternion(0, 0, 0, 1) +bones/46/position = Vector3(0, 0, 0) +bones/46/rotation = Quaternion(0, 0, 0, 1) +bones/47/position = Vector3(0, 0, 0) +bones/47/rotation = Quaternion(0, 0, 0, 1) +bones/48/position = Vector3(0, 0, 0) +bones/49/position = Vector3(0, 0, 0) +bones/49/rotation = Quaternion(0, 0, 0, 1) +bones/50/position = Vector3(0, 0, 0) +bones/50/rotation = Quaternion(0, 0, 0, 1) +bones/51/position = Vector3(0, 0, 0) +bones/51/rotation = Quaternion(0, 0, 0, 1) +bones/52/position = Vector3(0, 0, 0) +bones/53/position = Vector3(0, 0, 0) +bones/53/rotation = Quaternion(0, 0, 0, 1) +bones/54/position = Vector3(0, 0, 0) +bones/54/rotation = Quaternion(0, 0, 0, 1) +bones/55/position = Vector3(0, 0, 0) +bones/55/rotation = Quaternion(0, 0, 0, 1) +bones/56/position = Vector3(0, 0, 0) +bones/57/position = Vector3(0, 0, 0) +bones/57/rotation = Quaternion(0, 0, 0, 1) +bones/58/position = Vector3(0, 0, 0) +bones/58/rotation = Quaternion(0, 0, 0, 1) +bones/59/position = Vector3(0, 0, 0) +bones/59/rotation = Quaternion(0, 0, 0, 1) +bones/60/position = Vector3(0, 0, 0) +bones/60/rotation = Quaternion(0, 0, 0, 1) +bones/61/position = Vector3(0, 0, 0) +bones/62/position = Vector3(0, 0, 0) +bones/62/rotation = Quaternion(0, 0, 0, 1) +bones/63/position = Vector3(0, 0, 0) +bones/63/rotation = Quaternion(0, 0, 0, 1) +bones/64/position = Vector3(0, 0, 0) +bones/64/rotation = Quaternion(0, 0, 0, 1) +bones/65/position = Vector3(0, 0, 0) +bones/65/rotation = Quaternion(0, 0, 0, 1) +bones/66/position = Vector3(0, 0, 0) -[node name="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" index="1" unique_id=945472897] +[node name="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" index="1" unique_id=66984852] active = false [node name="AnimationPlayer2" type="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" unique_id=1255239074] -- 2.47.2 From 06198d595ff9f0f1a6de0b374767df467eced749 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Wed, 18 Feb 2026 22:13:51 +0100 Subject: [PATCH 20/29] Added test for embedded blend tree evaluation. --- tests/test_blendalot_animgraph.h | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_blendalot_animgraph.h b/tests/test_blendalot_animgraph.h index bc6c5e6..511b713 100644 --- a/tests/test_blendalot_animgraph.h +++ b/tests/test_blendalot_animgraph.h @@ -599,4 +599,61 @@ TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTreeGraph][Chan } } +TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTreeGraph][EmbeddedBlendTree] BlendTree with an embedded BlendTree subgraph") { + // Embedded BlendTree + Ref embedded_blend_tree; + embedded_blend_tree.instantiate(); + + // TestAnimationB + Ref animation_sampler_node_b; + animation_sampler_node_b.instantiate(); + animation_sampler_node_b->animation_name = "animation_library/TestAnimationB"; + + embedded_blend_tree->add_node(animation_sampler_node_b); + embedded_blend_tree->add_connection(animation_sampler_node_b, embedded_blend_tree->get_output_node(), "Output"); + + Ref blend_tree; + blend_tree.instantiate(); + + // Blend2 + Ref blend2; + blend2.instantiate(); + blend2->set_name("Blend2"); + blend2->blend_weight = 0.5; + blend2->sync = true; + + blend_tree->add_node(blend2); + + // TestAnimationA + Ref animation_sampler_node_a; + animation_sampler_node_a.instantiate(); + animation_sampler_node_a->animation_name = "animation_library/TestAnimationA"; + + blend_tree->add_node(animation_sampler_node_a); + + blend_tree->add_node(embedded_blend_tree); + + blend_tree->add_connection(animation_sampler_node_a, blend2, "Input0"); + blend_tree->add_connection(embedded_blend_tree, blend2, "Input1"); + blend_tree->add_connection(blend2, blend_tree->get_output_node(), "Output"); + + // Trigger initialization + animation_graph->set_root_animation_node(blend_tree); + GraphEvaluationContext &graph_context = animation_graph->get_context(); + REQUIRE(blend_tree->initialize(graph_context)); + + // Perform evaluation + AnimationData *graph_output = graph_context.animation_data_allocator.allocate(); + blend_tree->activate_inputs(Vector>()); + blend_tree->calculate_sync_track(Vector>()); + blend_tree->update_time(0.1); + blend_tree->evaluate(graph_context, LocalVector(), *graph_output); + + // Check values + AnimationData::TransformTrackValue *hip_transform_value = graph_output->get_value(test_animation_a->get_tracks()[0]->thash); + CHECK(hip_transform_value->loc[0] == doctest::Approx(0.15)); + CHECK(hip_transform_value->loc[1] == doctest::Approx(0.3)); + CHECK(hip_transform_value->loc[2] == doctest::Approx(0.45)); +} + } //namespace TestBlendalotAnimationGraph \ No newline at end of file -- 2.47.2 From d01c6fb474dbc4cb2454c9171bada342b01ee63e Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Thu, 19 Feb 2026 19:03:56 +0100 Subject: [PATCH 21/29] Minor editor fixes. --- demo/addons/blendalot/animation_graph_editor.gd | 1 + demo/addons/blendalot/blend_tree_editor.gd | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/demo/addons/blendalot/animation_graph_editor.gd b/demo/addons/blendalot/animation_graph_editor.gd index fdef1f6..759653e 100644 --- a/demo/addons/blendalot/animation_graph_editor.gd +++ b/demo/addons/blendalot/animation_graph_editor.gd @@ -29,6 +29,7 @@ func edit_animation_root_node(blt_node:BLTAnimationNode): animation_graph_root_node = blt_node push_graph_stack(blt_node) edit_graph(blt_node) + return push_warning("Cannot edit node %s. Graph type %s not yet supported." % [blt_node.resource_name, blt_node.get_class()]) diff --git a/demo/addons/blendalot/blend_tree_editor.gd b/demo/addons/blendalot/blend_tree_editor.gd index 187762a..896c079 100644 --- a/demo/addons/blendalot/blend_tree_editor.gd +++ b/demo/addons/blendalot/blend_tree_editor.gd @@ -198,7 +198,8 @@ func _on_blend_tree_graph_edit_node_selected(graph_node: Node) -> void: func _on_blend_tree_graph_edit_scroll_offset_changed(offset: Vector2) -> void: - blend_tree.graph_offset = offset + if is_instance_valid(blend_tree): + blend_tree.graph_offset = offset # -- 2.47.2 From a098bc1171fb6999da74f980770bbe3208094322 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Thu, 19 Feb 2026 19:05:14 +0100 Subject: [PATCH 22/29] Added test for synced blending of embedded blend tree. --- blendalot_animation_node.h | 16 ++++++-- demo/animation_tree_walk_run.tres | 23 +++++++++++ demo/main.tscn | 9 +++-- tests/test_blendalot_animgraph.h | 66 +++++++++++++++++++++---------- 4 files changed, 86 insertions(+), 28 deletions(-) diff --git a/blendalot_animation_node.h b/blendalot_animation_node.h index f7a6ef3..e3d07b3 100644 --- a/blendalot_animation_node.h +++ b/blendalot_animation_node.h @@ -257,8 +257,7 @@ public: double sync_position = 0.0; bool is_synced = false; - // TODO: 2026-02-17: how to initialize loop_mode e.g. for a BlendTree or a StateMachine? - Animation::LoopMode loop_mode = Animation::LOOP_LINEAR; + Animation::LoopMode loop_mode = Animation::LOOP_NONE; SyncTrack sync_track; }; NodeTimeInfo node_time_info; @@ -753,6 +752,8 @@ public: } tree_graph.nodes[0]->active = true; + tree_graph.nodes[0]->node_time_info.is_synced = node_time_info.is_synced; + for (uint32_t i = 0; i < tree_graph.nodes.size(); i++) { const Ref &node = tree_graph.nodes[i]; @@ -777,14 +778,21 @@ public: const NodeRuntimeData &node_runtime_data = _node_runtime_data[i]; node->calculate_sync_track(node_runtime_data.input_nodes); + + if (i == 1) { + node_time_info = node->node_time_info; + } } } void update_time(double p_delta) override { GodotProfileZone("SyncedBlendTree::update_time"); - tree_graph.nodes[0]->node_time_info.delta = p_delta; - tree_graph.nodes[0]->node_time_info.position += p_delta; + BLTAnimationNode::update_time(p_delta); + + tree_graph.nodes[0]->node_time_info.delta = node_time_info.delta; + tree_graph.nodes[0]->node_time_info.position = node_time_info.position; + tree_graph.nodes[0]->node_time_info.sync_position = node_time_info.sync_position; for (uint32_t i = 1; i < tree_graph.nodes.size(); i++) { const Ref &node = tree_graph.nodes[i]; diff --git a/demo/animation_tree_walk_run.tres b/demo/animation_tree_walk_run.tres index fd1de9c..bf53c65 100644 --- a/demo/animation_tree_walk_run.tres +++ b/demo/animation_tree_walk_run.tres @@ -8,7 +8,28 @@ animation = &"Walk-InPlace" [sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_lquwl"] +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_vyt75"] + +[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_hom0r"] + +[sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_vyt75"] + +[sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_1rfsi"] +nodes/BlendTree/node = SubResource("AnimationNodeBlendTree_vyt75") +nodes/BlendTree/position = Vector2(694.9995, 215.55058) + +[sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_8tpve"] +graph_offset = Vector2(-782, 179.47485) +nodes/Animation/node = SubResource("AnimationNodeAnimation_vyt75") +nodes/Animation/position = Vector2(-320, 140) +nodes/Blend2/node = SubResource("AnimationNodeBlend2_hom0r") +nodes/Blend2/position = Vector2(-115.66393, 127.37674) +nodes/BlendTree/node = SubResource("AnimationNodeBlendTree_1rfsi") +nodes/BlendTree/position = Vector2(-480, 400) +node_connections = [&"output", 0, &"Blend2", &"Blend2", 0, &"Animation", &"Blend2", 1, &"BlendTree"] + [resource] +graph_offset = Vector2(-217.4643, 82.84979) nodes/output/position = Vector2(540, 140) nodes/Animation/node = SubResource("AnimationNodeAnimation_1bvp3") nodes/Animation/position = Vector2(120, 80) @@ -16,4 +37,6 @@ nodes/Animation/position = Vector2(120, 80) "nodes/Animation 2/position" = Vector2(80, 320) nodes/Blend2/node = SubResource("AnimationNodeBlend2_lquwl") nodes/Blend2/position = Vector2(360, 180) +nodes/BlendTree/node = SubResource("AnimationNodeBlendTree_8tpve") +nodes/BlendTree/position = Vector2(778.0867, 295.33868) node_connections = [&"output", 0, &"Blend2", &"Blend2", 0, &"Animation", &"Blend2", 1, &"Animation 2"] diff --git a/demo/main.tscn b/demo/main.tscn index 036b08e..9e8744d 100644 --- a/demo/main.tscn +++ b/demo/main.tscn @@ -34,6 +34,7 @@ glow_enabled = true [sub_resource type="BLTAnimationNodeBlend2" id="BLTAnimationNodeBlend2_7mycd"] resource_name = "BLTAnimationNodeBlend2" position = Vector2(-320, -40) +blend_amount = 0.81 [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_272bh"] resource_name = "BLTAnimationNodeSampler" @@ -43,7 +44,7 @@ animation = &"animation_library/Walk-InPlace" [sub_resource type="BLTAnimationNodeBlendTree" id="BLTAnimationNodeBlendTree_5vw27"] resource_name = "BLTAnimationNodeBlendTree" position = Vector2(-640, -20) -graph_offset = Vector2(-766.67163, -102.823944) +graph_offset = Vector2(-760.67163, -24.823944) nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_272bh") nodes/BLTAnimationNodeSampler/graph_offset = Vector2(-490, 7) node_connections = ["Output", 0, "BLTAnimationNodeSampler"] @@ -51,11 +52,11 @@ node_connections = ["Output", 0, "BLTAnimationNodeSampler"] [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_kek77"] resource_name = "BLTAnimationNodeSampler" position = Vector2(-620, 140) -animation = &"animation_library/Walk-InPlace" +animation = &"animation_library/Run-InPlace" [sub_resource type="BLTAnimationNodeBlendTree" id="BLTAnimationNodeBlendTree_7mycd"] resource_name = "Root" -graph_offset = Vector2(-1054.4585, -50.771484) +graph_offset = Vector2(-869, -71) nodes/BLTAnimationNodeBlend2/node = SubResource("BLTAnimationNodeBlend2_7mycd") nodes/BLTAnimationNodeBlend2/graph_offset = Vector2(-320, -40) nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_kek77") @@ -318,7 +319,7 @@ libraries/animation_library = ExtResource("3_1bvp3") animation_player = NodePath("../AnimationPlayer2") tree_root = SubResource("BLTAnimationNodeBlendTree_7mycd") skeleton = NodePath("../Armature/Skeleton3D") -parameters/BLTAnimationNodeBlend2/blend_amount = 0.0 +parameters/BLTAnimationNodeBlend2/blend_amount = 0.81 [connection signal="value_changed" from="UI/MarginContainer/HBoxContainer/BlendWeightSlider" to="." method="_on_blend_weight_slider_value_changed"] diff --git a/tests/test_blendalot_animgraph.h b/tests/test_blendalot_animgraph.h index 511b713..85231a8 100644 --- a/tests/test_blendalot_animgraph.h +++ b/tests/test_blendalot_animgraph.h @@ -607,7 +607,6 @@ TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTreeGraph][Embe // TestAnimationB Ref animation_sampler_node_b; animation_sampler_node_b.instantiate(); - animation_sampler_node_b->animation_name = "animation_library/TestAnimationB"; embedded_blend_tree->add_node(animation_sampler_node_b); embedded_blend_tree->add_connection(animation_sampler_node_b, embedded_blend_tree->get_output_node(), "Output"); @@ -619,15 +618,11 @@ TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTreeGraph][Embe Ref blend2; blend2.instantiate(); blend2->set_name("Blend2"); - blend2->blend_weight = 0.5; - blend2->sync = true; - blend_tree->add_node(blend2); // TestAnimationA Ref animation_sampler_node_a; animation_sampler_node_a.instantiate(); - animation_sampler_node_a->animation_name = "animation_library/TestAnimationA"; blend_tree->add_node(animation_sampler_node_a); @@ -637,23 +632,54 @@ TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTreeGraph][Embe blend_tree->add_connection(embedded_blend_tree, blend2, "Input1"); blend_tree->add_connection(blend2, blend_tree->get_output_node(), "Output"); - // Trigger initialization - animation_graph->set_root_animation_node(blend_tree); - GraphEvaluationContext &graph_context = animation_graph->get_context(); - REQUIRE(blend_tree->initialize(graph_context)); + SUBCASE("Perform regular blend") { + animation_sampler_node_b->animation_name = "animation_library/TestAnimationB"; + animation_sampler_node_a->animation_name = "animation_library/TestAnimationA"; + blend2->blend_weight = 0.5; + blend2->sync = false; - // Perform evaluation - AnimationData *graph_output = graph_context.animation_data_allocator.allocate(); - blend_tree->activate_inputs(Vector>()); - blend_tree->calculate_sync_track(Vector>()); - blend_tree->update_time(0.1); - blend_tree->evaluate(graph_context, LocalVector(), *graph_output); + // Trigger initialization + animation_graph->set_root_animation_node(blend_tree); + GraphEvaluationContext &graph_context = animation_graph->get_context(); + REQUIRE(blend_tree->initialize(graph_context)); - // Check values - AnimationData::TransformTrackValue *hip_transform_value = graph_output->get_value(test_animation_a->get_tracks()[0]->thash); - CHECK(hip_transform_value->loc[0] == doctest::Approx(0.15)); - CHECK(hip_transform_value->loc[1] == doctest::Approx(0.3)); - CHECK(hip_transform_value->loc[2] == doctest::Approx(0.45)); + // Perform evaluation + AnimationData *graph_output = graph_context.animation_data_allocator.allocate(); + blend_tree->activate_inputs(Vector>()); + blend_tree->calculate_sync_track(Vector>()); + blend_tree->update_time(0.1); + blend_tree->evaluate(graph_context, LocalVector(), *graph_output); + + // Check values + AnimationData::TransformTrackValue *hip_transform_value = graph_output->get_value(test_animation_a->get_tracks()[0]->thash); + CHECK(hip_transform_value->loc[0] == doctest::Approx(0.15)); + CHECK(hip_transform_value->loc[1] == doctest::Approx(0.3)); + CHECK(hip_transform_value->loc[2] == doctest::Approx(0.45)); + } + SUBCASE("Perform synced blend") { + animation_sampler_node_b->animation_name = "animation_library/TestAnimationSyncA"; + animation_sampler_node_a->animation_name = "animation_library/TestAnimationSyncB"; + blend2->blend_weight = 0.5; + blend2->sync = true; + + // Trigger initialization + animation_graph->set_root_animation_node(blend_tree); + GraphEvaluationContext &graph_context = animation_graph->get_context(); + REQUIRE(blend_tree->initialize(graph_context)); + + // Perform evaluation + AnimationData *graph_output = graph_context.animation_data_allocator.allocate(); + blend_tree->activate_inputs(Vector>()); + blend_tree->calculate_sync_track(Vector>()); + blend_tree->update_time(0.825); + blend_tree->evaluate(graph_context, LocalVector(), *graph_output); + + // Check values + AnimationData::TransformTrackValue *hip_transform_value = graph_output->get_value(test_animation_a->get_tracks()[0]->thash); + CHECK(hip_transform_value->loc[0] == doctest::Approx(1.5)); + CHECK(hip_transform_value->loc[1] == doctest::Approx(3.0)); + CHECK(hip_transform_value->loc[2] == doctest::Approx(4.5)); + } } } //namespace TestBlendalotAnimationGraph \ No newline at end of file -- 2.47.2 From 0198847fd1d1ddbdbd0f4df4b15e30e96af593a9 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Fri, 20 Feb 2026 12:34:49 +0100 Subject: [PATCH 23/29] Disconnection works again in blend tree editor. --- demo/addons/blendalot/blend_tree_editor.gd | 9 +++++++++ demo/addons/blendalot/blend_tree_editor.tscn | 1 + 2 files changed, 10 insertions(+) diff --git a/demo/addons/blendalot/blend_tree_editor.gd b/demo/addons/blendalot/blend_tree_editor.gd index 896c079..32b6f69 100644 --- a/demo/addons/blendalot/blend_tree_editor.gd +++ b/demo/addons/blendalot/blend_tree_editor.gd @@ -154,6 +154,15 @@ func _on_blend_tree_graph_edit_connection_request(from_node: StringName, from_po print("Success!") +func _on_blend_tree_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void: + var blend_tree_source_node = blend_tree.get_node(from_node) + var blend_tree_target_node = blend_tree.get_node(to_node) + var target_port_name = blend_tree_target_node.get_input_names()[to_port] + blend_tree.remove_connection(blend_tree_source_node, blend_tree_target_node, target_port_name) + + blend_tree_graph_edit.disconnect_node(from_node, from_port, to_node, to_port) + + func _on_blend_tree_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void: for node_name:StringName in nodes: print("remove node '%s'" % node_name) diff --git a/demo/addons/blendalot/blend_tree_editor.tscn b/demo/addons/blendalot/blend_tree_editor.tscn index 57e51a7..c00b0a8 100644 --- a/demo/addons/blendalot/blend_tree_editor.tscn +++ b/demo/addons/blendalot/blend_tree_editor.tscn @@ -44,6 +44,7 @@ right_disconnects = true [connection signal="index_pressed" from="Panel/AddNodePopupMenu" to="." method="_on_add_node_popup_menu_index_pressed"] [connection signal="connection_request" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_connection_request"] [connection signal="delete_nodes_request" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_delete_nodes_request"] +[connection signal="disconnection_request" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_disconnection_request"] [connection signal="end_node_move" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_end_node_move"] [connection signal="node_deselected" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_node_deselected"] [connection signal="node_selected" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_node_selected"] -- 2.47.2 From 095f1e5d0cead88e4f06ddc0d88066efc1e12098 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Fri, 20 Feb 2026 13:40:12 +0100 Subject: [PATCH 24/29] Animations now selectable using OptionButton. --- blendalot_animation_node.h | 1 + .../blendalot/animation_graph_editor.gd | 6 ++++- demo/addons/blendalot/blend_tree_editor.gd | 22 ++++++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/blendalot_animation_node.h b/blendalot_animation_node.h index e3d07b3..9c97a82 100644 --- a/blendalot_animation_node.h +++ b/blendalot_animation_node.h @@ -350,6 +350,7 @@ public: void set_animation_player(AnimationPlayer *p_player); bool set_animation(const StringName &p_name); StringName get_animation() const; + AnimationPlayer *get_animation_player() const; TypedArray get_animations_as_typed_array() const; diff --git a/demo/addons/blendalot/animation_graph_editor.gd b/demo/addons/blendalot/animation_graph_editor.gd index 759653e..ce7c30e 100644 --- a/demo/addons/blendalot/animation_graph_editor.gd +++ b/demo/addons/blendalot/animation_graph_editor.gd @@ -6,12 +6,13 @@ class_name AnimationGraphEditor @onready var breadcrumb_button_container: HBoxContainer = %BreadcrumbButtons @onready var active_graph_control: Control = %ActiveGraphControl -var active_animation_graph_node:BLTAnimationGraph = null +var animation_graph:BLTAnimationGraph = null var animation_graph_root_node:BLTAnimationNode = null var graph_node_stack:Array[BLTAnimationNode] = [] var active_graph_edit:Control = null var active_graph_edit_index = -1 + func reset_graph_control(): for child in active_graph_control.get_children(): active_graph_control.remove_child(child) @@ -19,6 +20,7 @@ func reset_graph_control(): func edit_animation_root_node(blt_node:BLTAnimationNode): + print("Setting root node") graph_node_stack = [] active_graph_edit_index = -1 truncate_graph_stack(0) @@ -31,6 +33,8 @@ func edit_animation_root_node(blt_node:BLTAnimationNode): edit_graph(blt_node) return + assert(is_instance_valid(animation_graph)) + push_warning("Cannot edit node %s. Graph type %s not yet supported." % [blt_node.resource_name, blt_node.get_class()]) diff --git a/demo/addons/blendalot/blend_tree_editor.gd b/demo/addons/blendalot/blend_tree_editor.gd index 32b6f69..e1f1b34 100644 --- a/demo/addons/blendalot/blend_tree_editor.gd +++ b/demo/addons/blendalot/blend_tree_editor.gd @@ -105,12 +105,25 @@ func create_graph_node_for_blt_node(blt_node: BLTAnimationNode) -> GraphNode: result_graph_node.add_child(slot_label) result_graph_node.set_slot(i + result_slot_offset, true, 1, Color.WHITE, false, 1, Color.BLACK) + if blt_node.get_class() == "BLTAnimationNodeSampler": + var animation_sampler_node:BLTAnimationNodeSampler = blt_node as BLTAnimationNodeSampler + var animation_selector_button = OptionButton.new() + var animation_player:AnimationPlayer = animation_sampler_node.get_animation_player() + for animation_name in animation_player.get_animation_list(): + animation_selector_button.add_item(animation_name) + if animation_name == animation_sampler_node.animation: + animation_selector_button.select(animation_selector_button.item_count - 1) + + animation_selector_button.item_selected.connect(_on_animation_select.bind(animation_sampler_node, animation_selector_button)) + + result_graph_node.add_child(animation_selector_button) + blt_node.node_changed.connect(_trigger_graph_changed) return result_graph_node -func _trigger_graph_changed(): +func _trigger_graph_changed(_node_name): graph_changed.emit() @@ -253,3 +266,10 @@ func _on_node_double_click(graph_node:GraphNode): if blend_tree_node is BLTAnimationNodeBlendTree: edit_subgraph.emit(blend_tree_node) + +# +# Animation selection for BltAnimationNodeSampler +# +func _on_animation_select(index:int, blt_node_sampler:BLTAnimationNodeSampler, option_button:OptionButton): + blt_node_sampler.animation = option_button.get_item_text(index) + blt_node_sampler.node_changed.emit(blt_node_sampler.resource_name) -- 2.47.2 From c7660c7b192893cb7f65de46f9ba497497cde55e Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Fri, 20 Feb 2026 22:55:30 +0100 Subject: [PATCH 25/29] Replace SyncedAnimationGraph to BLTAnimationGraph. --- blendalot_animation_graph.cpp | 6 +++--- tests/test_sync_track.h | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/blendalot_animation_graph.cpp b/blendalot_animation_graph.cpp index 56d6109..2c3d54b 100644 --- a/blendalot_animation_graph.cpp +++ b/blendalot_animation_graph.cpp @@ -146,7 +146,7 @@ void BLTAnimationGraph::_graph_changed(const StringName &node_name) { } void BLTAnimationGraph::_notification(int p_what) { - GodotProfileZone("SyncedAnimationGraph::_notification"); + GodotProfileZone("BLTAnimationGraph::_notification"); switch (p_what) { case NOTIFICATION_ENTER_TREE: { @@ -295,7 +295,7 @@ void BLTAnimationGraph::_process_graph(double p_delta, bool p_update_only) { return; } - GodotProfileZone("SyncedAnimationGraph::_process_graph"); + GodotProfileZone("BLTAnimationGraph::_process_graph"); _update_properties(); @@ -311,7 +311,7 @@ void BLTAnimationGraph::_process_graph(double p_delta, bool p_update_only) { } void BLTAnimationGraph::_apply_animation_data(const AnimationData &output_data) const { - GodotProfileZone("SyncedAnimationGraph::_apply_animation_data"); + GodotProfileZone("BLTAnimationGraph::_apply_animation_data"); for (const KeyValue &K : output_data.value_buffer_offset) { const AnimationData::TrackValue *track_value = output_data.get_value(K.key); diff --git a/tests/test_sync_track.h b/tests/test_sync_track.h index a2fd1f7..63616b2 100644 --- a/tests/test_sync_track.h +++ b/tests/test_sync_track.h @@ -6,7 +6,7 @@ namespace TestBlendalotAnimationGraph { -TEST_CASE("[SyncedAnimationGraph][SyncTrack] Basic") { +TEST_CASE("[Blendalot][SyncTrack] Basic") { SyncTrack track_a; track_a.num_intervals = 2; track_a.duration = 2.0; @@ -84,7 +84,7 @@ TEST_CASE("[SyncedAnimationGraph][SyncTrack] Basic") { } } -TEST_CASE("[SyncedAnimationGraph][SyncTrack] Create Sync Track from markers") { +TEST_CASE("[Blendalot][SyncTrack] Create Sync Track from markers") { SyncTrack track = SyncTrack::create_from_markers(2.0f, { 0.9f, 0.2f }); WHEN("Querying Ratios") { @@ -138,7 +138,7 @@ TEST_CASE("[SyncedAnimationGraph][SyncTrack] Create Sync Track from markers") { } } -TEST_CASE("[SyncedAnimationGraph][SyncTrack] Sync Track blending") { +TEST_CASE("[Blendalot][SyncTrack] Sync Track blending") { SyncTrack track_a = SyncTrack::create_from_markers(2.0, { 0., 0.6, 1.8 }); SyncTrack track_b = SyncTrack::create_from_markers(1.5f, { 1.05, 1.35, 0.3 }); -- 2.47.2 From 3dd1ce42df7165d5981873ec50a594d476b82586 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Fri, 20 Feb 2026 23:23:06 +0100 Subject: [PATCH 26/29] Added initial support for blending of SyncTracks with differing numbers of intervals. Not sure that the resulting blends are correct, but leave it for now. --- blendalot_animation_node.cpp | 5 ++++ blendalot_math_helper.h | 22 ++++++++++++++++++ sync_track.h | 36 +++++++++++++++++++++-------- tests/test_sync_track.h | 45 +++++++++++++++++++++++++++++++++++- 4 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 blendalot_math_helper.h diff --git a/blendalot_animation_node.cpp b/blendalot_animation_node.cpp index f3751c0..2a3c60b 100644 --- a/blendalot_animation_node.cpp +++ b/blendalot_animation_node.cpp @@ -385,6 +385,10 @@ StringName BLTAnimationNodeSampler::get_animation() const { return animation_name; } +AnimationPlayer *BLTAnimationNodeSampler::get_animation_player() const { + return animation_player; +} + TypedArray BLTAnimationNodeSampler::get_animations_as_typed_array() const { TypedArray typed_arr; @@ -417,6 +421,7 @@ TypedArray BLTAnimationNodeSampler::get_animations_as_typed_array() void BLTAnimationNodeSampler::_bind_methods() { ClassDB::bind_method(D_METHOD("set_animation", "name"), &BLTAnimationNodeSampler::set_animation); ClassDB::bind_method(D_METHOD("get_animation"), &BLTAnimationNodeSampler::get_animation); + ClassDB::bind_method(D_METHOD("get_animation_player"), &BLTAnimationNodeSampler::get_animation_player); ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "animation"), "set_animation", "get_animation"); diff --git a/blendalot_math_helper.h b/blendalot_math_helper.h new file mode 100644 index 0000000..68c1de0 --- /dev/null +++ b/blendalot_math_helper.h @@ -0,0 +1,22 @@ +// +// Created by martin on 20.02.26. +// + +#ifndef MASTER_BLENDALOT_MATH_HELPER_H +#define MASTER_BLENDALOT_MATH_HELPER_H + +inline int greatest_common_divisor(int a, int b) { + while (b != 0) { + int temp = b; + b = a % b; + a = temp; + } + + return a; +} + +inline int least_common_multiple(int a, int b) { + return (a / greatest_common_divisor(a, b)) * b; +} + +#endif //MASTER_BLENDALOT_MATH_HELPER_H diff --git a/sync_track.h b/sync_track.h index 7a1f3f2..2cdb2e0 100644 --- a/sync_track.h +++ b/sync_track.h @@ -2,6 +2,7 @@ #include "core/templates/local_vector.h" +#include "blendalot_math_helper.h" #include #include @@ -21,7 +22,7 @@ * duration. Blended SyncTracks always have their first interval start at t = 0.0s. */ struct SyncTrack { - static constexpr int cSyncTrackMaxIntervals = 8; + static constexpr int cSyncTrackMaxIntervals = 32; SyncTrack() : duration(0.f), num_intervals(1) { @@ -59,6 +60,12 @@ struct SyncTrack { } double calc_ratio_from_sync_time(double sync_time) const { + // When blending SyncTracks with differing numbers of intervals the resulting SyncTrack may have + // additional repeats of the animation (=> "virtual sync periods", https://youtu.be/Jkv0pbp0ckQ?t=8178). + // + // Therefore, we first have to transform it back to the numbers of intervals we actually have. + sync_time = fmod(sync_time, num_intervals); + float interval_ratio = fmod(sync_time, 1.0f); int interval = int(sync_time - interval_ratio); @@ -126,19 +133,32 @@ struct SyncTrack { */ static SyncTrack blend(float weight, const SyncTrack &track_A, const SyncTrack &track_B) { - assert(track_A.num_intervals == track_B.num_intervals); + if (Math::is_zero_approx(weight)) { + return track_A; + } + + if (Math::is_zero_approx(1.0 - weight)) { + return track_B; + } SyncTrack result; - result.num_intervals = track_A.num_intervals; - result.duration = - (1.0f - weight) * track_A.duration + weight * track_B.duration; + if (track_A.num_intervals != track_B.num_intervals) { + result.num_intervals = least_common_multiple(track_A.num_intervals, track_B.num_intervals); + } else { + result.num_intervals = track_A.num_intervals; + } + assert(result.num_intervals < cSyncTrackMaxIntervals); + float track_A_repeats = static_cast(result.num_intervals / track_A.num_intervals); + float track_B_repeats = static_cast(result.num_intervals / track_B.num_intervals); + + result.duration = (1.0f - weight) * (track_A.duration * track_A_repeats) + weight * (track_B.duration * track_B_repeats); result.interval_start_ratio[0] = 0.f; for (int i = 0; i < result.num_intervals; i++) { - float interval_duration_A = track_A.interval_duration_ratio[i]; - float interval_duration_B = track_B.interval_duration_ratio[i]; + float interval_duration_A = track_A.interval_duration_ratio[i % track_A.num_intervals] / track_A_repeats; + float interval_duration_B = track_B.interval_duration_ratio[i % track_B.num_intervals] / track_B_repeats; result.interval_duration_ratio[i] = (1.0f - weight) * interval_duration_A + weight * interval_duration_B; @@ -152,8 +172,6 @@ struct SyncTrack { } } - assert(result.num_intervals < cSyncTrackMaxIntervals); - return result; } }; \ No newline at end of file diff --git a/tests/test_sync_track.h b/tests/test_sync_track.h index 63616b2..4dea640 100644 --- a/tests/test_sync_track.h +++ b/tests/test_sync_track.h @@ -203,4 +203,47 @@ TEST_CASE("[Blendalot][SyncTrack] Sync Track blending") { } } -} //namespace TestSyncedAnimationGraph \ No newline at end of file +TEST_CASE("[Blendalot][SyncTrack] Sync Track blending non-matching interval count") { + SyncTrack track_a = SyncTrack::create_from_markers(2.0, { 0., 0.6, 1.8 }); + SyncTrack track_b = SyncTrack::create_from_markers(1.5f, { 1.05 }); + + WHEN("Blending two synctracks with weight 0.") { + SyncTrack blended = SyncTrack::blend(0.f, track_a, track_b); + + blended.duration = track_a.duration; + blended.interval_start_ratio[0] = 0.0; + for (int i = 0; i < track_a.num_intervals; i++) { + CHECK(blended.interval_duration_ratio[i] == track_a.interval_duration_ratio[i]); + } + } + WHEN("Blending two synctracks with weight 1.") { + SyncTrack blended = SyncTrack::blend(1.f, track_a, track_b); + + blended.duration = track_b.duration; + blended.interval_start_ratio[0] = 0.0; + for (int i = 0; i < track_b.num_intervals; i++) { + CHECK(blended.interval_duration_ratio[i] == track_b.interval_duration_ratio[i]); + } + } + + WHEN("Blending with weight 0.2") { + float weight = 0.2f; + SyncTrack blended = SyncTrack::blend(weight, track_a, track_b); + + float track_a_repeats = static_cast(blended.num_intervals / track_a.num_intervals); + float track_b_repeats = static_cast(blended.num_intervals / track_b.num_intervals); + + CHECK( + blended.duration == doctest::Approx(2.5)); + CHECK( + blended.interval_start_ratio[0] == 0.0); + CHECK( + blended.interval_duration_ratio[0] == doctest::Approx((1.0 - weight) * track_a.interval_duration_ratio[0] / track_a_repeats + weight * track_b.interval_duration_ratio[0] / track_b_repeats)); + CHECK( + blended.interval_duration_ratio[1] == doctest::Approx((1.0 - weight) * track_a.interval_duration_ratio[1] / track_a_repeats + weight * track_b.interval_duration_ratio[0] / track_b_repeats)); + CHECK( + blended.interval_duration_ratio[2] == doctest::Approx((1.0 - weight) * track_a.interval_duration_ratio[2] / track_a_repeats + weight * track_b.interval_duration_ratio[0] / track_b_repeats)); + } +} + +} //namespace TestBlendalotAnimationGraph \ No newline at end of file -- 2.47.2 From d57fe50d5fc00e2a96ead41feb14c0e566633bce Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Sun, 22 Feb 2026 00:56:53 +0100 Subject: [PATCH 27/29] Added BLTAnimationNodeTimeScale. --- blendalot_animation_node.cpp | 272 ++++++++++++++++++++--------------- blendalot_animation_node.h | 50 ++++++- register_types.cpp | 1 + 3 files changed, 201 insertions(+), 122 deletions(-) diff --git a/blendalot_animation_node.cpp b/blendalot_animation_node.cpp index 2a3c60b..3cd933c 100644 --- a/blendalot_animation_node.cpp +++ b/blendalot_animation_node.cpp @@ -49,125 +49,6 @@ void BLTAnimationNode::_animation_node_removed(const ObjectID &p_oid, const Stri emit_signal(SNAME("animation_node_removed"), p_oid, p_node); } -void BLTAnimationNodeBlendTree::_bind_methods() { - ClassDB::bind_method(D_METHOD("add_node", "animation_node"), &BLTAnimationNodeBlendTree::add_node); - ClassDB::bind_method(D_METHOD("remove_node", "animation_node"), &BLTAnimationNodeBlendTree::remove_node); - ClassDB::bind_method(D_METHOD("get_node", "node_name"), &BLTAnimationNodeBlendTree::get_node); - ClassDB::bind_method(D_METHOD("get_output_node"), &BLTAnimationNodeBlendTree::get_output_node); - ClassDB::bind_method(D_METHOD("get_node_names"), &BLTAnimationNodeBlendTree::get_node_names_as_typed_array); - - ClassDB::bind_method(D_METHOD("is_connection_valid", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::is_connection_valid); - ClassDB::bind_method(D_METHOD("add_connection", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::add_connection); - ClassDB::bind_method(D_METHOD("remove_connection", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::remove_connection); - ClassDB::bind_method(D_METHOD("get_connections"), &BLTAnimationNodeBlendTree::get_connections_as_array); - - ClassDB::bind_method(D_METHOD("set_graph_offset", "graph_offset"), &BLTAnimationNodeBlendTree::set_graph_offset); - ClassDB::bind_method(D_METHOD("get_graph_offset"), &BLTAnimationNodeBlendTree::get_graph_offset); - ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "graph_offset", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_graph_offset", "get_graph_offset"); - - BIND_CONSTANT(CONNECTION_OK); - BIND_CONSTANT(CONNECTION_ERROR_GRAPH_ALREADY_INITIALIZED); - BIND_CONSTANT(CONNECTION_ERROR_NO_SOURCE_NODE); - BIND_CONSTANT(CONNECTION_ERROR_NO_TARGET_NODE); - BIND_CONSTANT(CONNECTION_ERROR_PARENT_EXISTS); - BIND_CONSTANT(CONNECTION_ERROR_TARGET_PORT_NOT_FOUND); - BIND_CONSTANT(CONNECTION_ERROR_TARGET_PORT_ALREADY_CONNECTED); - BIND_CONSTANT(CONNECTION_ERROR_CONNECTION_CREATES_LOOP); -} - -void BLTAnimationNodeBlendTree::_get_property_list(List *p_list) const { - for (const Ref &node : tree_graph.nodes) { - String prop_name = node->get_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 + "/graph_offset", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); - } - - p_list->push_back(PropertyInfo(Variant::ARRAY, "node_connections", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); -} - -bool BLTAnimationNodeBlendTree::_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 = tree_graph.nodes[node_index]; - return true; - } - } - - if (what == "graph_offset") { - if (node_index != -1) { - r_value = tree_graph.nodes[node_index]->position; - return true; - } - } - } else if (prop_name == "node_connections") { - Array conns; - conns.resize(tree_graph.connections.size() * 3); - - int idx = 0; - for (const BLTBlendTreeConnection &connection : tree_graph.connections) { - conns[idx * 3 + 0] = connection.target_node->get_name(); - conns[idx * 3 + 1] = connection.target_node->get_input_index(connection.target_port_name); - conns[idx * 3 + 2] = connection.source_node->get_name(); - idx++; - } - - r_value = conns; - return true; - } - - return false; -} - -bool BLTAnimationNodeBlendTree::_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 anode = p_value; - if (anode.is_valid()) { - anode->set_name(node_name); - add_node(anode); - } - return true; - } - - if (what == "graph_offset") { - int node_index = find_node_index_by_name(node_name); - if (node_index > -1) { - tree_graph.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 target_node = tree_graph.nodes[target_node_index]; - Vector target_input_names = target_node->get_input_names(); - - add_connection(tree_graph.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, const Skeleton3D *skeleton_3d, double p_time) { GodotProfileZone("AnimationData::sample_from_animation"); @@ -273,6 +154,9 @@ void AnimationDataAllocator::register_track_values(const Ref &animati default_data.allocate_track_values(animation, skeleton_3d); } +// +// BLTAnimationNodeSampler +// bool BLTAnimationNodeSampler::initialize(GraphEvaluationContext &context) { if (!BLTAnimationNode::initialize(context)) { return false; @@ -428,6 +312,34 @@ void BLTAnimationNodeSampler::_bind_methods() { ClassDB::bind_method(D_METHOD("get_animations"), &BLTAnimationNodeSampler::get_animations_as_typed_array); } +// +// BLTAnimationNodeTimeScale +// +void BLTAnimationNodeTimeScale::_get_property_list(List *p_list) const { + p_list->push_back(PropertyInfo(Variant::FLOAT, scale_name, PROPERTY_HINT_RANGE, "-10,10,0.01,or_less,or_greater")); +} + +bool BLTAnimationNodeTimeScale::_get(const StringName &p_name, Variant &r_value) const { + if (p_name == scale_name) { + r_value = scale; + return true; + } + + return false; +} + +bool BLTAnimationNodeTimeScale::_set(const StringName &p_name, const Variant &p_value) { + if (p_name == scale_name) { + scale = p_value; + return true; + } + + return false; +} + +// +// BLTAnimationNodeBlend2 +// void BLTAnimationNodeBlend2::evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) { GodotProfileZone("AnimationBlend2Node::evaluate"); @@ -494,6 +406,9 @@ bool BLTAnimationNodeBlend2::_set(const StringName &p_name, const Variant &p_val return false; } +// +// BLTAnimationNodeBlendTree +// BLTAnimationNodeBlendTree::BLTBlendTreeGraph::BLTBlendTreeGraph() { Ref output_node; output_node.instantiate(); @@ -761,3 +676,122 @@ BLTAnimationNodeBlendTree::ConnectionError BLTAnimationNodeBlendTree::BLTBlendTr return CONNECTION_OK; } + +void BLTAnimationNodeBlendTree::_bind_methods() { + ClassDB::bind_method(D_METHOD("add_node", "animation_node"), &BLTAnimationNodeBlendTree::add_node); + ClassDB::bind_method(D_METHOD("remove_node", "animation_node"), &BLTAnimationNodeBlendTree::remove_node); + ClassDB::bind_method(D_METHOD("get_node", "node_name"), &BLTAnimationNodeBlendTree::get_node); + ClassDB::bind_method(D_METHOD("get_output_node"), &BLTAnimationNodeBlendTree::get_output_node); + ClassDB::bind_method(D_METHOD("get_node_names"), &BLTAnimationNodeBlendTree::get_node_names_as_typed_array); + + ClassDB::bind_method(D_METHOD("is_connection_valid", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::is_connection_valid); + ClassDB::bind_method(D_METHOD("add_connection", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::add_connection); + ClassDB::bind_method(D_METHOD("remove_connection", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::remove_connection); + ClassDB::bind_method(D_METHOD("get_connections"), &BLTAnimationNodeBlendTree::get_connections_as_array); + + ClassDB::bind_method(D_METHOD("set_graph_offset", "graph_offset"), &BLTAnimationNodeBlendTree::set_graph_offset); + ClassDB::bind_method(D_METHOD("get_graph_offset"), &BLTAnimationNodeBlendTree::get_graph_offset); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "graph_offset", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_graph_offset", "get_graph_offset"); + + BIND_CONSTANT(CONNECTION_OK); + BIND_CONSTANT(CONNECTION_ERROR_GRAPH_ALREADY_INITIALIZED); + BIND_CONSTANT(CONNECTION_ERROR_NO_SOURCE_NODE); + BIND_CONSTANT(CONNECTION_ERROR_NO_TARGET_NODE); + BIND_CONSTANT(CONNECTION_ERROR_PARENT_EXISTS); + BIND_CONSTANT(CONNECTION_ERROR_TARGET_PORT_NOT_FOUND); + BIND_CONSTANT(CONNECTION_ERROR_TARGET_PORT_ALREADY_CONNECTED); + BIND_CONSTANT(CONNECTION_ERROR_CONNECTION_CREATES_LOOP); +} + +void BLTAnimationNodeBlendTree::_get_property_list(List *p_list) const { + for (const Ref &node : tree_graph.nodes) { + String prop_name = node->get_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 + "/graph_offset", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); + } + + p_list->push_back(PropertyInfo(Variant::ARRAY, "node_connections", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); +} + +bool BLTAnimationNodeBlendTree::_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 = tree_graph.nodes[node_index]; + return true; + } + } + + if (what == "graph_offset") { + if (node_index != -1) { + r_value = tree_graph.nodes[node_index]->position; + return true; + } + } + } else if (prop_name == "node_connections") { + Array conns; + conns.resize(tree_graph.connections.size() * 3); + + int idx = 0; + for (const BLTBlendTreeConnection &connection : tree_graph.connections) { + conns[idx * 3 + 0] = connection.target_node->get_name(); + conns[idx * 3 + 1] = connection.target_node->get_input_index(connection.target_port_name); + conns[idx * 3 + 2] = connection.source_node->get_name(); + idx++; + } + + r_value = conns; + return true; + } + + return false; +} + +bool BLTAnimationNodeBlendTree::_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 anode = p_value; + if (anode.is_valid()) { + anode->set_name(node_name); + add_node(anode); + } + return true; + } + + if (what == "graph_offset") { + int node_index = find_node_index_by_name(node_name); + if (node_index > -1) { + tree_graph.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 target_node = tree_graph.nodes[target_node_index]; + Vector target_input_names = target_node->get_input_names(); + + add_connection(tree_graph.nodes[source_node_index], target_node, target_input_names[target_node_port_index]); + } + return true; + } + + return false; +} \ No newline at end of file diff --git a/blendalot_animation_node.h b/blendalot_animation_node.h index 9c97a82..04d5310 100644 --- a/blendalot_animation_node.h +++ b/blendalot_animation_node.h @@ -105,7 +105,8 @@ struct AnimationData { buffer = other.buffer; } AnimationData(AnimationData &&other) noexcept : - value_buffer_offset(std::exchange(other.value_buffer_offset, AHashMap())), + // We skip copying the offset as that should be identical for all nodes within a BLTAnimationGraph. + // value_buffer_offset(std::exchange(other.value_buffer_offset, AHashMap())), buffer(std::exchange(other.buffer, LocalVector())) { } AnimationData &operator=(const AnimationData &other) { @@ -301,9 +302,10 @@ public: } virtual void evaluate(GraphEvaluationContext &context, const LocalVector &input_datas, AnimationData &output_data) { + GodotProfileZone("AnimationNode::evaluate"); // By default, use the AnimationData of the first input. if (input_datas.size() > 0) { - output_data = *input_datas[0]; + output_data = std::move(*input_datas[0]); } } @@ -365,6 +367,47 @@ protected: static void _bind_methods(); }; +class BLTAnimationNodeTimeScale : public BLTAnimationNode { + GDCLASS(BLTAnimationNodeTimeScale, BLTAnimationNode); + +public: + float scale = 1.0f; + +private: + Ref animation; + + Vector get_input_names() const override { + return { "Input" }; + } + + bool initialize(GraphEvaluationContext &context) override { + node_time_info = {}; + // TODO: it should not be necessary to force looping here. node_time_info.loop_mode = Animation::LOOP_LINEAR; + return true; + } + void calculate_sync_track(const Vector> &input_nodes) override { + if (node_time_info.is_synced) { + node_time_info.sync_track = input_nodes[0]->node_time_info.sync_track; + node_time_info.sync_track.duration *= scale; + } + } + void update_time(double p_time) override { + if (node_time_info.is_synced) { + return; + } + + BLTAnimationNode::update_time(p_time * scale); + } + +protected: + void _get_property_list(List *p_list) const; + bool _get(const StringName &p_name, Variant &r_value) const; + bool _set(const StringName &p_name, const Variant &p_value); + +private: + StringName scale_name = PNAME("scale"); +}; + class BLTAnimationNodeOutput : public BLTAnimationNode { GDCLASS(BLTAnimationNodeOutput, BLTAnimationNode); @@ -413,7 +456,8 @@ public: void calculate_sync_track(const Vector> &input_nodes) override { if (node_time_info.is_synced || sync) { - assert(input_nodes[0]->node_time_info.loop_mode == input_nodes[1]->node_time_info.loop_mode); + // TODO: figure out whether we need to enforce looping mode when syncing is enabled. + // assert(input_nodes[0]->node_time_info.loop_mode == input_nodes[1]->node_time_info.loop_mode); node_time_info.sync_track = SyncTrack::blend(blend_weight, input_nodes[0]->node_time_info.sync_track, input_nodes[1]->node_time_info.sync_track); } } diff --git a/register_types.cpp b/register_types.cpp index e33bf83..730d3c2 100644 --- a/register_types.cpp +++ b/register_types.cpp @@ -12,6 +12,7 @@ void initialize_blendalot_animgraph_module(ModuleInitializationLevel p_level) { ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_class(); ClassDB::register_class(); } -- 2.47.2 From 4a2ef2493da480db4345e1b7d2cc94588f7a7d15 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Mon, 23 Feb 2026 21:25:13 +0100 Subject: [PATCH 28/29] Fixed incomplete comment. --- tests/test_blendalot_animgraph.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_blendalot_animgraph.h b/tests/test_blendalot_animgraph.h index 85231a8..a06a52b 100644 --- a/tests/test_blendalot_animgraph.h +++ b/tests/test_blendalot_animgraph.h @@ -520,7 +520,7 @@ TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTreeGraph][Chan CHECK(subgraph_blend2a_initial == blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_a)].input_subtree_node_indices); CHECK(subgraph_blend2b_initial == blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_b)].input_subtree_node_indices); - // Check that we also do not + // Check that the connection is not present anymore. for (const BLTBlendTreeConnection &connection : blend_tree_graph.connections) { bool connection_equals_removed_connection = connection.source_node == blend2_node_b && connection.target_node == blend2_node_a && connection.target_port_name == "Input1"; CHECK(connection_equals_removed_connection == false); -- 2.47.2 From 20331d0765d07b32208bb421fcc3052fb4411c60 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Wed, 25 Feb 2026 18:33:44 +0100 Subject: [PATCH 29/29] Properly deactivate nodes to ensure proper evaluation after changed node connections. --- blendalot_animation_node.h | 20 +++++++++++--- tests/test_blendalot_animgraph.h | 46 ++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/blendalot_animation_node.h b/blendalot_animation_node.h index 04d5310..3672d3f 100644 --- a/blendalot_animation_node.h +++ b/blendalot_animation_node.h @@ -767,14 +767,23 @@ public: sort_nodes(); setup_runtime_data(); - for (const Ref &node : tree_graph.nodes) { + const HashSet &output_subtree = tree_graph.node_connection_info[0].input_subtree_node_indices; + + for (int i = 0; i < tree_graph.nodes.size(); i++) { + const Ref &node = tree_graph.nodes[i]; + + // Initialize, but skip validation of nodes that are not part of the active tree. + if (!output_subtree.has(i)) { + node->initialize(context); + continue; + } + if (!node->initialize(context)) { return false; } - } - // All inputs must have a connected node. - for (const NodeRuntimeData &node_runtime_data : _node_runtime_data) { + const NodeRuntimeData &node_runtime_data = _node_runtime_data[i]; + for (const Ref &input_node : node_runtime_data.input_nodes) { if (!input_node.is_valid()) { return false; @@ -887,6 +896,9 @@ public: for (const int child_index : tree_graph.node_connection_info[i].connected_child_node_index_at_port) { context.animation_data_allocator.free(_node_runtime_data[child_index].output_data); } + + // Node must be deactivated. It'll be activated when actually used next time. + node->active = false; } } diff --git a/tests/test_blendalot_animgraph.h b/tests/test_blendalot_animgraph.h index a06a52b..e78377d 100644 --- a/tests/test_blendalot_animgraph.h +++ b/tests/test_blendalot_animgraph.h @@ -15,6 +15,7 @@ struct BlendTreeFixture { Ref test_animation_a; Ref test_animation_b; + Ref test_animation_c; Ref test_animation_sync_a; Ref test_animation_sync_b; @@ -80,6 +81,16 @@ struct BlendTreeFixture { animation_library->add_animation("TestAnimationB", test_animation_b); + test_animation_c = memnew(Animation); + track_index = test_animation_c->add_track(Animation::TYPE_POSITION_3D); + CHECK(track_index == 0); + test_animation_c->track_insert_key(track_index, 0.0, Vector3(0., 0., 0.)); + test_animation_c->track_insert_key(track_index, 3.0, Vector3(2., 4., 6.)); + test_animation_c->track_set_path(track_index, NodePath(vformat("%s:%s", skeleton_node->get_path().get_concatenated_names(), "Hips"))); + test_animation_c->set_loop_mode(Animation::LOOP_LINEAR); + + animation_library->add_animation("TestAnimationC", test_animation_c); + test_animation_sync_a = memnew(Animation); track_index = test_animation_sync_a->add_track(Animation::TYPE_POSITION_3D); CHECK(track_index == 0); @@ -597,6 +608,41 @@ TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTreeGraph][Chan CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(animation_sampler_node_c_index_post_remove)); } } + + SUBCASE("Check evaluation of graph with modified connections") { + Ref blend_tree_node; + blend_tree_node.instantiate(); + blend_tree_node->add_node(animation_sampler_node_a); + blend_tree_node->add_node(animation_sampler_node_b); + blend_tree_node->add_node(animation_sampler_node_c); + blend_tree_node->add_node(blend2_node_a); + + animation_graph->set_root_animation_node(blend_tree_node); + GraphEvaluationContext &graph_context = animation_graph->get_context(); + CHECK(blend_tree_node->initialize(graph_context) == false); + + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_node->add_connection(animation_sampler_node_a, blend_tree_node->get_output_node(), "Output")); + CHECK(blend_tree_node->initialize(graph_context) == true); + + AnimationData *graph_output = graph_context.animation_data_allocator.allocate(); + blend_tree_node->activate_inputs(Vector>()); + blend_tree_node->calculate_sync_track(Vector>()); + blend_tree_node->update_time(0.825); + blend_tree_node->evaluate(graph_context, LocalVector(), *graph_output); + + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_node->add_connection(animation_sampler_node_b, blend2_node_a, "Input0")); + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_node->add_connection(animation_sampler_node_c, blend2_node_a, "Input1")); + + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_node->remove_connection(animation_sampler_node_a, blend_tree_node->get_output_node(), "Output")); + + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_node->add_connection(blend2_node_a, blend_tree_node->get_output_node(), "Output")); + CHECK(blend_tree_node->initialize(graph_context) == true); + + blend_tree_node->activate_inputs(Vector>()); + blend_tree_node->calculate_sync_track(Vector>()); + blend_tree_node->update_time(0.825); + blend_tree_node->evaluate(graph_context, LocalVector(), *graph_output); + } } TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTreeGraph][EmbeddedBlendTree] BlendTree with an embedded BlendTree subgraph") { -- 2.47.2