1use crate::model::{Clip, Interpolation, Skeleton, Track, TrackValues, Transform};
15use glam::{Mat4, Quat, Vec3};
16
17#[derive(Debug)]
19pub struct PoseGrid {
20 pub times: Vec<f32>,
22 bone_count: usize,
23 local: Vec<Transform>,
25 model: Vec<Mat4>,
26 model_rotation: Vec<Quat>,
27}
28
29impl PoseGrid {
30 fn index(&self, frame: usize, bone: usize) -> usize {
31 assert!(
32 frame < self.frame_count(),
33 "frame index {frame} outside PoseGrid frame count {}",
34 self.frame_count()
35 );
36 assert!(
37 bone < self.bone_count,
38 "bone index {bone} outside PoseGrid bone count {}",
39 self.bone_count
40 );
41 frame * self.bone_count + bone
42 }
43
44 pub fn frame_count(&self) -> usize {
46 self.times.len()
47 }
48
49 pub fn bone_count(&self) -> usize {
51 self.bone_count
52 }
53
54 pub fn local(&self, frame: usize, bone: usize) -> Transform {
60 self.local[self.index(frame, bone)]
61 }
62
63 pub fn model(&self, frame: usize, bone: usize) -> Mat4 {
69 self.model[self.index(frame, bone)]
70 }
71
72 pub fn model_position(&self, frame: usize, bone: usize) -> Vec3 {
78 self.model(frame, bone).w_axis.truncate()
79 }
80
81 pub fn model_rotation(&self, frame: usize, bone: usize) -> Quat {
89 self.model_rotation[self.index(frame, bone)]
90 }
91}
92
93pub fn default_frame_count(clip: &Clip) -> usize {
97 clip.tracks
98 .iter()
99 .map(Track::key_count)
100 .max()
101 .unwrap_or(2)
102 .max(2)
103}
104
105pub fn sample_clip(skeleton: &Skeleton, clip: &Clip, frames: usize) -> PoseGrid {
123 let frames = frames.max(2);
124 let duration = clip.duration_s as f32;
125 let times: Vec<f32> = (0..frames)
126 .map(|i| duration * i as f32 / (frames - 1) as f32)
127 .collect();
128
129 sample_clip_at_times(skeleton, clip, times)
130}
131
132pub(crate) fn sample_clip_at_times(skeleton: &Skeleton, clip: &Clip, times: Vec<f32>) -> PoseGrid {
138 let frames = times.len();
139 let nb = skeleton.bones.len();
140
141 let mut local = vec![Transform::IDENTITY; frames * nb];
142 for f in 0..frames {
143 for (b, bone) in skeleton.bones.iter().enumerate() {
144 local[f * nb + b] = bone.rest;
145 }
146 }
147
148 for track in &clip.tracks {
149 if track.times.is_empty() || track.bone >= nb {
150 continue;
151 }
152 for (f, &t) in times.iter().enumerate() {
153 let slot = &mut local[f * nb + track.bone];
154 match &track.values {
155 TrackValues::Vec3s(_) => {
156 let v = sample_vec3(track, t);
157 match track.property {
158 crate::model::Property::Translation => slot.translation = v,
159 crate::model::Property::Scale => slot.scale = v,
160 crate::model::Property::Rotation => {}
161 }
162 }
163 TrackValues::Quats(_) => slot.rotation = sample_quat(track, t),
164 }
165 }
166 }
167
168 let mut model = vec![Mat4::IDENTITY; frames * nb];
169 let mut model_rotation = vec![Quat::IDENTITY; frames * nb];
170 for f in 0..frames {
171 for (b, bone) in skeleton.bones.iter().enumerate() {
172 let m = local[f * nb + b].to_mat4();
173 model[f * nb + b] = match bone.parent {
174 Some(p) => model[f * nb + p] * m,
175 None => m,
176 };
177 let rotation = local[f * nb + b].rotation;
178 model_rotation[f * nb + b] = match bone.parent {
179 Some(p) => model_rotation[f * nb + p] * rotation,
180 None => rotation,
181 };
182 }
183 }
184
185 PoseGrid {
186 times,
187 bone_count: nb,
188 local,
189 model,
190 model_rotation,
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq)]
196pub enum TrackSample {
197 Vec3(Vec3),
199 Quat(Quat),
201}
202
203pub fn sample_track(track: &Track, t: f32) -> TrackSample {
211 match &track.values {
212 TrackValues::Vec3s(_) => TrackSample::Vec3(sample_vec3(track, t)),
213 TrackValues::Quats(_) => TrackSample::Quat(sample_quat(track, t)),
214 }
215}
216
217fn segment(times: &[f32], t: f32) -> (usize, usize, f32) {
225 let n = times.len();
226 if n <= 1 || t <= times[0] {
227 return (0, 0, 0.0);
228 }
229 if t >= times[n - 1] {
230 return (n - 1, n - 1, 0.0);
231 }
232 let k1 = times.partition_point(|&k| k <= t).min(n - 1);
233 if k1 == 0 {
234 return (0, 0, 0.0);
235 }
236 let k0 = k1 - 1;
237 let dt = times[k1] - times[k0];
238 let u = if dt > 0.0 { (t - times[k0]) / dt } else { 0.0 };
239 (k0, k1, u)
240}
241
242fn value_at<T: Copy + Default>(vals: &[T], index: usize) -> T {
246 vals.get(index).copied().unwrap_or_default()
247}
248
249fn hermite(u: f32) -> (f32, f32, f32, f32) {
251 let u2 = u * u;
252 let u3 = u2 * u;
253 (
254 2.0 * u3 - 3.0 * u2 + 1.0, u3 - 2.0 * u2 + u, -2.0 * u3 + 3.0 * u2, u3 - u2, )
259}
260
261fn sample_vec3(track: &Track, t: f32) -> Vec3 {
262 let TrackValues::Vec3s(vals) = &track.values else {
263 return Vec3::ZERO;
264 };
265 let (k0, k1, u) = segment(&track.times, t);
266 let v0 = value_at(vals, track.value_index(k0));
267 if k0 == k1 {
268 return v0;
269 }
270 let v1 = value_at(vals, track.value_index(k1));
271 match track.interpolation {
272 Interpolation::Step => v0,
273 Interpolation::Linear => v0.lerp(v1, u),
274 Interpolation::CubicSpline => {
275 let dt = track.times[k1] - track.times[k0];
276 let m0 = value_at(vals, 3 * k0 + 2) * dt;
278 let m1 = value_at(vals, 3 * k1) * dt;
279 let (h00, h10, h01, h11) = hermite(u);
280 v0 * h00 + m0 * h10 + v1 * h01 + m1 * h11
281 }
282 }
283}
284
285fn sample_quat(track: &Track, t: f32) -> Quat {
286 let TrackValues::Quats(vals) = &track.values else {
287 return Quat::IDENTITY;
288 };
289 let (k0, k1, u) = segment(&track.times, t);
290 let q0: Quat = value_at(vals, track.value_index(k0));
291 if k0 == k1 {
292 return q0.normalize();
293 }
294 let q1 = value_at(vals, track.value_index(k1));
295 match track.interpolation {
296 Interpolation::Step => q0.normalize(),
297 Interpolation::Linear => {
298 let q1 = if q0.dot(q1) < 0.0 { -q1 } else { q1 };
301 q0.normalize().slerp(q1.normalize(), u)
302 }
303 Interpolation::CubicSpline => {
304 let dt = track.times[k1] - track.times[k0];
307 let m0 = value_at(vals, 3 * k0 + 2).to_array();
308 let m1 = value_at(vals, 3 * k1).to_array();
309 let a0 = q0.to_array();
310 let a1 = q1.to_array();
311 let (h00, h10, h01, h11) = hermite(u);
312 let mut out = [0.0f32; 4];
313 for i in 0..4 {
314 out[i] = a0[i] * h00 + m0[i] * dt * h10 + a1[i] * h01 + m1[i] * dt * h11;
315 }
316 Quat::from_array(out).normalize()
317 }
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324 use crate::model::{Bone, Property};
325
326 fn track(times: Vec<f32>, quats: Vec<Quat>) -> Track {
327 Track {
328 bone: 0,
329 property: Property::Rotation,
330 interpolation: Interpolation::Linear,
331 times,
332 values: TrackValues::Quats(quats),
333 }
334 }
335
336 fn vec3_track(interpolation: Interpolation, times: Vec<f32>, vals: Vec<Vec3>) -> Track {
337 Track {
338 bone: 0,
339 property: Property::Translation,
340 interpolation,
341 times,
342 values: TrackValues::Vec3s(vals),
343 }
344 }
345
346 #[test]
347 #[should_panic(expected = "outside PoseGrid bone count")]
348 fn pose_grid_rejects_bone_index_at_frame_boundary() {
349 let skeleton = Skeleton {
350 bones: vec![Bone {
351 name: "root".into(),
352 parent: None,
353 rest: Transform::IDENTITY,
354 inverse_bind: None,
355 }],
356 };
357 let clip = Clip {
358 name: "idle".into(),
359 duration_s: 1.0,
360 tracks: Vec::new(),
361 };
362 let grid = sample_clip(&skeleton, &clip, 2);
363
364 let _ = grid.local(0, grid.bone_count());
365 }
366
367 #[test]
368 fn out_of_range_clip_track_is_skipped() {
369 let skeleton = Skeleton {
370 bones: vec![Bone {
371 name: "root".into(),
372 parent: None,
373 rest: Transform::IDENTITY,
374 inverse_bind: None,
375 }],
376 };
377 let clip = Clip {
378 name: "hostile".into(),
379 duration_s: 1.0,
380 tracks: vec![Track {
381 bone: 1,
382 property: Property::Translation,
383 interpolation: Interpolation::Linear,
384 times: vec![0.0],
385 values: TrackValues::Vec3s(vec![Vec3::splat(42.0)]),
386 }],
387 };
388
389 let grid = sample_clip(&skeleton, &clip, 2);
390 assert_eq!(grid.local(0, 0), Transform::IDENTITY);
391 assert_eq!(grid.local(1, 0), Transform::IDENTITY);
392 }
393
394 #[test]
397 fn nan_first_key_time_samples_without_panicking() {
398 let t = track(
399 vec![f32::NAN, 0.5, 1.0],
400 vec![Quat::IDENTITY, Quat::IDENTITY, Quat::IDENTITY],
401 );
402 for time in [-1.0, 0.0, 0.25, 0.75, 2.0] {
403 let TrackSample::Quat(q) = sample_track(&t, time) else {
404 panic!("rotation track samples a quat");
405 };
406 assert!(q.is_finite() || q.is_nan()); }
408 }
409
410 #[test]
411 fn all_nan_times_sample_without_panicking() {
412 let t = track(
413 vec![f32::NAN, f32::NAN],
414 vec![Quat::IDENTITY, Quat::IDENTITY],
415 );
416 sample_track(&t, 0.5);
417 }
418
419 #[test]
420 fn empty_track_samples_default_without_panicking() {
421 let t = track(vec![], vec![]);
422 let TrackSample::Quat(q) = sample_track(&t, 0.5) else {
423 panic!("rotation track samples a quat");
424 };
425 assert_eq!(q, Quat::IDENTITY.normalize());
426 }
427
428 #[test]
432 fn short_values_sample_default_without_panicking() {
433 let t = track(vec![0.0, 0.5, 1.0], vec![Quat::IDENTITY, Quat::IDENTITY]);
434 sample_track(&t, 0.75); }
436
437 #[test]
438 fn short_vec3_values_sample_default_without_panicking() {
439 let t = vec3_track(Interpolation::Linear, vec![0.0, 0.5, 1.0], vec![Vec3::ONE]);
440 let TrackSample::Vec3(v) = sample_track(&t, 0.75) else {
441 panic!("translation track samples a vec3");
442 };
443 assert!(v.is_finite());
444 }
445
446 #[test]
449 fn short_cubic_values_sample_default_without_panicking() {
450 let t = vec3_track(
451 Interpolation::CubicSpline,
452 vec![0.0, 1.0],
453 vec![Vec3::ZERO, Vec3::ONE, Vec3::ZERO], );
455 sample_track(&t, 0.5);
456 }
457}