Skip to main content

yog_body/
anim.rs

1//! Clips (`.yanim`) sampled at a time, and the pose they make: per bone a
2//! turn, a move and a scale on top of the bone's rest.
3
4use std::collections::BTreeMap;
5
6use crate::format::{AnimFile, ClipDef, Ease, KeyDef, Loop, TracksDef};
7use crate::math::{Vec3, rad};
8
9/// What a bone does on top of its rest: radians, pixels, a factor.
10#[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    /// `other` laid over this one at `weight` (turns and moves add, scales
29    /// multiply).
30    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    /// Between this pose and `to`.
37    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/// Bones by name → what they do. Names a model lacks are the vanilla
47/// model's parts.
48#[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    /// The bone's pose, to change.
57    pub fn bone(&mut self, bone: &str) -> &mut BonePose {
58        self.0.entry(bone.to_owned()).or_default()
59    }
60
61    /// `other` laid over this pose at `weight`.
62    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/// One track's keyframes, by time.
70#[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
113/// The Catmull-Rom spline's basis, doubled: row `i` weighs the four
114/// points for `tⁱ`.
115const 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/// An animation, ready.
155#[derive(Debug, Clone)]
156pub struct Clip {
157    pub name: String,
158    /// Seconds.
159    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    /// The clip's time `t` seconds after it started, by its loop; `None`
185    /// once a `once` clip is over.
186    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    /// Whether a `once` clip has ended by `t`.
198    pub fn finished(&self, t: f32) -> bool {
199        self.local_time(t).is_none()
200    }
201
202    /// The pose `t` seconds after the clip started.
203    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/// A `.yanim`'s clips.
225#[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}