gioser

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-voxel/src/director.rs Rust · 379 líneas · 14.7 KB Historia Quién tocó qué crudo

feat(3d): cosechar efectos genéricos de supay + refrescar voxel/director 333dca1d5 · Sergio ·

1//! `director` — una **timeline guionada** para filmar: describe qué hace cada
2//! actor y qué hace la cámara *en función del tiempo*, de forma **determinista**
3//! (mismo `t` → mismo estado → reproducible cuadro a cuadro). Es la capa de
4//! *dirección* que faltaba: en vez de hardcodear el bucle de la escena, se
5//! declara un [`Sequence`] (el "guion") y se reproduce.
6//!
7//! Modelo, deliberadamente chico:
8//! - [`ActorScript`] = keyframes `(t, posición de grilla, clip?, rumbo?)` de un
9//!   actor; `sample(t)` interpola la posición y decide el [`Clip`] (auto: camina
10//!   si se mueve, quieto si no) y el rumbo (auto: dirección de marcha).
11//! - [`Shot`] = un plano de cámara ([`CameraTrack`]) con su instante de inicio;
12//!   varios planos dan **cortes duros** (cambia de plano sin interpolar).
13//! - [`Sequence`] = el reparto de scripts + la lista de planos + duración.
14//!
15//! Es **contenido puro** (coordenadas de grilla, sin terreno ni GPU): el que
16//! reproduce (la app) mapea la posición de grilla al relieve y posa cada
17//! [`Actor`](crate::Actor) (con su cross-fade de clips). Reusable por cualquier
18//! película/juego voxel.
19
20use llimphi_3d::glam::Vec3;
21use llimphi_3d::{Camera3d, CameraTrack};
22
23use crate::actor::Clip;
24
25/// Umbral de desplazamiento (unidades de grilla por segmento) para considerar
26/// que un actor "se mueve" (y por tanto camina y mira hacia donde va).
27const MOVING_EPS: f32 = 0.05;
28
29/// Envuelve un ángulo a `[-π, π]` (la diferencia más corta entre dos rumbos).
30fn wrap_pi(mut a: f32) -> f32 {
31    use std::f32::consts::PI;
32    while a > PI {
33        a -= 2.0 * PI;
34    }
35    while a < -PI {
36        a += 2.0 * PI;
37    }
38    a
39}
40
41/// Un keyframe de actor: dónde está (grilla), opcionalmente qué clip reproduce y
42/// hacia dónde mira. `clip`/`face` en `None` = automático.
43#[derive(Debug, Clone, Copy)]
44pub struct ActorKey {
45    /// Instante (segundos).
46    pub t: f32,
47    /// Posición de grilla en `t`.
48    pub gx: f32,
49    pub gz: f32,
50    /// Clip explícito mientras se sale de esta key (`None` = auto: caminar/quieto).
51    pub clip: Option<Clip>,
52    /// Rumbo explícito (yaw; `None` = auto: dirección de marcha).
53    pub face: Option<f32>,
54}
55
56impl ActorKey {
57    /// Key en `(gx, gz)` a tiempo `t`, clip/rumbo automáticos.
58    pub fn at(t: f32, gx: f32, gz: f32) -> Self {
59        Self { t, gx, gz, clip: None, face: None }
60    }
61
62    /// Fija el clip a reproducir desde esta key (p.ej. quedarse saludando).
63    pub fn play(mut self, clip: Clip) -> Self {
64        self.clip = Some(clip);
65        self
66    }
67
68    /// Fija el rumbo (yaw) desde esta key (p.ej. mirar a la cámara al detenerse).
69    pub fn facing(mut self, yaw: f32) -> Self {
70        self.face = Some(yaw);
71        self
72    }
73}
74
75/// Lo que un [`ActorScript`] dicta para un instante: dónde poner al actor
76/// (grilla), hacia dónde mirar y qué clip animar.
77#[derive(Debug, Clone, Copy)]
78pub struct ActorSample {
79    pub gx: f32,
80    pub gz: f32,
81    pub facing: f32,
82    pub clip: Clip,
83}
84
85/// El guion de **un** actor: una lista de [`ActorKey`] ordenada en el tiempo.
86#[derive(Debug, Clone, Default)]
87pub struct ActorScript {
88    keys: Vec<ActorKey>,
89    /// **Tasa de cuadros propia** de este actor (`None` = la nativa de la peli,
90    /// fluida). Si está, el tiempo se *cuantiza* a esa rejilla antes de muestrear
91    /// posición **y** pose: el actor se mueve "a tirones" (animación *en doses*,
92    /// estilo stop-motion/Spider-Verse) aunque el render salga a 30/60. Es el
93    /// **sello de animación**: el Héroe a 12 fps contra un Avatar fluido a 60 da
94    /// una extrañeza que nadie obtiene usando el motor por defecto.
95    frame_rate: Option<u32>,
96}
97
98impl ActorScript {
99    /// Crea el guion (ordena las keys por `t`).
100    pub fn new(mut keys: Vec<ActorKey>) -> Self {
101        keys.sort_by(|a, b| a.t.total_cmp(&b.t));
102        Self { keys, frame_rate: None }
103    }
104
105    /// Fija la **tasa de cuadros propia** del actor (animación en doses). `0` o
106    /// quitarla = fluido (nativo). Ver [`quantize`](Self::quantize).
107    pub fn with_frame_rate(mut self, fps: Option<u32>) -> Self {
108        self.frame_rate = fps.filter(|&f| f > 0);
109        self
110    }
111
112    /// La tasa de cuadros propia del actor (`None` = fluido).
113    pub fn frame_rate(&self) -> Option<u32> {
114        self.frame_rate
115    }
116
117    /// **Cuantiza** un instante a la rejilla de cuadros del actor: si tiene
118    /// `frame_rate = Some(fps)`, devuelve el inicio del cuadro que contiene a `t`
119    /// (`floor(t·fps)/fps`); si no, devuelve `t` tal cual. El reproductor debe
120    /// muestrear **posición y pose** con este tiempo cuantizado para que el cuerpo
121    /// avance en escalones (no sólo se teletransporte la posición).
122    pub fn quantize(&self, t: f32) -> f32 {
123        match self.frame_rate {
124            Some(fps) => (t * fps as f32).floor() / fps as f32,
125            None => t,
126        }
127    }
128
129    /// Tiempo de la última key (duración del guion del actor).
130    pub fn duration(&self) -> f32 {
131        self.keys.last().map(|k| k.t).unwrap_or(0.0)
132    }
133
134    /// Instantes (segundos) en que este actor **arranca un gesto** (clip emote
135    /// explícito en una key). Son momentos expresivos del guion — el director los usa
136    /// para acentuar la música.
137    pub fn emote_onsets(&self) -> Vec<f32> {
138        self.keys.iter().filter(|k| k.clip.is_some_and(|c| c.is_emote())).map(|k| k.t).collect()
139    }
140
141    /// El estado dictado en `t`: posición interpolada + clip + rumbo. Antes/
142    /// después del rango, clampa a la primera/última key (quieto).
143    pub fn sample(&self, t: f32) -> ActorSample {
144        let keys = &self.keys;
145        if keys.is_empty() {
146            return ActorSample { gx: 0.0, gz: 0.0, facing: 0.0, clip: Clip::Idle };
147        }
148        let last = keys.len() - 1;
149        // Extremos: quieto en la primera/última key.
150        if t <= keys[0].t || last == 0 {
151            let k = &keys[0];
152            return ActorSample {
153                gx: k.gx,
154                gz: k.gz,
155                facing: k.face.unwrap_or(0.0),
156                clip: k.clip.unwrap_or(Clip::Idle),
157            };
158        }
159        if t >= keys[last].t {
160            let k = &keys[last];
161            return ActorSample {
162                gx: k.gx,
163                gz: k.gz,
164                facing: k.face.unwrap_or_else(|| self.motion_facing(last.saturating_sub(1))),
165                clip: k.clip.unwrap_or(Clip::Idle),
166            };
167        }
168        // Segmento `[i, i+1]` que contiene a `t`.
169        let i = keys.iter().rposition(|k| k.t <= t).unwrap_or(0).min(last - 1);
170        let (a, b) = (&keys[i], &keys[i + 1]);
171        let f = ((t - a.t) / (b.t - a.t).max(1e-6)).clamp(0.0, 1.0);
172        let (dx, dz) = (b.gx - a.gx, b.gz - a.gz);
173        let moving = dx.hypot(dz) > MOVING_EPS;
174        // Rumbo: si el guion lo fija en ambas keys, **gira suave** entre ambas
175        // (por el camino más corto); si sólo en la de salida, lo mantiene; si en
176        // ninguna, automático (dirección de marcha, o la última al detenerse).
177        let facing = match (a.face, b.face) {
178            (Some(fa), Some(fb)) => fa + wrap_pi(fb - fa) * f,
179            (Some(fa), None) => fa,
180            (None, _) if moving => dx.atan2(dz), // yaw=0 → +Z, como Actor::face_towards
181            (None, _) => self.motion_facing(i),
182        };
183        ActorSample {
184            gx: a.gx + dx * f,
185            gz: a.gz + dz * f,
186            clip: a.clip.unwrap_or(if moving { Clip::Walk } else { Clip::Idle }),
187            facing,
188        }
189    }
190
191    /// Rumbo del último segmento *con movimiento* hasta el índice `i` (para
192    /// mantener la orientación cuando el actor está detenido); `0` si nunca se
193    /// movió.
194    fn motion_facing(&self, i: usize) -> f32 {
195        for j in (0..=i).rev() {
196            if j + 1 < self.keys.len() {
197                let (a, b) = (&self.keys[j], &self.keys[j + 1]);
198                let (dx, dz) = (b.gx - a.gx, b.gz - a.gz);
199                if dx.hypot(dz) > MOVING_EPS {
200                    return dx.atan2(dz);
201                }
202            }
203        }
204        0.0
205    }
206}
207
208/// Un **plano** de cámara: una [`CameraTrack`] que arranca en `start` (segundos
209/// absolutos de la secuencia). Sus keys son **relativas** al plano (`t=0` =
210/// inicio del plano), así un plano es autocontenido y reubicable.
211#[derive(Debug, Clone)]
212pub struct Shot {
213    pub start: f32,
214    pub track: CameraTrack,
215}
216
217impl Shot {
218    pub fn new(start: f32, track: CameraTrack) -> Self {
219        Self { start, track }
220    }
221}
222
223/// El **guion completo**: el reparto (un [`ActorScript`] por actor), los planos
224/// de cámara (con cortes duros entre ellos) y la duración total. Reproducir =
225/// para cada `t`: `camera(t)` + `actor.sample(t)` por actor.
226#[derive(Debug, Clone, Default)]
227pub struct Sequence {
228    pub actors: Vec<ActorScript>,
229    shots: Vec<Shot>,
230    pub duration: f32,
231}
232
233impl Sequence {
234    /// Crea la secuencia (ordena los planos por inicio).
235    pub fn new(actors: Vec<ActorScript>, mut shots: Vec<Shot>, duration: f32) -> Self {
236        shots.sort_by(|a, b| a.start.total_cmp(&b.start));
237        Self { actors, shots, duration }
238    }
239
240    /// Cantidad de cuadros a `fps` para la duración total.
241    pub fn frames(&self, fps: u32) -> u32 {
242        (self.duration * fps as f32).round() as u32
243    }
244
245    /// La cámara en `t`: el plano activo (el último cuyo `start ≤ t`) muestreado a
246    /// `t − start`. El salto entre planos es un **corte duro** (sin interpolar).
247    pub fn camera(&self, t: f32) -> Camera3d {
248        let shot = self
249            .shots
250            .iter()
251            .rev()
252            .find(|s| s.start <= t)
253            .or_else(|| self.shots.first());
254        match shot {
255            Some(s) => s.track.sample(t - s.start),
256            None => Camera3d::default(),
257        }
258    }
259
260    /// Los **"beats del guion"**: los instantes (segundos, ordenados, sin repetir)
261    /// que merecen un acento musical — los **cortes de cámara** (inicio de cada plano
262    /// salvo el primero) y los **gestos** de los actores. Es lo que deja que la banda
263    /// sonora caiga *sobre la acción* en vez de sólo compartir duración. Dos tiempos a
264    /// menos de `EPS` se consideran el mismo acento.
265    pub fn beat_times(&self) -> Vec<f32> {
266        const EPS: f32 = 0.05;
267        let mut ts: Vec<f32> = Vec::new();
268        // Cortes de cámara (el primer plano no es un corte).
269        for s in self.shots.iter().skip(1) {
270            ts.push(s.start);
271        }
272        // Gestos de cada actor.
273        for a in &self.actors {
274            ts.extend(a.emote_onsets());
275        }
276        ts.retain(|&t| t >= 0.0 && t <= self.duration + EPS);
277        ts.sort_by(f32::total_cmp);
278        ts.dedup_by(|a, b| (*a - *b).abs() < EPS);
279        ts
280    }
281
282    /// Posición de grilla (sin altura) del **centroide** del reparto en `t`,
283    /// útil para apuntar la cámara al grupo. `None` si no hay actores.
284    pub fn cast_centroid(&self, t: f32) -> Option<Vec3> {
285        if self.actors.is_empty() {
286            return None;
287        }
288        let mut sx = 0.0;
289        let mut sz = 0.0;
290        for a in &self.actors {
291            let s = a.sample(t);
292            sx += s.gx;
293            sz += s.gz;
294        }
295        let n = self.actors.len() as f32;
296        Some(Vec3::new(sx / n, 0.0, sz / n))
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use std::f32::consts::FRAC_PI_2;
304
305    #[test]
306    fn camina_y_luego_emota() {
307        // De (0,0) a (10,0) en 2 s, después se queda saludando.
308        let s = ActorScript::new(vec![
309            ActorKey::at(0.0, 0.0, 0.0),
310            ActorKey::at(2.0, 10.0, 0.0).play(Clip::Wave).facing(FRAC_PI_2),
311        ]);
312        // A mitad del trayecto: posición media, caminando, mirando a +X.
313        let m = s.sample(1.0);
314        assert!((m.gx - 5.0).abs() < 1e-4);
315        assert_eq!(m.clip, Clip::Walk);
316        assert!((m.facing - FRAC_PI_2).abs() < 1e-4, "mueve en +X → yaw=π/2");
317        // Al final: quieto, saludando, mirando a donde dijimos.
318        let e = s.sample(3.0);
319        assert_eq!((e.gx, e.clip), (10.0, Clip::Wave));
320        assert!((e.facing - FRAC_PI_2).abs() < 1e-4);
321    }
322
323    #[test]
324    fn frame_rate_cuantiza_el_tiempo_en_doses() {
325        // Sin tasa propia: fluido (t pasa tal cual).
326        let fluido = ActorScript::new(vec![ActorKey::at(0.0, 0.0, 0.0)]);
327        assert_eq!(fluido.quantize(0.137), 0.137);
328        assert_eq!(fluido.frame_rate(), None);
329
330        // A 12 fps: el tiempo cae al inicio del cuadro (1/12 ≈ 0.0833).
331        let heroe = fluido.clone().with_frame_rate(Some(12));
332        assert_eq!(heroe.frame_rate(), Some(12));
333        // t=0.13 → cuadro 1 (0.0833); t=0.17 → cuadro 2 (0.1667).
334        assert!((heroe.quantize(0.13) - 1.0 / 12.0).abs() < 1e-6);
335        assert!((heroe.quantize(0.17) - 2.0 / 12.0).abs() < 1e-6);
336        // Dentro del mismo cuadro, dos tiempos distintos dan EL MISMO sample → "doses".
337        assert_eq!(heroe.quantize(0.090), heroe.quantize(0.160));
338        assert_eq!(heroe.sample(heroe.quantize(0.090)).gx, heroe.sample(heroe.quantize(0.160)).gx);
339
340        // 0 fps se trata como sin tasa (fluido).
341        assert_eq!(fluido.clone().with_frame_rate(Some(0)).frame_rate(), None);
342    }
343
344    #[test]
345    fn beats_del_guion_son_cortes_y_gestos() {
346        use llimphi_3d::CamKey;
347        // Actor que camina y a los 3 s saluda; otro que a los 4 s señala.
348        let a1 = ActorScript::new(vec![
349            ActorKey::at(0.0, 0.0, 0.0),
350            ActorKey::at(3.0, 5.0, 0.0).play(Clip::Wave),
351        ]);
352        let a2 = ActorScript::new(vec![
353            ActorKey::at(0.0, 2.0, 0.0),
354            ActorKey::at(4.0, 6.0, 0.0).play(Clip::Point),
355        ]);
356        let s0 = CameraTrack::new(vec![CamKey::look(0.0, Vec3::ZERO, Vec3::Z, 50.0)]);
357        let s1 = CameraTrack::new(vec![CamKey::look(0.0, Vec3::Y, Vec3::Z, 50.0)]);
358        // Dos planos: el corte está en 2.5 s (el primero no cuenta).
359        let seq = Sequence::new(vec![a1, a2], vec![Shot::new(0.0, s0), Shot::new(2.5, s1)], 5.0);
360        let beats = seq.beat_times();
361        // Esperado: corte 2.5, saludo 3.0, señal 4.0 (ordenados, sin el inicio).
362        assert_eq!(beats.len(), 3);
363        assert!((beats[0] - 2.5).abs() < 1e-4);
364        assert!((beats[1] - 3.0).abs() < 1e-4);
365        assert!((beats[2] - 4.0).abs() < 1e-4);
366    }
367
368    #[test]
369    fn cortes_de_camara_son_duros() {
370        use llimphi_3d::CamKey;
371        let near = CameraTrack::new(vec![CamKey::look(0.0, Vec3::ZERO, Vec3::Z, 50.0)]);
372        let far = CameraTrack::new(vec![CamKey::look(0.0, Vec3::new(100.0, 0.0, 0.0), Vec3::Z, 50.0)]);
373        let seq = Sequence::new(vec![], vec![Shot::new(0.0, near), Shot::new(1.0, far)], 2.0);
374        // Antes del corte: plano cercano (eye en origen).
375        assert!(seq.camera(0.5).eye.x.abs() < 1e-3);
376        // Después del corte (t≥1): plano lejano, sin interpolación intermedia.
377        assert!((seq.camera(1.5).eye.x - 100.0).abs() < 1e-3);
378    }
379}