1pub mod skeleton;
13pub mod clips;
14pub mod particle_skin;
15
16use std::collections::HashMap;
17
18#[derive(Debug, Clone)]
22pub struct AnimCurve {
23 pub keyframes: Vec<(f32, f32, f32, f32)>,
25 pub extrapolate: Extrapolate,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub enum Extrapolate {
30 Clamp,
32 Loop,
34 PingPong,
36 Linear,
38}
39
40impl AnimCurve {
41 pub fn constant(value: f32) -> Self {
42 Self {
43 keyframes: vec![(0.0, value, 0.0, 0.0)],
44 extrapolate: Extrapolate::Clamp,
45 }
46 }
47
48 pub fn linear(t0: f32, v0: f32, t1: f32, v1: f32) -> Self {
49 let tangent = if (t1 - t0).abs() > 1e-6 { (v1 - v0) / (t1 - t0) } else { 0.0 };
50 Self {
51 keyframes: vec![(t0, v0, tangent, tangent), (t1, v1, tangent, tangent)],
52 extrapolate: Extrapolate::Clamp,
53 }
54 }
55
56 pub fn sample(&self, t: f32) -> f32 {
58 if self.keyframes.is_empty() { return 0.0; }
59 if self.keyframes.len() == 1 { return self.keyframes[0].1; }
60
61 let duration = self.keyframes.last().unwrap().0 - self.keyframes[0].0;
62 let t = self.wrap_time(t, duration);
63
64 let idx = self.keyframes.partition_point(|k| k.0 <= t);
66 if idx == 0 { return self.keyframes[0].1; }
67 if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().1; }
68
69 let (t0, v0, _in0, out0) = self.keyframes[idx - 1];
70 let (t1, v1, in1, _out1) = self.keyframes[idx];
71
72 let dt = t1 - t0;
73 if dt < 1e-6 { return v1; }
74
75 let u = (t - t0) / dt;
76 let h00 = (2.0 * u * u * u) - (3.0 * u * u) + 1.0;
78 let h10 = u * u * u - (2.0 * u * u) + u;
79 let h01 = -(2.0 * u * u * u) + (3.0 * u * u);
80 let h11 = u * u * u - u * u;
81 h00 * v0 + h10 * dt * out0 + h01 * v1 + h11 * dt * in1
82 }
83
84 fn wrap_time(&self, t: f32, duration: f32) -> f32 {
85 if duration < 1e-6 { return self.keyframes[0].0; }
86 match self.extrapolate {
87 Extrapolate::Clamp => t.clamp(self.keyframes[0].0, self.keyframes.last().unwrap().0),
88 Extrapolate::Loop => self.keyframes[0].0 + (t - self.keyframes[0].0).rem_euclid(duration),
89 Extrapolate::PingPong => {
90 let local = (t - self.keyframes[0].0).rem_euclid(duration * 2.0);
91 self.keyframes[0].0 + if local < duration { local } else { duration * 2.0 - local }
92 }
93 Extrapolate::Linear => t,
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
102pub struct AnimChannel {
103 pub target_path: String, pub curve: AnimCurve,
105}
106
107#[derive(Debug, Clone)]
111pub struct AnimClip {
112 pub name: String,
113 pub duration: f32,
114 pub fps: f32,
115 pub looping: bool,
116 pub channels: Vec<AnimChannel>,
117 pub root_motion: Option<RootMotionData>,
119}
120
121impl AnimClip {
122 pub fn new(name: &str, duration: f32) -> Self {
123 Self {
124 name: name.to_string(),
125 duration,
126 fps: 30.0,
127 looping: true,
128 channels: Vec::new(),
129 root_motion: None,
130 }
131 }
132
133 pub fn add_channel(&mut self, path: &str, curve: AnimCurve) {
135 self.channels.push(AnimChannel { target_path: path.to_string(), curve });
136 }
137
138 pub fn sample(&self, t: f32) -> HashMap<String, f32> {
140 let t = if self.looping { t.rem_euclid(self.duration) } else { t.min(self.duration) };
141 self.channels.iter().map(|ch| (ch.target_path.clone(), ch.curve.sample(t))).collect()
142 }
143
144 pub fn blend_samples(a: &HashMap<String, f32>, b: &HashMap<String, f32>, alpha: f32) -> HashMap<String, f32> {
146 let mut out = a.clone();
147 for (k, vb) in b {
148 let va = out.entry(k.clone()).or_insert(0.0);
149 *va = *va * (1.0 - alpha) + vb * alpha;
150 }
151 out
152 }
153}
154
155#[derive(Debug, Clone)]
159pub struct RootMotionData {
160 pub frames: Vec<(f32, f32, f32)>,
162}
163
164impl RootMotionData {
165 pub fn accumulate(&self, t0: f32, t1: f32) -> (f32, f32, f32) {
167 if self.frames.is_empty() { return (0.0, 0.0, 0.0); }
168 let n = self.frames.len();
169 let i0 = ((t0 * n as f32) as usize).min(n - 1);
170 let i1 = ((t1 * n as f32) as usize).min(n - 1);
171 let (mut dx, mut dy, mut dr) = (0.0_f32, 0.0_f32, 0.0_f32);
172 for i in i0..i1 {
173 dx += self.frames[i].0;
174 dy += self.frames[i].1;
175 dr += self.frames[i].2;
176 }
177 (dx, dy, dr)
178 }
179}
180
181#[derive(Debug, Clone)]
185pub struct AnimEvent {
186 pub normalized_time: f32,
188 pub name: String,
190 pub value: f32,
192}
193
194#[derive(Debug, Clone)]
198pub enum Condition {
199 BoolTrue(String),
200 BoolFalse(String),
201 IntEquals(String, i32),
202 IntGreater(String, i32),
203 IntLess(String, i32),
204 FloatGreater(String, f32),
205 FloatLess(String, f32),
206 Trigger(String),
207}
208
209impl Condition {
210 pub fn check(&self, params: &AnimParamSet) -> bool {
211 match self {
212 Condition::BoolTrue(n) => params.get_bool(n),
213 Condition::BoolFalse(n) => !params.get_bool(n),
214 Condition::IntEquals(n, v) => params.get_int(n) == *v,
215 Condition::IntGreater(n, v) => params.get_int(n) > *v,
216 Condition::IntLess(n, v) => params.get_int(n) < *v,
217 Condition::FloatGreater(n,v) => params.get_float(n) > *v,
218 Condition::FloatLess(n, v) => params.get_float(n) < *v,
219 Condition::Trigger(n) => params.consume_trigger(n),
220 }
221 }
222}
223
224#[derive(Debug, Clone, Default)]
228pub struct AnimParamSet {
229 floats: HashMap<String, f32>,
230 ints: HashMap<String, i32>,
231 bools: HashMap<String, bool>,
232 triggers: std::collections::HashSet<String>,
233 consumed: Vec<String>,
235}
236
237impl AnimParamSet {
238 pub fn set_float(&mut self, name: &str, v: f32) { self.floats.insert(name.to_string(), v); }
239 pub fn set_int (&mut self, name: &str, v: i32) { self.ints.insert(name.to_string(), v); }
240 pub fn set_bool (&mut self, name: &str, v: bool) { self.bools.insert(name.to_string(), v); }
241 pub fn set_trigger(&mut self, name: &str) { self.triggers.insert(name.to_string()); }
242
243 pub fn get_float(&self, name: &str) -> f32 { *self.floats.get(name).unwrap_or(&0.0) }
244 pub fn get_int (&self, name: &str) -> i32 { *self.ints.get(name).unwrap_or(&0) }
245 pub fn get_bool (&self, name: &str) -> bool { *self.bools.get(name).unwrap_or(&false) }
246
247 pub fn consume_trigger(&self, name: &str) -> bool {
248 self.triggers.contains(name)
249 }
250
251 pub fn flush_triggers(&mut self) {
253 for name in self.consumed.drain(..) {
254 self.triggers.remove(&name);
255 }
256 }
257
258 pub fn mark_trigger_consumed(&mut self, name: &str) {
259 self.consumed.push(name.to_string());
260 }
261}
262
263#[derive(Debug, Clone)]
267pub struct AnimTransition {
268 pub from_state: String,
269 pub to_state: String,
270 pub blend_duration: f32,
272 pub conditions: Vec<Condition>,
274 pub exit_time: Option<f32>,
276 pub can_interrupt: bool,
278 pub priority: i32,
280}
281
282impl AnimTransition {
283 pub fn new(from: &str, to: &str, blend_secs: f32) -> Self {
284 Self {
285 from_state: from.to_string(),
286 to_state: to.to_string(),
287 blend_duration: blend_secs,
288 conditions: Vec::new(),
289 exit_time: None,
290 can_interrupt: false,
291 priority: 0,
292 }
293 }
294
295 pub fn with_condition(mut self, c: Condition) -> Self {
296 self.conditions.push(c);
297 self
298 }
299
300 pub fn with_exit_time(mut self, t: f32) -> Self {
301 self.exit_time = Some(t);
302 self
303 }
304
305 pub fn interruptible(mut self) -> Self {
306 self.can_interrupt = true;
307 self
308 }
309
310 pub fn is_ready(&self, params: &AnimParamSet, normalized_time: f32) -> bool {
311 if let Some(et) = self.exit_time {
313 if normalized_time < et { return false; }
314 }
315 self.conditions.iter().all(|c| c.check(params))
317 }
318}
319
320#[derive(Debug, Clone)]
324pub enum BlendTree {
325 Clip { clip_name: String, speed: f32 },
327
328 Linear1D {
330 param: String,
331 children: Vec<(f32, BlendTree)>, },
333
334 Directional2D {
336 param_x: String,
337 param_y: String,
338 children: Vec<([f32; 2], BlendTree)>, },
340
341 Additive {
343 base: Box<BlendTree>,
344 additive: Box<BlendTree>,
345 weight_param: Option<String>,
346 weight: f32,
347 },
348
349 Override {
351 base: Box<BlendTree>,
352 overlay: Box<BlendTree>,
353 mask: Vec<String>, weight: f32,
355 },
356}
357
358impl BlendTree {
359 pub fn evaluate(
361 &self,
362 clips: &HashMap<String, AnimClip>,
363 params: &AnimParamSet,
364 time: f32,
365 ) -> HashMap<String, f32> {
366 match self {
367 BlendTree::Clip { clip_name, speed } => {
368 if let Some(clip) = clips.get(clip_name) {
369 clip.sample(time * speed)
370 } else {
371 HashMap::new()
372 }
373 }
374
375 BlendTree::Linear1D { param, children } => {
376 if children.is_empty() { return HashMap::new(); }
377 let v = params.get_float(param);
378
379 let idx = children.partition_point(|(t, _)| *t <= v);
381
382 if idx == 0 {
383 return children[0].1.evaluate(clips, params, time);
384 }
385 if idx >= children.len() {
386 return children.last().unwrap().1.evaluate(clips, params, time);
387 }
388
389 let (t0, sub0) = &children[idx - 1];
390 let (t1, sub1) = &children[idx];
391 let alpha = if (t1 - t0).abs() > 1e-6 { (v - t0) / (t1 - t0) } else { 0.0 };
392
393 let a = sub0.evaluate(clips, params, time);
394 let b = sub1.evaluate(clips, params, time);
395 AnimClip::blend_samples(&a, &b, alpha.clamp(0.0, 1.0))
396 }
397
398 BlendTree::Directional2D { param_x, param_y, children } => {
399 if children.is_empty() { return HashMap::new(); }
400 let vx = params.get_float(param_x);
401 let vy = params.get_float(param_y);
402
403 let mut dists: Vec<(f32, usize)> = children.iter().enumerate().map(|(i, (pos, _))| {
405 let dx = pos[0] - vx;
406 let dy = pos[1] - vy;
407 (dx * dx + dy * dy, i)
408 }).collect();
409 dists.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
410
411 let (d0, i0) = dists[0];
412 let (d1, i1) = if dists.len() > 1 { dists[1] } else { dists[0] };
413
414 let total = d0 + d1;
415 let alpha = if total < 1e-6 { 0.0 } else { d0 / total };
416
417 let a = children[i0].1.evaluate(clips, params, time);
418 let b = children[i1].1.evaluate(clips, params, time);
419 AnimClip::blend_samples(&a, &b, alpha)
420 }
421
422 BlendTree::Additive { base, additive, weight_param, weight } => {
423 let base_pose = base.evaluate(clips, params, time);
424 let add_pose = additive.evaluate(clips, params, time);
425 let w = weight_param.as_ref().map(|p| params.get_float(p)).unwrap_or(*weight);
426 let mut out = base_pose;
428 for (k, v) in &add_pose {
429 let entry = out.entry(k.clone()).or_insert(0.0);
430 *entry += v * w;
431 }
432 out
433 }
434
435 BlendTree::Override { base, overlay, mask, weight } => {
436 let base_pose = base.evaluate(clips, params, time);
437 let overlay_pose = overlay.evaluate(clips, params, time);
438 let mut out = base_pose;
439 for (k, v) in &overlay_pose {
440 if mask.iter().any(|m| k.starts_with(m.as_str())) {
441 let entry = out.entry(k.clone()).or_insert(0.0);
442 *entry = *entry * (1.0 - weight) + v * weight;
443 }
444 }
445 out
446 }
447 }
448 }
449}
450
451#[derive(Debug, Clone)]
455pub struct AnimState {
456 pub name: String,
457 pub motion: StateMotion,
458 pub speed: f32,
460 pub speed_param: Option<String>,
461 pub events: Vec<AnimEvent>,
463 pub mirror: bool,
465 pub cycle_offset: f32,
467}
468
469#[derive(Debug, Clone)]
471pub enum StateMotion {
472 Clip(String),
473 BlendTree(BlendTree),
474 SubStateMachine(Box<AnimStateMachine>),
475 Empty,
476}
477
478impl AnimState {
479 pub fn clip(name: &str, clip_name: &str) -> Self {
480 Self {
481 name: name.to_string(),
482 motion: StateMotion::Clip(clip_name.to_string()),
483 speed: 1.0,
484 speed_param: None,
485 events: Vec::new(),
486 mirror: false,
487 cycle_offset: 0.0,
488 }
489 }
490
491 pub fn blend_tree(name: &str, tree: BlendTree) -> Self {
492 Self {
493 name: name.to_string(),
494 motion: StateMotion::BlendTree(tree),
495 speed: 1.0,
496 speed_param: None,
497 events: Vec::new(),
498 mirror: false,
499 cycle_offset: 0.0,
500 }
501 }
502
503 pub fn effective_speed(&self, params: &AnimParamSet) -> f32 {
504 self.speed_param.as_ref().map(|p| params.get_float(p)).unwrap_or(self.speed)
505 }
506}
507
508#[derive(Debug, Clone)]
512pub struct AnimLayer {
513 pub name: String,
514 pub weight: f32,
515 pub blend_mode: LayerBlend,
516 pub mask: Vec<String>,
518 pub machine: AnimStateMachine,
520}
521
522#[derive(Debug, Clone, Copy, PartialEq)]
523pub enum LayerBlend {
524 Override,
525 Additive,
526}
527
528impl AnimLayer {
529 pub fn new(name: &str, machine: AnimStateMachine) -> Self {
530 Self {
531 name: name.to_string(),
532 weight: 1.0,
533 blend_mode: LayerBlend::Override,
534 mask: Vec::new(),
535 machine,
536 }
537 }
538
539 pub fn additive(mut self) -> Self {
540 self.blend_mode = LayerBlend::Additive;
541 self
542 }
543
544 pub fn with_mask(mut self, paths: Vec<&str>) -> Self {
545 self.mask = paths.into_iter().map(|s| s.to_string()).collect();
546 self
547 }
548}
549
550#[derive(Debug, Clone)]
554struct ActiveTransition {
555 to_state: String,
556 elapsed: f32,
557 duration: f32,
558 destination_time: f32,
559}
560
561#[derive(Debug, Clone)]
565pub struct AnimStateMachine {
566 pub name: String,
567 pub states: HashMap<String, AnimState>,
568 pub transitions: Vec<AnimTransition>,
569 pub entry_state: Option<String>,
570 pub any_state_transitions: Vec<AnimTransition>,
571
572 pub current_state: Option<String>,
574 state_time: f32,
575 normalized_time: f32,
576 active_transition: Option<ActiveTransition>,
577 last_clip_duration: f32,
578 fired_events: Vec<AnimEvent>,
579}
580
581impl AnimStateMachine {
582 pub fn new(name: &str) -> Self {
583 Self {
584 name: name.to_string(),
585 states: HashMap::new(),
586 transitions: Vec::new(),
587 entry_state: None,
588 any_state_transitions: Vec::new(),
589 current_state: None,
590 state_time: 0.0,
591 normalized_time: 0.0,
592 active_transition: None,
593 last_clip_duration: 1.0,
594 fired_events: Vec::new(),
595 }
596 }
597
598 pub fn add_state(&mut self, state: AnimState) {
599 if self.entry_state.is_none() {
600 self.entry_state = Some(state.name.clone());
601 }
602 self.states.insert(state.name.clone(), state);
603 }
604
605 pub fn add_transition(&mut self, t: AnimTransition) {
606 self.transitions.push(t);
607 }
608
609 pub fn add_any_transition(&mut self, t: AnimTransition) {
610 self.any_state_transitions.push(t);
611 }
612
613 pub fn enter(&mut self) {
615 self.current_state = self.entry_state.clone();
616 self.state_time = 0.0;
617 self.normalized_time = 0.0;
618 self.active_transition = None;
619 }
620
621 pub fn update(
624 &mut self,
625 dt: f32,
626 params: &mut AnimParamSet,
627 clips: &HashMap<String, AnimClip>,
628 ) -> HashMap<String, f32> {
629 if self.current_state.is_none() { self.enter(); }
631
632 let cur_name = match &self.current_state {
633 Some(n) => n.clone(),
634 None => return HashMap::new(),
635 };
636
637 let cur_state = match self.states.get(&cur_name) {
638 Some(s) => s.clone(),
639 None => return HashMap::new(),
640 };
641
642 let speed = cur_state.effective_speed(params);
643 self.state_time += dt * speed;
644
645 let clip_dur = match &cur_state.motion {
647 StateMotion::Clip(c) => clips.get(c).map(|cl| cl.duration).unwrap_or(1.0),
648 _ => 1.0,
649 };
650 self.last_clip_duration = clip_dur;
651 self.normalized_time = (self.state_time / clip_dur.max(1e-6)).fract();
652
653 self.check_events(&cur_state, self.normalized_time);
655
656 if let Some(ref mut at) = self.active_transition {
658 at.elapsed += dt;
659 at.destination_time += dt;
660 if at.elapsed >= at.duration {
661 let to = at.to_state.clone();
663 let dest_t = at.destination_time;
664 self.active_transition = None;
665 self.current_state = Some(to.clone());
666 self.state_time = dest_t;
667 self.normalized_time = (dest_t / clip_dur.max(1e-6)).fract();
668 }
669 }
670
671 if self.active_transition.is_none() {
673 let triggered = self.find_transition(&cur_name, params, self.normalized_time);
674 if let Some(t) = triggered {
675 let to = t.to_state.clone();
676 let dur = t.blend_duration;
677 for cond in &t.conditions {
679 if let Condition::Trigger(n) = cond {
680 params.mark_trigger_consumed(n);
681 }
682 }
683 if dur < 1e-6 {
684 self.current_state = Some(to);
686 self.state_time = 0.0;
687 self.normalized_time = 0.0;
688 } else {
689 self.active_transition = Some(ActiveTransition {
690 to_state: to,
691 elapsed: 0.0,
692 duration: dur,
693 destination_time: 0.0,
694 });
695 }
696 }
697 }
698 params.flush_triggers();
699
700 let current_pose = self.sample_state(&cur_state, clips, params, self.state_time);
702
703 if let Some(ref at) = self.active_transition {
705 let alpha = (at.elapsed / at.duration.max(1e-6)).clamp(0.0, 1.0);
706 let alpha = smooth_step(alpha);
707 if let Some(dest_state) = self.states.get(&at.to_state).cloned() {
708 let dest_pose = self.sample_state(&dest_state, clips, params, at.destination_time);
709 return AnimClip::blend_samples(¤t_pose, &dest_pose, alpha);
710 }
711 }
712
713 current_pose
714 }
715
716 fn sample_state(
717 &self,
718 state: &AnimState,
719 clips: &HashMap<String, AnimClip>,
720 params: &AnimParamSet,
721 time: f32,
722 ) -> HashMap<String, f32> {
723 match &state.motion {
724 StateMotion::Clip(c) => {
725 if let Some(clip) = clips.get(c) {
726 clip.sample(time)
727 } else {
728 HashMap::new()
729 }
730 }
731 StateMotion::BlendTree(tree) => tree.evaluate(clips, params, time),
732 StateMotion::SubStateMachine(_) => HashMap::new(), StateMotion::Empty => HashMap::new(),
734 }
735 }
736
737 fn find_transition<'a>(
738 &'a self,
739 from: &str,
740 params: &AnimParamSet,
741 normalized_time: f32,
742 ) -> Option<&'a AnimTransition> {
743 let mut candidates: Vec<&AnimTransition> = self.any_state_transitions.iter()
745 .filter(|t| t.to_state != *from && t.is_ready(params, normalized_time))
746 .collect();
747
748 candidates.extend(self.transitions.iter()
750 .filter(|t| t.from_state == *from && t.is_ready(params, normalized_time)));
751
752 candidates.sort_by(|a, b| b.priority.cmp(&a.priority));
753 candidates.into_iter().next()
754 }
755
756 fn check_events(&mut self, state: &AnimState, normalized_time: f32) {
757 for ev in &state.events {
758 if (ev.normalized_time - normalized_time).abs() < 0.02 {
760 self.fired_events.push(ev.clone());
761 }
762 }
763 }
764
765 pub fn drain_events(&mut self) -> Vec<AnimEvent> {
767 std::mem::take(&mut self.fired_events)
768 }
769
770 pub fn current_state_name(&self) -> Option<&str> {
771 self.current_state.as_deref()
772 }
773
774 pub fn normalized_time(&self) -> f32 { self.normalized_time }
775 pub fn state_time(&self) -> f32 { self.state_time }
776 pub fn is_transitioning(&self) -> bool { self.active_transition.is_some() }
777}
778
779pub struct Animator {
784 pub layers: Vec<AnimLayer>,
785 pub clips: HashMap<String, AnimClip>,
786 pub params: AnimParamSet,
787 root_motion: (f32, f32, f32),
789 pub use_root_motion: bool,
791}
792
793impl Animator {
794 pub fn new() -> Self {
795 Self {
796 layers: Vec::new(),
797 clips: HashMap::new(),
798 params: AnimParamSet::default(),
799 root_motion: (0.0, 0.0, 0.0),
800 use_root_motion: false,
801 }
802 }
803
804 pub fn add_clip(&mut self, clip: AnimClip) {
805 self.clips.insert(clip.name.clone(), clip);
806 }
807
808 pub fn add_layer(&mut self, layer: AnimLayer) {
809 self.layers.push(layer);
810 }
811
812 pub fn set_float(&mut self, n: &str, v: f32) { self.params.set_float(n, v); }
813 pub fn set_int (&mut self, n: &str, v: i32) { self.params.set_int(n, v); }
814 pub fn set_bool (&mut self, n: &str, v: bool) { self.params.set_bool(n, v); }
815 pub fn set_trigger(&mut self, n: &str) { self.params.set_trigger(n); }
816
817 pub fn update(&mut self, dt: f32) -> HashMap<String, f32> {
819 let mut final_pose: HashMap<String, f32> = HashMap::new();
820
821 for layer in &mut self.layers {
822 let pose = layer.machine.update(dt, &mut self.params, &self.clips);
823 let weight = layer.weight;
824
825 let masked_pose: HashMap<String, f32> = if layer.mask.is_empty() {
827 pose
828 } else {
829 pose.into_iter()
830 .filter(|(k, _)| layer.mask.iter().any(|m| k.starts_with(m.as_str())))
831 .collect()
832 };
833
834 match layer.blend_mode {
835 LayerBlend::Override => {
836 for (k, v) in masked_pose {
837 let entry = final_pose.entry(k).or_insert(0.0);
838 *entry = *entry * (1.0 - weight) + v * weight;
839 }
840 }
841 LayerBlend::Additive => {
842 for (k, v) in masked_pose {
843 let entry = final_pose.entry(k).or_insert(0.0);
844 *entry += v * weight;
845 }
846 }
847 }
848 }
849
850 final_pose
851 }
852
853 pub fn consume_root_motion(&mut self) -> (f32, f32, f32) {
855 std::mem::take(&mut self.root_motion)
856 }
857
858 pub fn drain_events(&mut self) -> Vec<AnimEvent> {
860 self.layers.iter_mut().flat_map(|l| l.machine.drain_events()).collect()
861 }
862}
863
864impl Default for Animator {
865 fn default() -> Self { Self::new() }
866}
867
868pub struct AnimatorBuilder {
872 animator: Animator,
873}
874
875impl AnimatorBuilder {
876 pub fn new() -> Self {
877 Self { animator: Animator::new() }
878 }
879
880 pub fn clip(mut self, clip: AnimClip) -> Self {
881 self.animator.add_clip(clip);
882 self
883 }
884
885 pub fn layer(mut self, layer: AnimLayer) -> Self {
886 self.animator.add_layer(layer);
887 self
888 }
889
890 pub fn root_motion(mut self) -> Self {
891 self.animator.use_root_motion = true;
892 self
893 }
894
895 pub fn build(self) -> Animator {
896 self.animator
897 }
898}
899
900pub struct AnimPresets;
904
905impl AnimPresets {
906 pub fn humanoid_locomotion() -> AnimStateMachine {
908 let mut sm = AnimStateMachine::new("locomotion");
909
910 sm.add_state(AnimState::clip("idle", "humanoid_idle"));
911 sm.add_state(AnimState::clip("walk", "humanoid_walk"));
912 sm.add_state(AnimState::blend_tree("locomotion_blend",
913 BlendTree::Linear1D {
914 param: "speed".to_string(),
915 children: vec![
916 (0.0, BlendTree::Clip { clip_name: "humanoid_idle".to_string(), speed: 1.0 }),
917 (0.5, BlendTree::Clip { clip_name: "humanoid_walk".to_string(), speed: 1.0 }),
918 (1.0, BlendTree::Clip { clip_name: "humanoid_run".to_string(), speed: 1.0 }),
919 ],
920 }
921 ));
922 sm.add_state(AnimState::clip("jump_rise", "humanoid_jump_rise"));
923 sm.add_state(AnimState::clip("jump_fall", "humanoid_jump_fall"));
924 sm.add_state(AnimState::clip("land", "humanoid_land"));
925
926 sm.add_transition(AnimTransition::new("locomotion_blend", "jump_rise", 0.1)
927 .with_condition(Condition::Trigger("jump".to_string())));
928 sm.add_transition(AnimTransition::new("jump_rise", "jump_fall", 0.15)
929 .with_condition(Condition::FloatLess("velocity_y".to_string(), 0.0)));
930 sm.add_transition(AnimTransition::new("jump_fall", "land", 0.05)
931 .with_condition(Condition::BoolTrue("grounded".to_string())));
932 sm.add_transition(AnimTransition::new("land", "locomotion_blend", 0.2)
933 .with_exit_time(0.7));
934
935 sm.entry_state = Some("locomotion_blend".to_string());
936 sm
937 }
938
939 pub fn combat_humanoid() -> AnimStateMachine {
941 let mut sm = AnimStateMachine::new("combat");
942
943 sm.add_state(AnimState::clip("idle_combat", "combat_idle"));
944 sm.add_state(AnimState::clip("attack_light", "combat_attack_light"));
945 sm.add_state(AnimState::clip("attack_heavy", "combat_attack_heavy"));
946 sm.add_state(AnimState::clip("attack_combo2", "combat_attack_combo2"));
947 sm.add_state(AnimState::clip("dodge", "combat_dodge"));
948 sm.add_state(AnimState::clip("block", "combat_block"));
949 sm.add_state(AnimState::clip("hurt", "combat_hurt"));
950 sm.add_state(AnimState::clip("death", "combat_death"));
951
952 sm.add_transition(AnimTransition::new("idle_combat", "attack_light", 0.1)
954 .with_condition(Condition::Trigger("attack_light".to_string())));
955 sm.add_transition(AnimTransition::new("attack_light", "attack_combo2", 0.1)
956 .with_condition(Condition::Trigger("attack_light".to_string()))
957 .with_exit_time(0.4));
958 sm.add_transition(AnimTransition::new("attack_light", "idle_combat", 0.2)
959 .with_exit_time(0.9));
960 sm.add_transition(AnimTransition::new("attack_combo2", "idle_combat", 0.2)
961 .with_exit_time(0.9));
962
963 sm.add_transition(AnimTransition::new("idle_combat", "attack_heavy", 0.1)
965 .with_condition(Condition::Trigger("attack_heavy".to_string())));
966 sm.add_transition(AnimTransition::new("attack_heavy", "idle_combat", 0.2)
967 .with_exit_time(0.9));
968
969 sm.add_transition(AnimTransition::new("idle_combat", "dodge", 0.05)
971 .with_condition(Condition::Trigger("dodge".to_string())));
972 sm.add_transition(AnimTransition::new("dodge", "idle_combat", 0.1)
973 .with_exit_time(0.85));
974
975 sm.add_transition(AnimTransition::new("idle_combat", "block", 0.1)
977 .with_condition(Condition::BoolTrue("blocking".to_string())));
978 sm.add_transition(AnimTransition::new("block", "idle_combat", 0.15)
979 .with_condition(Condition::BoolFalse("blocking".to_string())));
980
981 sm.add_any_transition(AnimTransition::new("", "hurt", 0.05)
983 .with_condition(Condition::Trigger("hurt".to_string())));
984 sm.add_transition(AnimTransition::new("hurt", "idle_combat", 0.15)
985 .with_exit_time(0.8));
986
987 let mut death_t = AnimTransition::new("", "death", 0.05);
989 death_t.conditions.push(Condition::Trigger("death".to_string()));
990 death_t.priority = 100;
991 sm.add_any_transition(death_t);
992
993 sm.entry_state = Some("idle_combat".to_string());
994 sm
995 }
996
997 pub fn flying_creature() -> AnimStateMachine {
999 let mut sm = AnimStateMachine::new("flying");
1000
1001 sm.add_state(AnimState::clip("hover", "fly_hover"));
1002 sm.add_state(AnimState::clip("flap", "fly_flap"));
1003 sm.add_state(AnimState::clip("glide", "fly_glide"));
1004 sm.add_state(AnimState::clip("dive", "fly_dive"));
1005 sm.add_state(AnimState::clip("land", "fly_land"));
1006
1007 sm.add_transition(AnimTransition::new("hover", "flap", 0.2)
1008 .with_condition(Condition::FloatGreater("speed".to_string(), 0.3)));
1009 sm.add_transition(AnimTransition::new("flap", "glide", 0.3)
1010 .with_condition(Condition::FloatGreater("speed".to_string(), 0.8)));
1011 sm.add_transition(AnimTransition::new("glide", "flap", 0.2)
1012 .with_condition(Condition::FloatLess("speed".to_string(), 0.6)));
1013 sm.add_transition(AnimTransition::new("glide", "dive", 0.15)
1014 .with_condition(Condition::FloatLess("velocity_y".to_string(), -0.5)));
1015 sm.add_transition(AnimTransition::new("dive", "glide", 0.3)
1016 .with_condition(Condition::FloatGreater("velocity_y".to_string(), 0.0)));
1017 sm.add_any_transition(AnimTransition::new("", "land", 0.2)
1018 .with_condition(Condition::Trigger("land".to_string())));
1019 sm.add_transition(AnimTransition::new("land", "hover", 0.3)
1020 .with_exit_time(0.9));
1021
1022 sm.entry_state = Some("hover".to_string());
1023 sm
1024 }
1025}
1026
1027fn smooth_step(t: f32) -> f32 {
1030 let t = t.clamp(0.0, 1.0);
1031 t * t * (3.0 - 2.0 * t)
1032}
1033
1034#[cfg(test)]
1037mod tests {
1038 use super::*;
1039
1040 fn make_clip(name: &str, duration: f32) -> AnimClip {
1041 let mut clip = AnimClip::new(name, duration);
1042 clip.add_channel("pos_x", AnimCurve::linear(0.0, 0.0, duration, 1.0));
1043 clip
1044 }
1045
1046 #[test]
1047 fn test_anim_curve_sample() {
1048 let curve = AnimCurve::linear(0.0, 0.0, 1.0, 1.0);
1049 assert!((curve.sample(0.5) - 0.5).abs() < 0.01);
1050 assert!((curve.sample(0.0) - 0.0).abs() < 0.01);
1051 assert!((curve.sample(1.0) - 1.0).abs() < 0.01);
1052 }
1053
1054 #[test]
1055 fn test_anim_curve_clamp() {
1056 let curve = AnimCurve::linear(0.0, 5.0, 1.0, 10.0);
1057 assert!((curve.sample(-1.0) - 5.0).abs() < 0.01);
1058 assert!((curve.sample(2.0) - 10.0).abs() < 0.01);
1059 }
1060
1061 #[test]
1062 fn test_anim_clip_sample() {
1063 let clip = make_clip("test", 2.0);
1064 let pose = clip.sample(1.0);
1065 assert!(pose.contains_key("pos_x"));
1066 let v = pose["pos_x"];
1067 assert!(v > 0.4 && v < 0.6, "pos_x at t=1 of 2s clip should be ~0.5, got {}", v);
1068 }
1069
1070 #[test]
1071 fn test_blend_samples() {
1072 let mut a = HashMap::new(); a.insert("x".to_string(), 0.0_f32);
1073 let mut b = HashMap::new(); b.insert("x".to_string(), 1.0_f32);
1074 let blended = AnimClip::blend_samples(&a, &b, 0.5);
1075 assert!((blended["x"] - 0.5).abs() < 0.001);
1076 }
1077
1078 #[test]
1079 fn test_state_machine_transitions() {
1080 let mut sm = AnimStateMachine::new("test");
1081 sm.add_state(AnimState::clip("idle", "idle_clip"));
1082 sm.add_state(AnimState::clip("run", "run_clip"));
1083 sm.add_transition(AnimTransition::new("idle", "run", 0.1)
1084 .with_condition(Condition::Trigger("run".to_string())));
1085
1086 let mut clips = HashMap::new();
1087 clips.insert("idle_clip".to_string(), make_clip("idle_clip", 1.0));
1088 clips.insert("run_clip".to_string(), make_clip("run_clip", 1.0));
1089
1090 let mut params = AnimParamSet::default();
1091 sm.enter();
1092 sm.update(0.016, &mut params, &clips);
1093 assert_eq!(sm.current_state_name(), Some("idle"));
1094
1095 params.set_trigger("run");
1096 sm.update(0.016, &mut params, &clips);
1097 sm.update(0.15, &mut params, &clips);
1099 assert_eq!(sm.current_state_name(), Some("run"));
1100 }
1101
1102 #[test]
1103 fn test_blend_tree_linear() {
1104 let mut clips = HashMap::new();
1105 clips.insert("idle".to_string(), make_clip("idle", 1.0));
1106 clips.insert("walk".to_string(), make_clip("walk", 1.0));
1107 clips.insert("run".to_string(), make_clip("run", 1.0));
1108
1109 let tree = BlendTree::Linear1D {
1110 param: "speed".to_string(),
1111 children: vec![
1112 (0.0, BlendTree::Clip { clip_name: "idle".to_string(), speed: 1.0 }),
1113 (1.0, BlendTree::Clip { clip_name: "run".to_string(), speed: 1.0 }),
1114 ],
1115 };
1116
1117 let mut params = AnimParamSet::default();
1118 params.set_float("speed", 0.5);
1119 let pose = tree.evaluate(&clips, ¶ms, 0.5);
1120 let v = pose.get("pos_x").copied().unwrap_or(0.0);
1122 assert!(v > 0.0, "blend tree should produce non-zero values");
1123 }
1124
1125 #[test]
1126 fn test_animator_layers() {
1127 let mut animator = Animator::new();
1128 animator.add_clip(make_clip("idle_clip", 1.0));
1129
1130 let mut sm = AnimStateMachine::new("base");
1131 sm.add_state(AnimState::clip("idle", "idle_clip"));
1132
1133 animator.add_layer(AnimLayer::new("base", sm));
1134 let pose = animator.update(0.016);
1135 assert!(!pose.is_empty() || pose.is_empty(), "should not panic");
1136 }
1137
1138 #[test]
1139 fn test_anim_presets_locomotion() {
1140 let sm = AnimPresets::humanoid_locomotion();
1141 assert!(sm.states.contains_key("locomotion_blend"));
1142 assert!(sm.states.contains_key("jump_rise"));
1143 assert!(sm.transitions.len() >= 4);
1144 }
1145
1146 #[test]
1147 fn test_anim_presets_combat() {
1148 let sm = AnimPresets::combat_humanoid();
1149 assert!(sm.states.contains_key("attack_light"));
1150 assert!(sm.states.contains_key("death"));
1151 assert!(!sm.any_state_transitions.is_empty());
1152 }
1153
1154 #[test]
1155 fn test_smooth_step() {
1156 assert!((smooth_step(0.0) - 0.0).abs() < 1e-6);
1157 assert!((smooth_step(1.0) - 1.0).abs() < 1e-6);
1158 assert!((smooth_step(0.5) - 0.5).abs() < 1e-6);
1159 }
1160}