Compare commits

...

5 Commits

Author SHA1 Message Date
Martin Felis
095f1e5d0c Animations now selectable using OptionButton. 2026-02-20 13:40:12 +01:00
Martin Felis
0198847fd1 Disconnection works again in blend tree editor. 2026-02-20 12:34:49 +01:00
Martin Felis
a098bc1171 Added test for synced blending of embedded blend tree. 2026-02-19 19:05:14 +01:00
Martin Felis
d01c6fb474 Minor editor fixes. 2026-02-19 19:03:56 +01:00
Martin Felis
06198d595f Added test for embedded blend tree evaluation. 2026-02-18 22:13:51 +01:00
7 changed files with 163 additions and 11 deletions

View File

@ -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;
@ -351,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<StringName> get_animations_as_typed_array() const;
@ -753,6 +753,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<BLTAnimationNode> &node = tree_graph.nodes[i];
@ -777,14 +779,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<BLTAnimationNode> &node = tree_graph.nodes[i];

View File

@ -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)
@ -29,6 +31,9 @@ func edit_animation_root_node(blt_node:BLTAnimationNode):
animation_graph_root_node = blt_node
push_graph_stack(blt_node)
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()])

View File

@ -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()
@ -154,6 +167,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)
@ -198,6 +220,7 @@ func _on_blend_tree_graph_edit_node_selected(graph_node: Node) -> void:
func _on_blend_tree_graph_edit_scroll_offset_changed(offset: Vector2) -> void:
if is_instance_valid(blend_tree):
blend_tree.graph_offset = offset
@ -243,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)

View File

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

View File

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

View File

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

View File

@ -599,4 +599,87 @@ 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<BLTAnimationNodeBlendTree> embedded_blend_tree;
embedded_blend_tree.instantiate();
// TestAnimationB
Ref<BLTAnimationNodeSampler> animation_sampler_node_b;
animation_sampler_node_b.instantiate();
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<BLTAnimationNodeBlendTree> blend_tree;
blend_tree.instantiate();
// Blend2
Ref<BLTAnimationNodeBlend2> blend2;
blend2.instantiate();
blend2->set_name("Blend2");
blend_tree->add_node(blend2);
// TestAnimationA
Ref<BLTAnimationNodeSampler> animation_sampler_node_a;
animation_sampler_node_a.instantiate();
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");
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;
// 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<Ref<BLTAnimationNode>>());
blend_tree->calculate_sync_track(Vector<Ref<BLTAnimationNode>>());
blend_tree->update_time(0.1);
blend_tree->evaluate(graph_context, LocalVector<AnimationData *>(), *graph_output);
// Check values
AnimationData::TransformTrackValue *hip_transform_value = graph_output->get_value<AnimationData::TransformTrackValue>(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<Ref<BLTAnimationNode>>());
blend_tree->calculate_sync_track(Vector<Ref<BLTAnimationNode>>());
blend_tree->update_time(0.825);
blend_tree->evaluate(graph_context, LocalVector<AnimationData *>(), *graph_output);
// Check values
AnimationData::TransformTrackValue *hip_transform_value = graph_output->get_value<AnimationData::TransformTrackValue>(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