1use alloc::string::String;
12use alloc::vec::Vec;
13
14use crate::gfx::render_types::MAX_JOINTS;
15use crate::gfx::root_motion::RootTrack;
16use crate::gfx::transform::{
17 IDENTITY, Mat4, compose, mat4_affine_inverse, mat4_mul, quat_from_mat3, quat_slerp,
18 quat_to_mat3, rotation_mat3, trs_matrix,
19};
20use crate::math::rem_euclid;
21
22#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
25#[serde(default)]
26pub struct JointPose {
27 pub translation: [f32; 3],
29 pub rotation_deg: [f32; 3],
31 pub scale: [f32; 3],
33}
34
35impl Default for JointPose {
36 fn default() -> Self {
37 Self {
38 translation: [0.0, 0.0, 0.0],
39 rotation_deg: [0.0, 0.0, 0.0],
40 scale: [1.0, 1.0, 1.0],
41 }
42 }
43}
44
45impl JointPose {
46 pub fn to_matrix(&self) -> Mat4 {
48 trs_matrix(self.translation, self.rotation_deg, self.scale)
49 }
50
51 pub fn blend_matrix(&self, other: &JointPose, f: f32) -> Mat4 {
60 let mix = |a: [f32; 3], b: [f32; 3]| {
61 [
62 a[0] + (b[0] - a[0]) * f,
63 a[1] + (b[1] - a[1]) * f,
64 a[2] + (b[2] - a[2]) * f,
65 ]
66 };
67 let qa = quat_from_mat3(rotation_mat3(self.rotation_deg));
68 let qb = quat_from_mat3(rotation_mat3(other.rotation_deg));
69 let rotation = quat_to_mat3(quat_slerp(qa, qb, f));
70 compose(
71 rotation,
72 mix(self.scale, other.scale),
73 mix(self.translation, other.translation),
74 )
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct Joint {
81 pub name: String,
85 pub parent: Option<usize>,
88 pub bind: JointPose,
90}
91
92#[derive(Debug, Clone)]
95pub struct Skeleton {
96 joints: Vec<Joint>,
97 bind_locals: Vec<Mat4>,
101 inverse_bind: Vec<Mat4>,
103 bind_positions: Vec<[f32; 3]>,
107}
108
109impl Skeleton {
110 pub fn new(joints: Vec<Joint>) -> Self {
114 let bind_locals: Vec<Mat4> = joints.iter().map(|j| j.bind.to_matrix()).collect();
115 let mut world_bind: Vec<Mat4> = Vec::with_capacity(joints.len());
116 for (i, joint) in joints.iter().enumerate() {
117 let local = bind_locals[i];
118 let world = match joint.parent {
119 Some(p) if p < i => mat4_mul(world_bind[p], local),
120 _ => local,
121 };
122 world_bind.push(world);
123 }
124 let inverse_bind = world_bind.iter().map(|m| mat4_affine_inverse(*m)).collect();
125 let bind_positions = world_bind
126 .iter()
127 .map(|m| [m[3][0], m[3][1], m[3][2]])
128 .collect();
129 Self {
130 joints,
131 bind_locals,
132 inverse_bind,
133 bind_positions,
134 }
135 }
136
137 pub fn len(&self) -> usize {
139 self.joints.len()
140 }
141
142 pub fn is_empty(&self) -> bool {
144 self.joints.is_empty()
145 }
146
147 pub fn joints(&self) -> &[Joint] {
149 &self.joints
150 }
151
152 pub fn joint_index(&self, name: &str) -> Option<usize> {
155 (!name.is_empty()).then(|| self.joints.iter().position(|j| j.name == name))?
156 }
157
158 pub fn bind_position(&self, joint: usize) -> [f32; 3] {
160 self.bind_positions.get(joint).copied().unwrap_or([0.0; 3])
161 }
162
163 pub fn world_matrices_into(&self, local_poses: &[Mat4], out: &mut Vec<Mat4>) {
169 out.clear();
170 out.reserve(self.joints.len());
171 for (i, joint) in self.joints.iter().enumerate() {
172 let local = local_poses.get(i).copied().unwrap_or(self.bind_locals[i]);
173 let world_mat = match joint.parent {
174 Some(p) if p < i => mat4_mul(out[p], local),
175 _ => local,
176 };
177 out.push(world_mat);
178 }
179 }
180
181 pub fn skinning_matrices_into(&self, local_poses: &[Mat4], out: &mut Vec<Mat4>) {
190 self.world_matrices_into(local_poses, out);
191 let n = out.len().min(self.inverse_bind.len()).min(MAX_JOINTS);
192 for (i, ib) in self.inverse_bind[..n].iter().enumerate() {
193 out[i] = mat4_mul(out[i], *ib);
194 }
195 out.truncate(n);
196 if out.is_empty() {
197 out.push(IDENTITY);
198 }
199 }
200
201 pub fn bind_skinning_matrices(&self) -> Vec<Mat4> {
205 let mut out = Vec::new();
206 self.skinning_matrices_into(&self.bind_locals, &mut out);
207 out
208 }
209
210 pub fn bind_locals(&self) -> &[Mat4] {
213 &self.bind_locals
214 }
215}
216
217#[derive(Debug, Clone, Copy)]
219pub struct Keyframe {
220 pub time: f32,
222 pub pose: JointPose,
224}
225
226#[derive(Debug, Clone)]
228pub struct JointTrack {
229 pub joint: usize,
231 pub keys: Vec<Keyframe>,
233}
234
235impl JointTrack {
236 fn sample(&self, t: f32) -> Mat4 {
240 match self.keys.as_slice() {
241 [] => IDENTITY,
242 [only] => only.pose.to_matrix(),
243 keys => {
244 if t <= keys[0].time {
245 return keys[0].pose.to_matrix();
246 }
247 let last = keys[keys.len() - 1];
248 if t >= last.time {
249 return last.pose.to_matrix();
250 }
251 let i = keys.partition_point(|k| k.time < t);
254 let (a, b) = (keys[i - 1], keys[i]);
255 let span = (b.time - a.time).max(1e-6);
256 let f = (t - a.time) / span;
257 a.pose.blend_matrix(&b.pose, f)
258 }
259 }
260 }
261}
262
263#[derive(Debug, Clone)]
265pub struct AnimationClip {
266 pub duration: f32,
268 pub looping: bool,
270 pub tracks: Vec<JointTrack>,
272 pub morph_keys: Vec<(f32, Vec<f32>)>,
275 pub root: Option<RootTrack>,
280}
281
282impl AnimationClip {
283 pub fn sample_into(&self, t: f32, skeleton: &Skeleton, out: &mut Vec<Mat4>) {
287 self.sample_looped_into(t, self.looping, skeleton, out)
288 }
289
290 pub fn sample_looped_into(
294 &self,
295 t: f32,
296 looping: bool,
297 skeleton: &Skeleton,
298 out: &mut Vec<Mat4>,
299 ) {
300 let local_t = self.clip_time(t, looping);
301 out.clear();
302 out.extend_from_slice(skeleton.bind_locals());
303 for track in &self.tracks {
304 if track.joint < out.len() {
305 out[track.joint] = track.sample(local_t);
306 }
307 }
308 }
309
310 pub fn sample_morph_weights_into(&self, t: f32, looping: bool, out: &mut Vec<f32>) {
315 out.clear();
316 if self.morph_keys.is_empty() {
317 return;
318 }
319 let local_t = self.clip_time(t, looping);
320 let first = &self.morph_keys[0];
321 if local_t <= first.0 {
322 out.extend_from_slice(&first.1);
323 return;
324 }
325 for pair in self.morph_keys.windows(2) {
326 if local_t <= pair[1].0 {
327 let span = (pair[1].0 - pair[0].0).max(1e-6);
328 let f = (local_t - pair[0].0) / span;
329 let n = pair[0].1.len().max(pair[1].1.len());
330 out.extend((0..n).map(|i| {
331 let a = pair[0].1.get(i).copied().unwrap_or(0.0);
332 let b = pair[1].1.get(i).copied().unwrap_or(0.0);
333 a + (b - a) * f
334 }));
335 return;
336 }
337 }
338 out.extend_from_slice(&self.morph_keys[self.morph_keys.len() - 1].1);
339 }
340
341 fn clip_time(&self, t: f32, looping: bool) -> f32 {
344 if looping && self.duration > 1e-6 {
345 rem_euclid(t, self.duration)
346 } else {
347 t.clamp(0.0, self.duration)
348 }
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use crate::gfx::transform::blend_matrices;
356 use crate::math::atan2;
357 use alloc::vec;
358
359 fn approx(a: f32, b: f32) -> bool {
360 (a - b).abs() < 1e-4
361 }
362
363 fn chain() -> Skeleton {
365 Skeleton::new(vec![
366 Joint {
367 name: String::new(),
368 parent: None,
369 bind: JointPose::default(),
370 },
371 Joint {
372 name: String::new(),
373 parent: Some(0),
374 bind: JointPose {
375 translation: [0.0, 1.0, 0.0],
376 ..JointPose::default()
377 },
378 },
379 ])
380 }
381
382 #[test]
383 fn morph_weight_sampling_lerps_clamps_and_loops() {
384 let clip = AnimationClip {
385 duration: 1.0,
386 looping: false,
387 tracks: Vec::new(),
388 morph_keys: vec![(0.0, vec![0.0, 1.0]), (1.0, vec![1.0, 0.0])],
389 root: None,
390 };
391 let morph = |t: f32, looping: bool| {
392 let mut out = Vec::new();
393 clip.sample_morph_weights_into(t, looping, &mut out);
394 out
395 };
396 assert!(morph(-1.0, false)[0].abs() < 1e-6);
397 let mid = morph(0.5, false);
398 assert!(approx(mid[0], 0.5) && approx(mid[1], 0.5));
399 assert!(approx(morph(5.0, false)[0], 1.0), "clamps past the end");
400 let wrapped = morph(1.25, true);
402 assert!(approx(wrapped[0], 0.25));
403
404 let empty = AnimationClip {
405 duration: 1.0,
406 looping: true,
407 tracks: Vec::new(),
408 morph_keys: Vec::new(),
409 root: None,
410 };
411 let mut out = vec![9.0];
412 empty.sample_morph_weights_into(0.5, true, &mut out);
413 assert!(out.is_empty(), "no morph keys clears the output");
414 }
415
416 #[test]
417 fn bind_pose_skinning_matrices_are_identity() {
418 let sk = chain();
419 for m in sk.bind_skinning_matrices() {
420 for col in 0..4 {
421 for row in 0..4 {
422 assert!(approx(m[col][row], IDENTITY[col][row]));
423 }
424 }
425 }
426 }
427
428 #[test]
429 fn rotating_child_joint_moves_a_bound_point() {
430 let sk = chain();
435 let mut locals: Vec<Mat4> = sk.joints().iter().map(|j| j.bind.to_matrix()).collect();
436 locals[1] = JointPose {
437 translation: [0.0, 1.0, 0.0],
438 rotation_deg: [0.0, 90.0, 0.0],
439 scale: [1.0, 1.0, 1.0],
440 }
441 .to_matrix();
442 let mut skin = Vec::new();
443 sk.skinning_matrices_into(&locals, &mut skin);
444 let p = [1.0f32, 1.0, 0.0, 1.0];
446 let m = skin[1];
447 let out = [
448 m[0][0] * p[0] + m[1][0] * p[1] + m[2][0] * p[2] + m[3][0] * p[3],
449 m[0][1] * p[0] + m[1][1] * p[1] + m[2][1] * p[2] + m[3][1] * p[3],
450 m[0][2] * p[0] + m[1][2] * p[1] + m[2][2] * p[2] + m[3][2] * p[3],
451 ];
452 assert!(approx(out[0], 0.0), "x was {}", out[0]);
454 assert!(approx(out[1], 1.0), "y was {}", out[1]);
455 assert!(approx(out[2], -1.0), "z was {}", out[2]);
456 }
457
458 #[test]
459 fn clip_sampling_interpolates_between_keys() {
460 let sk = chain();
461 let clip = AnimationClip {
462 root: None,
463 duration: 2.0,
464 looping: true,
465 tracks: vec![JointTrack {
466 joint: 1,
467 keys: vec![
468 Keyframe {
469 time: 0.0,
470 pose: JointPose {
471 translation: [0.0, 1.0, 0.0],
472 ..JointPose::default()
473 },
474 },
475 Keyframe {
476 time: 2.0,
477 pose: JointPose {
478 translation: [0.0, 1.0, 0.0],
479 rotation_deg: [0.0, 90.0, 0.0],
480 ..JointPose::default()
481 },
482 },
483 ],
484 }],
485 morph_keys: Vec::new(),
486 };
487 let mut locals = Vec::new();
489 clip.sample_into(1.0, &sk, &mut locals);
490 let yaw = atan2(-locals[1][0][2], locals[1][0][0]).to_degrees();
492 assert!(approx(yaw, 45.0), "yaw was {}", yaw);
493 }
494
495 #[test]
496 fn many_key_track_samples_the_containing_segment() {
497 let keys: Vec<Keyframe> = (0..=20)
501 .map(|i| {
502 let time = i as f32 * 0.1;
503 Keyframe {
504 time,
505 pose: JointPose {
506 translation: [time, 0.0, 0.0],
507 ..JointPose::default()
508 },
509 }
510 })
511 .collect();
512 let track = JointTrack { joint: 0, keys };
513 let x_at = |t: f32| track.sample(t)[3][0];
514 assert!(approx(x_at(-0.5), 0.0), "clamps at the first key");
515 assert!(approx(x_at(5.0), 2.0), "clamps at the last key");
516 assert!(approx(x_at(0.7), 0.7), "exact key hit");
517 assert!(approx(x_at(1.234), 1.234), "lerps inside a segment");
518 }
519
520 #[test]
521 fn looping_clip_wraps_past_duration() {
522 let sk = chain();
523 let clip = AnimationClip {
524 root: None,
525 duration: 2.0,
526 looping: true,
527 tracks: vec![JointTrack {
528 joint: 1,
529 keys: vec![Keyframe {
530 time: 0.5,
531 pose: JointPose {
532 translation: [9.0, 1.0, 0.0],
533 ..JointPose::default()
534 },
535 }],
536 }],
537 morph_keys: Vec::new(),
538 };
539 let mut a = Vec::new();
541 clip.sample_into(0.5, &sk, &mut a);
542 let mut b = Vec::new();
543 clip.sample_into(2.5, &sk, &mut b);
544 assert_eq!(a[1], b[1]);
545 let ptr = a.as_ptr();
547 clip.sample_into(1.5, &sk, &mut a);
548 assert_eq!(a.as_ptr(), ptr, "warm sample buffer is reused in place");
549 }
550
551 #[test]
552 fn unparented_joint_is_treated_as_root() {
553 let sk = Skeleton::new(vec![Joint {
556 name: String::new(),
557 parent: Some(5),
558 bind: JointPose::default(),
559 }]);
560 assert_eq!(sk.len(), 1);
561 assert_eq!(sk.bind_skinning_matrices().len(), 1);
562 }
563
564 #[test]
565 fn joint_index_resolves_names_and_refuses_the_empty_one() {
566 let sk = Skeleton::new(vec![
567 Joint {
568 name: String::from("hips"),
569 parent: None,
570 bind: JointPose::default(),
571 },
572 Joint {
573 name: String::new(),
574 parent: Some(0),
575 bind: JointPose::default(),
576 },
577 ]);
578 assert_eq!(sk.joint_index("hips"), Some(0));
579 assert_eq!(sk.joint_index("missing"), None);
580 assert_eq!(sk.joint_index(""), None);
582 assert!(!sk.is_empty());
583 }
584
585 #[test]
586 fn blend_matrix_endpoints_match_keyframe_poses() {
587 let a = JointPose {
590 translation: [1.0, 2.0, 3.0],
591 rotation_deg: [10.0, 20.0, 30.0],
592 scale: [1.0, 1.5, 2.0],
593 };
594 let b = JointPose {
595 translation: [-4.0, 0.0, 5.0],
596 rotation_deg: [70.0, -40.0, 15.0],
597 scale: [2.0, 1.0, 0.5],
598 };
599 let at0 = a.blend_matrix(&b, 0.0);
600 let at1 = a.blend_matrix(&b, 1.0);
601 let ma = a.to_matrix();
602 let mb = b.to_matrix();
603 for c in 0..4 {
604 for row in 0..4 {
605 assert!(approx(at0[c][row], ma[c][row]), "f=0 [{}][{}]", c, row);
606 assert!(approx(at1[c][row], mb[c][row]), "f=1 [{}][{}]", c, row);
607 }
608 }
609 }
610
611 #[test]
612 fn blend_matrix_lerps_translation_and_scale() {
613 let a = JointPose {
616 translation: [0.0, 0.0, 0.0],
617 rotation_deg: [0.0, 0.0, 0.0],
618 scale: [1.0, 1.0, 1.0],
619 };
620 let b = JointPose {
621 translation: [4.0, 8.0, -2.0],
622 rotation_deg: [0.0, 0.0, 0.0],
623 scale: [3.0, 3.0, 3.0],
624 };
625 let m = a.blend_matrix(&b, 0.25);
626 assert!(approx(m[3][0], 1.0));
627 assert!(approx(m[3][1], 2.0));
628 assert!(approx(m[3][2], -0.5));
629 assert!(approx(m[0][0], 1.5));
631 assert!(approx(m[1][1], 1.5));
632 assert!(approx(m[2][2], 1.5));
633 }
634
635 #[test]
638 fn pose_blend_agrees_with_the_matrix_blend() {
639 let a = JointPose {
640 translation: [1.0, 2.0, 3.0],
641 rotation_deg: [10.0, 20.0, 30.0],
642 scale: [1.0, 1.5, 2.0],
643 };
644 let b = JointPose {
645 translation: [-4.0, 0.0, 5.0],
646 rotation_deg: [70.0, -40.0, 15.0],
647 scale: [2.0, 1.0, 0.5],
648 };
649 for f in [0.0, 0.25, 0.5, 1.0] {
650 let pose_space = a.blend_matrix(&b, f);
651 let matrix_space = blend_matrices(a.to_matrix(), b.to_matrix(), f);
652 for c in 0..4 {
653 for row in 0..4 {
654 assert!(
655 approx(pose_space[c][row], matrix_space[c][row]),
656 "f={f} [{c}][{row}]"
657 );
658 }
659 }
660 }
661 }
662}