gioser

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Sutichaykuna

⎇ main llimphi-voxel/src/apertura.rs Rust · 315 siq'ikuna · 12.9 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

1// =============================================================================
2//  llimphi-voxel :: apertura — la atmósfera y la caída de una escena
3// -----------------------------------------------------------------------------
4//  Las herramientas de autoría de la APERTURA de una escena:
5//
6//    * [`Atmosfera`] — la BRUMA (densidad + color) y el FUNDIDO DESDE NEGRO con
7//      que abre la escena. "Mucha bruma", "entra lento desde el negro".
8//    * [`Caida`] — la cámara PURA (sin cuerpo) que cae del cielo hacia una
9//      columna del mundo, mirando abajo y enderezándose al acercarse: "ve el
10//      mundo acercarse hasta caer". Reusa `Camera3d`/`cam_lerp` de `camera_rig`
11//      (la caída de `BirthSequence`, pero SIN sujeto).
12//
13//  Ambas son deterministas por tiempo (reproducibles cuadro a cuadro), serializan
14//  (viajan en el archivo de la escena) y se editan desde el estudio. El primer
15//  uso: la escena del NACIMIENTO del protagonista.
16// =============================================================================
17
18use llimphi_3d::glam::Vec3;
19use llimphi_3d::Camera3d;
20use serde::{Deserialize, Serialize};
21
22use crate::camera_rig::cam_lerp;
23
24/// Suavizado Hermite `3t²−2t³` (deriva nula en los extremos).
25fn smoothstep(x: f32) -> f32 {
26    let x = x.clamp(0.0, 1.0);
27    x * x * (3.0 - 2.0 * x)
28}
29
30/// La ATMÓSFERA de una escena: cuánta bruma hay, de qué color, y cuánto dura el
31/// fundido desde el negro al abrir. Autorable desde el estudio.
32#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
33pub struct Atmosfera {
34    /// Densidad de la bruma: un COEFICIENTE (`0` = aire limpio; ~1 = niebla de
35    /// tarde; ~2.5 = todo se lo traga la bruma). El render lo divide por el ancho
36    /// del mundo → `fog_density`.
37    pub bruma: f32,
38    /// Color de la bruma / horizonte hacia el que funden los lejos (RGB `0..1`).
39    pub color: [f32; 3],
40    /// Segundos de fundido DESDE NEGRO al abrir la escena (`0` = sin fundido).
41    pub desde_negro: f32,
42}
43
44impl Atmosfera {
45    /// El aire del nacimiento: bruma espesa gris-perla que abre lento desde el
46    /// negro.
47    pub fn nacimiento() -> Self {
48        Self { bruma: 2.5, color: [0.66, 0.65, 0.70], desde_negro: 3.0 }
49    }
50
51    /// Aire despejado, sin fundido — el neutro para escenas normales.
52    pub fn despejada() -> Self {
53        Self { bruma: 0.3, color: [0.62, 0.70, 0.85], desde_negro: 0.0 }
54    }
55
56    /// Cuánto NEGRO cubre la escena en `t`: `1` = negro total (t≈0), `0` = ya
57    /// visible (tras `desde_negro`). Un fundido suave. Sin fundido ⟹ siempre 0.
58    pub fn negro_en(&self, t: f32) -> f32 {
59        if self.desde_negro <= 0.0 {
60            return 0.0;
61        }
62        1.0 - smoothstep(t / self.desde_negro)
63    }
64
65    /// El COLOR de la niebla/horizonte en `t` (RGB u8): de NEGRO (t≈0, mientras
66    /// dura `desde_negro`) al `color` de la bruma. Es lo que hace "empezar en
67    /// negro y entrar a la bruma": como la cámara arranca alto entre niebla
68    /// densa, todo se funde a este color — negro al principio, gris-perla después.
69    /// Puro y determinista (el render sólo lo aplica).
70    pub fn color_niebla_en(&self, t: f32) -> [u8; 3] {
71        let visible = 1.0 - self.negro_en(t);
72        let tinte = |c: f32| (c * visible * 255.0).round().clamp(0.0, 255.0) as u8;
73        [tinte(self.color[0]), tinte(self.color[1]), tinte(self.color[2])]
74    }
75}
76
77/// La CAÍDA de apertura: la cámara pura cae desde `altura` hacia la columna
78/// `aterrizaje` del mundo, mirando abajo y enderezándose al acercarse. Sin
79/// cuerpo — es la cámara la que cae. Determinista por tiempo.
80#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
81pub struct Caida {
82    /// Altura de partida, en el cielo entre la bruma.
83    pub altura: f32,
84    /// Duración de la caída (seg).
85    pub duracion: f32,
86    /// Columna `(x, z)` del mundo hacia la que cae y aterriza.
87    pub aterrizaje: [f32; 2],
88    /// Altura del suelo en la columna de aterrizaje; el ojo termina [`Self::OJO`]
89    /// por encima.
90    pub suelo: f32,
91}
92
93impl Caida {
94    /// Altura del ojo al aterrizar (a la altura de una mirada humana).
95    pub const OJO: f32 = 1.6;
96
97    /// Una caída larga y lenta desde muy alto sobre una columna del mundo — el
98    /// nacimiento.
99    pub fn nacimiento(aterrizaje: [f32; 2], suelo: f32) -> Self {
100        Self { altura: 90.0, duracion: 7.0, aterrizaje, suelo }
101    }
102
103    /// La cámara al inicio: muy alto, mirando casi recto hacia abajo a la columna.
104    fn cielo(&self) -> Camera3d {
105        let [ax, az] = self.aterrizaje;
106        let eye = Vec3::new(ax, self.altura, az);
107        // Objetivo muy por debajo (mira abajo), con un pelín de +Z para no
108        // degenerar el vector "up" (mirar recto abajo lo dejaría indefinido).
109        let target = Vec3::new(ax, self.suelo, az + 0.01);
110        Camera3d { eye, target, ..Camera3d::default() }
111    }
112
113    /// La cámara al aterrizar: a ras del suelo, mirando al horizonte (+Z).
114    fn piso(&self) -> Camera3d {
115        let [ax, az] = self.aterrizaje;
116        let eye = Vec3::new(ax, self.suelo + Self::OJO, az);
117        let target = Vec3::new(ax, self.suelo + Self::OJO, az + 10.0);
118        Camera3d { eye, target, ..Camera3d::default() }
119    }
120
121    /// La cámara en el instante `t` de la caída: de "alto mirando abajo" a "a ras
122    /// mirando al frente", con desaceleración (el `smoothstep` de `cam_lerp`).
123    /// Coordenadas ABSOLUTAS del mundo (usa `aterrizaje`/`suelo`).
124    pub fn camara(&self, t: f32) -> Camera3d {
125        let u = (t / self.duracion.max(1e-3)).clamp(0.0, 1.0);
126        cam_lerp(&self.cielo(), &self.piso(), u)
127    }
128
129    /// Igual que [`camara`](Self::camara) pero anclada a un `centro` dado (el
130    /// punto de aterrizaje **en el frame del render**, que está centrado en el
131    /// origen). Reusa `altura`/`duracion` como offsets relativos; ignora
132    /// `aterrizaje`/`suelo` (que son coords absolutas). Es la que consume el
133    /// preview, donde la geometría vive desplazada `-half`.
134    pub fn camara_desde(&self, centro: Vec3, t: f32) -> Camera3d {
135        let cielo = Camera3d {
136            eye: centro + Vec3::new(0.0, self.altura, 0.0),
137            target: centro + Vec3::new(0.0, 0.0, 0.01), // mira abajo, al centro
138            ..Camera3d::default()
139        };
140        let piso = Camera3d {
141            eye: centro + Vec3::new(0.0, Self::OJO, 0.0),
142            target: centro + Vec3::new(0.0, Self::OJO, 10.0), // al frente (+Z)
143            ..Camera3d::default()
144        };
145        let u = (t / self.duracion.max(1e-3)).clamp(0.0, 1.0);
146        cam_lerp(&cielo, &piso, u)
147    }
148
149    /// ¿Ya terminó de caer en `t`?
150    pub fn aterrizo(&self, t: f32) -> bool {
151        t >= self.duracion
152    }
153}
154
155impl crate::studio::Project {
156    /// El proyecto de la escena del NACIMIENTO: un desierto vacío (sin
157    /// vegetación, sin agua, pocas montañas), envuelto en un mundo, y la escena
158    /// de la cámara PURA que cae desde el negro entre mucha bruma hasta aterrizar
159    /// —todavía sin cuerpo—. El primer artefacto autorable de la colaboración.
160    pub fn nacimiento() -> crate::studio::Project {
161        use crate::studio::{world_dim, MaterialDef, Mundo, Project, SceneSpec, PREVIEW_DIM_XZ};
162        use crate::worldgen::{Bioma, Material};
163
164        let mut p = Project { next_id: 1, ..Default::default() };
165
166        // Materiales mínimos: arena (suelo) y roca (acantilado). Nada más.
167        let arena = p.alloc_id();
168        p.materiales.push(MaterialDef::from_builtin(arena, Material::Sand));
169        let roca = p.alloc_id();
170        p.materiales.push(MaterialDef::from_builtin(roca, Material::Rock));
171
172        // Bioma "vacío puro": desierto sin vegetación, sin agua, pocas montañas.
173        let desierto = p.alloc_id();
174        p.biomas.push(Bioma {
175            id: desierto,
176            name: "vacío".into(),
177            base: 0.30,
178            dune: 0.06,
179            relief: 0.30,     // relieve suave
180            mountains: 0.06,  // "no tantas montañas"
181            water_level: 0.0, // sin agua
182            rivers: 0.0,      // sin ríos
183            peak_at: 1.0,     // sin picos nevados
184            ground: arena,
185            cliff: roca,
186            peak: None,
187            objetos: vec![], // sin vegetación ni objetos
188            minerales: vec![], // el vacío no tiene minerales
189            densidad_urbana: 0.4,
190            casas: Vec::new(),
191            fauna: vec![],
192            seres: vec![],   // todavía no hay cuerpo
193        });
194
195        // El mundo que lo envuelve.
196        let seed = 20_260_702;
197        let mundo = p.alloc_id();
198        p.mundos.push(Mundo { id: mundo, name: "el vacío".into(), seed, biomas: vec![desierto], mapa: None });
199
200        // La caída aterriza en el centro del mundo; el suelo sale del terreno real.
201        let dim = world_dim(PREVIEW_DIM_XZ);
202        let cx = dim[0] as f32 * 0.5;
203        let cz = dim[2] as f32 * 0.5;
204        let suelo = crate::terrain::column_height(cx as i32, cz as i32, dim, seed) as f32;
205
206        let caida = Caida::nacimiento([cx, cz], suelo);
207        let atmosfera = Atmosfera::nacimiento();
208
209        // La escena: cámara PURA (sin actores), la caída + un respiro final.
210        let escena_id = p.alloc_id();
211        p.escenas.push(SceneSpec {
212            distancia: None,
213            id: escena_id,
214            name: "el nacimiento".into(),
215            mundo,
216            duration: caida.duracion + 1.5,
217            actors: vec![],  // no hay cuerpo: es la cámara la que cae
218            shots: vec![],
219            handheld: 0.25, // un pulso leve: la cámara respira al caer, no es trípode
220            atmosfera: Some(atmosfera),
221            caida: Some(caida),
222            sucesos: Vec::new(),
223            utileria: Vec::new(),
224            cielo: None,
225            decorado: Vec::new(),
226            jardin: None,
227        });
228
229        p
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn el_fundido_va_de_negro_a_visible() {
239        let a = Atmosfera::nacimiento();
240        assert_eq!(a.negro_en(0.0), 1.0, "arranca en negro total");
241        assert_eq!(a.negro_en(a.desde_negro), 0.0, "al final ya se ve");
242        // Monótono decreciente.
243        let mut prev = a.negro_en(0.0);
244        for i in 1..=20 {
245            let t = a.desde_negro * i as f32 / 20.0;
246            let n = a.negro_en(t);
247            assert!(n <= prev + 1e-6, "el negro sólo se disipa");
248            prev = n;
249        }
250    }
251
252    #[test]
253    fn el_aire_despejado_no_funde() {
254        let a = Atmosfera::despejada();
255        assert_eq!(a.negro_en(0.0), 0.0);
256        assert!(a.bruma < Atmosfera::nacimiento().bruma, "despejada trae menos bruma");
257    }
258
259    #[test]
260    fn la_niebla_va_de_negro_al_color() {
261        let a = Atmosfera::nacimiento();
262        assert_eq!(a.color_niebla_en(0.0), [0, 0, 0], "arranca negro puro");
263        let fin = a.color_niebla_en(a.desde_negro);
264        // Al final = el color de la bruma en u8 (gris-perla), no negro.
265        assert!(fin.iter().all(|&c| c > 100), "termina en la bruma clara: {fin:?}");
266    }
267
268    #[test]
269    fn camara_desde_ancla_la_caida_a_un_centro() {
270        let c = Caida::nacimiento([0.0, 0.0], 0.0);
271        let centro = Vec3::new(5.0, 2.0, -3.0);
272        let alto = c.camara_desde(centro, 0.0);
273        let bajo = c.camara_desde(centro, c.duracion);
274        // Arranca `altura` sobre el centro; termina `OJO` sobre el centro.
275        assert!((alto.eye - (centro + Vec3::new(0.0, c.altura, 0.0))).length() < 1e-3);
276        assert!((bajo.eye - (centro + Vec3::new(0.0, Caida::OJO, 0.0))).length() < 1e-3);
277        assert!(alto.eye.y > bajo.eye.y, "desciende");
278    }
279
280    #[test]
281    fn la_caida_desciende_del_cielo_al_suelo() {
282        let c = Caida::nacimiento([64.0, 64.0], 12.0);
283        let alto = c.camara(0.0);
284        let bajo = c.camara(c.duracion);
285        assert!((alto.eye.y - c.altura).abs() < 1e-3, "arranca en la altura del cielo");
286        assert!((bajo.eye.y - (c.suelo + Caida::OJO)).abs() < 1e-3, "termina a ras del suelo");
287        assert!(alto.eye.y > bajo.eye.y, "cae: baja de altura");
288    }
289
290    #[test]
291    fn arranca_mirando_abajo_y_termina_mirando_al_frente() {
292        let c = Caida::nacimiento([64.0, 64.0], 12.0);
293        let alto = c.camara(0.0);
294        // Al inicio el objetivo está MUY por debajo del ojo → mira hacia abajo.
295        assert!(alto.target.y < alto.eye.y - 10.0, "mira abajo al caer");
296        let bajo = c.camara(c.duracion);
297        // Al final mira al frente (+Z), casi a la misma altura del ojo.
298        assert!(bajo.target.z > bajo.eye.z + 1.0, "termina mirando al frente");
299        assert!((bajo.target.y - bajo.eye.y).abs() < 1e-3, "mirada nivelada al horizonte");
300    }
301
302    #[test]
303    fn el_descenso_es_monotono() {
304        let c = Caida::nacimiento([64.0, 64.0], 12.0);
305        let mut prev = c.camara(0.0).eye.y;
306        for i in 1..=40 {
307            let t = c.duracion * i as f32 / 40.0;
308            let y = c.camara(t).eye.y;
309            assert!(y <= prev + 1e-4, "el ojo sólo desciende");
310            prev = y;
311        }
312        assert!(c.aterrizo(c.duracion));
313        assert!(!c.aterrizo(c.duracion * 0.5));
314    }
315}