diff --git a/demo/animation_library.res b/demo/animation_library.res index 36320fb..db95863 100644 Binary files a/demo/animation_library.res and b/demo/animation_library.res differ diff --git a/demo/main.tscn b/demo/main.tscn index 5d83e0f..ef86770 100644 --- a/demo/main.tscn +++ b/demo/main.tscn @@ -2,8 +2,8 @@ [ext_resource type="PackedScene" uid="uid://d1xcqdqr1qeu6" path="res://assets/MixamoAmy.glb" id="1_0xm2m"] [ext_resource type="AnimationNodeBlendTree" uid="uid://dbkgln7hoxxc8" path="res://embedded_statemachine.tres" id="2_h2yge"] -[ext_resource type="AnimationLibrary" uid="uid://dwubn740aqx51" path="res://animation_library.res" id="3_h2yge"] -[ext_resource type="SyncedBlendTree" uid="uid://cjeho6848x43q" path="res://synced_blend_tree_node.tres" id="4_1bvp3"] +[ext_resource type="AnimationLibrary" uid="uid://bjyfy6m2sr2kp" path="res://animation_library.tres" id="3_1bvp3"] +[ext_resource type="SyncedBlendTree" uid="uid://bijslmj4wd7ap" path="res://synced_blend_tree_node_limping.tres" id="4_1bvp3"] [node name="Node3D" type="Node3D"] @@ -19,16 +19,15 @@ parameters/Blend2/blend_amount = 0.44 "parameters/Embedded StateMachine/conditions/is_limping" = false [node name="AnimationPlayer" type="AnimationPlayer" parent="."] -active = false root_node = NodePath("../MixamoAmy") libraries = { -&"animation_library": ExtResource("3_h2yge") +&"animation_library": ExtResource("3_1bvp3") } [node name="SyncedAnimationGraph" type="SyncedAnimationGraph" parent="."] animation_player = NodePath("../AnimationPlayer") tree_root = ExtResource("4_1bvp3") skeleton = NodePath("../MixamoAmy/Armature/Skeleton3D") -parameters/AnimationBlend2Node/blend_amount = 0.24 +parameters/AnimationBlend2Node/blend_amount = 0.0 [editable path="MixamoAmy"] diff --git a/demo/walk_limp_blend_tree.tres b/demo/walk_limp_blend_tree.tres index 7ce2652..afb8810 100644 --- a/demo/walk_limp_blend_tree.tres +++ b/demo/walk_limp_blend_tree.tres @@ -1,12 +1,14 @@ -[gd_resource type="AnimationNodeBlendTree" load_steps=4 format=3 uid="uid://c7o0gt3li5p4g"] +[gd_resource type="AnimationNodeBlendTree" load_steps=4 format=3 uid="uid://c7o0gt3li5p4b"] [sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_fpiwu"] -animation = &"Limping-InPlace" +animation = &"animation_library/Limping-InPlace" [sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_4sffn"] -animation = &"Walk-InPlace" +animation = &"animation_library/Walk-InPlace" [sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_w6plo"] +sync = true +blend_amount = 0.5 [resource] nodes/output/position = Vector2(860, 160) diff --git a/sync_track.h b/sync_track.h index 52be59a..7a1f3f2 100644 --- a/sync_track.h +++ b/sync_track.h @@ -7,17 +7,18 @@ /** @class SyncTrack used for synced animation blending. * - * A SyncTrack consists of multiple SyncInterval that are adjacent to each other. + * A SyncTrack consists of multiple sync intervals that are adjacent to each other. * * Important definitions: * * - Absolute Time: time within an animation duration in seconds. - * - Ratio: time relative to the animations duration, e.g. 0.5 corresponds to 50% of the duration. - * - SyncTime is a floating point value where the integer parts defines the SyncInterval and the + * - Ratio: time relative to the SyncTrack duration, e.g. 0.5 corresponds to 50% of the duration. + * - SyncTime is a floating point value where the integer parts defines the sync interval and the * fractional part the fraction within the interval. I.e. a SyncTime of 5.332 means it is ~33% * through interval 5. * - * A SyncInterval is defined by a ratio of the starting point and the ratio of the interval's duration. + * A sync interval is defined by a ratio of the starting point and the ratio of the interval's + * duration. Blended SyncTracks always have their first interval start at t = 0.0s. */ struct SyncTrack { static constexpr int cSyncTrackMaxIntervals = 8; @@ -119,6 +120,10 @@ struct SyncTrack { return result; } + /** Creates a blended SyncTrack from two input SyncTracks + * + * \note the first interval will always start at sync time 0, i.e. interval_start_ratio[0] = 0.0. + */ static SyncTrack blend(float weight, const SyncTrack &track_A, const SyncTrack &track_B) { assert(track_A.num_intervals == track_B.num_intervals); @@ -129,17 +134,7 @@ struct SyncTrack { result.duration = (1.0f - weight) * track_A.duration + weight * track_B.duration; - float interval_0_offset = - track_B.interval_start_ratio[0] - track_A.interval_start_ratio[0]; - if (interval_0_offset > 0.5f) { - interval_0_offset = -fmodf(1.f - interval_0_offset, 1.0f); - } else if (interval_0_offset < -0.5) { - interval_0_offset = fmodf(1.f + interval_0_offset, 1.0f); - } - - result.interval_start_ratio[0] = fmodf( - 1.0 + (1.0f - weight) * track_A.interval_start_ratio[0] + weight * (track_A.interval_start_ratio[0] + interval_0_offset), - 1.0f); + 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]; diff --git a/synced_animation_node.cpp b/synced_animation_node.cpp index bc66892..df8e938 100644 --- a/synced_animation_node.cpp +++ b/synced_animation_node.cpp @@ -201,43 +201,63 @@ void AnimationData::sample_from_animation(const Ref &animation, const } bool AnimationSamplerNode::initialize(GraphEvaluationContext &context) { + SyncedAnimationNode::initialize(context); + 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)); return false; } - if (animation_name == "animation_library/TestAnimationA") { - // Corresponds to the walking animation - node_time_info.sync_track = SyncTrack::create_from_markers(animation->get_length(), { 0.8117, 0.314 }); - print_line(vformat("Using hardcoded sync track for animation %s.", animation_name)); - } else if (animation_name == "animation_library/TestAnimationB") { - // Corresponds to the running animation - node_time_info.sync_track = SyncTrack::create_from_markers(animation->get_length(), { 0.6256, 0.2721 }); - print_line(vformat("Using hardcoded sync track for animation %s.", animation_name)); - } else if (animation_name == "animation_library/TestAnimationC") { - // Corresponds to the limping animation - node_time_info.sync_track = SyncTrack::create_from_markers(animation->get_length(), { 0.0674, 1.1047 }); - print_line(vformat("Using hardcoded sync track for animation %s.", animation_name)); + 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); } - node_time_info.length = animation->get_length(); - node_time_info.loop_mode = Animation::LOOP_LINEAR; + 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; } +void AnimationSamplerNode::update_time(double p_time) { + SyncedAnimationNode::update_time(p_time); + + if (node_time_info.is_synced) { + // Any potential looping has already been performed in the sync-controlling node. + return; + } + + if (node_time_info.loop_mode != Animation::LOOP_NONE) { + if (node_time_info.loop_mode == Animation::LOOP_LINEAR) { + if (!Math::is_zero_approx(animation->get_length())) { + node_time_info.position = Math::fposmod(node_time_info.position, static_cast(animation->get_length())); + } + } else { + assert(false && !"Ping-pong looping not yet supported"); + } + } +} + void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) { assert(inputs.size() == 0); - double sample_time = node_time_info.position; - if (node_time_info.is_synced) { - sample_time = node_time_info.sync_track.calc_ratio_from_sync_time(node_time_info.sync_position) * animation->get_length(); + node_time_info.position = node_time_info.sync_track.calc_ratio_from_sync_time(node_time_info.sync_position) * animation->get_length(); } output.clear(); - output.sample_from_animation(animation, context.skeleton_3d, sample_time); + output.sample_from_animation(animation, context.skeleton_3d, node_time_info.position); } void AnimationSamplerNode::set_animation(const StringName &p_name) { @@ -276,7 +296,7 @@ void AnimationBlend2Node::_bind_methods() { } void AnimationBlend2Node::get_parameter_list(List *p_list) const { - p_list->push_back(PropertyInfo(Variant::FLOAT, blend_amount, PROPERTY_HINT_RANGE, "0,1,0.01,or_less,or_greater")); + p_list->push_back(PropertyInfo(Variant::FLOAT, blend_weight_pname, PROPERTY_HINT_RANGE, "0,1,0.01,or_less,or_greater")); } void AnimationBlend2Node::set_parameter(const StringName &p_name, const Variant &p_value) { @@ -290,7 +310,7 @@ Variant AnimationBlend2Node::get_parameter(const StringName &p_name) const { } Variant AnimationBlend2Node::get_parameter_default_value(const StringName &p_parameter) const { - if (p_parameter == blend_amount) { + if (p_parameter == blend_weight_pname) { return blend_weight; } @@ -298,23 +318,34 @@ Variant AnimationBlend2Node::get_parameter_default_value(const StringName &p_par } void AnimationBlend2Node::_get_property_list(List *p_list) const { - p_list->push_back(PropertyInfo(Variant::FLOAT, blend_amount, PROPERTY_HINT_RANGE, "0,1,0.01,or_less,or_greater")); + 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 AnimationBlend2Node::_get(const StringName &p_name, Variant &r_value) const { - if (p_name == blend_amount) { + if (p_name == blend_weight_pname) { r_value = blend_weight; return true; } + if (p_name == sync_pname) { + r_value = sync; + return true; + } + return false; } bool AnimationBlend2Node::_set(const StringName &p_name, const Variant &p_value) { - if (p_name == blend_amount) { + if (p_name == blend_weight_pname) { blend_weight = p_value; return true; } + if (p_name == sync_pname) { + sync = p_value; + return true; + } + return false; } \ No newline at end of file diff --git a/synced_animation_node.h b/synced_animation_node.h index dd44ef9..41c1fd7 100644 --- a/synced_animation_node.h +++ b/synced_animation_node.h @@ -220,14 +220,12 @@ protected: public: struct NodeTimeInfo { - double length = 0.0; + double delta = 0.0; double position = 0.0; double sync_position = 0.0; - double delta = 0.0; - double sync_delta = 0.0; bool is_synced = false; - Animation::LoopMode loop_mode = Animation::LOOP_LINEAR; + Animation::LoopMode loop_mode = Animation::LOOP_NONE; SyncTrack sync_track; }; NodeTimeInfo node_time_info; @@ -237,7 +235,10 @@ public: Vector2 position; virtual ~SyncedAnimationNode() override = default; - virtual bool initialize(GraphEvaluationContext &context) { return true; } + virtual bool initialize(GraphEvaluationContext &context) { + node_time_info = {}; + return true; + } virtual void activate_inputs(Vector> input_nodes) { // By default, all inputs nodes are activated. @@ -250,6 +251,7 @@ public: // By default, use the SyncTrack of the first input. if (input_nodes.size() > 0) { node_time_info.sync_track = input_nodes[0]->node_time_info.sync_track; + node_time_info.loop_mode = input_nodes[0]->node_time_info.loop_mode; } } virtual void update_time(double p_time) { @@ -258,25 +260,6 @@ public: } else { node_time_info.delta = p_time; node_time_info.position += p_time; - if (node_time_info.position > node_time_info.length) { - switch (node_time_info.loop_mode) { - case Animation::LOOP_NONE: { - node_time_info.position = node_time_info.length; - break; - } - case Animation::LOOP_LINEAR: { - assert(node_time_info.length > 0.0); - while (node_time_info.position > node_time_info.length) { - node_time_info.position -= node_time_info.length; - } - break; - } - case Animation::LOOP_PINGPONG: { - assert(false && !"Not yet implemented."); - break; - } - } - } } } virtual void evaluate(GraphEvaluationContext &context, const LocalVector &input_datas, AnimationData &output_data) { @@ -317,6 +300,7 @@ private: Ref animation; bool initialize(GraphEvaluationContext &context) override; + void update_time(double p_time) override; void evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) override; protected: @@ -336,7 +320,6 @@ class AnimationBlend2Node : public SyncedAnimationNode { GDCLASS(AnimationBlend2Node, SyncedAnimationNode); public: - StringName blend_amount = PNAME("blend_amount"); float blend_weight = 0.0f; bool sync = true; @@ -344,6 +327,17 @@ public: inputs.push_back("Input0"); inputs.push_back("Input1"); } + + bool initialize(GraphEvaluationContext &context) override { + bool result = SyncedAnimationNode::initialize(context); + + 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; + } void activate_inputs(Vector> input_nodes) override { for (const Ref &node : input_nodes) { node->active = true; @@ -352,17 +346,28 @@ public: node->node_time_info.is_synced = node_time_info.is_synced || sync; } } + void calculate_sync_track(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); 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); - node_time_info.length = node_time_info.sync_track.duration; } } + void update_time(double p_delta) override { SyncedAnimationNode::update_time(p_delta); if (sync && !node_time_info.is_synced) { - node_time_info.sync_position = node_time_info.sync_track.calc_sync_from_abs_time(node_time_info.position); + if (node_time_info.loop_mode != Animation::LOOP_NONE) { + if (node_time_info.loop_mode == Animation::LOOP_LINEAR) { + 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"); + } + } + } } } void evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) override; @@ -381,6 +386,10 @@ 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 blend_weight_pname = PNAME("blend_amount"); + StringName sync_pname = PNAME("sync"); }; struct BlendTreeConnection { @@ -663,6 +672,14 @@ 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."); diff --git a/tests/test_sync_track.h b/tests/test_sync_track.h index aa72fcc..83fa35a 100644 --- a/tests/test_sync_track.h +++ b/tests/test_sync_track.h @@ -66,16 +66,20 @@ TEST_CASE("[SyncedAnimationGraph][SyncTrack] Basic") { WHEN("Blending two synctracks with weight 0.") { SyncTrack blended = SyncTrack::blend(0.f, track_a, track_b); - THEN("Result must equal track_A") { - REQUIRE(track_a == blended); + 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); - THEN("Result must equal track_B") { - REQUIRE(track_b == blended); + 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]); } } } @@ -153,16 +157,20 @@ TEST_CASE("[SyncedAnimationGraph][SyncTrack] Sync Track blending") { WHEN("Blending two synctracks with weight 0.") { SyncTrack blended = SyncTrack::blend(0.f, track_a, track_b); - THEN("Result must equal track_A") { - REQUIRE(track_a == blended); + 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); - THEN("Result must equal track_B") { - REQUIRE(track_b == blended); + 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]); } } @@ -173,7 +181,7 @@ TEST_CASE("[SyncedAnimationGraph][SyncTrack] Sync Track blending") { REQUIRE( blended.duration == (1.0f - weight) * track_a.duration + weight * track_b.duration); REQUIRE( - blended.interval_start_ratio[0] == fmodf((1.0f - weight) * (track_a.interval_start_ratio[0] + 1.0f) + weight * (track_b.interval_start_ratio[0]), 1.0f)); + blended.interval_start_ratio[0] == 0.0); REQUIRE( blended.interval_duration_ratio[1] == (1.0f - weight) * (track_a.interval_duration_ratio[1]) + weight * (track_b.interval_duration_ratio[1])); REQUIRE( @@ -187,7 +195,7 @@ TEST_CASE("[SyncedAnimationGraph][SyncTrack] Sync Track blending") { REQUIRE( blended.duration == (1.0f - weight) * track_b.duration + weight * track_a.duration); REQUIRE( - blended.interval_start_ratio[0] == fmodf((1.0f - weight) * (track_b.interval_start_ratio[0]) + weight * (track_a.interval_start_ratio[0] + 1.0f), 1.0f)); + blended.interval_start_ratio[0] == 0.0); REQUIRE( blended.interval_duration_ratio[1] == (1.0f - weight) * (track_b.interval_duration_ratio[1]) + weight * (track_a.interval_duration_ratio[1])); REQUIRE( diff --git a/tests/test_synced_animation_graph.h b/tests/test_synced_animation_graph.h index ef3799e..9314da7 100644 --- a/tests/test_synced_animation_graph.h +++ b/tests/test_synced_animation_graph.h @@ -15,10 +15,23 @@ struct SyncedAnimationGraphFixture { Ref test_animation_a; Ref test_animation_b; + Ref test_animation_sync_a; + Ref test_animation_sync_b; + Ref animation_library; SyncedAnimationGraph *synced_animation_graph; SyncedAnimationGraphFixture() { + SyncedAnimationGraph *scene_animation_graph = dynamic_cast(SceneTree::get_singleton()->get_root()->find_child("SyncedAnimationGraphFixtureTestNode", true, false)); + + if (scene_animation_graph == nullptr) { + setup_test_scene(); + } + + assign_scene_variables(); + } + + void setup_test_scene() { character_node = memnew(Node); character_node->set_name("CharacterNode"); SceneTree::get_singleton()->get_root()->add_child(character_node); @@ -38,6 +51,7 @@ struct SyncedAnimationGraphFixture { SceneTree::get_singleton()->get_root()->add_child(player_node); synced_animation_graph = memnew(SyncedAnimationGraph); + synced_animation_graph->set_name("SyncedAnimationGraphFixtureTestNode"); SceneTree::get_singleton()->get_root()->add_child(synced_animation_graph); synced_animation_graph->set_animation_player(player_node->get_path()); @@ -51,6 +65,7 @@ struct SyncedAnimationGraphFixture { 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"))); + test_animation_a->set_loop_mode(Animation::LOOP_LINEAR); animation_library.instantiate(); animation_library->add_animation("TestAnimationA", test_animation_a); @@ -61,11 +76,67 @@ struct SyncedAnimationGraphFixture { 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"))); + test_animation_b->set_loop_mode(Animation::LOOP_LINEAR); animation_library->add_animation("TestAnimationB", test_animation_b); + test_animation_sync_a = memnew(Animation); + track_index = test_animation_sync_a->add_track(Animation::TYPE_POSITION_3D); + CHECK(track_index == 0); + test_animation_sync_a->track_insert_key(track_index, 0.0, Vector3(0., 0., 0.)); + test_animation_sync_a->track_insert_key(track_index, 0.4, Vector3(1., 2., 3.)); + test_animation_sync_a->set_length(2.0); + test_animation_sync_a->track_set_path(track_index, NodePath(vformat("%s:%s", skeleton_node->get_path().get_concatenated_names(), "Hips"))); + test_animation_sync_a->add_marker("0", 0.0); + test_animation_sync_a->add_marker("1", 0.4); + test_animation_sync_a->track_set_interpolation_type(track_index, Animation::INTERPOLATION_LINEAR); + test_animation_sync_a->set_loop_mode(Animation::LOOP_LINEAR); + + animation_library->add_animation("TestAnimationSyncA", test_animation_sync_a); + + test_animation_sync_b = memnew(Animation); + track_index = test_animation_sync_b->add_track(Animation::TYPE_POSITION_3D); + CHECK(track_index == 0); + test_animation_sync_b->track_insert_key(track_index, 0.1, Vector3(2., 4., 6.)); + test_animation_sync_b->track_insert_key(track_index, 0.2, Vector3(0., 0., 0.)); + test_animation_sync_b->set_length(1.0); + test_animation_sync_b->track_set_path(track_index, NodePath(vformat("%s:%s", skeleton_node->get_path().get_concatenated_names(), "Hips"))); + test_animation_sync_b->add_marker("1", 0.1); + test_animation_sync_b->add_marker("0", 0.2); + test_animation_sync_b->track_set_interpolation_type(track_index, Animation::INTERPOLATION_LINEAR); + test_animation_sync_b->set_loop_mode(Animation::LOOP_LINEAR); + + animation_library->add_animation("TestAnimationSyncB", test_animation_sync_b); + player_node->add_animation_library("animation_library", animation_library); } + + void assign_scene_variables() { + synced_animation_graph = dynamic_cast(SceneTree::get_singleton()->get_root()->find_child("SyncedAnimationGraphFixtureTestNode", true, false)); + REQUIRE(synced_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))); + REQUIRE(skeleton_node != nullptr); + player_node = dynamic_cast((SceneTree::get_singleton()->get_root()->find_child("AnimationPlayer", true, false))); + REQUIRE(player_node != nullptr); + + skeleton_node->reset_bone_poses(); + hip_bone_index = skeleton_node->find_bone("Hips"); + REQUIRE(hip_bone_index > -1); + + animation_library = player_node->get_animation_library("animation_library"); + REQUIRE(animation_library.is_valid()); + + test_animation_a = animation_library->get_animation("TestAnimationA"); + REQUIRE(test_animation_a.is_valid()); + test_animation_b = animation_library->get_animation("TestAnimationB"); + REQUIRE(test_animation_b.is_valid()); + test_animation_sync_a = animation_library->get_animation("TestAnimationSyncA"); + REQUIRE(test_animation_sync_a.is_valid()); + test_animation_sync_b = animation_library->get_animation("TestAnimationSyncB"); + REQUIRE(test_animation_sync_b.is_valid()); + } }; namespace TestSyncedAnimationGraph { @@ -177,7 +248,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph } } -TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] SyncedAnimationGraph with an AnimationSampler as root node") { +TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] SyncedAnimationGraph evaluation with an AnimationSampler as root node") { Ref animation_sampler_node; animation_sampler_node.instantiate(); animation_sampler_node->animation_name = "animation_library/TestAnimationA"; @@ -199,7 +270,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph CHECK(hip_bone_position.z == doctest::Approx(0.03)); } -TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph][BlendTree] BlendTree with a AnimationSamplerNode connected to the output") { +TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph][BlendTree] BlendTree evaluation with a AnimationSamplerNode connected to the output") { Ref synced_blend_tree_node; synced_blend_tree_node.instantiate(); @@ -229,7 +300,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 with a Blend2Node connected to the output") { +TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph][BlendTree][Blend2Node] BlendTree evaluation with a Blend2Node connected to the output") { Ref synced_blend_tree_node; synced_blend_tree_node.instantiate(); @@ -250,7 +321,9 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph // Blend2 Ref blend2_node; blend2_node.instantiate(); + blend2_node->name = "Blend2"; blend2_node->blend_weight = 0.5; + blend2_node->sync = false; synced_blend_tree_node->add_node(blend2_node); @@ -271,61 +344,108 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph synced_animation_graph->set_root_animation_node(synced_blend_tree_node); - Vector3 hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; + SUBCASE("Perform default evaluation") { + 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)); + 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); + SceneTree::get_singleton()->process(0.5); - hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; + 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)); + CHECK(hip_bone_position.x == doctest::Approx(0.75)); + CHECK(hip_bone_position.y == doctest::Approx(1.5)); + CHECK(hip_bone_position.z == doctest::Approx(2.25)); + } - // Test saving and loading of the blend tree to a resource - ResourceSaver::save(synced_blend_tree_node, "synced_blend_tree_node.tres"); + SUBCASE("Evaluate tree such that animations get looped") { + Vector3 hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; - REQUIRE(ClassDB::class_exists("AnimationSamplerNode")); + 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)); - // Load blend tree - Ref loaded_synced_blend_tree = ResourceLoader::load("synced_blend_tree_node.tres"); - REQUIRE(loaded_synced_blend_tree.is_valid()); + SceneTree::get_singleton()->process(1.2); - loaded_synced_blend_tree->initialize(synced_animation_graph->get_context()); - synced_animation_graph->set_root_animation_node(loaded_synced_blend_tree); + hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; - // Re-evaluate using a different time. All animation samplers will start again from 0. - SceneTree::get_singleton()->process(0.2); + CHECK(hip_bone_position.x == doctest::Approx(0.3)); + CHECK(hip_bone_position.y == doctest::Approx(0.6)); + CHECK(hip_bone_position.z == doctest::Approx(0.9)); + } - hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; + SUBCASE("Evaluate synced blend") { + 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()); - CHECK(hip_bone_position.x == doctest::Approx(0.3)); - CHECK(hip_bone_position.y == doctest::Approx(0.6)); - CHECK(hip_bone_position.z == doctest::Approx(0.9)); -} + REQUIRE(synced_animation_graph->get_root_animation_node().ptr() == synced_blend_tree_node.ptr()); -TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph][BlendTree][Blend2Node] Serialize AnimationTree" * doctest::skip(true)) { - AnimationTree *animation_tree = memnew(AnimationTree); + // By blending both animations we get a SyncTrack of duration 1.5s with the following + // intervals: + // 0: 0.825s + // 1: 0.675s + // By updating by 0s we get to the start of interval 0, an update by 0.65s to the start of interval 1, and + // another update by 0.85 to the start again to interval 0. + SceneTree::get_singleton()->process(0.); - character_node->add_child(animation_tree); - animation_tree->set_animation_player(player_node->get_path()); - animation_tree->set_root_node(character_node->get_path()); - Ref animation_node_animation; - animation_node_animation.instantiate(); - animation_node_animation->set_animation("TestAnimationA"); + Vector3 hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; - Ref animation_node_blend_tree; - animation_node_blend_tree.instantiate(); - animation_node_blend_tree->add_node("SamplerTestAnimationA", animation_node_animation, Vector2(0, 0)); - animation_node_blend_tree->connect_node("output", 0, "SamplerTestAnimationA"); - animation_node_blend_tree->setup_local_to_scene(); + 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)); - animation_tree->set_root_animation_node(animation_node_blend_tree); + // By updating again by 0.825s we get loop to the start of the 0th interval where + // both TrackValues are zero. + SceneTree::get_singleton()->process(0.825); - ResourceSaver::save(animation_node_blend_tree, "animation_tree.tres"); + hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; + + CHECK(hip_bone_position.x == doctest::Approx(1.5)); + CHECK(hip_bone_position.y == doctest::Approx(3.0)); + CHECK(hip_bone_position.z == doctest::Approx(4.5)); + + // By updating again by 0.675s we loop to the start of the 0th interval where both + // TrackValues are zero. + SceneTree::get_singleton()->process(0.675); + + 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)); + } + + SUBCASE("Save, load and evaluate the SyncedBlendTree") { + // Test saving and loading of the blend tree to a resource + ResourceSaver::save(synced_blend_tree_node, "synced_blend_tree_node.tres"); + + REQUIRE(ClassDB::class_exists("AnimationSamplerNode")); + + // Load blend tree + Ref 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")); + REQUIRE(loaded_blend2_node.is_valid()); + 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); + + // Re-evaluate using a different time. All animation samplers will start again from 0. + SceneTree::get_singleton()->process(0.2); + + Vector3 hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; + + CHECK(hip_bone_position.x == doctest::Approx(0.3)); + CHECK(hip_bone_position.y == doctest::Approx(0.6)); + CHECK(hip_bone_position.z == doctest::Approx(0.9)); + } } } //namespace TestSyncedAnimationGraph \ No newline at end of file