tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 660Markdown 302TOML 157RON 312382 Definiciones

⎇ main llimphi-3d/src/cinema.rs Rust · 139 líneas · 4.8 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

1//! `CameraTrack` — interpolación de cámara por **keyframes** en el tiempo, el
2//! ingrediente "cine" del motor: en vez de una `Camera3d` fija o atada a input,
3//! una secuencia de poses `(t, eye, target, fov)` que se interpolan suave para
4//! producir un **movimiento de cámara guionado** (travelling, grúa, dolly,
5//! corte). Determinista por construcción → ideal para *filmar* frame a frame.
6//!
7//! Es genérico del motor 3D (no sabe de voxels ni de juegos): cualquier app que
8//! quiera una cámara animada lo usa. La *dirección* de actores/eventos vive en
9//! la capa de contenido (la app), no aquí.
10
11use glam::Vec3;
12
13use crate::camera::Camera3d;
14
15/// Una pose de cámara anclada a un instante `t` (segundos). Entre keys
16/// consecutivas, [`CameraTrack::sample`] interpola `eye`/`target`/`fovy_rad`.
17#[derive(Debug, Clone, Copy)]
18pub struct CamKey {
19    /// Instante de la pose, en segundos desde el inicio.
20    pub t: f32,
21    /// Posición del ojo.
22    pub eye: Vec3,
23    /// Punto al que mira.
24    pub target: Vec3,
25    /// Campo de visión vertical (radianes) en esta pose.
26    pub fovy_rad: f32,
27}
28
29impl CamKey {
30    /// Atajo: una pose mirando de `eye` a `target` con FOV en **grados**.
31    pub fn look(t: f32, eye: Vec3, target: Vec3, fov_deg: f32) -> Self {
32        Self { t, eye, target, fovy_rad: fov_deg.to_radians() }
33    }
34}
35
36/// Secuencia de [`CamKey`] ordenada en el tiempo. `sample(t)` devuelve la
37/// `Camera3d` interpolada; fuera de rango hace *clamp* a la primera/última pose.
38#[derive(Debug, Clone, Default)]
39pub struct CameraTrack {
40    keys: Vec<CamKey>,
41}
42
43impl CameraTrack {
44    /// Crea el track a partir de las keys (se ordenan por `t`). Un track vacío
45    /// o de una sola key es válido (devuelve siempre esa pose).
46    pub fn new(mut keys: Vec<CamKey>) -> Self {
47        keys.sort_by(|a, b| a.t.total_cmp(&b.t));
48        Self { keys }
49    }
50
51    /// Duración total (el `t` de la última key), o `0.0` si está vacío.
52    pub fn duration(&self) -> f32 {
53        self.keys.last().map(|k| k.t).unwrap_or(0.0)
54    }
55
56    /// La cámara interpolada en el instante `t` (segundos). Entre dos keys usa
57    /// **smoothstep** (acelera/desacelera suave, sin tirones) sobre la fracción
58    /// del segmento; antes de la primera / después de la última, clampa.
59    pub fn sample(&self, t: f32) -> Camera3d {
60        match self.keys.as_slice() {
61            [] => Camera3d::default(),
62            [only] => cam_of(only),
63            keys => {
64                // Clamp a los extremos.
65                if t <= keys[0].t {
66                    return cam_of(&keys[0]);
67                }
68                if t >= keys[keys.len() - 1].t {
69                    return cam_of(&keys[keys.len() - 1]);
70                }
71                // Segmento que contiene a `t`: última key con `t_key <= t`
72                // (existe y no es la última, por el clamp de arriba).
73                let i = keys.iter().rposition(|k| k.t <= t).unwrap_or(0).min(keys.len() - 2);
74                let (a, b) = (&keys[i], &keys[i + 1]);
75                let span = (b.t - a.t).max(1e-6);
76                let f = smoothstep((t - a.t) / span);
77                Camera3d {
78                    eye: a.eye.lerp(b.eye, f),
79                    target: a.target.lerp(b.target, f),
80                    fovy_rad: a.fovy_rad + (b.fovy_rad - a.fovy_rad) * f,
81                    ..Camera3d::default()
82                }
83            }
84        }
85    }
86}
87
88/// Construye una `Camera3d` (con `up`/planos por defecto) desde una key.
89fn cam_of(k: &CamKey) -> Camera3d {
90    Camera3d {
91        eye: k.eye,
92        target: k.target,
93        fovy_rad: k.fovy_rad,
94        ..Camera3d::default()
95    }
96}
97
98/// Suavizado Hermite clásico `3t²−2t³` en `[0,1]` (deriva nula en los extremos).
99fn smoothstep(x: f32) -> f32 {
100    let x = x.clamp(0.0, 1.0);
101    x * x * (3.0 - 2.0 * x)
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    fn track() -> CameraTrack {
109        CameraTrack::new(vec![
110            CamKey::look(0.0, Vec3::new(0.0, 0.0, 0.0), Vec3::Z, 60.0),
111            CamKey::look(2.0, Vec3::new(10.0, 0.0, 0.0), Vec3::Z, 40.0),
112        ])
113    }
114
115    #[test]
116    fn clamp_en_los_extremos() {
117        let tr = track();
118        assert_eq!(tr.sample(-1.0).eye, Vec3::ZERO);
119        assert_eq!(tr.sample(5.0).eye.x, 10.0);
120        assert_eq!(tr.duration(), 2.0);
121    }
122
123    #[test]
124    fn interpola_la_mitad_con_smoothstep() {
125        let tr = track();
126        // En la mitad temporal, smoothstep(0.5)=0.5 → punto medio exacto.
127        let c = tr.sample(1.0);
128        assert!((c.eye.x - 5.0).abs() < 1e-4, "x={}", c.eye.x);
129        assert!((c.fovy_rad - 50_f32.to_radians()).abs() < 1e-4);
130    }
131
132    #[test]
133    fn smoothstep_acelera_suave() {
134        let tr = track();
135        // A 1/4 del tiempo, smoothstep(0.25)=0.15625 < 0.25 (arranca lento).
136        let c = tr.sample(0.5);
137        assert!(c.eye.x < 2.5, "debería ir más lento al principio: x={}", c.eye.x);
138    }
139}