1use std::collections::HashMap;
12use glam::{Quat, Vec3};
13
14use super::skeleton::{BoneId, Pose, Skeleton, Transform3D};
15
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub enum ChannelTarget {
21 Translation,
22 Rotation,
23 Scale,
24 BlendShape(String),
25}
26
27#[derive(Debug, Clone)]
31pub struct Vec3Key {
32 pub time: f32,
33 pub value: Vec3,
34 pub in_tangent: Vec3,
36 pub out_tangent: Vec3,
38}
39
40#[derive(Debug, Clone)]
42pub struct QuatKey {
43 pub time: f32,
44 pub value: Quat,
45}
46
47#[derive(Debug, Clone)]
49pub struct F32Key {
50 pub time: f32,
51 pub value: f32,
52}
53
54fn hermite_vec3(p0: Vec3, m0: Vec3, p1: Vec3, m1: Vec3, t: f32) -> Vec3 {
61 let t2 = t * t;
62 let t3 = t2 * t;
63 let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
64 let h10 = t3 - 2.0 * t2 + t;
65 let h01 = -2.0 * t3 + 3.0 * t2;
66 let h11 = t3 - t2;
67 p0 * h00 + m0 * h10 + p1 * h01 + m1 * h11
68}
69
70fn squad(q0: Quat, q1: Quat, s0: Quat, s1: Quat, t: f32) -> Quat {
75 let slerp_q = q0.slerp(q1, t);
76 let slerp_s = s0.slerp(s1, t);
77 slerp_q.slerp(slerp_s, 2.0 * t * (1.0 - t))
78}
79
80fn squad_inner(q_prev: Quat, q_curr: Quat, q_next: Quat) -> Quat {
82 let q_inv = q_curr.conjugate();
83 let a = q_inv * q_next;
85 let b = q_inv * q_prev;
87 let la = quat_log(a);
89 let lb = quat_log(b);
90 let avg = (la + lb) * (-0.25);
91 q_curr * quat_exp(avg)
92}
93
94fn quat_log(q: Quat) -> Quat {
96 let v = Vec3::new(q.x, q.y, q.z);
97 let len = v.length();
98 if len < 1e-6 {
99 return Quat::from_xyzw(0.0, 0.0, 0.0, 0.0);
100 }
101 let angle = q.w.clamp(-1.0, 1.0).acos();
102 let coeff = if angle.abs() < 1e-6 { 1.0 } else { angle / len };
103 let v2 = v * coeff;
104 Quat::from_xyzw(v2.x, v2.y, v2.z, 0.0)
105}
106
107fn quat_exp(q: Quat) -> Quat {
109 let v = Vec3::new(q.x, q.y, q.z);
110 let theta = v.length();
111 if theta < 1e-6 {
112 return Quat::IDENTITY;
113 }
114 let sin_t = theta.sin();
115 let cos_t = theta.cos();
116 let coeff = sin_t / theta;
117 Quat::from_xyzw(v.x * coeff, v.y * coeff, v.z * coeff, cos_t).normalize()
118}
119
120fn lerp_f32(a: f32, b: f32, t: f32) -> f32 {
122 a + (b - a) * t
123}
124
125#[derive(Debug, Clone)]
129pub struct AnimationChannel {
130 pub bone_id: BoneId,
131 pub target: ChannelTarget,
132 pub data: ChannelData,
133}
134
135#[derive(Debug, Clone)]
137pub enum ChannelData {
138 Translation(Vec<Vec3Key>),
139 Rotation(Vec<QuatKey>),
140 Scale(Vec<Vec3Key>),
141 BlendShape(Vec<F32Key>),
142}
143
144impl AnimationChannel {
145 pub fn translation(bone_id: BoneId, keys: Vec<Vec3Key>) -> Self {
147 Self { bone_id, target: ChannelTarget::Translation, data: ChannelData::Translation(keys) }
148 }
149
150 pub fn rotation(bone_id: BoneId, keys: Vec<QuatKey>) -> Self {
152 Self { bone_id, target: ChannelTarget::Rotation, data: ChannelData::Rotation(keys) }
153 }
154
155 pub fn scale(bone_id: BoneId, keys: Vec<Vec3Key>) -> Self {
157 Self { bone_id, target: ChannelTarget::Scale, data: ChannelData::Scale(keys) }
158 }
159
160 pub fn blend_shape(bone_id: BoneId, shape_name: impl Into<String>, keys: Vec<F32Key>) -> Self {
162 Self {
163 bone_id,
164 target: ChannelTarget::BlendShape(shape_name.into()),
165 data: ChannelData::BlendShape(keys),
166 }
167 }
168
169 pub fn sample_translation(&self, t: f32) -> Option<Vec3> {
171 if let ChannelData::Translation(ref keys) = self.data {
172 Some(sample_vec3_hermite(keys, t))
173 } else {
174 None
175 }
176 }
177
178 pub fn sample_rotation(&self, t: f32) -> Option<Quat> {
180 if let ChannelData::Rotation(ref keys) = self.data {
181 Some(sample_quat_squad(keys, t))
182 } else {
183 None
184 }
185 }
186
187 pub fn sample_scale(&self, t: f32) -> Option<Vec3> {
189 if let ChannelData::Scale(ref keys) = self.data {
190 Some(sample_vec3_hermite(keys, t))
191 } else {
192 None
193 }
194 }
195
196 pub fn sample_blend_shape(&self, t: f32) -> Option<f32> {
198 if let ChannelData::BlendShape(ref keys) = self.data {
199 Some(sample_f32_linear(keys, t))
200 } else {
201 None
202 }
203 }
204}
205
206fn sample_vec3_hermite(keys: &[Vec3Key], t: f32) -> Vec3 {
209 if keys.is_empty() { return Vec3::ZERO; }
210 if keys.len() == 1 { return keys[0].value; }
211 if t <= keys[0].time { return keys[0].value; }
212 if t >= keys.last().unwrap().time { return keys.last().unwrap().value; }
213
214 let idx = keys.partition_point(|k| k.time <= t);
215 let i = idx.saturating_sub(1);
216 let j = idx.min(keys.len() - 1);
217 let k0 = &keys[i];
218 let k1 = &keys[j];
219 let span = (k1.time - k0.time).max(1e-7);
220 let u = (t - k0.time) / span;
221 hermite_vec3(k0.value, k0.out_tangent * span, k1.value, k1.in_tangent * span, u)
222}
223
224fn sample_quat_squad(keys: &[QuatKey], t: f32) -> Quat {
225 if keys.is_empty() { return Quat::IDENTITY; }
226 if keys.len() == 1 { return keys[0].value; }
227 if t <= keys[0].time { return keys[0].value; }
228 if t >= keys.last().unwrap().time { return keys.last().unwrap().value; }
229
230 let idx = keys.partition_point(|k| k.time <= t);
231 let i = idx.saturating_sub(1);
232 let j = idx.min(keys.len() - 1);
233
234 let q0 = keys[i].value;
235 let q1 = keys[j].value;
236
237 let q_prev = if i > 0 { keys[i - 1].value } else { q0 };
239 let q_next = if j + 1 < keys.len() { keys[j + 1].value } else { q1 };
240
241 let s0 = squad_inner(q_prev, q0, q1);
242 let s1 = squad_inner(q0, q1, q_next);
243
244 let span = (keys[j].time - keys[i].time).max(1e-7);
245 let u = (t - keys[i].time) / span;
246 squad(q0, q1, s0, s1, u).normalize()
247}
248
249fn sample_f32_linear(keys: &[F32Key], t: f32) -> f32 {
250 if keys.is_empty() { return 0.0; }
251 if keys.len() == 1 { return keys[0].value; }
252 if t <= keys[0].time { return keys[0].value; }
253 if t >= keys.last().unwrap().time { return keys.last().unwrap().value; }
254
255 let idx = keys.partition_point(|k| k.time <= t);
256 let i = idx.saturating_sub(1);
257 let j = idx.min(keys.len() - 1);
258 let span = (keys[j].time - keys[i].time).max(1e-7);
259 let u = (t - keys[i].time) / span;
260 lerp_f32(keys[i].value, keys[j].value, u)
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum LoopMode {
268 Once,
270 Loop,
272 PingPong,
274 ClampForever,
276}
277
278impl LoopMode {
279 pub fn remap(self, t: f32, duration: f32) -> f32 {
281 if duration < 1e-6 { return 0.0; }
282 match self {
283 LoopMode::Once | LoopMode::ClampForever => t.clamp(0.0, duration),
284 LoopMode::Loop => t.rem_euclid(duration),
285 LoopMode::PingPong => {
286 let period = duration * 2.0;
287 let local = t.rem_euclid(period);
288 if local <= duration { local } else { period - local }
289 }
290 }
291 }
292}
293
294#[derive(Debug, Clone)]
298pub struct AnimationEvent {
299 pub time: f32,
301 pub name: String,
302 pub payload: String,
303}
304
305impl AnimationEvent {
306 pub fn new(time: f32, name: impl Into<String>, payload: impl Into<String>) -> Self {
307 Self { time, name: name.into(), payload: payload.into() }
308 }
309}
310
311#[derive(Debug, Clone)]
315pub struct AnimationClip {
316 pub name: String,
317 pub duration: f32,
318 pub channels: Vec<AnimationChannel>,
319 pub loop_mode: LoopMode,
320 pub events: Vec<AnimationEvent>,
321}
322
323impl AnimationClip {
324 pub fn new(name: impl Into<String>, duration: f32) -> Self {
325 Self {
326 name: name.into(),
327 duration,
328 channels: Vec::new(),
329 loop_mode: LoopMode::Loop,
330 events: Vec::new(),
331 }
332 }
333
334 pub fn with_loop_mode(mut self, mode: LoopMode) -> Self {
335 self.loop_mode = mode;
336 self
337 }
338
339 pub fn with_channel(mut self, ch: AnimationChannel) -> Self {
340 self.channels.push(ch);
341 self
342 }
343
344 pub fn with_event(mut self, event: AnimationEvent) -> Self {
345 self.events.push(event);
346 self
347 }
348
349 pub fn add_channel(&mut self, ch: AnimationChannel) {
351 self.channels.push(ch);
352 }
353
354 pub fn add_event(&mut self, event: AnimationEvent) {
356 self.events.push(event);
357 }
358
359 pub fn events_in_range(&self, prev_t: f32, cur_t: f32) -> Vec<&AnimationEvent> {
361 self.events.iter()
362 .filter(|e| e.time > prev_t && e.time <= cur_t)
363 .collect()
364 }
365
366 pub fn constant_pose(name: impl Into<String>, duration: f32, snapshot: Vec<(BoneId, Transform3D)>) -> Self {
368 let mut clip = Self::new(name, duration);
369 for (bone_id, xform) in snapshot {
370 let t_keys = vec![Vec3Key {
371 time: 0.0,
372 value: xform.translation,
373 in_tangent: Vec3::ZERO,
374 out_tangent: Vec3::ZERO,
375 }];
376 let r_keys = vec![QuatKey { time: 0.0, value: xform.rotation }];
377 let s_keys = vec![Vec3Key {
378 time: 0.0,
379 value: xform.scale,
380 in_tangent: Vec3::ZERO,
381 out_tangent: Vec3::ZERO,
382 }];
383 clip.add_channel(AnimationChannel::translation(bone_id, t_keys));
384 clip.add_channel(AnimationChannel::rotation(bone_id, r_keys));
385 clip.add_channel(AnimationChannel::scale(bone_id, s_keys));
386 }
387 clip
388 }
389}
390
391pub struct AnimationClipSampler<'a> {
396 pub clip: &'a AnimationClip,
397 pub skeleton: &'a Skeleton,
398 pub time: f32,
400 prev_time: f32,
402 pub speed: f32,
403}
404
405impl<'a> AnimationClipSampler<'a> {
406 pub fn new(clip: &'a AnimationClip, skeleton: &'a Skeleton) -> Self {
407 Self {
408 clip,
409 skeleton,
410 time: 0.0,
411 prev_time: 0.0,
412 speed: 1.0,
413 }
414 }
415
416 pub fn with_speed(mut self, speed: f32) -> Self {
417 self.speed = speed;
418 self
419 }
420
421 pub fn advance(&mut self, dt: f32) {
423 self.prev_time = self.time;
424 self.time += dt * self.speed;
425 }
426
427 pub fn reset(&mut self) {
429 self.prev_time = 0.0;
430 self.time = 0.0;
431 }
432
433 pub fn is_finished(&self) -> bool {
435 matches!(self.clip.loop_mode, LoopMode::Once | LoopMode::ClampForever)
436 && self.time >= self.clip.duration
437 }
438
439 pub fn normalized_time(&self) -> f32 {
441 let dur = self.clip.duration.max(1e-6);
442 (self.clip.loop_mode.remap(self.time, dur) / dur).clamp(0.0, 1.0)
443 }
444
445 pub fn sample_into(&self, base_pose: &mut Pose) -> Vec<&AnimationEvent> {
449 let t = self.clip.loop_mode.remap(self.time, self.clip.duration);
450 self.apply_channels(base_pose, t);
451
452 let prev_t = self.clip.loop_mode.remap(self.prev_time, self.clip.duration);
453 let cur_t = t;
454 if cur_t >= prev_t {
455 self.clip.events_in_range(prev_t, cur_t)
456 } else {
457 let mut evts = self.clip.events_in_range(prev_t, self.clip.duration);
459 evts.extend(self.clip.events_in_range(0.0, cur_t));
460 evts
461 }
462 }
463
464 pub fn sample_at(&self, time_sec: f32) -> Pose {
466 let t = self.clip.loop_mode.remap(time_sec, self.clip.duration);
467 let mut pose = self.skeleton.rest_pose();
468 self.apply_channels(&mut pose, t);
469 pose
470 }
471
472 fn apply_channels(&self, pose: &mut Pose, t: f32) {
473 for ch in &self.clip.channels {
476 let idx = ch.bone_id.index();
477 if idx >= pose.local_transforms.len() { continue; }
478
479 match &ch.data {
480 ChannelData::Translation(keys) => {
481 pose.local_transforms[idx].translation = sample_vec3_hermite(keys, t);
482 }
483 ChannelData::Rotation(keys) => {
484 pose.local_transforms[idx].rotation = sample_quat_squad(keys, t);
485 }
486 ChannelData::Scale(keys) => {
487 pose.local_transforms[idx].scale = sample_vec3_hermite(keys, t);
488 }
489 ChannelData::BlendShape(_) => {
490 }
492 }
493 }
494 }
495}
496
497#[derive(Debug, Default)]
501pub struct ClipRegistry {
502 clips: HashMap<String, AnimationClip>,
503}
504
505impl ClipRegistry {
506 pub fn new() -> Self { Self::default() }
507
508 pub fn register(&mut self, clip: AnimationClip) -> bool {
510 self.clips.insert(clip.name.clone(), clip).is_some()
511 }
512
513 pub fn unregister(&mut self, name: &str) -> Option<AnimationClip> {
515 self.clips.remove(name)
516 }
517
518 pub fn get(&self, name: &str) -> Option<&AnimationClip> {
520 self.clips.get(name)
521 }
522
523 pub fn get_mut(&mut self, name: &str) -> Option<&mut AnimationClip> {
525 self.clips.get_mut(name)
526 }
527
528 pub fn len(&self) -> usize { self.clips.len() }
530 pub fn is_empty(&self) -> bool { self.clips.is_empty() }
531
532 pub fn names(&self) -> impl Iterator<Item = &str> {
534 self.clips.keys().map(|s| s.as_str())
535 }
536
537 pub fn iter(&self) -> impl Iterator<Item = (&str, &AnimationClip)> {
539 self.clips.iter().map(|(k, v)| (k.as_str(), v))
540 }
541}
542
543#[derive(Debug, Clone)]
547pub struct BlendShape {
548 pub name: String,
549 pub deltas: Vec<Vec3>,
551}
552
553impl BlendShape {
554 pub fn new(name: impl Into<String>, deltas: Vec<Vec3>) -> Self {
555 Self { name: name.into(), deltas }
556 }
557
558 pub fn apply(&self, positions: &mut [Vec3], weight: f32) {
560 for (pos, delta) in positions.iter_mut().zip(self.deltas.iter()) {
561 *pos += *delta * weight;
562 }
563 }
564}
565
566#[derive(Debug, Clone, Default)]
568pub struct BlendShapeSet {
569 shapes: HashMap<String, BlendShape>,
570}
571
572impl BlendShapeSet {
573 pub fn new() -> Self { Self::default() }
574
575 pub fn add(&mut self, shape: BlendShape) {
576 self.shapes.insert(shape.name.clone(), shape);
577 }
578
579 pub fn get(&self, name: &str) -> Option<&BlendShape> {
580 self.shapes.get(name)
581 }
582
583 pub fn len(&self) -> usize { self.shapes.len() }
584 pub fn is_empty(&self) -> bool { self.shapes.is_empty() }
585
586 pub fn apply_all(&self, positions: &mut [Vec3], weights: &HashMap<String, f32>) {
588 for (name, shape) in &self.shapes {
589 let w = weights.get(name).copied().unwrap_or(0.0);
590 if w.abs() > 1e-6 {
591 shape.apply(positions, w);
592 }
593 }
594 }
595}
596
597#[derive(Debug, Default)]
601pub struct BlendShapeAnimator {
602 tracks: HashMap<String, Vec<F32Key>>,
604 pub time: f32,
606 pub speed: f32,
607}
608
609impl BlendShapeAnimator {
610 pub fn new() -> Self {
611 Self { tracks: HashMap::new(), time: 0.0, speed: 1.0 }
612 }
613
614 pub fn add_track(&mut self, shape_name: impl Into<String>, keys: Vec<F32Key>) {
616 self.tracks.insert(shape_name.into(), keys);
617 }
618
619 pub fn advance(&mut self, dt: f32) {
621 self.time += dt * self.speed;
622 }
623
624 pub fn evaluate(&self) -> HashMap<String, f32> {
626 self.tracks.iter()
627 .map(|(name, keys)| (name.clone(), sample_f32_linear(keys, self.time)))
628 .collect()
629 }
630
631 pub fn weight_of(&self, shape_name: &str) -> f32 {
633 self.tracks.get(shape_name)
634 .map(|keys| sample_f32_linear(keys, self.time))
635 .unwrap_or(0.0)
636 }
637
638 pub fn track_count(&self) -> usize { self.tracks.len() }
640}
641
642#[derive(Debug, Clone, Default)]
650pub struct RootMotion {
651 pub delta_translation: Vec3,
652 pub delta_rotation: Quat,
653}
654
655impl RootMotion {
656 pub fn zero() -> Self {
657 Self {
658 delta_translation: Vec3::ZERO,
659 delta_rotation: Quat::IDENTITY,
660 }
661 }
662
663 pub fn extract_root_motion(clip: &AnimationClip, current_time: f32, dt: f32) -> Self {
668 let dur = clip.duration.max(1e-6);
669 let t0 = clip.loop_mode.remap(current_time, dur);
670 let t1 = clip.loop_mode.remap(current_time + dt, dur);
671
672 let mut pos0 = Vec3::ZERO;
673 let mut pos1 = Vec3::ZERO;
674 let mut rot0 = Quat::IDENTITY;
675 let mut rot1 = Quat::IDENTITY;
676
677 for ch in &clip.channels {
678 if ch.bone_id != BoneId(0) { continue; }
679 match &ch.data {
680 ChannelData::Translation(keys) => {
681 pos0 = sample_vec3_hermite(keys, t0);
682 pos1 = sample_vec3_hermite(keys, t1);
683 }
684 ChannelData::Rotation(keys) => {
685 rot0 = sample_quat_squad(keys, t0);
686 rot1 = sample_quat_squad(keys, t1);
687 }
688 _ => {}
689 }
690 }
691
692 let delta_rotation = (rot0.conjugate() * rot1).normalize();
694
695 Self {
696 delta_translation: pos1 - pos0,
697 delta_rotation,
698 }
699 }
700
701 pub fn accumulate(&self, other: &RootMotion) -> RootMotion {
703 RootMotion {
704 delta_translation: self.delta_translation + other.delta_translation,
705 delta_rotation: (self.delta_rotation * other.delta_rotation).normalize(),
706 }
707 }
708
709 pub fn is_zero(&self) -> bool {
710 self.delta_translation.length_squared() < 1e-10
711 && (self.delta_rotation.w - 1.0).abs() < 1e-6
712 }
713}
714
715#[cfg(test)]
718mod tests {
719 use super::*;
720 use super::super::skeleton::SkeletonBuilder;
721
722 fn two_bone_skeleton() -> Skeleton {
723 SkeletonBuilder::new()
724 .add_bone("root", None, Transform3D::identity())
725 .add_bone("child", Some("root"), Transform3D::new(Vec3::new(0.0, 1.0, 0.0), Quat::IDENTITY, Vec3::ONE))
726 .build()
727 }
728
729 fn linear_translation_clip(bone_id: BoneId, start: Vec3, end: Vec3, duration: f32) -> AnimationClip {
730 let keys = vec![
731 Vec3Key { time: 0.0, value: start, in_tangent: Vec3::ZERO, out_tangent: Vec3::ZERO },
732 Vec3Key { time: duration, value: end, in_tangent: Vec3::ZERO, out_tangent: Vec3::ZERO },
733 ];
734 AnimationClip::new("test", duration)
735 .with_channel(AnimationChannel::translation(bone_id, keys))
736 }
737
738 #[test]
739 fn test_loop_mode_remap_loop() {
740 let mode = LoopMode::Loop;
741 assert!((mode.remap(2.5, 2.0) - 0.5).abs() < 1e-5);
742 }
743
744 #[test]
745 fn test_loop_mode_remap_ping_pong() {
746 let mode = LoopMode::PingPong;
747 assert!((mode.remap(0.0, 1.0) - 0.0).abs() < 1e-5);
748 assert!((mode.remap(1.5, 1.0) - 0.5).abs() < 1e-5);
749 assert!((mode.remap(2.0, 1.0) - 0.0).abs() < 1e-5);
750 }
751
752 #[test]
753 fn test_loop_mode_clamp() {
754 let mode = LoopMode::ClampForever;
755 assert!((mode.remap(-1.0, 2.0) - 0.0).abs() < 1e-5);
756 assert!((mode.remap(5.0, 2.0) - 2.0).abs() < 1e-5);
757 }
758
759 #[test]
760 fn test_hermite_vec3_midpoint() {
761 let keys = vec![
762 Vec3Key { time: 0.0, value: Vec3::ZERO, in_tangent: Vec3::ZERO, out_tangent: Vec3::ZERO },
763 Vec3Key { time: 1.0, value: Vec3::new(1.0, 0.0, 0.0), in_tangent: Vec3::ZERO, out_tangent: Vec3::ZERO },
764 ];
765 let mid = sample_vec3_hermite(&keys, 0.5);
766 assert!((mid.x - 0.5).abs() < 0.01, "Expected ~0.5, got {}", mid.x);
767 }
768
769 #[test]
770 fn test_sampler_translation_at_endpoints() {
771 let skel = two_bone_skeleton();
772 let clip = linear_translation_clip(BoneId(0), Vec3::ZERO, Vec3::new(1.0, 0.0, 0.0), 1.0);
773 let sampler = AnimationClipSampler::new(&clip, &skel);
774
775 let pose_start = sampler.sample_at(0.0);
776 let pose_end = sampler.sample_at(1.0);
777 assert!(pose_start.local_transforms[0].translation.x.abs() < 1e-5);
778 assert!((pose_end.local_transforms[0].translation.x - 1.0).abs() < 1e-5);
779 }
780
781 #[test]
782 fn test_sampler_advance_and_is_finished() {
783 let skel = two_bone_skeleton();
784 let mut clip = linear_translation_clip(BoneId(0), Vec3::ZERO, Vec3::X, 1.0);
785 clip.loop_mode = LoopMode::Once;
786 let mut sampler = AnimationClipSampler::new(&clip, &skel);
787 sampler.advance(2.0);
788 assert!(sampler.is_finished());
789 }
790
791 #[test]
792 fn test_clip_registry_register_get() {
793 let mut reg = ClipRegistry::new();
794 let clip = AnimationClip::new("idle", 1.5);
795 reg.register(clip);
796 assert!(reg.get("idle").is_some());
797 assert!(reg.get("walk").is_none());
798 }
799
800 #[test]
801 fn test_clip_registry_unregister() {
802 let mut reg = ClipRegistry::new();
803 reg.register(AnimationClip::new("run", 0.8));
804 let removed = reg.unregister("run");
805 assert!(removed.is_some());
806 assert!(reg.get("run").is_none());
807 }
808
809 #[test]
810 fn test_animation_event_in_range() {
811 let clip = AnimationClip::new("test", 2.0)
812 .with_event(AnimationEvent::new(0.5, "footstep", "left"))
813 .with_event(AnimationEvent::new(1.5, "footstep", "right"));
814 let evts = clip.events_in_range(0.0, 1.0);
815 assert_eq!(evts.len(), 1);
816 assert_eq!(evts[0].name, "footstep");
817 }
818
819 #[test]
820 fn test_blend_shape_apply() {
821 let deltas = vec![Vec3::new(0.1, 0.0, 0.0); 3];
822 let shape = BlendShape::new("smile", deltas);
823 let mut positions = vec![Vec3::ZERO; 3];
824 shape.apply(&mut positions, 0.5);
825 assert!((positions[0].x - 0.05).abs() < 1e-6);
826 }
827
828 #[test]
829 fn test_blend_shape_animator_evaluate() {
830 let mut animator = BlendShapeAnimator::new();
831 let keys = vec![
832 F32Key { time: 0.0, value: 0.0 },
833 F32Key { time: 1.0, value: 1.0 },
834 ];
835 animator.add_track("blink", keys);
836 animator.time = 0.5;
837 let weights = animator.evaluate();
838 let w = weights["blink"];
839 assert!((w - 0.5).abs() < 0.01);
840 }
841
842 #[test]
843 fn test_root_motion_extract_zero_for_static_clip() {
844 let keys = vec![
846 Vec3Key { time: 0.0, value: Vec3::new(1.0, 0.0, 0.0), in_tangent: Vec3::ZERO, out_tangent: Vec3::ZERO },
847 Vec3Key { time: 1.0, value: Vec3::new(1.0, 0.0, 0.0), in_tangent: Vec3::ZERO, out_tangent: Vec3::ZERO },
848 ];
849 let clip = AnimationClip::new("static", 1.0)
850 .with_channel(AnimationChannel::translation(BoneId(0), keys));
851 let rm = RootMotion::extract_root_motion(&clip, 0.0, 0.5);
852 assert!(rm.delta_translation.length() < 1e-4);
853 }
854
855 #[test]
856 fn test_root_motion_extract_moving_clip() {
857 let keys = vec![
858 Vec3Key { time: 0.0, value: Vec3::ZERO, in_tangent: Vec3::ZERO, out_tangent: Vec3::ZERO },
859 Vec3Key { time: 1.0, value: Vec3::new(2.0, 0.0, 0.0), in_tangent: Vec3::ZERO, out_tangent: Vec3::ZERO },
860 ];
861 let clip = AnimationClip::new("run", 1.0)
862 .with_channel(AnimationChannel::translation(BoneId(0), keys));
863 let rm = RootMotion::extract_root_motion(&clip, 0.0, 0.5);
864 assert!(rm.delta_translation.x > 0.5 && rm.delta_translation.x < 1.5,
866 "Expected ~1.0, got {}", rm.delta_translation.x);
867 }
868
869 #[test]
870 fn test_f32_key_linear_interp() {
871 let keys = vec![
872 F32Key { time: 0.0, value: 0.0 },
873 F32Key { time: 2.0, value: 4.0 },
874 ];
875 let v = sample_f32_linear(&keys, 1.0);
876 assert!((v - 2.0).abs() < 1e-5);
877 }
878}