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    pub fn sample(&self, frame: f32) -> Option<f32> {
258        match self.frame_numbers.len() {
259            0 => None,
260            1 => Some(self.weights[0]),
261            _ => {
262                let next_index = self
263                    .frame_numbers
264                    .partition_point(|keyframe| (*keyframe as f32) <= frame);
265                if next_index == 0 {
266                    return Some(self.weights[0]);
267                }
268                if next_index >= self.frame_numbers.len() {
269                    return Some(self.weights[self.weights.len() - 1]);
270                }
271
272                let prev_index = next_index - 1;
273                let prev_frame = self.frame_numbers[prev_index] as f32;
274                let next_frame = self.frame_numbers[next_index] as f32;
275                let frame_t = if next_frame == prev_frame {
276                    0.0
277                } else {
278                    ((frame - prev_frame) / (next_frame - prev_frame)).clamp(0.0, 1.0)
279                };
280                Some(lerp(
281                    self.weights[prev_index],
282                    self.weights[next_index],
283                    frame_t,
284                ))
285            }
286        }
287    }
288
289    pub fn frame_range(&self) -> Option<(u32, u32)> {
290        Some((*self.frame_numbers.first()?, *self.frame_numbers.last()?))
291    }
292}
293
294#[derive(Clone, Debug)]
295pub struct MorphAnimationBinding {
296    pub morph: MorphIndex,
297    pub track: MorphTrack,
298}
299
300#[derive(Clone, Debug, PartialEq, Eq)]
301pub struct PropertyKeyframe {
302    pub frame: u32,
303    pub ik_enabled: Box<[u8]>,
304}
305
306impl PropertyKeyframe {
307    pub fn new(frame: u32, ik_enabled: Vec<bool>) -> Self {
308        Self {
309            frame,
310            ik_enabled: ik_enabled
311                .into_iter()
312                .map(u8::from)
313                .collect::<Vec<_>>()
314                .into_boxed_slice(),
315        }
316    }
317}
318
319#[derive(Clone, Debug)]
320pub struct PropertyAnimationBinding {
321    frame_numbers: Box<[u32]>,
322    ik_enabled: Box<[Box<[u8]>]>,
323}
324
325impl PropertyAnimationBinding {
326    pub fn from_keyframes(mut keyframes: Vec<PropertyKeyframe>) -> Self {
327        keyframes.sort_by_key(|keyframe| keyframe.frame);
328
329        let mut frame_numbers = Vec::with_capacity(keyframes.len());
330        let mut ik_enabled = Vec::with_capacity(keyframes.len());
331        for keyframe in keyframes {
332            frame_numbers.push(keyframe.frame);
333            ik_enabled.push(keyframe.ik_enabled);
334        }
335
336        Self {
337            frame_numbers: frame_numbers.into_boxed_slice(),
338            ik_enabled: ik_enabled.into_boxed_slice(),
339        }
340    }
341
342    pub fn keyframe_count(&self) -> usize {
343        self.frame_numbers.len()
344    }
345
346    pub fn sample(&self, frame: f32) -> Option<&[u8]> {
347        match self.frame_numbers.len() {
348            0 => None,
349            _ => {
350                let next_index = self
351                    .frame_numbers
352                    .partition_point(|keyframe| (*keyframe as f32) <= frame);
353                if next_index == 0 {
354                    None
355                } else {
356                    Some(&self.ik_enabled[next_index - 1])
357                }
358            }
359        }
360    }
361
362    pub fn frame_range(&self) -> Option<(u32, u32)> {
363        Some((*self.frame_numbers.first()?, *self.frame_numbers.last()?))
364    }
365}
366
367#[derive(Clone, Copy, Debug, PartialEq)]
368pub struct BoneSample {
369    pub bone: BoneIndex,
370    pub position: Vec3A,
371    pub rotation: Quat,
372}
373
374#[derive(Clone, Copy, Debug, PartialEq)]
375pub struct MorphSample {
376    pub morph: MorphIndex,
377    pub weight: f32,
378}
379
380#[derive(Clone, Debug, Default, PartialEq)]
381pub struct ClipSample {
382    bone_samples: Vec<BoneSample>,
383    morph_samples: Vec<MorphSample>,
384    ik_enabled: Option<Vec<u8>>,
385}
386
387impl ClipSample {
388    pub fn with_capacity(bone_capacity: usize, morph_capacity: usize) -> Self {
389        Self {
390            bone_samples: Vec::with_capacity(bone_capacity),
391            morph_samples: Vec::with_capacity(morph_capacity),
392            ik_enabled: None,
393        }
394    }
395
396    pub fn bone_samples(&self) -> &[BoneSample] {
397        &self.bone_samples
398    }
399
400    pub fn morph_samples(&self) -> &[MorphSample] {
401        &self.morph_samples
402    }
403
404    pub fn ik_enabled(&self) -> Option<&[u8]> {
405        self.ik_enabled.as_deref()
406    }
407
408    pub fn apply_to_pose(&self, pose: &mut PoseArena) {
409        pose.reset_local_pose();
410        for sample in self.bone_samples.iter() {
411            pose.set_local_position_offset(sample.bone, sample.position);
412            pose.set_local_rotation(sample.bone, sample.rotation);
413        }
414        for sample in self.morph_samples.iter() {
415            pose.set_morph_weight(sample.morph, sample.weight);
416        }
417        if let Some(ik_enabled) = self.ik_enabled.as_ref() {
418            for (ik_index, enabled) in ik_enabled.iter().enumerate() {
419                pose.set_ik_enabled(ik_index, *enabled != 0);
420            }
421        }
422    }
423}
424
425#[derive(Clone, Copy, Debug, PartialEq)]
426pub struct ClipFrameBounds {
427    pub start: f32,
428    pub end: f32,
429}
430
431impl ClipFrameBounds {
432    pub const fn new(start: f32, end: f32) -> Self {
433        Self { start, end }
434    }
435}
436
437enum ClipSampleEvent<'a> {
438    Bone(BoneIndex, Vec3A, Quat),
439    Morph(MorphIndex, f32),
440    IkEnabled(&'a [u8]),
441}
442
443#[derive(Clone, Debug, Default)]
444pub struct AnimationClip {
445    bone_tracks: Box<[BoneAnimationBinding]>,
446    morph_tracks: Box<[MorphAnimationBinding]>,
447    property_track: Option<PropertyAnimationBinding>,
448}
449
450impl AnimationClip {
451    pub fn new(bone_tracks: Vec<BoneAnimationBinding>) -> Self {
452        Self::new_with_morphs(bone_tracks, Vec::new())
453    }
454
455    pub fn new_with_morphs(
456        bone_tracks: Vec<BoneAnimationBinding>,
457        morph_tracks: Vec<MorphAnimationBinding>,
458    ) -> Self {
459        Self::new_full(bone_tracks, morph_tracks, None)
460    }
461
462    pub fn new_full(
463        bone_tracks: Vec<BoneAnimationBinding>,
464        morph_tracks: Vec<MorphAnimationBinding>,
465        property_track: Option<PropertyAnimationBinding>,
466    ) -> Self {
467        Self {
468            bone_tracks: bone_tracks.into_boxed_slice(),
469            morph_tracks: morph_tracks.into_boxed_slice(),
470            property_track,
471        }
472    }
473
474    pub fn builder() -> AnimationClipBuilder {
475        AnimationClipBuilder::new()
476    }
477
478    pub fn sample_at(&self, frame: f32) -> ClipSample {
479        let mut sample = ClipSample::with_capacity(self.bone_tracks.len(), self.morph_tracks.len());
480        self.sample_into(frame, &mut sample);
481        sample
482    }
483
484    pub fn sample_into(&self, frame: f32, sample: &mut ClipSample) {
485        sample.bone_samples.clear();
486        sample.morph_samples.clear();
487        let mut ik_enabled = sample.ik_enabled.take();
488        let mut has_ik_state = false;
489        self.visit_samples(frame, |event| match event {
490            ClipSampleEvent::Bone(bone, position, rotation) => {
491                sample.bone_samples.push(BoneSample {
492                    bone,
493                    position,
494                    rotation,
495                });
496            }
497            ClipSampleEvent::Morph(morph, weight) => {
498                sample.morph_samples.push(MorphSample { morph, weight });
499            }
500            ClipSampleEvent::IkEnabled(state) => {
501                let buffer = ik_enabled.get_or_insert_with(Vec::new);
502                buffer.clear();
503                buffer.extend_from_slice(state);
504                has_ik_state = true;
505            }
506        });
507        sample.ik_enabled = if has_ik_state { ik_enabled } else { None };
508    }
509
510    pub fn apply_to_pose(&self, frame: f32, pose: &mut PoseArena) {
511        pose.reset_local_pose();
512        self.visit_samples(frame, |event| match event {
513            ClipSampleEvent::Bone(bone, position, rotation) => {
514                pose.set_local_position_offset(bone, position);
515                pose.set_local_rotation(bone, rotation);
516            }
517            ClipSampleEvent::Morph(morph, weight) => {
518                pose.set_morph_weight(morph, weight);
519            }
520            ClipSampleEvent::IkEnabled(ik_enabled) => {
521                for (ik_index, enabled) in ik_enabled.iter().enumerate() {
522                    pose.set_ik_enabled(ik_index, *enabled != 0);
523                }
524            }
525        });
526    }
527
528    fn visit_samples(&self, frame: f32, mut on_event: impl FnMut(ClipSampleEvent<'_>)) {
529        for binding in self.bone_tracks.iter() {
530            if let Some((position, rotation)) = binding.track.sample(frame) {
531                on_event(ClipSampleEvent::Bone(binding.bone, position, rotation));
532            }
533        }
534        for binding in self.morph_tracks.iter() {
535            if let Some(weight) = binding.track.sample(frame) {
536                on_event(ClipSampleEvent::Morph(binding.morph, weight));
537            }
538        }
539        if let Some(ik_enabled) = self
540            .property_track
541            .as_ref()
542            .and_then(|track| track.sample(frame))
543        {
544            on_event(ClipSampleEvent::IkEnabled(ik_enabled));
545        }
546    }
547
548    pub fn bone_tracks(&self) -> &[BoneAnimationBinding] {
549        &self.bone_tracks
550    }
551
552    pub fn morph_tracks(&self) -> &[MorphAnimationBinding] {
553        &self.morph_tracks
554    }
555
556    pub fn property_track(&self) -> Option<&PropertyAnimationBinding> {
557        self.property_track.as_ref()
558    }
559
560    pub fn bone_track_count(&self) -> usize {
561        self.bone_tracks.len()
562    }
563
564    /// Returns one authored local bone track by clip order.
565    pub fn bone_track(&self, index: usize) -> Option<&BoneAnimationBinding> {
566        self.bone_tracks.get(index)
567    }
568
569    pub fn morph_track_count(&self) -> usize {
570        self.morph_tracks.len()
571    }
572
573    pub fn has_property_track(&self) -> bool {
574        self.property_track.is_some()
575    }
576
577    pub fn frame_range(&self) -> Option<(u32, u32)> {
578        let mut range: Option<(u32, u32)> = None;
579        for binding in self.bone_tracks.iter() {
580            merge_frame_range(&mut range, binding.track.frame_range());
581        }
582        for binding in self.morph_tracks.iter() {
583            merge_frame_range(&mut range, binding.track.frame_range());
584        }
585        if let Some(property_track) = self.property_track.as_ref() {
586            merge_frame_range(&mut range, property_track.frame_range());
587        }
588        range
589    }
590
591    pub fn frame_bounds(&self) -> Option<ClipFrameBounds> {
592        self.frame_range()
593            .map(|(first, last)| ClipFrameBounds::new(first as f32, last as f32))
594    }
595
596    pub fn find_bone_track(&self, bone: BoneIndex) -> Option<&MovableBoneTrack> {
597        self.bone_tracks
598            .iter()
599            .find(|binding| binding.bone == bone)
600            .map(|binding| &binding.track)
601    }
602
603    pub fn find_morph_track(&self, morph: MorphIndex) -> Option<&MorphTrack> {
604        self.morph_tracks
605            .iter()
606            .find(|binding| binding.morph == morph)
607            .map(|binding| &binding.track)
608    }
609}
610
611#[derive(Clone, Debug, Default)]
612pub struct AnimationClipBuilder {
613    bone_tracks: Vec<BoneAnimationBinding>,
614    morph_tracks: Vec<MorphAnimationBinding>,
615    property_track: Option<PropertyAnimationBinding>,
616}
617
618impl AnimationClipBuilder {
619    pub fn new() -> Self {
620        Self::default()
621    }
622
623    pub fn with_bone_track(mut self, binding: BoneAnimationBinding) -> Self {
624        self.bone_tracks.push(binding);
625        self
626    }
627
628    pub fn with_morph_track(mut self, binding: MorphAnimationBinding) -> Self {
629        self.morph_tracks.push(binding);
630        self
631    }
632
633    pub fn with_property_track(mut self, track: PropertyAnimationBinding) -> Self {
634        self.property_track = Some(track);
635        self
636    }
637
638    pub fn push_bone_track(&mut self, binding: BoneAnimationBinding) -> &mut Self {
639        self.bone_tracks.push(binding);
640        self
641    }
642
643    pub fn push_morph_track(&mut self, binding: MorphAnimationBinding) -> &mut Self {
644        self.morph_tracks.push(binding);
645        self
646    }
647
648    pub fn set_property_track(&mut self, track: PropertyAnimationBinding) -> &mut Self {
649        self.property_track = Some(track);
650        self
651    }
652
653    pub fn build(self) -> AnimationClip {
654        AnimationClip::new_full(self.bone_tracks, self.morph_tracks, self.property_track)
655    }
656}
657
658fn merge_frame_range(target: &mut Option<(u32, u32)>, range: Option<(u32, u32)>) {
659    let Some((first, last)) = range else {
660        return;
661    };
662    *target = Some(match *target {
663        Some((current_first, current_last)) => (current_first.min(first), current_last.max(last)),
664        None => (first, last),
665    });
666}
667
668fn lerp(a: f32, b: f32, t: f32) -> f32 {
669    a + (b - a) * t
670}
671
672fn bezier_interpolation(x1: f32, x2: f32, y1: f32, y2: f32, x: f32) -> f32 {
673    let mut c = 0.5;
674    let mut t = c;
675    let mut s = 1.0 - t;
676
677    let mut sst3;
678    let mut stt3;
679    let mut ttt;
680
681    for _ in 0..BEZIER_ITERATIONS {
682        sst3 = 3.0 * s * s * t;
683        stt3 = 3.0 * s * t * t;
684        ttt = t * t * t;
685
686        let ft = sst3 * x1 + stt3 * x2 + ttt - x;
687        if ft == 0.0 {
688            return sst3 * y1 + stt3 * y2 + ttt;
689        }
690
691        c *= 0.5;
692        t += if ft < 0.0 { c } else { -c };
693        s = 1.0 - t;
694    }
695
696    sst3 = 3.0 * s * s * t;
697    stt3 = 3.0 * s * t * t;
698    ttt = t * t * t;
699    sst3 * y1 + stt3 * y2 + ttt
700}
701
702#[cfg(test)]
703mod tests {
704    use glam::{Quat, Vec3A};
705
706    use super::*;
707
708    fn assert_near(actual: f32, expected: f32) {
709        let delta = (actual - expected).abs();
710        assert!(
711            delta < 1.0e-4,
712            "actual={actual:?} expected={expected:?} delta={delta:?}"
713        );
714    }
715
716    fn assert_vec3a_near(actual: Vec3A, expected: Vec3A) {
717        let delta = (actual - expected).abs();
718        assert!(
719            delta.x < 1.0e-4 && delta.y < 1.0e-4 && delta.z < 1.0e-4,
720            "actual={actual:?} expected={expected:?} delta={delta:?}"
721        );
722    }
723
724    #[test]
725    fn linear_interpolation_maps_half_to_half() {
726        assert_near(InterpolationScalar::linear().evaluate(0.5), 0.5);
727    }
728
729    #[test]
730    fn mmd_camera_ease_out_matches_native_subdivision_points() {
731        let interpolation = InterpolationScalar {
732            x1: 0,
733            y1: 127,
734            x2: 127,
735            y2: 127,
736        };
737
738        assert_near(interpolation.evaluate(1.0 / 6.0), 0.5933867);
739        assert_near(interpolation.evaluate(1.0 / 3.0), 0.76974934);
740        assert_near(interpolation.evaluate(0.5), 0.875);
741        assert_near(interpolation.evaluate(2.0 / 3.0), 0.9420012);
742        assert_near(interpolation.evaluate(5.0 / 6.0), 0.9825947);
743    }
744
745    #[test]
746    fn samples_movable_bone_track() {
747        let track = MovableBoneTrack::from_keyframes(vec![
748            MovableBoneKeyframe::new(20, Vec3A::new(10.0, 0.0, 0.0), Quat::IDENTITY),
749            MovableBoneKeyframe::new(10, Vec3A::ZERO, Quat::IDENTITY),
750        ]);
751
752        let (position, rotation) = track.sample(15.0).unwrap();
753
754        assert_vec3a_near(position, Vec3A::new(5.0, 0.0, 0.0));
755        assert_near(rotation.dot(Quat::IDENTITY).abs(), 1.0);
756    }
757
758    #[test]
759    fn samples_morph_track() {
760        let track = MorphTrack::from_keyframes(vec![
761            MorphKeyframe::new(60, 1.0),
762            MorphKeyframe::new(0, 0.0),
763        ]);
764
765        assert_near(track.sample(30.0).unwrap(), 0.5);
766    }
767
768    #[test]
769    fn samples_property_track_as_step_state() {
770        let track = PropertyAnimationBinding::from_keyframes(vec![
771            PropertyKeyframe::new(30, vec![false, true]),
772            PropertyKeyframe::new(0, vec![true, true]),
773        ]);
774
775        assert_eq!(track.sample(-1.0), None);
776        assert_eq!(track.sample(29.0).unwrap(), &[1, 1]);
777        assert_eq!(track.sample(30.0).unwrap(), &[0, 1]);
778        assert_eq!(track.sample(60.0).unwrap(), &[0, 1]);
779    }
780
781    #[test]
782    fn property_track_returns_none_before_first_keyframe() {
783        let track =
784            PropertyAnimationBinding::from_keyframes(vec![PropertyKeyframe::new(30, vec![false])]);
785
786        assert_eq!(track.sample(29.0), None);
787        assert_eq!(track.sample(30.0).unwrap(), &[0]);
788    }
789
790    #[test]
791    fn clip_frame_range_spans_all_track_types() {
792        let bone_track = BoneAnimationBinding {
793            bone: BoneIndex(0),
794            track: MovableBoneTrack::from_keyframes(vec![
795                MovableBoneKeyframe::new(30, Vec3A::ZERO, Quat::IDENTITY),
796                MovableBoneKeyframe::new(10, Vec3A::ZERO, Quat::IDENTITY),
797            ]),
798        };
799        let morph_track = MorphAnimationBinding {
800            morph: MorphIndex(0),
801            track: MorphTrack::from_keyframes(vec![
802                MorphKeyframe::new(20, 0.0),
803                MorphKeyframe::new(60, 1.0),
804            ]),
805        };
806        let property_track = PropertyAnimationBinding::from_keyframes(vec![
807            PropertyKeyframe::new(5, vec![true]),
808            PropertyKeyframe::new(40, vec![false]),
809        ]);
810        let clip =
811            AnimationClip::new_full(vec![bone_track], vec![morph_track], Some(property_track));
812
813        assert_eq!(clip.frame_range(), Some((5, 60)));
814    }
815
816    #[test]
817    fn clip_frame_bounds_match_integer_frame_range() {
818        let clip = AnimationClip::new_full(
819            vec![BoneAnimationBinding {
820                bone: BoneIndex(0),
821                track: MovableBoneTrack::from_keyframes(vec![
822                    MovableBoneKeyframe::new(10, Vec3A::ZERO, Quat::IDENTITY),
823                    MovableBoneKeyframe::new(20, Vec3A::ZERO, Quat::IDENTITY),
824                ]),
825            }],
826            vec![MorphAnimationBinding {
827                morph: MorphIndex(0),
828                track: MorphTrack::from_keyframes(vec![
829                    MorphKeyframe::new(5, 0.0),
830                    MorphKeyframe::new(30, 1.0),
831                ]),
832            }],
833            None,
834        );
835
836        assert_eq!(clip.frame_range(), Some((5, 30)));
837        assert_eq!(clip.frame_bounds(), Some(ClipFrameBounds::new(5.0, 30.0)));
838    }
839
840    #[test]
841    fn clip_builder_matches_full_constructor() {
842        let bone_track = BoneAnimationBinding {
843            bone: BoneIndex(0),
844            track: MovableBoneTrack::from_keyframes(vec![
845                MovableBoneKeyframe::new(0, Vec3A::ZERO, Quat::IDENTITY),
846                MovableBoneKeyframe::new(10, Vec3A::new(10.0, 0.0, 0.0), Quat::IDENTITY),
847            ]),
848        };
849        let morph_track = MorphAnimationBinding {
850            morph: MorphIndex(0),
851            track: MorphTrack::from_keyframes(vec![
852                MorphKeyframe::new(0, 0.0),
853                MorphKeyframe::new(10, 1.0),
854            ]),
855        };
856        let property_track = PropertyAnimationBinding::from_keyframes(vec![
857            PropertyKeyframe::new(0, vec![true, true]),
858            PropertyKeyframe::new(10, vec![false, true]),
859        ]);
860
861        let direct = AnimationClip::new_full(
862            vec![bone_track.clone()],
863            vec![morph_track.clone()],
864            Some(property_track.clone()),
865        );
866        let built = AnimationClip::builder()
867            .with_bone_track(bone_track)
868            .with_morph_track(morph_track)
869            .with_property_track(property_track)
870            .build();
871
872        assert_eq!(built.bone_track_count(), direct.bone_track_count());
873        assert_eq!(built.morph_track_count(), direct.morph_track_count());
874        assert_eq!(built.has_property_track(), direct.has_property_track());
875        assert_eq!(built.frame_range(), direct.frame_range());
876        assert_eq!(built.sample_at(5.0), direct.sample_at(5.0));
877    }
878
879    #[test]
880    fn clip_sample_applies_same_pose_as_clip() {
881        let clip = AnimationClip::new_full(
882            vec![BoneAnimationBinding {
883                bone: BoneIndex(0),
884                track: MovableBoneTrack::from_keyframes(vec![
885                    MovableBoneKeyframe::new(0, Vec3A::ZERO, Quat::IDENTITY),
886                    MovableBoneKeyframe::new(10, Vec3A::new(2.0, 4.0, 6.0), Quat::IDENTITY),
887                ]),
888            }],
889            vec![MorphAnimationBinding {
890                morph: MorphIndex(0),
891                track: MorphTrack::from_keyframes(vec![
892                    MorphKeyframe::new(0, 0.0),
893                    MorphKeyframe::new(10, 1.0),
894                ]),
895            }],
896            Some(PropertyAnimationBinding::from_keyframes(vec![
897                PropertyKeyframe::new(0, vec![true, true]),
898                PropertyKeyframe::new(10, vec![false, true]),
899            ])),
900        );
901
902        let mut from_clip = PoseArena::new_with_counts(1, 1, 2);
903        clip.apply_to_pose(5.0, &mut from_clip);
904
905        let mut from_sample = PoseArena::new_with_counts(1, 1, 2);
906        let sample = clip.sample_at(5.0);
907        sample.apply_to_pose(&mut from_sample);
908
909        assert_vec3a_near(
910            from_sample.local_position_offset(BoneIndex(0)),
911            from_clip.local_position_offset(BoneIndex(0)),
912        );
913        assert_near(
914            from_sample
915                .local_rotation(BoneIndex(0))
916                .dot(from_clip.local_rotation(BoneIndex(0))),
917            1.0,
918        );
919        assert_near(
920            from_sample.morph_weight(MorphIndex(0)),
921            from_clip.morph_weight(MorphIndex(0)),
922        );
923        assert_eq!(from_sample.ik_enabled(), from_clip.ik_enabled());
924    }
925
926    #[test]
927    fn clip_sample_into_reuses_output_and_matches_sample_at() {
928        let clip = AnimationClip::builder()
929            .with_bone_track(BoneAnimationBinding {
930                bone: BoneIndex(0),
931                track: MovableBoneTrack::from_keyframes(vec![
932                    MovableBoneKeyframe::new(0, Vec3A::ZERO, Quat::IDENTITY),
933                    MovableBoneKeyframe::new(10, Vec3A::new(10.0, 0.0, 0.0), Quat::IDENTITY),
934                ]),
935            })
936            .with_morph_track(MorphAnimationBinding {
937                morph: MorphIndex(0),
938                track: MorphTrack::from_keyframes(vec![
939                    MorphKeyframe::new(0, 0.0),
940                    MorphKeyframe::new(10, 1.0),
941                ]),
942            })
943            .with_property_track(PropertyAnimationBinding::from_keyframes(vec![
944                PropertyKeyframe::new(0, vec![true, false]),
945                PropertyKeyframe::new(10, vec![false, true]),
946            ]))
947            .build();
948
949        let expected = clip.sample_at(5.0);
950        let mut sample = ClipSample::with_capacity(1, 1);
951        clip.sample_into(5.0, &mut sample);
952        assert_eq!(sample, expected);
953
954        let bone_capacity = sample.bone_samples.capacity();
955        let morph_capacity = sample.morph_samples.capacity();
956        let ik_capacity = sample
957            .ik_enabled
958            .as_ref()
959            .expect("property sample should include IK state")
960            .capacity();
961        clip.sample_into(0.0, &mut sample);
962        assert!(sample.bone_samples.capacity() >= bone_capacity);
963        assert!(sample.morph_samples.capacity() >= morph_capacity);
964        assert!(
965            sample
966                .ik_enabled
967                .as_ref()
968                .expect("property sample should include IK state")
969                .capacity()
970                >= ik_capacity
971        );
972    }
973
974    #[test]
975    fn clip_exposes_track_collections_and_lookup() {
976        let clip = AnimationClip::builder()
977            .with_bone_track(BoneAnimationBinding {
978                bone: BoneIndex(3),
979                track: MovableBoneTrack::from_keyframes(vec![MovableBoneKeyframe::new(
980                    12,
981                    Vec3A::ZERO,
982                    Quat::IDENTITY,
983                )]),
984            })
985            .with_morph_track(MorphAnimationBinding {
986                morph: MorphIndex(4),
987                track: MorphTrack::from_keyframes(vec![MorphKeyframe::new(8, 0.25)]),
988            })
989            .with_property_track(PropertyAnimationBinding::from_keyframes(vec![
990                PropertyKeyframe::new(6, vec![false]),
991            ]))
992            .build();
993
994        assert_eq!(clip.bone_tracks().len(), 1);
995        assert_eq!(clip.bone_tracks()[0].bone, BoneIndex(3));
996        assert_eq!(clip.bone_tracks()[0].track.keyframe_count(), 1);
997        assert_eq!(clip.bone_tracks()[0].track.frame_range(), Some((12, 12)));
998        assert_eq!(
999            clip.find_bone_track(BoneIndex(3)).unwrap().keyframe_count(),
1000            1
1001        );
1002
1003        assert_eq!(clip.morph_tracks().len(), 1);
1004        assert_eq!(clip.morph_tracks()[0].morph, MorphIndex(4));
1005        assert_eq!(clip.morph_tracks()[0].track.keyframe_count(), 1);
1006        assert_eq!(clip.morph_tracks()[0].track.frame_range(), Some((8, 8)));
1007        assert_eq!(
1008            clip.find_morph_track(MorphIndex(4))
1009                .unwrap()
1010                .keyframe_count(),
1011            1
1012        );
1013
1014        let property_track = clip.property_track().unwrap();
1015        assert_eq!(property_track.keyframe_count(), 1);
1016        assert_eq!(property_track.frame_range(), Some((6, 6)));
1017        assert_eq!(property_track.sample(5.0), None);
1018    }
1019
1020    #[test]
1021    fn empty_clip_frame_range_is_none() {
1022        assert_eq!(AnimationClip::default().frame_range(), None);
1023        assert_eq!(AnimationClip::default().frame_bounds(), None);
1024    }
1025}