From 46f940a67c9e0751cecad83abfa8bf6fa182d2ab Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Sat, 27 Dec 2025 16:27:54 +0100 Subject: [PATCH] Added Blend2 node (no syncing, yet). --- synced_animation_graph.cpp | 2 +- synced_animation_node.cpp | 6 +- synced_animation_node.h | 47 +++++++----- tests/test_synced_animation_graph.h | 109 ++++++++++++++++++++++++---- 4 files changed, 128 insertions(+), 36 deletions(-) diff --git a/synced_animation_graph.cpp b/synced_animation_graph.cpp index e9f8f6f..be4dc6f 100644 --- a/synced_animation_graph.cpp +++ b/synced_animation_graph.cpp @@ -171,7 +171,7 @@ void SyncedAnimationGraph::_process_graph(double p_delta, bool p_update_only) { graph_root_node->activate_inputs(Vector>()); graph_root_node->calculate_sync_track(Vector>()); graph_root_node->update_time(p_delta); - graph_root_node->evaluate(graph_context, Vector(), graph_output); + graph_root_node->evaluate(graph_context, LocalVector(), graph_output); _apply_animation_data(graph_output); } diff --git a/synced_animation_node.cpp b/synced_animation_node.cpp index 4cd6bbd..2b005e3 100644 --- a/synced_animation_node.cpp +++ b/synced_animation_node.cpp @@ -71,12 +71,14 @@ void AnimationSamplerNode::initialize(GraphEvaluationContext &context) { node_time_info.loop_mode = Animation::LOOP_LINEAR; } -void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const Vector &inputs, AnimationData &output) { +void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) { assert(inputs.size() == 0); output.clear(); output.sample_from_animation(animation, context.skeleton_3d, node_time_info.position); } -void AnimationBlend2Node::evaluate(GraphEvaluationContext &context, const Vector &inputs, AnimationData &output) { +void AnimationBlend2Node::evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) { + output = *inputs[0]; + output.blend(*inputs[1], blend_weight); } \ No newline at end of file diff --git a/synced_animation_node.h b/synced_animation_node.h index ceaf820..fbfe887 100644 --- a/synced_animation_node.h +++ b/synced_animation_node.h @@ -72,6 +72,7 @@ struct AnimationData { TrackValue *clone() const override { PositionTrackValue *result = memnew(PositionTrackValue); + result->track = track; result->bone_idx = bone_idx; result->position = position; return result; @@ -100,6 +101,7 @@ struct AnimationData { TrackValue *clone() const override { RotationTrackValue *result = memnew(RotationTrackValue); + result->track = track; result->bone_idx = bone_idx; result->rotation = rotation; return result; @@ -135,7 +137,7 @@ struct AnimationData { } void - set_value(const Animation::TypeHash& thash, TrackValue *value) { + set_value(const Animation::TypeHash &thash, TrackValue *value) { if (!track_values.has(thash)) { track_values.insert(thash, value); } else { @@ -262,7 +264,7 @@ public: } } } - virtual void evaluate(GraphEvaluationContext &context, const Vector &input_datas, AnimationData &output_data) { + 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) { output_data = *input_datas[0]; @@ -294,7 +296,7 @@ private: Ref animation; void initialize(GraphEvaluationContext &context) override; - void evaluate(GraphEvaluationContext &context, const Vector &inputs, AnimationData &output) override; + void evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) override; }; class OutputNode : public SyncedAnimationNode { @@ -313,7 +315,7 @@ public: inputs.push_back("Input1"); } - void evaluate(GraphEvaluationContext &context, const Vector &inputs, AnimationData &output) override; + void evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) override; }; struct BlendTreeConnection { @@ -519,22 +521,16 @@ struct BlendTreeBuilder { class SyncedBlendTree : public SyncedAnimationNode { Vector> nodes; - struct NodeRuntimeData { - Vector> input_nodes; - Vector input_data; - AnimationData *output_data = nullptr; - }; - LocalVector _node_runtime_data; - BlendTreeBuilder tree_builder; bool tree_initialized = false; - void setup_tree() { + void sort_nodes() { nodes.clear(); _node_runtime_data.clear(); - tree_builder.sort_nodes_and_references(); + } + void setup_runtime_data() { // Add nodes and allocate runtime data for (int i = 0; i < tree_builder.nodes.size(); i++) { const Ref node = tree_builder.nodes[i]; @@ -559,11 +555,16 @@ class SyncedBlendTree : public SyncedAnimationNode { node_runtime_data.input_nodes.push_back(nodes[connected_node_index]); } } - - tree_initialized = true; } public: + struct NodeRuntimeData { + Vector> input_nodes; + LocalVector input_data; + AnimationData *output_data = nullptr; + }; + LocalVector _node_runtime_data; + Ref get_output_node() const { return tree_builder.nodes[0]; } @@ -598,11 +599,14 @@ public: // overrides from SyncedAnimationNode void initialize(GraphEvaluationContext &context) override { - setup_tree(); + sort_nodes(); + setup_runtime_data(); for (Ref node : nodes) { node->initialize(context); } + + tree_initialized = true; } void activate_inputs(Vector> input_nodes) override { @@ -654,7 +658,7 @@ public: } } - void evaluate(GraphEvaluationContext &context, const Vector &input_datas, AnimationData &output_data) override { + void evaluate(GraphEvaluationContext &context, const LocalVector &input_datas, AnimationData &output_data) override { for (int i = nodes.size() - 1; i > 0; i--) { const Ref &node = nodes[i]; @@ -664,13 +668,22 @@ public: NodeRuntimeData &node_runtime_data = _node_runtime_data[i]; + // Populate the inputs + for (unsigned int j = 0; j < node_runtime_data.input_data.size(); j++) { + int child_index = tree_builder.node_connection_info[i].connected_child_node_index_at_port[j]; + node_runtime_data.input_data[j] = _node_runtime_data[child_index].output_data; + } + + // Set output pointer if (i == 1) { node_runtime_data.output_data = &output_data; } else { node_runtime_data.output_data = memnew(AnimationData); } + node->evaluate(context, node_runtime_data.input_data, *node_runtime_data.output_data); + // All inputs have been consumed and can now be freed. for (int child_index : tree_builder.node_connection_info[i].connected_child_node_index_at_port) { memfree(_node_runtime_data[child_index].output_data); } diff --git a/tests/test_synced_animation_graph.h b/tests/test_synced_animation_graph.h index 91a07ed..eeb65ef 100644 --- a/tests/test_synced_animation_graph.h +++ b/tests/test_synced_animation_graph.h @@ -12,7 +12,8 @@ struct SyncedAnimationGraphFixture { int hip_bone_index = -1; - Ref test_animation; + Ref test_animation_a; + Ref test_animation_b; Ref animation_library; SyncedAnimationGraph *synced_animation_graph; @@ -31,17 +32,8 @@ struct SyncedAnimationGraphFixture { player_node = memnew(AnimationPlayer); player_node->set_name("AnimationPlayer"); - test_animation = memnew(Animation); - const int track_index = test_animation->add_track(Animation::TYPE_POSITION_3D); - CHECK(track_index == 0); - test_animation->track_insert_key(track_index, 0.0, Vector3(0., 0., 0.)); - test_animation->track_insert_key(track_index, 1.0, Vector3(1., 2., 3.)); - test_animation->track_set_path(track_index, NodePath(vformat("%s:%s", skeleton_node->get_path().get_concatenated_names(), "Hips"))); + setup_animations(); - animation_library.instantiate(); - animation_library->add_animation("TestAnimation", test_animation); - - player_node->add_animation_library("animation_library", animation_library); SceneTree::get_singleton()->get_root()->add_child(player_node); synced_animation_graph = memnew(SyncedAnimationGraph); @@ -50,6 +42,29 @@ struct SyncedAnimationGraphFixture { synced_animation_graph->set_animation_player(player_node->get_path()); synced_animation_graph->set_skeleton(skeleton_node->get_path()); } + + void setup_animations() { + test_animation_a = memnew(Animation); + int track_index = test_animation_a->add_track(Animation::TYPE_POSITION_3D); + CHECK(track_index == 0); + test_animation_a->track_insert_key(track_index, 0.0, Vector3(0., 0., 0.)); + test_animation_a->track_insert_key(track_index, 1.0, Vector3(1., 2., 3.)); + test_animation_a->track_set_path(track_index, NodePath(vformat("%s:%s", skeleton_node->get_path().get_concatenated_names(), "Hips"))); + + animation_library.instantiate(); + animation_library->add_animation("TestAnimationA", test_animation_a); + + test_animation_b = memnew(Animation); + track_index = test_animation_b->add_track(Animation::TYPE_POSITION_3D); + CHECK(track_index == 0); + test_animation_b->track_insert_key(track_index, 0.0, Vector3(0., 0., 0.)); + test_animation_b->track_insert_key(track_index, 1.0, Vector3(2., 4., 6.)); + test_animation_b->track_set_path(track_index, NodePath(vformat("%s:%s", skeleton_node->get_path().get_concatenated_names(), "Hips"))); + + animation_library->add_animation("TestAnimationB", test_animation_b); + + player_node->add_animation_library("animation_library", animation_library); + } }; namespace TestSyncedAnimationGraph { @@ -135,13 +150,13 @@ TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") { TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] Test AnimationData blending") { AnimationData data_t0; - data_t0.sample_from_animation(test_animation, skeleton_node, 0.0); + data_t0.sample_from_animation(test_animation_a, skeleton_node, 0.0); AnimationData data_t1; - data_t1.sample_from_animation(test_animation, skeleton_node, 1.0); + data_t1.sample_from_animation(test_animation_a, skeleton_node, 1.0); AnimationData data_t0_5; - data_t0_5.sample_from_animation(test_animation, skeleton_node, 0.5); + data_t0_5.sample_from_animation(test_animation_a, skeleton_node, 0.5); AnimationData data_blended = data_t0; data_blended.blend(data_t1, 0.5); @@ -164,7 +179,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] SyncedAnimationGraph with an AnimationSampler as root node") { Ref animation_sampler_node; animation_sampler_node.instantiate(); - animation_sampler_node->animation_name = "animation_library/TestAnimation"; + animation_sampler_node->animation_name = "animation_library/TestAnimationA"; synced_animation_graph->set_graph_root_node(animation_sampler_node); @@ -189,7 +204,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph Ref animation_sampler_node; animation_sampler_node.instantiate(); - animation_sampler_node->animation_name = "animation_library/TestAnimation"; + animation_sampler_node->animation_name = "animation_library/TestAnimationA"; synced_blend_tree_node->add_node(animation_sampler_node); REQUIRE(synced_blend_tree_node->add_connection(animation_sampler_node, synced_blend_tree_node->get_output_node(), "Input")); @@ -213,4 +228,66 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph CHECK(hip_bone_position.z == doctest::Approx(0.03)); } +TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph][BlendTree][Blend2Node] BlendTree with a Blend2Node connected to the output") { + Ref synced_blend_tree_node; + synced_blend_tree_node.instantiate(); + + // TestAnimationA + Ref animation_sampler_node_a; + animation_sampler_node_a.instantiate(); + animation_sampler_node_a->animation_name = "animation_library/TestAnimationA"; + + synced_blend_tree_node->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"; + + synced_blend_tree_node->add_node(animation_sampler_node_b); + + // Blend2 + Ref blend2_node; + blend2_node.instantiate(); + blend2_node->blend_weight = 0.5; + + synced_blend_tree_node->add_node(blend2_node); + + // Connect nodes + Vector blend2_inputs; + blend2_node->get_input_names(blend2_inputs); + REQUIRE(synced_blend_tree_node->add_connection(animation_sampler_node_a, blend2_node, blend2_inputs[0])); + REQUIRE(synced_blend_tree_node->add_connection(animation_sampler_node_b, blend2_node, blend2_inputs[1])); + REQUIRE(synced_blend_tree_node->add_connection(blend2_node, synced_blend_tree_node->get_output_node(), "Input")); + + synced_blend_tree_node->initialize(synced_animation_graph->get_context()); + + // int sampler_node_1_index = synced_blend_tree_node->get_node_index(animation_sampler_node_1); + // const SyncedBlendTree::NodeRuntimeData &sampler_node_1_runtime_data = synced_blend_tree_node->_node_runtime_data[sampler_node_1_index]; + + // int sampler_node_2_index = synced_blend_tree_node->get_node_index(animation_sampler_node_2); + // const SyncedBlendTree::NodeRuntimeData &sampler_node_2_runtime_data = synced_blend_tree_node->_node_runtime_data[sampler_node_2_index]; + int blend2_node_index = synced_blend_tree_node->get_node_index(blend2_node); + const SyncedBlendTree::NodeRuntimeData &blend2_runtime_data = synced_blend_tree_node->_node_runtime_data[blend2_node_index]; + + CHECK(blend2_runtime_data.input_nodes[0] == animation_sampler_node_a); + CHECK(blend2_runtime_data.input_nodes[1] == animation_sampler_node_b); + + synced_animation_graph->set_graph_root_node(synced_blend_tree_node); + + Vector3 hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; + + CHECK(hip_bone_position.x == doctest::Approx(0.0)); + CHECK(hip_bone_position.y == doctest::Approx(0.0)); + CHECK(hip_bone_position.z == doctest::Approx(0.0)); + + SceneTree::get_singleton()->process(0.5); + + hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; + + CHECK(hip_bone_position.x == doctest::Approx(0.75)); + CHECK(hip_bone_position.y == doctest::Approx(1.5)); + CHECK(hip_bone_position.z == doctest::Approx(2.25)); +} + } //namespace TestSyncedAnimationGraph \ No newline at end of file