tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
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}