Compare commits

...

1 Commits

Author SHA1 Message Date
Martin Felis
3dd1ce42df 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-21 21:30:54 +01:00
4 changed files with 98 additions and 10 deletions

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 = float track_A_repeats = static_cast<float>(result.num_intervals / track_A.num_intervals);
(1.0f - weight) * track_A.duration + weight * track_B.duration; float track_B_repeats = static_cast<float>(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] / track_A_repeats;
float interval_duration_B = track_B.interval_duration_ratio[i]; float interval_duration_B = track_B.interval_duration_ratio[i % track_B.num_intervals] / track_B_repeats;
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

@ -203,4 +203,47 @@ TEST_CASE("[Blendalot][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);
float track_a_repeats = static_cast<float>(blended.num_intervals / track_a.num_intervals);
float track_b_repeats = static_cast<float>(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