Added SyncTrack class.
This commit is contained in:
parent
810c6bd9d7
commit
f1641f3ac3
164
sync_track.h
Normal file
164
sync_track.h
Normal file
@ -0,0 +1,164 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/templates/local_vector.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
|
||||
/** @class SyncTrack used for synced animation blending.
|
||||
*
|
||||
* A SyncTrack consists of multiple SyncInterval 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
|
||||
* 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.
|
||||
*/
|
||||
struct SyncTrack {
|
||||
static constexpr int cSyncTrackMaxIntervals = 8;
|
||||
|
||||
SyncTrack() :
|
||||
duration(0.f), num_intervals(1) {
|
||||
for (int i = 0; i < cSyncTrackMaxIntervals; i++) {
|
||||
interval_start_ratio[i] = 0.f;
|
||||
interval_duration_ratio[i] = 0.f;
|
||||
}
|
||||
|
||||
interval_duration_ratio[0] = 1.0f;
|
||||
}
|
||||
|
||||
float duration = 1.0;
|
||||
int num_intervals = 1;
|
||||
float interval_start_ratio
|
||||
[cSyncTrackMaxIntervals]; //< Starting time of interval in absolute time.
|
||||
float interval_duration_ratio[cSyncTrackMaxIntervals]; //<
|
||||
|
||||
float calc_sync_from_abs_time(float abs_time) const {
|
||||
for (int i = 0; i < num_intervals; i++) {
|
||||
float query_abs_time = abs_time;
|
||||
float interval_start = interval_start_ratio[i] * duration;
|
||||
float interval_end =
|
||||
interval_start + interval_duration_ratio[i] * duration;
|
||||
|
||||
if (query_abs_time < interval_start) {
|
||||
query_abs_time += duration;
|
||||
}
|
||||
if (query_abs_time >= interval_start && query_abs_time < interval_end) {
|
||||
return float(i) + (query_abs_time - interval_start) / (interval_end - interval_start);
|
||||
}
|
||||
}
|
||||
|
||||
assert(false && "Invalid absolute time");
|
||||
return -1.f;
|
||||
}
|
||||
|
||||
float calc_ratio_from_sync_time(float sync_time) const {
|
||||
float interval_ratio = fmodf(sync_time, 1.0f);
|
||||
int interval = int(sync_time - interval_ratio);
|
||||
|
||||
return fmodf(
|
||||
interval_start_ratio[interval] + interval_duration_ratio[interval] * interval_ratio,
|
||||
1.0f);
|
||||
}
|
||||
|
||||
bool operator==(const SyncTrack &other) const {
|
||||
bool result = duration == other.duration && num_intervals == other.num_intervals;
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_intervals; i++) {
|
||||
if ((fabsf(interval_start_ratio[i] - other.interval_start_ratio[i]) > 1.0e-5) || (fabsf(interval_duration_ratio[i] - other.interval_duration_ratio[i]) > 1.0e-5)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Constructs SyncTrack from markers.
|
||||
*
|
||||
* Markers are specified in absolute time and must be >= 0 and <= duration. They define the
|
||||
* start of the interval. The last marker is implicitly the (possibly looped) first marker.
|
||||
*/
|
||||
static SyncTrack create_from_markers(
|
||||
float duration,
|
||||
const LocalVector<float> &markers) {
|
||||
assert(markers.size() > 0);
|
||||
assert(markers.size() < cSyncTrackMaxIntervals);
|
||||
|
||||
SyncTrack result;
|
||||
result.duration = duration;
|
||||
result.num_intervals = markers.size();
|
||||
|
||||
for (unsigned int i = 0; i < markers.size(); i++) {
|
||||
assert(markers[i] >= 0.f && markers[i] <= duration);
|
||||
|
||||
int end_index = i == (markers.size() - 1) ? 0 : i + 1;
|
||||
|
||||
float interval_start = markers[i];
|
||||
float interval_end = markers[end_index];
|
||||
|
||||
if (interval_end == interval_start) {
|
||||
interval_end = interval_start + duration;
|
||||
} else if (interval_end < interval_start) {
|
||||
interval_end += duration;
|
||||
}
|
||||
|
||||
result.interval_start_ratio[i] = interval_start / duration;
|
||||
result.interval_duration_ratio[i] =
|
||||
(interval_end - interval_start) / duration;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static SyncTrack
|
||||
blend(float weight, const SyncTrack &track_A, const SyncTrack &track_B) {
|
||||
assert(track_A.num_intervals == track_B.num_intervals);
|
||||
|
||||
SyncTrack result;
|
||||
result.num_intervals = track_A.num_intervals;
|
||||
|
||||
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);
|
||||
|
||||
for (int i = 0; i < result.num_intervals; i++) {
|
||||
float interval_duration_A = track_A.interval_duration_ratio[i];
|
||||
float interval_duration_B = track_B.interval_duration_ratio[i];
|
||||
result.interval_duration_ratio[i] =
|
||||
(1.0f - weight) * interval_duration_A + weight * interval_duration_B;
|
||||
|
||||
if (i < cSyncTrackMaxIntervals) {
|
||||
result.interval_start_ratio[i + 1] =
|
||||
result.interval_start_ratio[i] + result.interval_duration_ratio[i];
|
||||
if (result.interval_start_ratio[i + 1] > 1.0f) {
|
||||
result.interval_start_ratio[i + 1] =
|
||||
fmodf(result.interval_start_ratio[i + 1], 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert(result.num_intervals < cSyncTrackMaxIntervals);
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
#include "core/io/resource.h"
|
||||
#include "scene/3d/skeleton_3d.h"
|
||||
#include "sync_track.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
@ -192,9 +193,6 @@ protected:
|
||||
}
|
||||
};
|
||||
|
||||
struct SyncTrack {
|
||||
};
|
||||
|
||||
struct GraphEvaluationContext {
|
||||
AnimationPlayer *animation_player = nullptr;
|
||||
Skeleton3D *skeleton_3d = nullptr;
|
||||
|
||||
198
tests/test_sync_track.h
Normal file
198
tests/test_sync_track.h
Normal file
@ -0,0 +1,198 @@
|
||||
#pragma once
|
||||
|
||||
#include "../sync_track.h"
|
||||
|
||||
#include "tests/test_macros.h"
|
||||
|
||||
namespace TestSyncedAnimationGraph {
|
||||
|
||||
TEST_CASE("[SyncedAnimationGraph][SyncTrack] Basic") {
|
||||
SyncTrack track_a;
|
||||
track_a.num_intervals = 2;
|
||||
track_a.duration = 2.0;
|
||||
track_a.interval_start_ratio[0] = 0.f;
|
||||
track_a.interval_duration_ratio[0] = 0.7;
|
||||
track_a.interval_start_ratio[1] = 0.7f;
|
||||
track_a.interval_duration_ratio[1] = 0.3;
|
||||
|
||||
SyncTrack track_b;
|
||||
track_b.num_intervals = 2;
|
||||
track_b.duration = 1.5;
|
||||
track_b.interval_start_ratio[0] = 0.0f;
|
||||
track_b.interval_duration_ratio[0] = 0.6;
|
||||
track_b.interval_start_ratio[1] = 0.6f;
|
||||
track_b.interval_duration_ratio[1] = 0.4;
|
||||
|
||||
WHEN("Calculating sync time of track_B at 0.5 duration") {
|
||||
float sync_time_at_0_75 =
|
||||
track_b.calc_sync_from_abs_time(0.5 * track_b.duration);
|
||||
REQUIRE(sync_time_at_0_75 == doctest::Approx(0.83333));
|
||||
}
|
||||
|
||||
WHEN("Calculating sync time of track_B at 0.6 duration") {
|
||||
float sync_time_at_0_6 =
|
||||
track_b.calc_sync_from_abs_time(0.6 * track_b.duration);
|
||||
REQUIRE(sync_time_at_0_6 == doctest::Approx(1.0));
|
||||
}
|
||||
|
||||
WHEN("Calculating sync time of track_B at 0.7 duration") {
|
||||
float sync_time_at_0_7 =
|
||||
track_b.calc_sync_from_abs_time(0.7 * track_b.duration);
|
||||
REQUIRE(sync_time_at_0_7 == doctest::Approx(1.25));
|
||||
}
|
||||
|
||||
WHEN("Calculating sync time of track_B at 0.0 duration") {
|
||||
float sync_time_at_1_0 =
|
||||
track_b.calc_sync_from_abs_time(0.0 * track_b.duration);
|
||||
REQUIRE(sync_time_at_1_0 == doctest::Approx(0.0));
|
||||
}
|
||||
|
||||
WHEN("Calculating sync time of track_B at 1.0 duration") {
|
||||
float sync_time_at_1_0 =
|
||||
track_b.calc_sync_from_abs_time(0.9999 * track_b.duration);
|
||||
REQUIRE(sync_time_at_1_0 == doctest::Approx(2.0).epsilon(0.001f));
|
||||
}
|
||||
|
||||
WHEN("Calculating ratio from sync time on track_A at 0.83333") {
|
||||
float ratio = track_a.calc_ratio_from_sync_time(0.83333333);
|
||||
REQUIRE(ratio == doctest::Approx(0.5833333));
|
||||
}
|
||||
|
||||
WHEN("Calculating ratio from sync time on track_A at 0.83333") {
|
||||
float ratio = track_a.calc_ratio_from_sync_time(1.25);
|
||||
REQUIRE(ratio == doctest::Approx(0.775));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("[SyncedAnimationGraph][SyncTrack] Create Sync Track from markers") {
|
||||
SyncTrack track = SyncTrack::create_from_markers(2.0f, { 0.9f, 0.2f });
|
||||
|
||||
WHEN("Querying Ratios") {
|
||||
CHECK(track.interval_start_ratio[0] == doctest::Approx(0.45f));
|
||||
CHECK(track.interval_duration_ratio[0] == doctest::Approx(0.65f));
|
||||
|
||||
CHECK(track.interval_start_ratio[1] == doctest::Approx(0.1f));
|
||||
CHECK(track.interval_duration_ratio[1] == doctest::Approx(0.35f));
|
||||
|
||||
WHEN("Querying ratio at sync time at 0.001") {
|
||||
float ratio = track.calc_ratio_from_sync_time(0.0001f);
|
||||
CHECK(ratio == doctest::Approx(0.45).epsilon(0.001));
|
||||
}
|
||||
|
||||
WHEN("Querying ratio at sync time at 0.9999") {
|
||||
float ratio = track.calc_ratio_from_sync_time(0.9999f);
|
||||
CHECK(ratio == doctest::Approx(0.1).epsilon(0.001));
|
||||
}
|
||||
|
||||
WHEN("Querying ratio at sync time at 1.001") {
|
||||
float ratio = track.calc_ratio_from_sync_time(1.0001f);
|
||||
CHECK(ratio == doctest::Approx(0.1).epsilon(0.001));
|
||||
}
|
||||
|
||||
WHEN("Querying ratio at sync time at 1.9999") {
|
||||
float ratio = track.calc_ratio_from_sync_time(1.9999f);
|
||||
CHECK(ratio == doctest::Approx(0.45).epsilon(0.001));
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Querying SyncTime from Absolute Time") {
|
||||
WHEN("Querying absolute time at 0.9001s") {
|
||||
float sync_time = track.calc_sync_from_abs_time(0.9001f);
|
||||
CHECK(sync_time == doctest::Approx(0.0).epsilon(0.001));
|
||||
}
|
||||
|
||||
WHEN("Querying absolute time at 0.2001s") {
|
||||
float sync_time = track.calc_sync_from_abs_time(0.2001f);
|
||||
CHECK(sync_time == doctest::Approx(1.0).epsilon(0.001));
|
||||
}
|
||||
|
||||
WHEN("Querying absolute time at 0.8999s") {
|
||||
float sync_time = track.calc_sync_from_abs_time(0.8999f);
|
||||
CHECK(sync_time == doctest::Approx(1.999).epsilon(0.001));
|
||||
}
|
||||
|
||||
WHEN("Querying absolute time at 1.9999s") {
|
||||
float sync_time = track.calc_sync_from_abs_time(1.9999f);
|
||||
CHECK(sync_time == doctest::Approx(0.84615384).epsilon(0.001));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("[SyncedAnimationGraph][SyncTrack] Sync Track blending") {
|
||||
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 });
|
||||
|
||||
WHEN("Calculating A's durations") {
|
||||
CHECK(track_a.interval_duration_ratio[0] == doctest::Approx(0.3));
|
||||
CHECK(track_a.interval_duration_ratio[1] == doctest::Approx(0.6));
|
||||
CHECK(track_a.interval_duration_ratio[2] == doctest::Approx(0.1));
|
||||
}
|
||||
|
||||
WHEN("Calculating B's durations") {
|
||||
CHECK(track_b.interval_duration_ratio[0] == doctest::Approx(0.2));
|
||||
CHECK(track_b.interval_duration_ratio[1] == doctest::Approx(0.3));
|
||||
CHECK(track_b.interval_duration_ratio[2] == doctest::Approx(0.5));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Blending with weight 0.2") {
|
||||
float weight = 0.2f;
|
||||
SyncTrack blended = SyncTrack::blend(weight, track_a, track_b);
|
||||
|
||||
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));
|
||||
REQUIRE(
|
||||
blended.interval_duration_ratio[1] == (1.0f - weight) * (track_a.interval_duration_ratio[1]) + weight * (track_b.interval_duration_ratio[1]));
|
||||
REQUIRE(
|
||||
blended.interval_duration_ratio[2] == (1.0f - weight) * (track_a.interval_duration_ratio[2]) + weight * (track_b.interval_duration_ratio[2]));
|
||||
}
|
||||
|
||||
WHEN("Inverted blending with weight 0.2") {
|
||||
float weight = 0.2f;
|
||||
SyncTrack blended = SyncTrack::blend(weight, track_b, track_a);
|
||||
|
||||
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));
|
||||
REQUIRE(
|
||||
blended.interval_duration_ratio[1] == (1.0f - weight) * (track_b.interval_duration_ratio[1]) + weight * (track_a.interval_duration_ratio[1]));
|
||||
REQUIRE(
|
||||
blended.interval_duration_ratio[2] == (1.0f - weight) * (track_b.interval_duration_ratio[2]) + weight * (track_a.interval_duration_ratio[2]));
|
||||
}
|
||||
}
|
||||
|
||||
} //namespace TestSyncedAnimationGraph
|
||||
Loading…
x
Reference in New Issue
Block a user