Compare commits

..

2 Commits

Author SHA1 Message Date
Martin Felis
88b1caf885 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.
2026-02-20 23:23:06 +01:00
Martin Felis
c7660c7b19 Replace SyncedAnimationGraph to BLTAnimationGraph. 2026-02-20 22:55:30 +01:00
5 changed files with 103 additions and 16 deletions

View File

@ -146,7 +146,7 @@ void BLTAnimationGraph::_graph_changed(const StringName &node_name) {
} }
void BLTAnimationGraph::_notification(int p_what) { void BLTAnimationGraph::_notification(int p_what) {
GodotProfileZone("SyncedAnimationGraph::_notification"); GodotProfileZone("BLTAnimationGraph::_notification");
switch (p_what) { switch (p_what) {
case NOTIFICATION_ENTER_TREE: { case NOTIFICATION_ENTER_TREE: {
@ -295,7 +295,7 @@ void BLTAnimationGraph::_process_graph(double p_delta, bool p_update_only) {
return; return;
} }
GodotProfileZone("SyncedAnimationGraph::_process_graph"); GodotProfileZone("BLTAnimationGraph::_process_graph");
_update_properties(); _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 { void BLTAnimationGraph::_apply_animation_data(const AnimationData &output_data) const {
GodotProfileZone("SyncedAnimationGraph::_apply_animation_data"); GodotProfileZone("BLTAnimationGraph::_apply_animation_data");
for (const KeyValue<Animation::TypeHash, size_t> &K : output_data.value_buffer_offset) { for (const KeyValue<Animation::TypeHash, size_t> &K : output_data.value_buffer_offset) {
const AnimationData::TrackValue *track_value = output_data.get_value<AnimationData::TrackValue>(K.key); const AnimationData::TrackValue *track_value = output_data.get_value<AnimationData::TrackValue>(K.key);

View File

@ -385,6 +385,10 @@ StringName BLTAnimationNodeSampler::get_animation() const {
return animation_name; return animation_name;
} }
AnimationPlayer *BLTAnimationNodeSampler::get_animation_player() const {
return animation_player;
}
TypedArray<StringName> BLTAnimationNodeSampler::get_animations_as_typed_array() const { TypedArray<StringName> BLTAnimationNodeSampler::get_animations_as_typed_array() const {
TypedArray<StringName> typed_arr; TypedArray<StringName> typed_arr;
@ -417,6 +421,7 @@ TypedArray<StringName> BLTAnimationNodeSampler::get_animations_as_typed_array()
void BLTAnimationNodeSampler::_bind_methods() { void BLTAnimationNodeSampler::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_animation", "name"), &BLTAnimationNodeSampler::set_animation); 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"), &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"); ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "animation"), "set_animation", "get_animation");

22
blendalot_math_helper.h Normal file
View File

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

View File

@ -2,6 +2,7 @@
#include "core/templates/local_vector.h" #include "core/templates/local_vector.h"
#include "blendalot_math_helper.h"
#include <cassert> #include <cassert>
#include <cmath> #include <cmath>
@ -21,7 +22,7 @@
* duration. Blended SyncTracks always have their first interval start at t = 0.0s. * duration. Blended SyncTracks always have their first interval start at t = 0.0s.
*/ */
struct SyncTrack { struct SyncTrack {
static constexpr int cSyncTrackMaxIntervals = 8; static constexpr int cSyncTrackMaxIntervals = 32;
SyncTrack() : SyncTrack() :
duration(0.f), num_intervals(1) { duration(0.f), num_intervals(1) {
@ -59,6 +60,12 @@ struct SyncTrack {
} }
double calc_ratio_from_sync_time(double sync_time) const { 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); float interval_ratio = fmod(sync_time, 1.0f);
int interval = int(sync_time - interval_ratio); int interval = int(sync_time - interval_ratio);
@ -126,19 +133,32 @@ struct SyncTrack {
*/ */
static SyncTrack static SyncTrack
blend(float weight, const SyncTrack &track_A, const SyncTrack &track_B) { 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; SyncTrack result;
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; result.num_intervals = track_A.num_intervals;
}
assert(result.num_intervals < cSyncTrackMaxIntervals);
result.duration = int track_A_repeats = result.num_intervals / track_A.num_intervals;
(1.0f - weight) * track_A.duration + weight * track_B.duration; int track_B_repeats = 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; result.interval_start_ratio[0] = 0.f;
for (int i = 0; i < result.num_intervals; i++) { for (int i = 0; i < result.num_intervals; i++) {
float interval_duration_A = track_A.interval_duration_ratio[i]; float interval_duration_A = track_A.interval_duration_ratio[i % track_A.num_intervals];
float interval_duration_B = track_B.interval_duration_ratio[i]; float interval_duration_B = track_B.interval_duration_ratio[i % track_B.num_intervals];
result.interval_duration_ratio[i] = result.interval_duration_ratio[i] =
(1.0f - weight) * interval_duration_A + weight * interval_duration_B; (1.0f - weight) * interval_duration_A + weight * interval_duration_B;
@ -152,8 +172,6 @@ struct SyncTrack {
} }
} }
assert(result.num_intervals < cSyncTrackMaxIntervals);
return result; return result;
} }
}; };

View File

@ -6,7 +6,7 @@
namespace TestBlendalotAnimationGraph { namespace TestBlendalotAnimationGraph {
TEST_CASE("[SyncedAnimationGraph][SyncTrack] Basic") { TEST_CASE("[Blendalot][SyncTrack] Basic") {
SyncTrack track_a; SyncTrack track_a;
track_a.num_intervals = 2; track_a.num_intervals = 2;
track_a.duration = 2.0; 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 }); SyncTrack track = SyncTrack::create_from_markers(2.0f, { 0.9f, 0.2f });
WHEN("Querying Ratios") { 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_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 }); SyncTrack track_b = SyncTrack::create_from_markers(1.5f, { 1.05, 1.35, 0.3 });
@ -203,4 +203,46 @@ TEST_CASE("[SyncedAnimationGraph][SyncTrack] Sync Track blending") {
} }
} }
} //namespace TestSyncedAnimationGraph 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);
int track_b_repeats = 3;
REQUIRE(
blended.duration == (1.0f - weight) * track_a.duration + weight * track_b.duration * track_b_repeats);
REQUIRE(
blended.interval_start_ratio[0] == 0.0);
REQUIRE(
blended.interval_duration_ratio[0] == (1.0f - weight) * (track_a.interval_duration_ratio[0]) + weight * (track_b.interval_duration_ratio[0]));
REQUIRE(
blended.interval_duration_ratio[1] == (1.0f - weight) * (track_a.interval_duration_ratio[1]) + weight * (track_b.interval_duration_ratio[0]));
REQUIRE(
blended.interval_duration_ratio[2] == (1.0f - weight) * (track_a.interval_duration_ratio[2]) + weight * (track_b.interval_duration_ratio[0]));
}
}
} //namespace TestBlendalotAnimationGraph