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}