Skip to main content

mmd_anim_runtime/
animation.rs

1use glam::{Quat, Vec3A};
2
3use crate::{BoneIndex, MorphIndex, PoseArena};
4
5const BEZIER_ITERATIONS: usize = 12;
6const MMD_INTERPOLATION_SCALE: f32 = 1.0 / 127.0;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub struct InterpolationScalar {
10    pub x1: u8,
11    pub y1: u8,
12    pub x2: u8,
13    pub y2: u8,
14}
15
16impl InterpolationScalar {
17    pub const fn linear() -> Self {
18        Self {
19            x1: 20,
20            y1: 20,
21            x2: 107,
22            y2: 107,
23        }
24    }
25
26    pub fn evaluate(self, x: f32) -> f32 {
27        let x = x.clamp(0.0, 1.0);
28        if x <= 0.0 {
29            return 0.0;
30        }
31        if x >= 1.0 {
32            return 1.0;
33        }
34        if self.x1 == self.y1 && self.x2 == self.y2 {
35            return x;
36        }
37        bezier_interpolation(
38            self.x1 as f32 * MMD_INTERPOLATION_SCALE,
39            self.x2 as f32 * MMD_INTERPOLATION_SCALE,
40            self.y1 as f32 * MMD_INTERPOLATION_SCALE,
41            self.y2 as f32 * MMD_INTERPOLATION_SCALE,
42            x,
43        )
44    }
45}
46
47impl Default for InterpolationScalar {
48    fn default() -> Self {
49        Self::linear()
50    }
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub struct InterpolationVector3 {
55    pub x: InterpolationScalar,
56    pub y: InterpolationScalar,
57    pub z: InterpolationScalar,
58}
59
60impl InterpolationVector3 {
61    pub const fn linear() -> Self {
62        Self {
63            x: InterpolationScalar::linear(),
64            y: InterpolationScalar::linear(),
65            z: InterpolationScalar::linear(),
66        }
67    }
68}
69
70impl Default for InterpolationVector3 {
71    fn default() -> Self {
72        Self::linear()
73    }
74}
75
76#[derive(Clone, Debug)]
77pub struct MovableBoneKeyframe {
78    pub frame: u32,
79    pub position: Vec3A,
80    pub rotation: Quat,
81    pub position_interpolation: InterpolationVector3,
82    pub rotation_interpolation: InterpolationScalar,
83}
84
85impl MovableBoneKeyframe {
86    pub fn new(frame: u32, position: Vec3A, rotation: Quat) -> Self {
87        Self {
88            frame,
89            position,
90            rotation,
91            position_interpolation: InterpolationVector3::linear(),
92            rotation_interpolation: InterpolationScalar::linear(),
93        }
94    }
95}
96
97#[derive(Clone, Debug)]
98pub struct MovableBoneTrack {
99    frame_numbers: Box<[u32]>,
100    positions: Box<[Vec3A]>,
101    rotations: Box<[Quat]>,
102    position_interpolations: Box<[InterpolationVector3]>,
103    rotation_interpolations: Box<[InterpolationScalar]>,
104}
105
106impl MovableBoneTrack {
107    pub fn from_keyframes(mut keyframes: Vec<MovableBoneKeyframe>) -> Self {
108        keyframes.sort_by_key(|keyframe| keyframe.frame);
109
110        let mut frame_numbers = Vec::with_capacity(keyframes.len());
111        let mut positions = Vec::with_capacity(keyframes.len());
112        let mut rotations = Vec::with_capacity(keyframes.len());
113        let mut position_interpolations = Vec::with_capacity(keyframes.len());
114        let mut rotation_interpolations = Vec::with_capacity(keyframes.len());
115
116        for keyframe in keyframes {
117            frame_numbers.push(keyframe.frame);
118            positions.push(keyframe.position);
119            rotations.push(keyframe.rotation.normalize());
120            position_interpolations.push(keyframe.position_interpolation);
121            rotation_interpolations.push(keyframe.rotation_interpolation);
122        }
123
124        Self {
125            frame_numbers: frame_numbers.into_boxed_slice(),
126            positions: positions.into_boxed_slice(),
127            rotations: rotations.into_boxed_slice(),
128            position_interpolations: position_interpolations.into_boxed_slice(),
129            rotation_interpolations: rotation_interpolations.into_boxed_slice(),
130        }
131    }
132
133    pub fn keyframe_count(&self) -> usize {
134        self.frame_numbers.len()
135    }
136
137    /// Returns an owned view of one authored keyframe.
138    ///
139    /// The interpolation fields on the returned keyframe retain the clip's
140    /// incoming-segment convention: for key `i > 0` they describe the segment
141    /// from key `i - 1` to key `i`.  Callers that expose the first key should
142    /// treat its interpolation as having no incoming segment.
143    pub fn keyframe(&self, index: usize) -> Option<MovableBoneKeyframe> {
144        Some(MovableBoneKeyframe {
145            frame: *self.frame_numbers.get(index)?,
146            position: *self.positions.get(index)?,
147            rotation: *self.rotations.get(index)?,
148            position_interpolation: *self.position_interpolations.get(index)?,
149            rotation_interpolation: *self.rotation_interpolations.get(index)?,
150        })
151    }
152
153    pub fn sample(&self, frame: f32) -> Option<(Vec3A, Quat)> {
154        match self.frame_numbers.len() {
155            0 => None,
156            1 => Some((self.positions[0], self.rotations[0])),
157            _ => {
158                let next_index = self.find_next_keyframe(frame);
159                if next_index == 0 {
160                    return Some((self.positions[0], self.rotations[0]));
161                }
162                if next_index >= self.frame_numbers.len() {
163                    let last = self.frame_numbers.len() - 1;
164                    return Some((self.positions[last], self.rotations[last]));
165                }
166
167                let prev_index = next_index - 1;
168                let prev_frame = self.frame_numbers[prev_index] as f32;
169                let next_frame = self.frame_numbers[next_index] as f32;
170                let frame_t = if next_frame == prev_frame {
171                    0.0
172                } else {
173                    ((frame - prev_frame) / (next_frame - prev_frame)).clamp(0.0, 1.0)
174                };
175
176                let interpolation = self.position_interpolations[next_index];
177                let position = Vec3A::new(
178                    lerp(
179                        self.positions[prev_index].x,
180                        self.positions[next_index].x,
181                        interpolation.x.evaluate(frame_t),
182                    ),
183                    lerp(
184                        self.positions[prev_index].y,
185                        self.positions[next_index].y,
186                        interpolation.y.evaluate(frame_t),
187                    ),
188                    lerp(
189                        self.positions[prev_index].z,
190                        self.positions[next_index].z,
191                        interpolation.z.evaluate(frame_t),
192                    ),
193                );
194
195                let rotation_t = self.rotation_interpolations[next_index].evaluate(frame_t);
196                let rotation =
197                    self.rotations[prev_index].slerp(self.rotations[next_index], rotation_t);
198
199                Some((position, rotation))
200            }
201        }
202    }
203
204    fn find_next_keyframe(&self, frame: f32) -> usize {
205        self.frame_numbers
206            .partition_point(|keyframe| (*keyframe as f32) <= frame)
207    }
208
209    pub fn frame_range(&self) -> Option<(u32, u32)> {
210        Some((*self.frame_numbers.first()?, *self.frame_numbers.last()?))
211    }
212}
213
214#[derive(Clone, Debug)]
215pub struct BoneAnimationBinding {
216    pub bone: BoneIndex,
217    pub track: MovableBoneTrack,
218}
219
220#[derive(Clone, Copy, Debug, PartialEq)]
221pub struct MorphKeyframe {
222    pub frame: u32,
223    pub weight: f32,
224}
225
226impl MorphKeyframe {
227    pub fn new(frame: u32, weight: f32) -> Self {
228        Self { frame, weight }
229    }
230}
231
232#[derive(Clone, Debug)]
233pub struct MorphTrack {
234    frame_numbers: Box<[u32]>,
235    weights: Box<[f32]>,
236}
237
238impl MorphTrack {
239    pub fn from_keyframes(mut keyframes: Vec<MorphKeyframe>) -> Self {
240        keyframes.sort_by_key(|keyframe| keyframe.frame);
241        let mut frame_numbers = Vec::with_capacity(keyframes.len());
242        let mut weights = Vec::with_capacity(keyframes.len());
243        for keyframe in keyframes {
244            frame_numbers.push(keyframe.frame);
245            weights.push(keyframe.weight);
246        }
247        Self {
248            frame_numbers: frame_numbers.into_boxed_slice(),
249            weights: weights.into_boxed_slice(),
250        }
251    }
252
253    pub fn keyframe_count(&self) -> usize {
254        self.frame_numbers.len()
255    }
256
257    /// Returns an owned view of one authored morph keyframe.
258    pub fn keyframe(&self, index: usize) -> Option<MorphKeyframe> {
259        Some(MorphKeyframe {
260            frame: *self.frame_numbers.get(index)?,
261            weight: *self.weights.get(index)?,
262        })
263    }
264
265    pub fn sample(&self, frame: f32) -> Option<f32> {
266        match self.frame_numbers.len() {
267            0 => None,
268            1 => Some(self.weights[0]),
269            _ => {
270                let next_index = self
271                    .frame_numbers
272                    .partition_point(|keyframe| (*keyframe as f32) <= frame);
273                if next_index == 0 {
274                    return Some(self.weights[0]);
275                }
276                if next_index >= self.frame_numbers.len() {
277                    return Some(self.weights[self.weights.len() - 1]);
278                }
279
280                let prev_index = next_index - 1;
281                let prev_frame = self.frame_numbers[prev_index] as f32;
282                let next_frame = self.frame_numbers[next_index] as f32;
283                let frame_t = if next_frame == prev_frame {
284                    0.0
285                } else {
286                    ((frame - prev_frame) / (next_frame - prev_frame)).clamp(0.0, 1.0)
287                };
288                Some(lerp(
289                    self.weights[prev_index],
290                    self.weights[next_index],
291                    frame_t,
292                ))
293            }
294        }
295    }
296
297    pub fn frame_range(&self) -> Option<(u32, u32)> {
298        Some((*self.frame_numbers.first()?, *self.frame_numbers.last()?))
299    }
300}
301
302#[derive(Clone, Debug)]
303pub struct MorphAnimationBinding {
304    pub morph: MorphIndex,
305    pub track: MorphTrack,
306}
307
308#[derive(Clone, Debug, PartialEq, Eq)]
309pub struct PropertyKeyframe {
310    pub frame: u32,
311    pub ik_enabled: Box<[u8]>,
312}
313
314impl PropertyKeyframe {
315    pub fn new(frame: u32, ik_enabled: Vec<bool>) -> Self {
316        Self {
317            frame,
318            ik_enabled: ik_enabled
319                .into_iter()
320                .map(u8::from)
321                .collect::<Vec<_>>()
322                .into_boxed_slice(),
323        }
324    }
325}
326
327#[derive(Clone, Debug)]
328pub struct PropertyAnimationBinding {
329    frame_numbers: Box<[u32]>,
330    ik_enabled: Box<[Box<[u8]>]>,
331}
332
333impl PropertyAnimationBinding {
334    pub fn from_keyframes(mut keyframes: Vec<PropertyKeyframe>) -> Self {
335        keyframes.sort_by_key(|keyframe| keyframe.frame);
336
337        let mut frame_numbers = Vec::with_capacity(keyframes.len());
338        let mut ik_enabled = Vec::with_capacity(keyframes.len());
339        for keyframe in keyframes {
340            frame_numbers.push(keyframe.frame);
341            ik_enabled.push(keyframe.ik_enabled);
342        }
343
344        Self {
345            frame_numbers: frame_numbers.into_boxed_slice(),
346            ik_enabled: ik_enabled.into_boxed_slice(),
347        }
348    }
349
350    pub fn keyframe_count(&self) -> usize {
351        self.frame_numbers.len()
352    }
353
354    /// Returns an owned view of one authored property keyframe.
355    pub fn keyframe(&self, index: usize) -> Option<PropertyKeyframe> {
356        Some(PropertyKeyframe {
357            frame: *self.frame_numbers.get(index)?,
358            ik_enabled: self.ik_enabled.get(index)?.clone(),
359        })
360    }
361
362    /// Returns the total number of packed IK state bytes across all keys.
363    pub fn ik_enabled_count(&self) -> usize {
364        self.ik_enabled.iter().map(|state| state.len()).sum()
365    }
366
367    pub fn sample(&self, frame: f32) -> Option<&[u8]> {
368        match self.frame_numbers.len() {
369            0 => None,
370            _ => {
371                let next_index = self
372                    .frame_numbers
373                    .partition_point(|keyframe| (*keyframe as f32) <= frame);
374                if next_index == 0 {
375                    None
376                } else {
377                    Some(&self.ik_enabled[next_index - 1])
378                }
379            }
380        }
381    }
382
383    pub fn frame_range(&self) -> Option<(u32, u32)> {
384        Some((*self.frame_numbers.first()?, *self.frame_numbers.last()?))
385    }
386}
387
388#[derive(Clone, Copy, Debug, PartialEq)]
389pub struct BoneSample {
390    pub bone: BoneIndex,
391    pub position: Vec3A,
392    pub rotation: Quat,
393}
394
395#[derive(Clone, Copy, Debug, PartialEq)]
396pub struct MorphSample {
397    pub morph: MorphIndex,
398    pub weight: f32,
399}
400
401#[derive(Clone, Debug, Default, PartialEq)]
402pub struct ClipSample {
403    bone_samples: Vec<BoneSample>,
404    morph_samples: Vec<MorphSample>,
405    ik_enabled: Option<Vec<u8>>,
406}
407
408impl ClipSample {
409    pub fn with_capacity(bone_capacity: usize, morph_capacity: usize) -> Self {
410        Self {
411            bone_samples: Vec::with_capacity(bone_capacity),
412            morph_samples: Vec::with_capacity(morph_capacity),
413            ik_enabled: None,
414        }
415    }
416
417    pub fn bone_samples(&self) -> &[BoneSample] {
418        &self.bone_samples
419    }
420
421    pub fn morph_samples(&self) -> &[MorphSample] {
422        &self.morph_samples
423    }
424
425    pub fn ik_enabled(&self) -> Option<&[u8]> {
426        self.ik_enabled.as_deref()
427    }
428
429    pub fn apply_to_pose(&self, pose: &mut PoseArena) {
430        pose.reset_local_pose();
431        for sample in self.bone_samples.iter() {
432            pose.set_local_position_offset(sample.bone, sample.position);
433            pose.set_local_rotation(sample.bone, sample.rotation);
434        }
435        for sample in self.morph_samples.iter() {
436            pose.set_morph_weight(sample.morph, sample.weight);
437        }
438        if let Some(ik_enabled) = self.ik_enabled.as_ref() {
439            for (ik_index, enabled) in ik_enabled.iter().enumerate() {
440                pose.set_ik_enabled(ik_index, *enabled != 0);
441            }
442        }
443    }
444}
445
446#[derive(Clone, Copy, Debug, PartialEq)]
447pub struct ClipFrameBounds {
448    pub start: f32,
449    pub end: f32,
450}
451
452impl ClipFrameBounds {
453    pub const fn new(start: f32, end: f32) -> Self {
454        Self { start, end }
455    }
456}
457
458enum ClipSampleEvent<'a> {
459    Bone(BoneIndex, Vec3A, Quat),
460    Morph(MorphIndex, f32),
461    IkEnabled(&'a [u8]),
462}
463
464#[derive(Clone, Debug, Default)]
465pub struct AnimationClip {
466    bone_tracks: Box<[BoneAnimationBinding]>,
467    morph_tracks: Box<[MorphAnimationBinding]>,
468    property_track: Option<PropertyAnimationBinding>,
469}
470
471impl AnimationClip {
472    pub fn new(bone_tracks: Vec<BoneAnimationBinding>) -> Self {
473        Self::new_with_morphs(bone_tracks, Vec::new())
474    }
475
476    pub fn new_with_morphs(
477        bone_tracks: Vec<BoneAnimationBinding>,
478        morph_tracks: Vec<MorphAnimationBinding>,
479    ) -> Self {
480        Self::new_full(bone_tracks, morph_tracks, None)
481    }
482
483    pub fn new_full(
484        bone_tracks: Vec<BoneAnimationBinding>,
485        morph_tracks: Vec<MorphAnimationBinding>,
486        property_track: Option<PropertyAnimationBinding>,
487    ) -> Self {
488        Self {
489            bone_tracks: bone_tracks.into_boxed_slice(),
490            morph_tracks: morph_tracks.into_boxed_slice(),
491            property_track,
492        }
493    }
494
495    pub fn builder() -> AnimationClipBuilder {
496        AnimationClipBuilder::new()
497    }
498
499    pub fn sample_at(&self, frame: f32) -> ClipSample {
500        let mut sample = ClipSample::with_capacity(self.bone_tracks.len(), self.morph_tracks.len());
501        self.sample_into(frame, &mut sample);
502        sample
503    }
504
505    pub fn sample_into(&self, frame: f32, sample: &mut ClipSample) {
506        sample.bone_samples.clear();
507        sample.morph_samples.clear();
508        let mut ik_enabled = sample.ik_enabled.take();
509        let mut has_ik_state = false;
510        self.visit_samples(frame, |event| match event {
511            ClipSampleEvent::Bone(bone, position, rotation) => {
512                sample.bone_samples.push(BoneSample {
513                    bone,
514                    position,
515                    rotation,
516                });
517            }
518            ClipSampleEvent::Morph(morph, weight) => {
519                sample.morph_samples.push(MorphSample { morph, weight });
520            }
521            ClipSampleEvent::IkEnabled(state) => {
522                let buffer = ik_enabled.get_or_insert_with(Vec::new);
523                buffer.clear();
524                buffer.extend_from_slice(state);
525                has_ik_state = true;
526            }
527        });
528        sample.ik_enabled = if has_ik_state { ik_enabled } else { None };
529    }
530
531    pub fn apply_to_pose(&self, frame: f32, pose: &mut PoseArena) {
532        pose.reset_local_pose();
533        self.visit_samples(frame, |event| match event {
534            ClipSampleEvent::Bone(bone, position, rotation) => {
535                pose.set_local_position_offset(bone, position);
536                pose.set_local_rotation(bone, rotation);
537            }
538            ClipSampleEvent::Morph(morph, weight) => {
539                pose.set_morph_weight(morph, weight);
540            }
541            ClipSampleEvent::IkEnabled(ik_enabled) => {
542                for (ik_index, enabled) in ik_enabled.iter().enumerate() {
543                    pose.set_ik_enabled(ik_index, *enabled != 0);
544                }
545            }
546        });
547    }
548
549    fn visit_samples(&self, frame: f32, mut on_event: impl FnMut(ClipSampleEvent<'_>)) {
550        for binding in self.bone_tracks.iter() {
551            if let Some((position, rotation)) = binding.track.sample(frame) {
552                on_event(ClipSampleEvent::Bone(binding.bone, position, rotation));
553            }
554        }
555        for binding in self.morph_tracks.iter() {
556            if let Some(weight) = binding.track.sample(frame) {
557                on_event(ClipSampleEvent::Morph(binding.morph, weight));
558            }
559        }
560        if let Some(ik_enabled) = self
561            .property_track
562            .as_ref()
563            .and_then(|track| track.sample(frame))
564        {
565            on_event(ClipSampleEvent::IkEnabled(ik_enabled));
566        }
567    }
568
569    pub fn bone_tracks(&self) -> &[BoneAnimationBinding] {
570        &self.bone_tracks
571    }
572
573    pub fn morph_tracks(&self) -> &[MorphAnimationBinding] {
574        &self.morph_tracks
575    }
576
577    pub fn property_track(&self) -> Option<&PropertyAnimationBinding> {
578        self.property_track.as_ref()
579    }
580
581    pub fn bone_track_count(&self) -> usize {
582        self.bone_tracks.len()
583    }
584
585    /// Returns one authored local bone track by clip order.
586    pub fn bone_track(&self, index: usize) -> Option<&BoneAnimationBinding> {
587        self.bone_tracks.get(index)
588    }
589
590    pub fn morph_track_count(&self) -> usize {
591        self.morph_tracks.len()
592    }
593
594    /// Returns one authored morph track by clip order.
595    pub fn morph_track(&self, index: usize) -> Option<&MorphAnimationBinding> {
596        self.morph_tracks.get(index)
597    }
598
599    pub fn has_property_track(&self) -> bool {
600        self.property_track.is_some()
601    }
602
603    pub fn frame_range(&self) -> Option<(u32, u32)> {
604        let mut range: Option<(u32, u32)> = None;
605        for binding in self.bone_tracks.iter() {
606            merge_frame_range(&mut range, binding.track.frame_range());
607        }
608        for binding in self.morph_tracks.iter() {
609            merge_frame_range(&mut range, binding.track.frame_range());
610        }
611        if let Some(property_track) = self.property_track.as_ref() {
612            merge_frame_range(&mut range, property_track.frame_range());
613        }
614        range
615    }
616
617    pub fn frame_bounds(&self) -> Option<ClipFrameBounds> {
618        self.frame_range()
619            .map(|(first, last)| ClipFrameBounds::new(first as f32, last as f32))
620    }
621
622    pub fn find_bone_track(&self, bone: BoneIndex) -> Option<&MovableBoneTrack> {
623        self.bone_tracks
624            .iter()
625            .find(|binding| binding.bone == bone)
626            .map(|binding| &binding.track)
627    }
628
629    pub fn find_morph_track(&self, morph: MorphIndex) -> Option<&MorphTrack> {
630        self.morph_tracks
631            .iter()
632            .find(|binding| binding.morph == morph)
633            .map(|binding| &binding.track)
634    }
635}
636
637#[derive(Clone, Debug, Default)]
638pub struct AnimationClipBuilder {
639    bone_tracks: Vec<BoneAnimationBinding>,
640    morph_tracks: Vec<MorphAnimationBinding>,
641    property_track: Option<PropertyAnimationBinding>,
642}
643
644impl AnimationClipBuilder {
645    pub fn new() -> Self {
646        Self::default()
647    }
648
649    pub fn with_bone_track(mut self, binding: BoneAnimationBinding) -> Self {
650        self.bone_tracks.push(binding);
651        self
652    }
653
654    pub fn with_morph_track(mut self, binding: MorphAnimationBinding) -> Self {
655        self.morph_tracks.push(binding);
656        self
657    }
658
659    pub fn with_property_track(mut self, track: PropertyAnimationBinding) -> Self {
660        self.property_track = Some(track);
661        self
662    }
663
664    pub fn push_bone_track(&mut self, binding: BoneAnimationBinding) -> &mut Self {
665        self.bone_tracks.push(binding);
666        self
667    }
668
669    pub fn push_morph_track(&mut self, binding: MorphAnimationBinding) -> &mut Self {
670        self.morph_tracks.push(binding);
671        self
672    }
673
674    pub fn set_property_track(&mut self, track: PropertyAnimationBinding) -> &mut Self {
675        self.property_track = Some(track);
676        self
677    }
678
679    pub fn build(self) -> AnimationClip {
680        AnimationClip::new_full(self.bone_tracks, self.morph_tracks, self.property_track)
681    }
682}
683
684fn merge_frame_range(target: &mut Option<(u32, u32)>, range: Option<(u32, u32)>) {
685    let Some((first, last)) = range else {
686        return;
687    };
688    *target = Some(match *target {
689        Some((current_first, current_last)) => (current_first.min(first), current_last.max(last)),
690        None => (first, last),
691    });
692}
693
694fn lerp(a: f32, b: f32, t: f32) -> f32 {
695    a + (b - a) * t
696}
697
698fn bezier_interpolation(x1: f32, x2: f32, y1: f32, y2: f32, x: f32) -> f32 {
699    let mut c = 0.5;
700    let mut t = c;
701    let mut s = 1.0 - t;
702
703    let mut sst3;
704    let mut stt3;
705    let mut ttt;
706
707    for _ in 0..BEZIER_ITERATIONS {
708        sst3 = 3.0 * s * s * t;
709        stt3 = 3.0 * s * t * t;
710        ttt = t * t * t;
711
712        let ft = sst3 * x1 + stt3 * x2 + ttt - x;
713        if ft == 0.0 {
714            return sst3 * y1 + stt3 * y2 + ttt;
715        }
716
717        c *= 0.5;
718        t += if ft < 0.0 { c } else { -c };
719        s = 1.0 - t;
720    }
721
722    sst3 = 3.0 * s * s * t;
723    stt3 = 3.0 * s * t * t;
724    ttt = t * t * t;
725    sst3 * y1 + stt3 * y2 + ttt
726}
727
728#[cfg(test)]
729mod tests {
730    use glam::{Quat, Vec3A};
731
732    use super::*;
733
734    fn assert_near(actual: f32, expected: f32) {
735        let delta = (actual - expected).abs();
736        assert!(
737            delta < 1.0e-4,
738            "actual={actual:?} expected={expected:?} delta={delta:?}"
739        );
740    }
741
742    fn assert_vec3a_near(actual: Vec3A, expected: Vec3A) {
743        let delta = (actual - expected).abs();
744        assert!(
745            delta.x < 1.0e-4 && delta.y < 1.0e-4 && delta.z < 1.0e-4,
746            "actual={actual:?} expected={expected:?} delta={delta:?}"
747        );
748    }
749
750    #[test]
751    fn linear_interpolation_maps_half_to_half() {
752        assert_near(InterpolationScalar::linear().evaluate(0.5), 0.5);
753    }
754
755    #[test]
756    fn mmd_camera_ease_out_matches_native_subdivision_points() {
757        let interpolation = InterpolationScalar {
758            x1: 0,
759            y1: 127,
760            x2: 127,
761            y2: 127,
762        };
763
764        assert_near(interpolation.evaluate(1.0 / 6.0), 0.5933867);
765        assert_near(interpolation.evaluate(1.0 / 3.0), 0.76974934);
766        assert_near(interpolation.evaluate(0.5), 0.875);
767        assert_near(interpolation.evaluate(2.0 / 3.0), 0.9420012);
768        assert_near(interpolation.evaluate(5.0 / 6.0), 0.9825947);
769    }
770
771    #[test]
772    fn samples_movable_bone_track() {
773        let track = MovableBoneTrack::from_keyframes(vec![
774            MovableBoneKeyframe::new(20, Vec3A::new(10.0, 0.0, 0.0), Quat::IDENTITY),
775            MovableBoneKeyframe::new(10, Vec3A::ZERO, Quat::IDENTITY),
776        ]);
777
778        let (position, rotation) = track.sample(15.0).unwrap();
779
780        assert_vec3a_near(position, Vec3A::new(5.0, 0.0, 0.0));
781        assert_near(rotation.dot(Quat::IDENTITY).abs(), 1.0);
782    }
783
784    #[test]
785    fn samples_morph_track() {
786        let track = MorphTrack::from_keyframes(vec![
787            MorphKeyframe::new(60, 1.0),
788            MorphKeyframe::new(0, 0.0),
789        ]);
790
791        assert_near(track.sample(30.0).unwrap(), 0.5);
792    }
793
794    #[test]
795    fn samples_property_track_as_step_state() {
796        let track = PropertyAnimationBinding::from_keyframes(vec![
797            PropertyKeyframe::new(30, vec![false, true]),
798            PropertyKeyframe::new(0, vec![true, true]),
799        ]);
800
801        assert_eq!(track.sample(-1.0), None);
802        assert_eq!(track.sample(29.0).unwrap(), &[1, 1]);
803        assert_eq!(track.sample(30.0).unwrap(), &[0, 1]);
804        assert_eq!(track.sample(60.0).unwrap(), &[0, 1]);
805    }
806
807    #[test]
808    fn property_track_returns_none_before_first_keyframe() {
809        let track =
810            PropertyAnimationBinding::from_keyframes(vec![PropertyKeyframe::new(30, vec![false])]);
811
812        assert_eq!(track.sample(29.0), None);
813        assert_eq!(track.sample(30.0).unwrap(), &[0]);
814    }
815
816    #[test]
817    fn clip_frame_range_spans_all_track_types() {
818        let bone_track = BoneAnimationBinding {
819            bone: BoneIndex(0),
820            track: MovableBoneTrack::from_keyframes(vec![
821                MovableBoneKeyframe::new(30, Vec3A::ZERO, Quat::IDENTITY),
822                MovableBoneKeyframe::new(10, Vec3A::ZERO, Quat::IDENTITY),
823            ]),
824        };
825        let morph_track = MorphAnimationBinding {
826            morph: MorphIndex(0),
827            track: MorphTrack::from_keyframes(vec![
828                MorphKeyframe::new(20, 0.0),
829                MorphKeyframe::new(60, 1.0),
830            ]),
831        };
832        let property_track = PropertyAnimationBinding::from_keyframes(vec![
833            PropertyKeyframe::new(5, vec![true]),
834            PropertyKeyframe::new(40, vec![false]),
835        ]);
836        let clip =
837            AnimationClip::new_full(vec![bone_track], vec![morph_track], Some(property_track));
838
839        assert_eq!(clip.frame_range(), Some((5, 60)));
840    }
841
842    #[test]
843    fn clip_frame_bounds_match_integer_frame_range() {
844        let clip = AnimationClip::new_full(
845            vec![BoneAnimationBinding {
846                bone: BoneIndex(0),
847                track: MovableBoneTrack::from_keyframes(vec![
848                    MovableBoneKeyframe::new(10, Vec3A::ZERO, Quat::IDENTITY),
849                    MovableBoneKeyframe::new(20, Vec3A::ZERO, Quat::IDENTITY),
850                ]),
851            }],
852            vec![MorphAnimationBinding {
853                morph: MorphIndex(0),
854                track: MorphTrack::from_keyframes(vec![
855                    MorphKeyframe::new(5, 0.0),
856                    MorphKeyframe::new(30, 1.0),
857                ]),
858            }],
859            None,
860        );
861
862        assert_eq!(clip.frame_range(), Some((5, 30)));
863        assert_eq!(clip.frame_bounds(), Some(ClipFrameBounds::new(5.0, 30.0)));
864    }
865
866    #[test]
867    fn clip_builder_matches_full_constructor() {
868        let bone_track = BoneAnimationBinding {
869            bone: BoneIndex(0),
870            track: MovableBoneTrack::from_keyframes(vec![
871                MovableBoneKeyframe::new(0, Vec3A::ZERO, Quat::IDENTITY),
872                MovableBoneKeyframe::new(10, Vec3A::new(10.0, 0.0, 0.0), Quat::IDENTITY),
873            ]),
874        };
875        let morph_track = MorphAnimationBinding {
876            morph: MorphIndex(0),
877            track: MorphTrack::from_keyframes(vec![
878                MorphKeyframe::new(0, 0.0),
879                MorphKeyframe::new(10, 1.0),
880            ]),
881        };
882        let property_track = PropertyAnimationBinding::from_keyframes(vec![
883            PropertyKeyframe::new(0, vec![true, true]),
884            PropertyKeyframe::new(10, vec![false, true]),
885        ]);
886
887        let direct = AnimationClip::new_full(
888            vec![bone_track.clone()],
889            vec![morph_track.clone()],
890            Some(property_track.clone()),
891        );
892        let built = AnimationClip::builder()
893            .with_bone_track(bone_track)
894            .with_morph_track(morph_track)
895            .with_property_track(property_track)
896            .build();
897
898        assert_eq!(built.bone_track_count(), direct.bone_track_count());
899        assert_eq!(built.morph_track_count(), direct.morph_track_count());
900        assert_eq!(built.has_property_track(), direct.has_property_track());
901        assert_eq!(built.frame_range(), direct.frame_range());
902        assert_eq!(built.sample_at(5.0), direct.sample_at(5.0));
903    }
904
905    #[test]
906    fn clip_sample_applies_same_pose_as_clip() {
907        let clip = AnimationClip::new_full(
908            vec![BoneAnimationBinding {
909                bone: BoneIndex(0),
910                track: MovableBoneTrack::from_keyframes(vec![
911                    MovableBoneKeyframe::new(0, Vec3A::ZERO, Quat::IDENTITY),
912                    MovableBoneKeyframe::new(10, Vec3A::new(2.0, 4.0, 6.0), Quat::IDENTITY),
913                ]),
914            }],
915            vec![MorphAnimationBinding {
916                morph: MorphIndex(0),
917                track: MorphTrack::from_keyframes(vec![
918                    MorphKeyframe::new(0, 0.0),
919                    MorphKeyframe::new(10, 1.0),
920                ]),
921            }],
922            Some(PropertyAnimationBinding::from_keyframes(vec![
923                PropertyKeyframe::new(0, vec![true, true]),
924                PropertyKeyframe::new(10, vec![false, true]),
925            ])),
926        );
927
928        let mut from_clip = PoseArena::new_with_counts(1, 1, 2);
929        clip.apply_to_pose(5.0, &mut from_clip);
930
931        let mut from_sample = PoseArena::new_with_counts(1, 1, 2);
932        let sample = clip.sample_at(5.0);
933        sample.apply_to_pose(&mut from_sample);
934
935        assert_vec3a_near(
936            from_sample.local_position_offset(BoneIndex(0)),
937            from_clip.local_position_offset(BoneIndex(0)),
938        );
939        assert_near(
940            from_sample
941                .local_rotation(BoneIndex(0))
942                .dot(from_clip.local_rotation(BoneIndex(0))),
943            1.0,
944        );
945        assert_near(
946            from_sample.morph_weight(MorphIndex(0)),
947            from_clip.morph_weight(MorphIndex(0)),
948        );
949        assert_eq!(from_sample.ik_enabled(), from_clip.ik_enabled());
950    }
951
952    #[test]
953    fn clip_sample_into_reuses_output_and_matches_sample_at() {
954        let clip = AnimationClip::builder()
955            .with_bone_track(BoneAnimationBinding {
956                bone: BoneIndex(0),
957                track: MovableBoneTrack::from_keyframes(vec![
958                    MovableBoneKeyframe::new(0, Vec3A::ZERO, Quat::IDENTITY),
959                    MovableBoneKeyframe::new(10, Vec3A::new(10.0, 0.0, 0.0), Quat::IDENTITY),
960                ]),
961            })
962            .with_morph_track(MorphAnimationBinding {
963                morph: MorphIndex(0),
964                track: MorphTrack::from_keyframes(vec![
965                    MorphKeyframe::new(0, 0.0),
966                    MorphKeyframe::new(10, 1.0),
967                ]),
968            })
969            .with_property_track(PropertyAnimationBinding::from_keyframes(vec![
970                PropertyKeyframe::new(0, vec![true, false]),
971                PropertyKeyframe::new(10, vec![false, true]),
972            ]))
973            .build();
974
975        let expected = clip.sample_at(5.0);
976        let mut sample = ClipSample::with_capacity(1, 1);
977        clip.sample_into(5.0, &mut sample);
978        assert_eq!(sample, expected);
979
980        let bone_capacity = sample.bone_samples.capacity();
981        let morph_capacity = sample.morph_samples.capacity();
982        let ik_capacity = sample
983            .ik_enabled
984            .as_ref()
985            .expect("property sample should include IK state")
986            .capacity();
987        clip.sample_into(0.0, &mut sample);
988        assert!(sample.bone_samples.capacity() >= bone_capacity);
989        assert!(sample.morph_samples.capacity() >= morph_capacity);
990        assert!(
991            sample
992                .ik_enabled
993                .as_ref()
994                .expect("property sample should include IK state")
995                .capacity()
996                >= ik_capacity
997        );
998    }
999
1000    #[test]
1001    fn clip_exposes_track_collections_and_lookup() {
1002        let clip = AnimationClip::builder()
1003            .with_bone_track(BoneAnimationBinding {
1004                bone: BoneIndex(3),
1005                track: MovableBoneTrack::from_keyframes(vec![MovableBoneKeyframe::new(
1006                    12,
1007                    Vec3A::ZERO,
1008                    Quat::IDENTITY,
1009                )]),
1010            })
1011            .with_morph_track(MorphAnimationBinding {
1012                morph: MorphIndex(4),
1013                track: MorphTrack::from_keyframes(vec![MorphKeyframe::new(8, 0.25)]),
1014            })
1015            .with_property_track(PropertyAnimationBinding::from_keyframes(vec![
1016                PropertyKeyframe::new(6, vec![false]),
1017            ]))
1018            .build();
1019
1020        assert_eq!(clip.bone_tracks().len(), 1);
1021        assert_eq!(clip.bone_tracks()[0].bone, BoneIndex(3));
1022        assert_eq!(clip.bone_tracks()[0].track.keyframe_count(), 1);
1023        assert_eq!(clip.bone_tracks()[0].track.frame_range(), Some((12, 12)));
1024        assert_eq!(
1025            clip.find_bone_track(BoneIndex(3)).unwrap().keyframe_count(),
1026            1
1027        );
1028
1029        assert_eq!(clip.morph_tracks().len(), 1);
1030        assert_eq!(clip.morph_tracks()[0].morph, MorphIndex(4));
1031        assert_eq!(clip.morph_tracks()[0].track.keyframe_count(), 1);
1032        assert_eq!(clip.morph_tracks()[0].track.frame_range(), Some((8, 8)));
1033        assert_eq!(
1034            clip.find_morph_track(MorphIndex(4))
1035                .unwrap()
1036                .keyframe_count(),
1037            1
1038        );
1039
1040        let property_track = clip.property_track().unwrap();
1041        assert_eq!(property_track.keyframe_count(), 1);
1042        assert_eq!(property_track.frame_range(), Some((6, 6)));
1043        assert_eq!(property_track.sample(5.0), None);
1044    }
1045
1046    #[test]
1047    fn empty_clip_frame_range_is_none() {
1048        assert_eq!(AnimationClip::default().frame_range(), None);
1049        assert_eq!(AnimationClip::default().frame_bounds(), None);
1050    }
1051}