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 { rotation: Vec3::ZERO, position: Vec3::ZERO, scale: Vec3::ONE }
20 }
21}
22
23impl BonePose {
24 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 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#[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 pub fn bone(&mut self, bone: &str) -> &mut BonePose {
54 self.0.entry(bone.to_owned()).or_default()
55 }
56
57 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#[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#[derive(Debug, Clone)]
130pub struct Clip {
131 pub name: String,
132 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 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 pub fn finished(&self, t: f32) -> bool {
164 self.local_time(t).is_none()
165 }
166
167 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#[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}