tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ main llimphi-voxel-render/src/preview.rs Rust · 745 líneas · 33.7 KB Historia Quién tocó qué crudo

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

1//! Preview 3D en vivo de un [`WorldRecipe`]: genera el `VoxelGrid` de la receta y
2//! lo compone con [`Scene3d`] sobre el target del canvas, **confinado** al rect del
3//! panel. Se **regenera** cuando cambia la "generación" (un contador que el editor
4//! incrementa al tocar un parámetro / cambiar de mundo) o el `dim`.
5//!
6//! Además del terreno, sabe **posar actores**: guarda el grid para consultar la
7//! altura del suelo ([`ground_at`](WorldPreview::ground_at)) y mantiene un pool de
8//! [`Renderer3d`] para dibujar las mallas de los actores de una escena en vivo.
9
10use llimphi_3d::glam::{Mat4, Vec3, Vec4};
11use llimphi_3d::{Atmosphere, Camera3d, NightSky, PointLight, Renderer3d, Scene3d, SdfSer, Vertex3d, VoxelGrid, VoxelRenderer};
12use llimphi_hal::wgpu;
13use llimphi_voxel::{
14    disparar_sucesos, reparto_mixto, CharSpec, Clip, EcuacionSim, Enjambre, EnCuadro, FieldDef,
15    GrowthSim, Habitante, MundoRender, Program, Suceso, WaterSim, CELL_WATER, SCENE_SUN,
16};
17
18/// Tope de habitantes de la bandada del preview. Supera las 16 del uniform de seres
19/// SDF a propósito: el reparto mixto manda a malla lo que no entra, así que el techo
20/// acá es el gusto (no recargar el preview), no el motor.
21pub const TOPE_MANADA: usize = 32;
22
23/// Formato de la textura intermedia de Llimphi (target de `gpu_paint_with`).
24pub const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
25
26/// **Densidad de bruma de una escena** (numerador; la densidad real es
27/// `SCENE_FOG / dim[0]`). Más alta que la del preview de un mundo: la ventana
28/// voxel sigue siendo una caja finita de `dim`, así que sin bruma su **borde** se
29/// ve como una isla cuadrada flotando en el cielo. Con esto el borde (a ~`dim/2`
30/// del reparto centrado) se funde con el horizonte → la escena se lee infinita,
31/// mientras el reparto (cerca de la cámara) queda nítido. Es el mismo truco del
32/// `--flythrough`.
33pub const SCENE_FOG: f32 = 2.6;
34
35/// El mundo de preview: terreno de la receta + atmósfera + actores posados.
36pub struct WorldPreview {
37    scene: Scene3d,
38    voxel: VoxelRenderer,
39    /// El grid generado (se conserva para consultar la altura del suelo al posar
40    /// actores).
41    grid: VoxelGrid,
42    dim: [u32; 3],
43    /// Generación de la receta con la que se construyó el grid actual.
44    built_gen: u64,
45    /// Columna de **mundo** `[wx, wz]` donde cae la esquina local `(0,0)` de la
46    /// ventana actual. `[0, 0]` = mundo centrado en el origen (preview de un mundo /
47    /// personaje); distinto cuando la ventana **sigue al reparto** de una escena
48    /// (mundo infinito). Ver [`Self::ensure_window`] / [`Self::ground_at_world`].
49    origin: [i32; 2],
50    /// Simulación de agua en curso (ley Fluir), si está activa. `None` = terreno
51    /// estático. Ver [`Self::ensure_sim`] / [`Self::sim_step`].
52    sim: Option<WaterSim>,
53    /// Crecimiento de plantas en curso (ley Crecer), si está activo.
54    growth: Option<GrowthSim>,
55    /// Ley autorada (Ecuacion) corriendo sobre un material del mundo, si está activa:
56    /// recolorea sus celdas por el campo. Ver [`Self::ensure_ecuacion`] / [`Self::ecuacion_step`].
57    ecuacion: Option<EcuacionSim>,
58    /// Bandada de habitantes (conducta) deambulando sobre el grid, cada uno con el
59    /// cuerpo del Ser que representa. Vacía = sin vida.
60    manada: Vec<(Habitante, CharSpec)>,
61    /// Contexto de *picking* (al dirigir): `(inv_view_proj, ojo, altura_del_plano)` en
62    /// espacio centrado, para resolver un click de pantalla a una columna de mundo.
63    pick: Option<(Mat4, Vec3, f32)>,
64    /// Pool de renderers de actor (uno por actor; la malla se re-sube por frame).
65    actor_r: Vec<Renderer3d>,
66    /// `gen` del grid de **interior** mostrado (editor de interiores), si aplica.
67    /// Distinto de `built_gen` (biomas) para no confundir los dos orígenes de grid.
68    interior_tag: Option<u64>,
69    /// La **puesta** estampada en la ventana actual (jardín, corredores del
70    /// reparto, decorado). Si la escena pide otra, la ventana se rearma: vive en
71    /// la grilla, no en un uniform.
72    puesta_hecha: llimphi_voxel::Puesta,
73    /// Las florcitas de los canteros de la puesta, como seres (macizos). Se
74    /// arman al estampar la ventana; el cuadro las suma con lo que el reparto
75    /// deja ([`llimphi_voxel::sumar_macizos`]).
76    macizos: Vec<llimphi_3d::SdfSer>,
77}
78
79impl WorldPreview {
80    /// Grid fuente del preview: el `override` (un mundo ya generado — p. ej. desde
81    /// un **mapa real** vía `nahual-geo-voxel`) si viene y calza el `dim`, o el grid
82    /// procedural del bioma. Un solo lugar decide el origen para que `build` y
83    /// `rebuild_if` no diverjan.
84    fn grid_fuente(mr: &MundoRender, dim: [u32; 3], override_grid: Option<&VoxelGrid>) -> VoxelGrid {
85        match override_grid {
86            Some(g) if g.dim() == dim => g.clone(),
87            _ => mr.bioma.generate_window(mr.seed, &mr.palette, dim, [0, 0]),
88        }
89    }
90
91    /// Construye el preview generando el grid de `mr` (bioma+semilla+paleta) a `dim`,
92    /// centrado en el origen. Si `override_grid` viene y calza `dim`, usa ESE grid
93    /// (mundo desde mapa) en vez del procedural.
94    pub fn build(
95        device: &wgpu::Device,
96        queue: &wgpu::Queue,
97        mr: &MundoRender,
98        dim: [u32; 3],
99        gen: u64,
100        override_grid: Option<&VoxelGrid>,
101    ) -> Self {
102        let grid = Self::grid_fuente(mr, dim, override_grid);
103        let voxel = Self::make_voxel(device, queue, &grid, dim);
104        Self {
105            scene: Scene3d::new(),
106            voxel,
107            grid,
108            dim,
109            built_gen: gen,
110            origin: [0, 0],
111            sim: None,
112            growth: None,
113            ecuacion: None,
114            manada: Vec::new(),
115            pick: None,
116            actor_r: Vec::new(),
117            interior_tag: None,
118            puesta_hecha: llimphi_voxel::Puesta::default(),
119            macizos: Vec::new(),
120        }
121    }
122
123    /// Registra la cámara para *picking* (al dirigir): `inv_vp` = inversa de la
124    /// `view_proj` con la que se renderiza, `eye` el ojo (centrado), `y0` la altura del
125    /// plano de suelo donde caen los waypoints. Lo setea el preview de escena cada cuadro.
126    pub fn set_pick(&mut self, inv_vp: Mat4, eye: Vec3, y0: f32) {
127        self.pick = Some((inv_vp, eye, y0));
128    }
129
130    /// Resuelve un click (en NDC `[-1,1]`) a una **columna de mundo** `(gx, gz)`,
131    /// intersectando el rayo de cámara con el plano de suelo `y0`. `None` si no hay
132    /// contexto, el rayo no baja, o cae fuera del mundo.
133    pub fn pick_world(&self, ndc_x: f32, ndc_y: f32) -> Option<(f32, f32)> {
134        let (inv_vp, eye, y0) = self.pick?;
135        // Punto en el plano lejano, desproyectado a espacio centrado.
136        let p = inv_vp * Vec4::new(ndc_x, ndc_y, 1.0, 1.0);
137        if p.w.abs() < 1e-6 {
138            return None;
139        }
140        let far = p.truncate() / p.w;
141        let dir = (far - eye).normalize_or_zero();
142        if dir.y.abs() < 1e-4 {
143            return None; // rayo casi horizontal: no corta el plano
144        }
145        let t = (y0 - eye.y) / dir.y;
146        if t <= 0.0 {
147            return None; // el plano está detrás de la cámara
148        }
149        let hit = eye + dir * t; // espacio centrado
150        let half = Vec3::new(self.dim[0] as f32, self.dim[1] as f32, self.dim[2] as f32) * 0.5;
151        let gx = hit.x + half.x;
152        let gz = hit.z + half.z;
153        if gx < 0.0 || gz < 0.0 || gx >= self.dim[0] as f32 || gz >= self.dim[2] as f32 {
154            return None;
155        }
156        Some((gx, gz))
157    }
158
159    /// Asegura una **bandada** a partir de `pobladores` (`(ser, cuántos)`): spawnea
160    /// `Σ cuántos` habitantes sobre el grid (esparcidos alrededor del centro), cada uno
161    /// con la conducta y el cuerpo de su Ser. Si ya hay esa cantidad, sólo refresca la
162    /// conducta de cada uno (para que editar los parámetros se note en vivo). Tope de
163    /// [`TOPE_MANADA`] para no recargar el preview — más alto que las 16 del uniform
164    /// porque los que no entran como seres SDF se dibujan como malla (ver
165    /// [`reparto_mixto`](llimphi_voxel::reparto_mixto)), no desaparecen.
166    pub fn ensure_manada(&mut self, pobladores: &[(CharSpec, usize)]) {
167        let total: usize = pobladores.iter().map(|(_, n)| n).sum::<usize>().min(TOPE_MANADA);
168        if self.manada.len() != total {
169            self.manada.clear();
170            let [dx, _, dz] = self.dim;
171            let (cx, cz) = (dx as i32 / 2, dz as i32 / 2);
172            let mut k = 0u32;
173            'fill: for (spec, n) in pobladores {
174                for _ in 0..*n {
175                    if self.manada.len() >= TOPE_MANADA {
176                        break 'fill;
177                    }
178                    // Esparcido determinista alrededor del centro (±~18 voxels).
179                    let ox = (k.wrapping_mul(7) % 37) as i32 - 18;
180                    let oz = (k.wrapping_mul(13) % 37) as i32 - 18;
181                    let x = (cx + ox).clamp(1, dx as i32 - 2) as u32;
182                    let z = (cz + oz).clamp(1, dz as i32 - 2) as u32;
183                    let h = Habitante::spawn(&self.grid, x, z, spec.conducta.clone(), 1 + k);
184                    self.manada.push((h, spec.clone()));
185                    k += 1;
186                }
187            }
188        } else {
189            // Misma cantidad: refrescar el spec (conducta/cuerpo/colores) de cada uno
190            // desde los pobladores, en el mismo orden de llenado, para que las ediciones
191            // en vivo (parámetros e impulsos autorados) se noten sin re-spawnear.
192            let mut idx = 0usize;
193            'refresh: for (spec, n) in pobladores {
194                for _ in 0..*n {
195                    if idx >= self.manada.len() {
196                        break 'refresh;
197                    }
198                    let (h, stored) = &mut self.manada[idx];
199                    *stored = spec.clone();
200                    h.set_conducta(spec.conducta.clone());
201                    idx += 1;
202                }
203            }
204        }
205    }
206
207    /// Detiene la bandada.
208    pub fn clear_manada(&mut self) {
209        self.manada.clear();
210    }
211
212    /// `true` si hay bandada activa.
213    pub fn tiene_manada(&self) -> bool {
214        !self.manada.is_empty()
215    }
216
217    /// Avanza la bandada `dt` (cada uno ve a los demás y a la `amenaza`) y arma los
218    /// cuerpos a renderizar: los **humanoides** como seres SDF (posados en coords de
219    /// grilla, el motor los traza) y los **rigs** como metas de malla (matriz + malla,
220    /// espacio centrado − dim/2). Caminan según su andar.
221    /// `mirada` (coords de GRILLA, típicamente el ojo de la cámara) hace que los
222    /// humanoides giren cabeza y pupilas hacia ahí mientras caminan — la bandada te
223    /// mira. `None` = cada uno mira hacia donde va.
224    pub fn manada_metas(
225        &mut self,
226        dt: f32,
227        amenaza: Option<Vec3>,
228        mirada: Option<Vec3>,
229    ) -> (Vec<(Mat4, Vec<Vertex3d>, Vec<u16>)>, Vec<SdfSer>) {
230        let posiciones: Vec<Vec3> = self.manada.iter().map(|(h, _)| h.pos()).collect();
231        for (h, _) in &mut self.manada {
232            h.step(&self.grid, &posiciones, amenaza, dt);
233        }
234        let half = Vec3::new(self.dim[0] as f32, self.dim[1] as f32, self.dim[2] as f32) * 0.5;
235        let mut metas = Vec::new();
236        let mut cuadro: Vec<EnCuadro> = Vec::new();
237        for (h, spec) in &self.manada {
238            if spec.cuerpo.is_none() {
239                cuadro.push(EnCuadro {
240                    spec,
241                    pos: h.pos(),
242                    yaw: h.heading(),
243                    clip: Clip::Walk,
244                    fase: h.fase,
245                });
246            } else {
247                metas.push(spec.to_meta(h.pos() - half, h.heading(), Clip::Walk, h.fase, None));
248            }
249        }
250        // Por el REPARTO, no crudo: la bandada llega a 16 y con la cara entera son
251        // ~500 primitivas contra las 240 del uniform. El reparto ordena por cercanía,
252        // le baja el detalle a los del fondo y así entran todos.
253        let ojo = mirada.unwrap_or(half + Vec3::new(0.0, half.y, -half.z));
254        // Reparto MIXTO: los de cerca como seres SDF (cara y todo), los que no entran
255        // en el uniform como malla de cajas — antes se caían del cuadro sin aviso.
256        let rep = reparto_mixto(&cuadro, ojo, mirada.is_some(), half);
257        metas.extend(rep.mallas);
258        (metas, rep.sers)
259    }
260
261    /// Arranca el **crecimiento** (ley Crecer) del material `planta`: esconde sus
262    /// celdas del grid y las irá revelando de abajo hacia arriba a `velocidad`.
263    pub fn ensure_growth(&mut self, queue: &wgpu::Queue, planta: [u8; 3], velocidad: f32) {
264        if self.growth.is_none() {
265            let g = GrowthSim::from_grid(&self.grid, planta, velocidad);
266            for (pos, _) in g.cells() {
267                self.grid.clear(pos[0], pos[1], pos[2]);
268            }
269            self.voxel.sync(queue, &mut self.grid);
270            self.growth = Some(g);
271        }
272    }
273
274    /// **Desmorona el mundo** donde lo dicten los sucesos que caen en `(t0, t1]`: la
275    /// materia se desprende al `enjambre` y el grid se sube a la GPU si algo cambió.
276    /// Devuelve cuántas motas nuevas salieron. Ver
277    /// [`disparar_sucesos`](llimphi_voxel::disparar_sucesos).
278    pub fn desmoronar(
279        &mut self,
280        queue: &wgpu::Queue,
281        sucesos: &[Suceso],
282        t0: f32,
283        t1: f32,
284        enjambre: &mut Enjambre,
285    ) -> usize {
286        let n = disparar_sucesos(sucesos, t0, t1, &mut self.grid, enjambre);
287        if n > 0 {
288            self.voxel.sync(queue, &mut self.grid);
289        }
290        n
291    }
292
293    /// Vuelca las motas del enjambre a la capa de cajas del ray-march, las más
294    /// cercanas al `ojo` primero (coords de grilla). Reemplaza lo que hubiera.
295    pub fn set_motas(&mut self, enjambre: &Enjambre, ojo: Vec3) {
296        self.voxel.entities = enjambre.entidades(64, Some(ojo));
297    }
298
299    /// Detiene el crecimiento (el próximo `rebuild_if` repone las plantas completas).
300    pub fn clear_growth(&mut self) {
301        self.growth = None;
302    }
303
304    /// Revela el siguiente lote de celdas de planta y lo sube a la GPU. No-op sin
305    /// crecimiento activo.
306    pub fn growth_step(&mut self, queue: &wgpu::Queue) {
307        let Self { growth, grid, voxel, .. } = self;
308        let Some(g) = growth else { return };
309        let batch = g.step();
310        if batch.is_empty() {
311            return;
312        }
313        for (pos, color) in batch {
314            grid.set(pos[0], pos[1], pos[2], color);
315        }
316        voxel.sync(queue, grid);
317    }
318
319    /// Arranca una **ley autorada** (Ecuacion) sobre las celdas del material `color`
320    /// (± `tol`), si no hay una ya corriendo. `campos` define el estado y `vis` el campo
321    /// que tiñe el color. No mueve celdas: las recolorea.
322    pub fn ensure_ecuacion(&mut self, color: [u8; 3], tol: i32, campos: &[FieldDef], vis: usize) {
323        if self.ecuacion.is_none() {
324            self.ecuacion = Some(EcuacionSim::from_grid(&self.grid, color, tol, campos.to_vec(), vis));
325        }
326    }
327
328    /// Detiene la ley autorada (el próximo `rebuild_if` repone los colores originales).
329    pub fn clear_ecuacion(&mut self) {
330        self.ecuacion = None;
331    }
332
333    /// Avanza un paso de la ley autorada y **sube sólo las celdas recoloreadas** a la
334    /// GPU. No-op si no hay una corriendo.
335    pub fn ecuacion_step(&mut self, queue: &wgpu::Queue, program: &Program, params: &[f32]) {
336        let Self { ecuacion, grid, voxel, .. } = self;
337        let Some(sim) = ecuacion else { return };
338        let changes = sim.step(program, params);
339        if changes.is_empty() {
340            return;
341        }
342        for (pos, color) in changes {
343            grid.set(pos[0], pos[1], pos[2], color);
344        }
345        voxel.sync(queue, grid);
346    }
347
348    /// Arranca la **simulación de agua** (ley Fluir) desde el grid actual, si no hay
349    /// una ya corriendo. `agua` es el color con que se identifican/pintan las celdas;
350    /// `(gravedad, horizontal)` son los parámetros de la ley del material líquido.
351    pub fn ensure_sim(&mut self, agua: [u8; 3], gravedad: f32, horizontal: f32) {
352        if self.sim.is_none() {
353            self.sim = Some(WaterSim::with_params(&self.grid, agua, gravedad, horizontal));
354        }
355    }
356
357    /// Detiene la simulación (el próximo `rebuild_if` repone el terreno estático).
358    pub fn clear_sim(&mut self) {
359        self.sim = None;
360    }
361
362    /// Avanza un paso de la simulación de agua y **sube sólo lo cambiado** a la GPU
363    /// (`VoxelRenderer::sync` sobre la caja dirty). No-op si no hay simulación.
364    pub fn sim_step(&mut self, queue: &wgpu::Queue, agua: [u8; 3]) {
365        let Self { sim, grid, voxel, .. } = self;
366        let Some(sim) = sim else { return };
367        for (pos, state) in sim.step() {
368            if state == CELL_WATER {
369                grid.set(pos[0], pos[1], pos[2], agua);
370            } else {
371                grid.clear(pos[0], pos[1], pos[2]);
372            }
373        }
374        voxel.sync(queue, grid);
375    }
376
377    /// Arma el `VoxelRenderer` de un grid con la atmósfera diurna del editor.
378    fn make_voxel(
379        device: &wgpu::Device,
380        queue: &wgpu::Queue,
381        grid: &VoxelGrid,
382        dim: [u32; 3],
383    ) -> VoxelRenderer {
384        let mut voxel = VoxelRenderer::new(device, queue, FMT, grid);
385        voxel.sun_dir = SCENE_SUN; // sol bajo: luz rasante, claroscuro; el plano Backlight mira hacia aquí
386        voxel.atmosphere = Self::atmosfera_de_dia(dim);
387        voxel
388    }
389
390    /// La atmósfera diurna del editor.
391    fn atmosfera_de_dia(dim: [u32; 3]) -> Atmosphere {
392        Atmosphere {
393            sky_zenith: [64, 118, 196],
394            sky_horizon: [202, 218, 236],
395            fog_density: 1.1 / dim[0] as f32, // bruma de desierto: medio que dispersa → god rays legibles
396            god_rays: 0.9, // haces de sol cruzando la niebla — sello anti-Minecraft
397        }
398    }
399
400    /// Si `gen`/`dim` cambiaron desde el último build, **regenera** el mundo.
401    pub fn rebuild_if(
402        &mut self,
403        device: &wgpu::Device,
404        queue: &wgpu::Queue,
405        mr: &MundoRender,
406        dim: [u32; 3],
407        gen: u64,
408        override_grid: Option<&VoxelGrid>,
409    ) {
410        if gen != self.built_gen || dim != self.dim {
411            self.grid = Self::grid_fuente(mr, dim, override_grid);
412            self.puesta_hecha = llimphi_voxel::Puesta::default(); // grilla nueva, sin puesta
413        self.macizos.clear();
414            self.voxel = Self::make_voxel(device, queue, &self.grid, dim);
415            self.dim = dim;
416            self.built_gen = gen;
417            self.interior_tag = None; // el grid ahora es de bioma, no de interior
418            self.origin = [0, 0]; // centrado en el origen
419            self.sim = None; // el grid se rehízo: cualquier simulación queda obsoleta
420            self.growth = None;
421            self.ecuacion = None;
422            self.manada.clear();
423        }
424    }
425
426    /// Muestra un **grid arbitrario** (editor de interiores: piso + muebles),
427    /// reconstruyendo el voxel sólo cuando cambia `gen` (o el tamaño). Invalida el
428    /// caché de bioma para que volver a Mundos/Biomas regenere el terreno.
429    pub fn show_grid(
430        &mut self,
431        device: &wgpu::Device,
432        queue: &wgpu::Queue,
433        grid: VoxelGrid,
434        dim: [u32; 3],
435        gen: u64,
436    ) {
437        if self.interior_tag != Some(gen) || dim != self.dim {
438            self.grid = grid;
439            self.puesta_hecha = llimphi_voxel::Puesta::default(); // grilla nueva, sin puesta
440        self.macizos.clear();
441            self.voxel = Self::make_voxel(device, queue, &self.grid, dim);
442            self.dim = dim;
443            self.interior_tag = Some(gen);
444            self.built_gen = u64::MAX; // fuerza rebuild del bioma al volver
445            self.origin = [0, 0];
446            self.sim = None;
447            self.growth = None;
448            self.ecuacion = None;
449            self.manada.clear();
450        }
451    }
452
453    /// Fija la **iluminación** del voxel actual (sol/cielo + luces puntuales). Lo usa
454    /// el editor de interiores para atenuar el ambiente y encender las lámparas cada
455    /// cuadro (barato: sólo actualiza uniforms, no reconstruye el grid).
456    pub fn set_interior_lighting(&mut self, sun_dir: [f32; 3], atmosphere: Atmosphere, lights: Vec<PointLight>) {
457        self.voxel.sun_dir = sun_dir;
458        self.voxel.atmosphere = atmosphere;
459        self.voxel.lights = lights;
460    }
461
462    /// **Asegura la ventana de una escena en un mundo infinito**: regenera el grid
463    /// si cambió la receta (`gen`) o el `origin` de ventana. A diferencia de
464    /// [`Self::rebuild_if`] (mundo finito centrado), aquí el `origin` lo fija el
465    /// caller para que la ventana **siga al reparto** ([`window_origin_for_cast`]
466    /// (llimphi_voxel::window_origin_for_cast)); como sólo regenera al cruzar un
467    /// paso, es barato por cuadro. Para posar actores sobre el relieve usa
468    /// [`Self::ground_at_world`] (toma coords de mundo, no de ventana).
469    pub fn ensure_window(
470        &mut self,
471        device: &wgpu::Device,
472        queue: &wgpu::Queue,
473        mr: &MundoRender,
474        gen: u64,
475        origin: [i32; 2],
476    ) {
477        self.ensure_window_con(device, queue, mr, gen, origin, &llimphi_voxel::Puesta::default());
478    }
479
480    /// Igual que [`Self::ensure_window`], con la **puesta** de la escena: el
481    /// jardín, el suelo llano por donde camina el reparto y el decorado (el
482    /// palacio, el kiosko) en su lugar del mundo. Cambiar la puesta rearma la
483    /// ventana, igual que cambiar la receta o el origen.
484    pub fn ensure_window_con(
485        &mut self,
486        device: &wgpu::Device,
487        queue: &wgpu::Queue,
488        mr: &MundoRender,
489        gen: u64,
490        origin: [i32; 2],
491        puesta: &llimphi_voxel::Puesta,
492    ) {
493        if gen != self.built_gen || origin != self.origin || *puesta != self.puesta_hecha {
494            self.grid = mr.bioma.generate_window(mr.seed, &mr.palette, self.dim, origin);
495            let florcitas = llimphi_voxel::estampar_puesta(&mut self.grid, origin, &mr.bioma, mr.seed, puesta);
496            self.macizos = llimphi_voxel::macizos(&florcitas);
497            self.voxel = Self::make_voxel(device, queue, &self.grid, self.dim);
498            self.built_gen = gen;
499            self.origin = origin;
500            self.puesta_hecha = puesta.clone();
501        }
502        // Bruma de escena (cada cuadro, sólo setea un campo): disuelve el BORDE de la
503        // ventana finita en el horizonte → la escena se ve infinita, no una isla
504        // cuadrada. El preview de Mundos (rebuild_if) queda nítido, sin esto.
505        self.voxel.atmosphere.fog_density = SCENE_FOG / self.dim[0] as f32;
506    }
507
508    /// Regenera la ventana del mundo en un **origen** de grilla `[wx, wz]` (para
509    /// volar un mundo infinito: el terreno es función pura de mundo, así que mover
510    /// el origen scrollea relieve nuevo de forma continua). `fog` ajusta la niebla
511    /// (más densa esconde los bordes de la ventana en un flythrough).
512    pub fn set_window(
513        &mut self,
514        device: &wgpu::Device,
515        queue: &wgpu::Queue,
516        mr: &MundoRender,
517        origin: [i32; 2],
518        fog: f32,
519    ) {
520        self.grid = mr.bioma.generate_window(mr.seed, &mr.palette, self.dim, origin);
521        self.puesta_hecha = llimphi_voxel::Puesta::default(); // grilla nueva, sin puesta
522        self.macizos.clear();
523        self.voxel = Self::make_voxel(device, queue, &self.grid, self.dim);
524        self.voxel.atmosphere.fog_density = fog;
525        self.origin = origin;
526    }
527
528    /// Sobrescribe **sol + atmósfera** del render voxel (para un look cinematográfico
529    /// puntual, p.ej. la hora dorada de un flythrough). Aditivo: no toca el preview en
530    /// vivo salvo que se llame. Aplicar **después** de [`set_window`](Self::set_window)
531    /// (que recrea el voxel y resetea estas perillas a su default).
532    pub fn set_lighting(&mut self, sun_dir: [f32; 3], atmosphere: Atmosphere) {
533        self.voxel.sun_dir = sun_dir;
534        self.voxel.atmosphere = atmosphere;
535    }
536
537    /// Setea las perillas **globales** de detalle sub-voxel del ray-march: `rugosidad`
538    /// (bump) + `bisel` (suavizado por isosuperficie). En Biomas/Mundos el detalle FINO
539    /// viaja horneado POR VOXEL en el grid (cada material el suyo); aquí el caller sólo
540    /// sube el `bisel` global (= mayor bisel del bioma) para **encender** el trazado de
541    /// isosuperficie, que el bisel por voxel necesita para modular la normal. Un voxel
542    /// sin detalle autorado cae a estas perillas globales. Barato (sólo uniform);
543    /// `0.0/0.0` = voxel duro clásico. El swatch de Materiales sí lo usa como look global
544    /// (single-material). Ver `VoxelGrid::set_detail`.
545    pub fn set_detalle(&mut self, rugosidad: f32, freq: f32, bisel: f32) {
546        self.voxel.rugosidad_amp = rugosidad;
547        self.voxel.rugosidad_freq = freq;
548        self.voxel.bisel = bisel;
549    }
550
551    /// Prende/apaga la **silueta sub-voxel POR MATERIAL** del ray-march: con ella, un
552    /// material duro (bisel 0 por voxel) conserva el contorno blocky mientras otro liso
553    /// lo redondea, en el mismo frame (sin ella, el `bisel` por voxel sólo cambiaba la
554    /// normal, no la silueta — que salía siempre de la isosuperficie global). Cuesta
555    /// samplear el detalle vecino por evaluación del campo → el caller la prende sólo
556    /// cuando el bioma tiene materiales con bisel distinto. Ver `VoxelRenderer::silueta_por_voxel`.
557    pub fn set_silueta_por_voxel(&mut self, on: bool) {
558        self.voxel.silueta_por_voxel = on;
559    }
560
561    /// Fija la **cantidad de oclusión ambiental** `[0,1]` del ray-march (`1` = clásica,
562    /// retrocompat; baja para seres/blobs redondeados, donde la AO axial pinta anillos sobre
563    /// la esfera). Barato (sólo uniform). Ver `VoxelRenderer::ao_amount`.
564    #[allow(dead_code)] // perilla del preview todavía sin control en la UI
565    pub fn set_ao(&mut self, amount: f32) {
566        self.voxel.ao_amount = amount;
567    }
568
569    /// Aplica la **atmósfera de apertura** de una escena en el instante `t`: la
570    /// bruma (densidad) + el fundido DESDE NEGRO (el color de niebla va de negro
571    /// a la bruma). Se llama cada cuadro antes de `render_scene`. Como la cámara
572    /// de la caída arranca alto entre niebla densa, todo se funde a este color →
573    /// negro al principio, mundo entre bruma después.
574    pub fn set_atmosfera_escena(&mut self, atm: &llimphi_voxel::Atmosfera, t: f32) {
575        let color = atm.color_niebla_en(t);
576        self.voxel.atmosphere.sky_horizon = color; // la niebla funde hacia aquí
577        self.voxel.atmosphere.sky_zenith = color; // cielo cerrado por la bruma
578        self.voxel.atmosphere.fog_density = atm.bruma / self.dim[0] as f32;
579    }
580
581    /// Aplica el **cielo de noche** de una escena, o lo apaga con `None`. De noche
582    /// la luz viene de la luna y el cielo toma sus colores; la bruma y los haces
583    /// se quedan como estaban (son del aire, no de la hora). Al apagarlo vuelve
584    /// la luz de día del estudio — sólo si estaba de noche, para no pisar la luz
585    /// que haya fijado quien filma. Barato: sólo uniforms.
586    pub fn set_cielo(&mut self, cielo: Option<&llimphi_voxel::Cielo>) {
587        match cielo {
588            Some(c) => {
589                let a = self.voxel.atmosphere;
590                self.voxel.sun_dir = c.luna;
591                self.voxel.atmosphere = c.atmosfera(a.fog_density, a.god_rays);
592                self.voxel.night = c.night_sky();
593            }
594            None => {
595                if self.voxel.night.amount > 0.0 {
596                    let a = self.voxel.atmosphere;
597                    let dia = Self::atmosfera_de_dia(self.dim);
598                    self.voxel.sun_dir = SCENE_SUN;
599                    self.voxel.atmosphere = Atmosphere { fog_density: a.fog_density, ..dia };
600                }
601                self.voxel.night = NightSky::default();
602            }
603        }
604    }
605
606    /// Las **luces puntuales** de la escena (la estrella que alumbra), en coords de
607    /// grilla. Se fijan cada cuadro: una lista vacía las apaga.
608    pub fn set_luces(&mut self, luces: Vec<PointLight>) {
609        self.voxel.lights = luces;
610    }
611
612    /// Reposa un jugador NUEVO sobre el centro de la ventana actual — para
613    /// "entrar a la escena" a pie.
614    pub fn spawn_jugador(&self) -> llimphi_voxel::Player {
615        llimphi_voxel::Player::spawn_on(&self.grid, self.dim[0] / 2, self.dim[2] / 2)
616    }
617
618    /// Avanza al jugador un paso contra el terreno de la ventana (caminar, con
619    /// gravedad+colisión) o en vuelo libre (`volar`), y devuelve el OJO en el
620    /// frame del render (centrado `-half`, como el resto de la geometría).
621    pub fn step_jugador(
622        &self,
623        j: &mut llimphi_voxel::Player,
624        wish: Vec3,
625        salto: bool,
626        dt: f32,
627        volar: bool,
628    ) -> Vec3 {
629        if volar {
630            j.volar(wish, dt);
631        } else {
632            j.step(&self.grid, wish, salto, dt);
633        }
634        let half = Vec3::new(self.dim[0] as f32, self.dim[1] as f32, self.dim[2] as f32) * 0.5;
635        j.eye() - half
636    }
637
638    /// Posición (espacio de grilla, igual que el render del voxel) del **suelo**
639    /// sobre la columna `(gx, gz)`: pies un voxel por encima del terreno (o `y=0` si
640    /// la columna está vacía). Para parar un actor sobre el relieve.
641    pub fn ground_at(&self, gx: u32, gz: u32) -> Vec3 {
642        let gx = gx.min(self.dim[0] - 1);
643        let gz = gz.min(self.dim[2] - 1);
644        let top = self.grid.height_at(gx, gz).map(|y| y as f32 + 1.0).unwrap_or(0.0);
645        Vec3::new(gx as f32 + 0.5, top, gz as f32 + 0.5)
646    }
647
648    /// Igual que [`Self::ground_at`] pero tomando una columna de **mundo** `(wx,
649    /// wz)`: la mapea a la ventana actual restándole el [`origin`](Self) y devuelve
650    /// la posición en **espacio de ventana** (grilla `[0, dim]`, igual que
651    /// `ground_at`) — el caller le resta `dim/2` para llevarla al espacio centrado
652    /// del shader. Para posar actores de una escena en un mundo infinito: la columna
653    /// se busca donde realmente cae en la ventana que sigue al reparto. Fuera de la
654    /// ventana, se clampa al borde.
655    pub fn ground_at_world(&self, wx: i32, wz: i32) -> Vec3 {
656        let lx = (wx - self.origin[0]).clamp(0, self.dim[0] as i32 - 1) as u32;
657        let lz = (wz - self.origin[1]).clamp(0, self.dim[2] as i32 - 1) as u32;
658        let top = self.grid.height_at(lx, lz).map(|y| y as f32 + 1.0).unwrap_or(0.0);
659        Vec3::new(lx as f32 + 0.5, top, lz as f32 + 0.5)
660    }
661
662    /// **Dónde pisa** quien está en la posición de mundo `(gx, gz)`, sin
663    /// redondear a la columna. Es lo que hay que usar para posar a alguien que
664    /// se mueve.
665    ///
666    /// [`Self::ground_at_world`] devuelve el centro de una columna entera, así
667    /// que quien camina avanza a saltos de un voxel —y la cámara, que mira al
668    /// promedio del reparto, salta con él—. Medido en la película de Margarita:
669    /// entre cuadros la imagen cambia 0,17 de mediana y **6–9** en cada salto,
670    /// uno cada cuatro cuadros en el desfile; y los elefantes del palacio, que
671    /// cruzan cinco columnas en seis segundos, quedaban quietos un segundo y
672    /// medio entre salto y salto. Acá `x/z` siguen al guion tal cual y la altura
673    /// se interpola entre las cuatro columnas vecinas. En un `gx` entero da lo
674    /// mismo que `ground_at_world`.
675    pub fn macizos(&self) -> &[llimphi_3d::SdfSer] {
676        &self.macizos
677    }
678
679    pub fn pie_en_mundo(&self, gx: f32, gz: f32) -> Vec3 {
680        let tope = |c: i32, r: i32| -> f32 {
681            let lx = (c - self.origin[0]).clamp(0, self.dim[0] as i32 - 1) as u32;
682            let lz = (r - self.origin[1]).clamp(0, self.dim[2] as i32 - 1) as u32;
683            self.grid.height_at(lx, lz).map(|y| y as f32 + 1.0).unwrap_or(0.0)
684        };
685        let (cx, cz) = (gx.floor(), gz.floor());
686        let (fx, fz) = (gx - cx, gz - cz);
687        let (cx, cz) = (cx as i32, cz as i32);
688        let alto = (tope(cx, cz) * (1.0 - fx) + tope(cx + 1, cz) * fx) * (1.0 - fz)
689            + (tope(cx, cz + 1) * (1.0 - fx) + tope(cx + 1, cz + 1) * fx) * fz;
690        let max = |n: u32| n as f32 - 0.5;
691        Vec3::new(
692            (gx - self.origin[0] as f32 + 0.5).clamp(0.5, max(self.dim[0])),
693            alto,
694            (gz - self.origin[1] as f32 + 0.5).clamp(0.5, max(self.dim[2])),
695        )
696    }
697
698    /// Compone **sólo el terreno** sobre `target`, confinado al rect del canvas.
699    pub fn render(
700        &mut self,
701        device: &wgpu::Device,
702        queue: &wgpu::Queue,
703        encoder: &mut wgpu::CommandEncoder,
704        target: &wgpu::TextureView,
705        viewport: (u32, u32),
706        rect: (f32, f32, f32, f32),
707        camera: &Camera3d,
708    ) {
709        self.voxel.sers.clear();
710        self.scene.render_in(
711            device, queue, encoder, target, viewport, rect, camera, Some(&self.voxel), &[],
712        );
713    }
714
715    /// Compone terreno + **actores** en el mismo depth compartido — para reproducir
716    /// una escena. Los humanoides van como `sers` (SDF: el ray-marcher los traza con
717    /// toon + cara por decals); los rigs y marcadores como mallas `actors` (matriz +
718    /// vértices). Mantiene el pool de renderers al tamaño del reparto de mallas.
719    #[allow(clippy::too_many_arguments)]
720    pub fn render_scene(
721        &mut self,
722        device: &wgpu::Device,
723        queue: &wgpu::Queue,
724        encoder: &mut wgpu::CommandEncoder,
725        target: &wgpu::TextureView,
726        viewport: (u32, u32),
727        rect: (f32, f32, f32, f32),
728        camera: &Camera3d,
729        actors: &[(Mat4, Vec<Vertex3d>, Vec<u16>)],
730        sers: Vec<SdfSer>,
731    ) {
732        self.voxel.sers = sers;
733        while self.actor_r.len() < actors.len() {
734            self.actor_r.push(Renderer3d::new(device, FMT));
735        }
736        for (r, (model, v, i)) in self.actor_r.iter_mut().zip(actors) {
737            r.set_geometry(device, v, i);
738            r.set_model(*model);
739        }
740        let refs: Vec<&Renderer3d> = self.actor_r.iter().take(actors.len()).collect();
741        self.scene.render_in(
742            device, queue, encoder, target, viewport, rect, camera, Some(&self.voxel), &refs,
743        );
744    }
745}