tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
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}