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 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 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 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 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 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 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}