concinnity_core/gfx/
root_motion.rs1use alloc::vec::Vec;
8
9use crate::math::vec3::{lerp, sub};
10use crate::math::{floor, rem_euclid};
11
12#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Serialize, serde::Deserialize)]
15pub struct RootKey {
16 pub time: f32,
18 pub translation: [f32; 3],
20}
21
22#[derive(Debug, Clone, Default)]
26pub struct RootTrack {
27 pub keys: Vec<RootKey>,
29}
30
31impl RootTrack {
32 pub fn sample(&self, t: f32) -> [f32; 3] {
35 match self.keys.as_slice() {
36 [] => [0.0; 3],
37 [only] => only.translation,
38 keys => {
39 if t <= keys[0].time {
40 return keys[0].translation;
41 }
42 let last = keys[keys.len() - 1];
43 if t >= last.time {
44 return last.translation;
45 }
46 for w in keys.windows(2) {
47 let (a, b) = (w[0], w[1]);
48 if t >= a.time && t <= b.time {
49 let span = (b.time - a.time).max(1e-6);
50 let f = (t - a.time) / span;
51 return lerp(a.translation, b.translation, f);
52 }
53 }
54 last.translation
55 }
56 }
57 }
58
59 pub fn delta(&self, t0: f32, t1: f32, duration: f32, looping: bool) -> [f32; 3] {
65 if !looping || duration <= 1e-6 {
66 return sub(self.sample(t1), self.sample(t0));
67 }
68 let cycles = floor(t1 / duration) - floor(t0 / duration);
69 let per_cycle = sub(self.sample(duration), self.sample(0.0));
70 let within = sub(
71 self.sample(rem_euclid(t1, duration)),
72 self.sample(rem_euclid(t0, duration)),
73 );
74 [
75 within[0] + cycles * per_cycle[0],
76 within[1] + cycles * per_cycle[1],
77 within[2] + cycles * per_cycle[2],
78 ]
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use alloc::vec;
86
87 fn walk_x() -> RootTrack {
89 RootTrack {
90 keys: vec![
91 RootKey {
92 time: 0.0,
93 translation: [0.0; 3],
94 },
95 RootKey {
96 time: 1.0,
97 translation: [2.0, 0.0, 0.0],
98 },
99 ],
100 }
101 }
102
103 #[test]
104 fn sample_clamps_and_lerps() {
105 let track = walk_x();
106 assert_eq!(track.sample(-1.0), [0.0; 3]);
107 assert_eq!(track.sample(2.0), [2.0, 0.0, 0.0]);
108 assert!((track.sample(0.25)[0] - 0.5).abs() < 1e-6);
109 assert_eq!(RootTrack::default().sample(0.5), [0.0; 3]);
110 }
111
112 #[test]
113 fn delta_within_one_cycle() {
114 let track = walk_x();
115 let d = track.delta(0.25, 0.75, 1.0, true);
116 assert!((d[0] - 1.0).abs() < 1e-6);
117 }
118
119 #[test]
120 fn delta_across_a_wrap_adds_the_cycle_displacement() {
121 let track = walk_x();
122 let d = track.delta(0.75, 1.25, 1.0, true);
124 assert!((d[0] - 1.0).abs() < 1e-5, "{d:?}");
125 }
126
127 #[test]
128 fn delta_across_multiple_wraps_loses_no_ground() {
129 let track = walk_x();
130 let d = track.delta(0.1, 3.5, 1.0, true);
132 assert!((d[0] - 6.8).abs() < 1e-5, "{d:?}");
133 }
134
135 #[test]
136 fn non_looping_delta_clamps_at_the_end() {
137 let track = walk_x();
138 let d = track.delta(0.5, 5.0, 1.0, false);
139 assert!((d[0] - 1.0).abs() < 1e-6, "holds the final key: {d:?}");
140 }
141
142 #[test]
143 fn zero_duration_degrades_to_clamped_endpoint_sampling() {
144 let track = walk_x();
145 let d = track.delta(0.0, 1.0, 0.0, true);
148 assert!((d[0] - 2.0).abs() < 1e-6, "{d:?}");
149 }
150}