1use std::collections::BTreeMap;
5
6use crate::format::{AnimFile, ClipDef, Ease, KeyDef, Loop, TracksDef};
7use crate::math::{Vec3, rad};
8
9#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct BonePose {
12 pub rotation: Vec3,
13 pub position: Vec3,
14 pub scale: Vec3,
15}
16
17impl Default for BonePose {
18 fn default() -> Self {
19 Self {
20 rotation: Vec3::ZERO,
21 position: Vec3::ZERO,
22 scale: Vec3::ONE,
23 }
24 }
25}
26
27impl BonePose {
28 pub fn add(&mut self, other: &BonePose, weight: f32) {
31 self.rotation = self.rotation + other.rotation * weight;
32 self.position = self.position + other.position * weight;
33 self.scale = self.scale.scale(Vec3::ONE.lerp(other.scale, weight));
34 }
35
36 pub fn lerp(&self, to: &BonePose, t: f32) -> BonePose {
38 BonePose {
39 rotation: self.rotation.lerp(to.rotation, t),
40 position: self.position.lerp(to.position, t),
41 scale: self.scale.lerp(to.scale, t),
42 }
43 }
44}
45
46#[derive(Debug, Clone, Default, PartialEq)]
49pub struct Pose(pub BTreeMap<String, BonePose>);
50
51impl Pose {
52 pub fn get(&self, bone: &str) -> BonePose {
53 self.0.get(bone).copied().unwrap_or_default()
54 }
55
56 pub fn bone(&mut self, bone: &str) -> &mut BonePose {
58 self.0.entry(bone.to_owned()).or_default()
59 }
60
61 pub fn add(&mut self, other: &Pose, weight: f32) {
63 for (name, p) in &other.0 {
64 self.bone(name).add(p, weight);
65 }
66 }
67}
68
69#[derive(Debug, Clone, Default)]
71struct Track(Vec<(f32, Vec3, Ease)>);
72
73impl Track {
74 fn new(keys: &[(String, KeyDef)], f: impl Fn(Vec3) -> Vec3) -> Result<Track, String> {
75 let mut out = Vec::with_capacity(keys.len());
76 for (t, k) in keys {
77 let time: f32 = t
78 .trim()
79 .parse()
80 .map_err(|_| format!("`{t}` isn't a time in seconds"))?;
81 out.push((time, f(Vec3::from_array(k.value)), k.ease));
82 }
83 out.sort_by(|a, b| a.0.total_cmp(&b.0));
84 Ok(Track(out))
85 }
86
87 fn sample(&self, t: f32) -> Option<Vec3> {
88 let k = &self.0;
89 let first = k.first()?;
90 if k.len() == 1 || t <= first.0 {
91 return Some(first.1);
92 }
93 let last = k.last()?;
94 if t >= last.0 {
95 return Some(last.1);
96 }
97 let i = k.iter().rposition(|key| key.0 <= t)?;
98 let (a, b) = (&k[i], &k[i + 1]);
99 let span = (b.0 - a.0).max(1e-6);
100 let s = (t - a.0) / span;
101 Some(match a.2 {
102 Ease::Step => a.1,
103 Ease::Linear => a.1.lerp(b.1, s),
104 Ease::Smooth => {
105 let p0 = if i > 0 { k[i - 1].1 } else { a.1 };
106 let p3 = k.get(i + 2).map_or(b.1, |n| n.1);
107 catmull_rom(p0, a.1, b.1, p3, s)
108 }
109 })
110 }
111}
112
113const CATMULL_ROM: [[f32; 4]; 4] = [
116 [0.0, 2.0, 0.0, 0.0],
117 [-1.0, 0.0, 1.0, 0.0],
118 [2.0, -5.0, 4.0, -1.0],
119 [-1.0, 3.0, -3.0, 1.0],
120];
121
122fn catmull_rom(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
123 let (points, powers) = ([p0, p1, p2, p3], [1.0, t, t * t, t * t * t]);
124 let doubled = CATMULL_ROM
125 .iter()
126 .zip(powers)
127 .fold(Vec3::ZERO, |sum, (row, tp)| {
128 sum + row
129 .iter()
130 .zip(points)
131 .fold(Vec3::ZERO, |s, (&w, p)| s + p * w)
132 * tp
133 });
134 doubled * 0.5
135}
136
137#[derive(Debug, Clone, Default)]
138struct Tracks {
139 rotation: Track,
140 position: Track,
141 scale: Track,
142}
143
144impl Tracks {
145 fn new(d: &TracksDef) -> Result<Tracks, String> {
146 Ok(Tracks {
147 rotation: Track::new(&d.rotation.0, rad)?,
148 position: Track::new(&d.position.0, |v| v)?,
149 scale: Track::new(&d.scale.0, |v| v)?,
150 })
151 }
152}
153
154#[derive(Debug, Clone)]
156pub struct Clip {
157 pub name: String,
158 pub length: f32,
160 pub repeat: Loop,
161 bones: Vec<(String, Tracks)>,
162}
163
164impl Clip {
165 pub fn new(name: &str, d: &ClipDef) -> Result<Clip, String> {
166 let bones = d
167 .bones
168 .0
169 .iter()
170 .map(|(b, t)| {
171 Tracks::new(t)
172 .map(|t| (b.clone(), t))
173 .map_err(|e| format!("clip `{name}`, bone `{b}`: {e}"))
174 })
175 .collect::<Result<_, _>>()?;
176 Ok(Clip {
177 name: name.to_owned(),
178 length: d.length.max(0.0),
179 repeat: d.repeat,
180 bones,
181 })
182 }
183
184 pub fn local_time(&self, t: f32) -> Option<f32> {
187 if self.length <= 0.0 {
188 return Some(0.0);
189 }
190 match self.repeat {
191 Loop::Loop => Some(t.rem_euclid(self.length)),
192 Loop::Hold => Some(t.clamp(0.0, self.length)),
193 Loop::Once => (t <= self.length).then_some(t.max(0.0)),
194 }
195 }
196
197 pub fn finished(&self, t: f32) -> bool {
199 self.local_time(t).is_none()
200 }
201
202 pub fn sample(&self, t: f32) -> Pose {
204 let mut pose = Pose::default();
205 let Some(t) = self.local_time(t) else {
206 return pose;
207 };
208 for (bone, tracks) in &self.bones {
209 let p = pose.bone(bone);
210 if let Some(r) = tracks.rotation.sample(t) {
211 p.rotation = r;
212 }
213 if let Some(m) = tracks.position.sample(t) {
214 p.position = m;
215 }
216 if let Some(s) = tracks.scale.sample(t) {
217 p.scale = s;
218 }
219 }
220 pose
221 }
222}
223
224#[derive(Debug, Clone, Default)]
226pub struct Clips(pub Vec<Clip>);
227
228impl Clips {
229 pub fn new(file: &AnimFile) -> Result<Clips, String> {
230 file.0
231 .0
232 .iter()
233 .map(|(n, c)| Clip::new(n, c))
234 .collect::<Result<_, _>>()
235 .map(Clips)
236 }
237
238 pub fn get(&self, name: &str) -> Option<&Clip> {
239 self.0.iter().find(|c| c.name == name)
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use yog_data::Ordered;
247
248 fn clip(repeat: Loop, ease: Ease) -> Clip {
249 let rot = Ordered(vec![
250 (
251 "0".into(),
252 KeyDef {
253 value: [0.0, 0.0, 0.0],
254 ease,
255 },
256 ),
257 (
258 "1".into(),
259 KeyDef {
260 value: [90.0, 0.0, 0.0],
261 ease,
262 },
263 ),
264 ]);
265 let def = ClipDef {
266 length: 1.0,
267 repeat,
268 bones: Ordered(vec![(
269 "arm".into(),
270 TracksDef {
271 rotation: rot,
272 ..Default::default()
273 },
274 )]),
275 };
276 Clip::new("swing", &def).unwrap()
277 }
278
279 #[test]
280 fn keys_are_blended() {
281 let r = clip(Loop::Once, Ease::Linear)
282 .sample(0.5)
283 .get("arm")
284 .rotation;
285 assert!((r.x - std::f32::consts::FRAC_PI_4).abs() < 1e-5);
286 let s = clip(Loop::Once, Ease::Step).sample(0.5).get("arm").rotation;
287 assert_eq!(s.x, 0.0);
288 }
289
290 #[test]
291 fn loops() {
292 let c = clip(Loop::Loop, Ease::Linear);
293 assert!(
294 (c.sample(1.5).get("arm").rotation.x - c.sample(0.5).get("arm").rotation.x).abs()
295 < 1e-5
296 );
297 assert!(clip(Loop::Once, Ease::Linear).finished(1.5));
298 assert!(!clip(Loop::Hold, Ease::Linear).finished(1.5));
299 }
300
301 #[test]
302 fn smooth_keys_pass_through_their_values() {
303 let c = clip(Loop::Once, Ease::Smooth);
304 assert!(c.sample(1.0).get("arm").rotation.x > 1.5);
305 assert_eq!(c.sample(0.0).get("arm").rotation.x, 0.0);
306 }
307}