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