Skip to main content

kvas_engine/
animation.rs

1//! Serializable transform keyframes with quaternion interpolation, scrubbing and runtime playback.
2use crate::{components::{Component, Transform}, reflect::Field};
3use glam::Vec3;
4use kvas_ecs::World;
5use serde::{Deserialize, Serialize};
6
7/// Local-space transform at an absolute clip time in seconds.
8#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
9pub struct TransformKey {
10    /// Seconds from clip start, finite and non-negative.
11    pub time: f32,
12    /// Local pose sampled at this time.
13    pub pose: Transform,
14}
15
16/// Interpolation between adjacent keyframes.
17#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq)]
18pub enum Interpolation {
19    /// Hold the previous pose until the next key.
20    Step,
21    /// Linear position/scale and shortest-path quaternion rotation.
22    #[default]
23    Linear,
24    /// Smoothstep position/scale and quaternion rotation.
25    Smooth,
26}
27
28/// Authorable local-transform clip. Animate static/kinematic entities; dynamic physics owns its pose.
29#[derive(Clone, Debug, Serialize, Deserialize)]
30#[serde(default)]
31pub struct TransformAnimation {
32    /// Ordered poses; use [`Self::set_key`] when recording keys.
33    pub keys: Vec<TransformKey>,
34    /// Playback cursor in seconds; also used for editor scrubbing.
35    pub time: f32,
36    /// Playback multiplier; negative values play backwards.
37    pub speed: f32,
38    /// Repeat from the start after reaching the last key.
39    pub looping: bool,
40    /// Runtime playback switch.
41    pub playing: bool,
42    /// Curve used between keys.
43    pub interpolation: Interpolation,
44}
45
46impl Default for TransformAnimation {
47    fn default() -> Self { Self { keys: Vec::new(), time: 0.0, speed: 1.0, looping: true, playing: true, interpolation: Interpolation::Linear } }
48}
49
50impl Component for TransformAnimation {
51    fn fields(&mut self) -> Vec<Field<'_>> {
52        vec![Field::bool("playing", &mut self.playing), Field::bool("looping", &mut self.looping), Field::f32("speed", &mut self.speed)]
53    }
54}
55
56impl TransformAnimation {
57    /// Last key time, or zero for an empty clip.
58    pub fn duration(&self) -> f32 { self.keys.iter().map(|k| k.time).filter(|t| t.is_finite()).fold(0.0, f32::max) }
59
60    /// Inserts or replaces a key within 0.1 ms. Invalid times/poses leave the clip unchanged.
61    pub fn set_key(&mut self, time: f32, pose: Transform) -> bool {
62        if !time.is_finite() || time < 0.0 || !pose.is_finite() { return false; }
63        self.keys.retain(|k| (k.time - time).abs() > 0.0001);
64        self.keys.push(TransformKey { time, pose });
65        self.keys.sort_by(|a, b| a.time.total_cmp(&b.time));
66        true
67    }
68
69    /// Samples without changing playback state. Unsorted imported keys and duplicate times are tolerated.
70    pub fn sample(&self, time: f32) -> Option<Transform> {
71        if !time.is_finite() { return None; }
72        let valid = |k: &&TransformKey| k.time.is_finite() && k.time >= 0.0 && k.pose.is_finite();
73        let first = self.keys.iter().filter(valid).min_by(|a, b| a.time.total_cmp(&b.time))?;
74        let a = self.keys.iter().filter(valid).filter(|k| k.time <= time).max_by(|a, b| a.time.total_cmp(&b.time)).unwrap_or(first);
75        let Some(b) = self.keys.iter().filter(valid).filter(|k| k.time > time).min_by(|a, b| a.time.total_cmp(&b.time)) else { return Some(a.pose); };
76        let mut t = ((time - a.time) / (b.time - a.time).max(1e-6)).clamp(0.0, 1.0);
77        t = match self.interpolation { Interpolation::Step => 0.0, Interpolation::Linear => t, Interpolation::Smooth => t * t * (3.0 - 2.0 * t) };
78        let mut pose = Transform { position: a.pose.position.lerp(b.pose.position, t), scale: a.pose.scale.lerp(b.pose.scale, t), rotation: Vec3::ZERO };
79        pose.set_quat(a.pose.quat().slerp(b.pose.quat(), t));
80        Some(pose)
81    }
82}
83
84/// Advances all enabled clips. Disabled clips leave their transform untouched.
85pub fn update(world: &mut World, dt: f32) {
86    if !dt.is_finite() || dt < 0.0 { return; }
87    for e in world.entities_with::<TransformAnimation>() {
88        let pose = {
89            let Some(clip) = world.get_mut::<TransformAnimation>(e) else { continue };
90            if !clip.playing || !clip.speed.is_finite() || !clip.time.is_finite() { continue; }
91            let duration = clip.duration();
92            clip.time += dt * clip.speed;
93            if duration > 0.0 && clip.looping { clip.time = clip.time.rem_euclid(duration); }
94            else if clip.time >= duration || clip.time < 0.0 { clip.time = clip.time.clamp(0.0, duration); clip.playing = false; }
95            clip.sample(clip.time)
96        };
97        if let (Some(pose), Some(target)) = (pose, world.get_mut::<Transform>(e)) { *target = pose; }
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    #[test]
105    fn keys_interpolate_shortest_rotation_and_reject_invalid_data() {
106        let mut clip = TransformAnimation::default();
107        let mut a = Transform::at(Vec3::ZERO); a.rotation.y = 350.0;
108        let mut b = Transform::at(Vec3::X * 10.0); b.rotation.y = 10.0;
109        assert!(clip.set_key(2.0, b)); assert!(clip.set_key(0.0, a));
110        let mid = clip.sample(1.0).unwrap();
111        assert!((mid.position.x - 5.0).abs() < 1e-5);
112        assert!(mid.forward().dot(glam::Vec3::NEG_Z) > 0.999);
113        assert!(!clip.set_key(f32::NAN, a));
114        clip.interpolation = Interpolation::Step;
115        assert_eq!(clip.sample(1.0).unwrap().position, Vec3::ZERO);
116    }
117    #[test]
118    fn persisted_clip_runs_and_stops_in_the_shared_runtime() {
119        let mut scene = crate::scene::Scene::default(); let e = scene.spawn("animated");
120        let mut clip = TransformAnimation { looping: false, ..Default::default() };
121        clip.set_key(0.0, Transform::default()); clip.set_key(1.0, Transform::at(Vec3::X));
122        scene.world.insert(e, clip);
123        let reg = crate::registry::Registry::default();
124        let (scene, warnings) = crate::scene::Scene::from_json(&reg, &scene.to_json(&reg)).unwrap(); assert!(warnings.is_empty());
125        let mut rt = crate::runtime::Runtime::new(scene, None);
126        for _ in 0..12 { rt.tick(0.1); }
127        let e = crate::hierarchy::find_by_name(&rt.scene.world, "animated").unwrap();
128        assert_eq!(rt.scene.world.get::<Transform>(e).unwrap().position, Vec3::X);
129        assert!(!rt.scene.world.get::<TransformAnimation>(e).unwrap().playing);
130    }
131}