tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ 168cd187519c0ad575d3ce63baee2f164c38d9d1 llimphi-voxel-render/src/preview.rs Rust · 638 lines · 28.5 KB History Who touched what raw

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