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 { rotation: Vec3::ZERO, position: Vec3::ZERO, scale: Vec3::ONE }
20    }
21}
22
23impl BonePose {
24    /// `other` laid over this one at `weight` (turns and moves add, scales
25    /// multiply).
26    pub fn add(&mut self, other: &BonePose, weight: f32) {
27        self.rotation = self.rotation + other.rotation * weight;
28        self.position = self.position + other.position * weight;
29        self.scale = self.scale.scale(Vec3::ONE.lerp(other.scale, weight));
30    }
31
32    /// Between this pose and `to`.
33    pub fn lerp(&self, to: &BonePose, t: f32) -> BonePose {
34        BonePose {
35            rotation: self.rotation.lerp(to.rotation, t),
36            position: self.position.lerp(to.position, t),
37            scale: self.scale.lerp(to.scale, t),
38        }
39    }
40}
41
42/// Bones by name → what they do. Names a model lacks are the vanilla
43/// model's parts.
44#[derive(Debug, Clone, Default, PartialEq)]
45pub struct Pose(pub BTreeMap<String, BonePose>);
46
47impl Pose {
48    pub fn get(&self, bone: &str) -> BonePose {
49        self.0.get(bone).copied().unwrap_or_default()
50    }
51
52    /// The bone's pose, to change.
53    pub fn bone(&mut self, bone: &str) -> &mut BonePose {
54        self.0.entry(bone.to_owned()).or_default()
55    }
56
57    /// `other` laid over this pose at `weight`.
58    pub fn add(&mut self, other: &Pose, weight: f32) {
59        for (name, p) in &other.0 {
60            self.bone(name).add(p, weight);
61        }
62    }
63}
64
65/// One track's keyframes, by time.
66#[derive(Debug, Clone, Default)]
67struct Track(Vec<(f32, Vec3, Ease)>);
68
69impl Track {
70    fn new(keys: &[(String, KeyDef)], f: impl Fn(Vec3) -> Vec3) -> Result<Track, String> {
71        let mut out = Vec::with_capacity(keys.len());
72        for (t, k) in keys {
73            let time: f32 = t.trim().parse().map_err(|_| format!("`{t}` isn't a time in seconds"))?;
74            out.push((time, f(Vec3::from_array(k.value)), k.ease));
75        }
76        out.sort_by(|a, b| a.0.total_cmp(&b.0));
77        Ok(Track(out))
78    }
79
80    fn sample(&self, t: f32) -> Option<Vec3> {
81        let k = &self.0;
82        let first = k.first()?;
83        if k.len() == 1 || t <= first.0 {
84            return Some(first.1);
85        }
86        let last = k.last()?;
87        if t >= last.0 {
88            return Some(last.1);
89        }
90        let i = k.iter().rposition(|key| key.0 <= t)?;
91        let (a, b) = (&k[i], &k[i + 1]);
92        let span = (b.0 - a.0).max(1e-6);
93        let s = (t - a.0) / span;
94        Some(match a.2 {
95            Ease::Step => a.1,
96            Ease::Linear => a.1.lerp(b.1, s),
97            Ease::Smooth => {
98                let p0 = if i > 0 { k[i - 1].1 } else { a.1 };
99                let p3 = k.get(i + 2).map_or(b.1, |n| n.1);
100                catmull_rom(p0, a.1, b.1, p3, s)
101            }
102        })
103    }
104}
105
106fn catmull_rom(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
107    let (t2, t3) = (t * t, t * t * t);
108    (p1 * 2.0 + (p2 - p0) * t + (p0 * 2.0 - p1 * 5.0 + p2 * 4.0 - p3) * t2 + (p1 * 3.0 - p0 - p2 * 3.0 + p3) * t3) * 0.5
109}
110
111#[derive(Debug, Clone, Default)]
112struct Tracks {
113    rotation: Track,
114    position: Track,
115    scale: Track,
116}
117
118impl Tracks {
119    fn new(d: &TracksDef) -> Result<Tracks, String> {
120        Ok(Tracks {
121            rotation: Track::new(&d.rotation.0, rad)?,
122            position: Track::new(&d.position.0, |v| v)?,
123            scale: Track::new(&d.scale.0, |v| v)?,
124        })
125    }
126}
127
128/// An animation, ready.
129#[derive(Debug, Clone)]
130pub struct Clip {
131    pub name: String,
132    /// Seconds.
133    pub length: f32,
134    pub repeat: Loop,
135    bones: Vec<(String, Tracks)>,
136}
137
138impl Clip {
139    pub fn new(name: &str, d: &ClipDef) -> Result<Clip, String> {
140        let bones = d
141            .bones
142            .0
143            .iter()
144            .map(|(b, t)| Tracks::new(t).map(|t| (b.clone(), t)).map_err(|e| format!("clip `{name}`, bone `{b}`: {e}")))
145            .collect::<Result<_, _>>()?;
146        Ok(Clip { name: name.to_owned(), length: d.length.max(0.0), repeat: d.repeat, bones })
147    }
148
149    /// The clip's time `t` seconds after it started, by its loop; `None`
150    /// once a `once` clip is over.
151    pub fn local_time(&self, t: f32) -> Option<f32> {
152        if self.length <= 0.0 {
153            return Some(0.0);
154        }
155        match self.repeat {
156            Loop::Loop => Some(t.rem_euclid(self.length)),
157            Loop::Hold => Some(t.clamp(0.0, self.length)),
158            Loop::Once => (t <= self.length).then_some(t.max(0.0)),
159        }
160    }
161
162    /// Whether a `once` clip has ended by `t`.
163    pub fn finished(&self, t: f32) -> bool {
164        self.local_time(t).is_none()
165    }
166
167    /// The pose `t` seconds after the clip started.
168    pub fn sample(&self, t: f32) -> Pose {
169        let mut pose = Pose::default();
170        let Some(t) = self.local_time(t) else { return pose };
171        for (bone, tracks) in &self.bones {
172            let p = pose.bone(bone);
173            if let Some(r) = tracks.rotation.sample(t) {
174                p.rotation = r;
175            }
176            if let Some(m) = tracks.position.sample(t) {
177                p.position = m;
178            }
179            if let Some(s) = tracks.scale.sample(t) {
180                p.scale = s;
181            }
182        }
183        pose
184    }
185}
186
187/// A `.yanim`'s clips.
188#[derive(Debug, Clone, Default)]
189pub struct Clips(pub Vec<Clip>);
190
191impl Clips {
192    pub fn new(file: &AnimFile) -> Result<Clips, String> {
193        file.0.0.iter().map(|(n, c)| Clip::new(n, c)).collect::<Result<_, _>>().map(Clips)
194    }
195
196    pub fn get(&self, name: &str) -> Option<&Clip> {
197        self.0.iter().find(|c| c.name == name)
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use yog_data::Ordered;
205
206    fn clip(repeat: Loop, ease: Ease) -> Clip {
207        let rot = Ordered(vec![
208            ("0".into(), KeyDef { value: [0.0, 0.0, 0.0], ease }),
209            ("1".into(), KeyDef { value: [90.0, 0.0, 0.0], ease }),
210        ]);
211        let def = ClipDef {
212            length: 1.0,
213            repeat,
214            bones: Ordered(vec![("arm".into(), TracksDef { rotation: rot, ..Default::default() })]),
215        };
216        Clip::new("swing", &def).unwrap()
217    }
218
219    #[test]
220    fn keys_are_blended() {
221        let r = clip(Loop::Once, Ease::Linear).sample(0.5).get("arm").rotation;
222        assert!((r.x - std::f32::consts::FRAC_PI_4).abs() < 1e-5);
223        let s = clip(Loop::Once, Ease::Step).sample(0.5).get("arm").rotation;
224        assert_eq!(s.x, 0.0);
225    }
226
227    #[test]
228    fn loops() {
229        let c = clip(Loop::Loop, Ease::Linear);
230        assert!((c.sample(1.5).get("arm").rotation.x - c.sample(0.5).get("arm").rotation.x).abs() < 1e-5);
231        assert!(clip(Loop::Once, Ease::Linear).finished(1.5));
232        assert!(!clip(Loop::Hold, Ease::Linear).finished(1.5));
233    }
234
235    #[test]
236    fn smooth_keys_pass_through_their_values() {
237        let c = clip(Loop::Once, Ease::Smooth);
238        assert!(c.sample(1.0).get("arm").rotation.x > 1.5);
239        assert_eq!(c.sample(0.0).get("arm").rotation.x, 0.0);
240    }
241}