tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 168cd187519c0ad575d3ce63baee2f164c38d9d1
llimphi-voxel-render/src/preview.rs
History
Who touched what
raw
chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos
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}