//! Contenido de la escena: terreno voxel + atmósfera + un "monumento" malla //! flotante. Aísla el *qué se muestra* del *cómo se maneja* (`main.rs`), así la //! app puede ganar personalidad (más estructuras, entidades, reglas) tocando //! sólo aquí, sin reescribir el bucle. use llimphi_3d::glam::{Mat4, Vec3}; use llimphi_3d::{ Atmosphere, Camera3d, Hud, HudQuad, Renderer3d, Scene3d, VoxelGrid, VoxelRenderer, }; use llimphi_ui::llimphi_hal::wgpu; use llimphi_voxel::{ multitud_caminando, Bicho, BichoUso, Bioma, BiomaPalette, Caminante, CasaTipo, CasaUso, Critter, DetalleSer, Enjambre, Player, Sembrada, Sembrado, }; /// Paleta de bichos (ovejas, marrones, terrosos, celestes…). const HERD_PALETTE: [[u8; 3]; 5] = [ [236, 236, 232], [205, 170, 125], [120, 92, 70], [96, 132, 176], [206, 198, 96], ]; /// Formato de la textura intermedia de Llimphi (target de `gpu_paint_with`). pub const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; /// Mundo render-able + editable: el motor voxel (terreno) + una malla de /// triángulos (el monumento), compuestos por [`Scene3d`] en un depth /// compartido. Conserva el [`VoxelGrid`] para editar (romper/colocar) por /// raycast y subir incremental con `sync`. pub struct World { scene: Scene3d, voxel: VoxelRenderer, monument: Renderer3d, grid: VoxelGrid, dim: [u32; 3], /// Jugador en primera persona (modo "explorar"): camina el terreno con /// gravedad y colisión. En modo órbita simplemente se ignora. player: Player, /// Bichos que deambulan el terreno (misma física que el jugador). Se /// dibujan como cajas en el pase de ray-march (`VoxelRenderer::entities`). critters: Vec, /// Overlay screen-space para la mira de primera persona. hud: Hud, /// Si dibujar el monumento-malla flotante (se apaga p.ej. en `--poses`). show_monument: bool, /// **Mapa real** para streaming infinito (modo `--mapa`): si está, la ventana se /// regenera de aquí al caminar, sin borde. `None` en los mundos procedurales. mapa: Option>, /// Columna de mundo `[wx, wz]` donde cae la esquina local `(0,0)` de la ventana. origin: [i32; 2], /// Origen de mundo INICIAL de la ventana (al construir). El origen de streaming /// que consume `scroll_to` (que arranca en `[0,0,0]` como el `brick_origin` del /// renderer) es `origin - origin0` → así el ring buffer sube sólo la franja nueva. origin0: [i32; 2], /// **Ceniza**: los voxels que se desprendieron del mundo y andan por el aire. /// Se dibujan como cajas en el mismo pase que los critters. ceniza: Enjambre, /// **La siembra del bioma**: cada animal y cada planta que la ventana estampó, /// con lo necesario para sacarlo y volver a ponerlo. Es lo que deja que el que /// tenés al lado sea un ser analítico y el del fondo siga siendo su estampa de /// voxels. Vacía en los mundos que no vienen de un bioma (mapa real, desierto). siembra: Vec, /// El bioma que sembró, para volver a estampar lo que se aleja. bioma: Option<(Bioma, BiomaPalette)>, /// Índices de `siembra` que AHORA son ser SDF (y están borrados del grid). en_sdf: Vec, /// Gente caminando cerca del jugador (la multitud del catálogo). gente: Vec, /// Reloj del mundo: la fase de todo lo que se mueve (paso, viento, aleteo). reloj: f32, } impl World { /// Paso de reubicación de la ventana de streaming (voxels): se regenera sólo al /// cruzar un múltiplo de esto → no se rehace el mundo cada voxel caminado. const STREAM_STEP: i32 = 16; /// **Mundo desde un mapa real, STREAMEADO** (modo `--mapa`): la ventana de `dim` /// se desliza sobre un `MapaVoxel` grande a medida que el jugador camina, así el /// mundo **no tiene borde** (Minecraft-style: se explora sin agotarlo; más allá /// del mapa, el relieve del borde/mar se extiende). Reusa física/cámara/HUD. pub fn build_mapa( device: &wgpu::Device, queue: &wgpu::Queue, mapa: nahual_geo_voxel::MapaVoxel, dim: [u32; 3], ) -> Self { // Ventana inicial centrada en el centro del mapa. let origin = [ mapa.proj.ancho as i32 / 2 - dim[0] as i32 / 2, mapa.proj.largo as i32 / 2 - dim[2] as i32 / 2, ]; let grid = mapa.generar_ventana(dim, origin); let mut voxel = VoxelRenderer::new(device, queue, FMT, &grid); voxel.sun_dir = [0.5, 0.55, 0.42]; voxel.atmosphere = Atmosphere { sky_zenith: [72, 124, 198], sky_horizon: [206, 220, 236], // Niebla moderada que difumina el borde lejano de la ventana. fog_density: 1.1 / dim[0] as f32, god_rays: 0.4, }; let monument = Renderer3d::new(device, FMT); // Reposar en una CALLE (columna baja), no en un techo. let (cx, cz) = (dim[0] / 2, dim[2] / 2); let (mut sx, mut sz, mut best) = (cx, cz, u32::MAX); let r = (dim[0] / 4).max(8); for z in cz.saturating_sub(r)..(cz + r).min(dim[2] - 1) { for x in cx.saturating_sub(r)..(cx + r).min(dim[0] - 1) { if let Some(h) = grid.height_at(x, z) { if h < best { best = h; sx = x; sz = z; } } } } let player = Player::spawn_on(&grid, sx, sz); let hud = Hud::new(device, FMT); let mut world = Self { scene: Scene3d::new(), voxel, monument, grid, dim, player, critters: Vec::new(), hud, show_monument: false, mapa: Some(std::sync::Arc::new(mapa)), origin, origin0: origin, ceniza: Enjambre::ceniza(0x1CE), siembra: Vec::new(), bioma: None, en_sdf: Vec::new(), gente: Vec::new(), reloj: 0.0, }; for _ in 0..40 { world.tick_sin_gpu(1.0 / 30.0); } world } /// **Streaming toroidal**: si el jugador se alejó del centro más de un /// [`STREAM_STEP`] (múltiplo de brick), desliza la ventana regenerando el terreno /// del `MapaVoxel` en el origen nuevo y **sube sólo la franja que entra** vía /// [`VoxelRenderer::scroll_to`] (ring buffer, sin re-subir la ventana entera → sin /// tirón). Traslada al jugador el mismo delta → queda quieto en el mundo mientras /// el terreno se re-centra bajo él. Sin borde. No-op fuera de `--mapa`. pub fn follow_mapa(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) { let Some(mapa) = self.mapa.clone() else { return }; let step = Self::STREAM_STEP; let cx = self.dim[0] as i32 / 2; let cz = self.dim[2] as i32 / 2; let ddx = ((self.player.pos.x.floor() as i32 - cx) / step) * step; let ddz = ((self.player.pos.z.floor() as i32 - cz) / step) * step; if ddx == 0 && ddz == 0 { return; } self.origin[0] += ddx; self.origin[1] += ddz; self.grid = mapa.generar_ventana(self.dim, self.origin); // Origen de streaming del renderer = delta acumulado desde el arranque // (su `brick_origin` empezó en 0). Sube sólo la franja nueva. let stream = [self.origin[0] - self.origin0[0], 0, self.origin[1] - self.origin0[1]]; self.voxel.scroll_to(device, queue, stream, &self.grid); // El jugador retrocede el mismo delta → posición de mundo intacta. self.player.pos.x -= ddx as f32; self.player.pos.z -= ddz as f32; } /// Construye el mundo: terreno procedural (de `llimphi-voxel`) subido al /// brick pool, atmósfera diurna, y un cubo-malla coloreado como monumento. /// Necesita `device`/`queue` (de ahí que se construya perezoso, en la 1ª /// pintada GPU). pub fn build(device: &wgpu::Device, queue: &wgpu::Queue, dim_xz: u32, seed: u32) -> Self { let dy = (dim_xz * 4 / 10).max(48); let dim = [dim_xz, dy, dim_xz]; let grid = llimphi_voxel::terrain(dim, seed); let mut voxel = VoxelRenderer::new(device, queue, FMT, &grid); voxel.sun_dir = [0.55, 0.5, 0.32]; voxel.atmosphere = Atmosphere { sky_zenith: [64, 118, 196], sky_horizon: [202, 218, 236], fog_density: 0.5 / dim_xz as f32, god_rays: 0.6, }; let monument = Renderer3d::new(device, FMT); // Jugador posado sobre la columna central del terreno. let player = Player::spawn_on(&grid, dim[0] / 2, dim[2] / 2); // Manada esparcida por el interior, en una grilla con jitter // determinista (cada bicho su semilla → rumbos distintos). let margin = 16u32; let (cols, rows) = (7u32, 5u32); let mut critters = Vec::with_capacity((cols * rows) as usize); let mut k: u32 = 0x9e3779b9; let span_x = dim[0].saturating_sub(2 * margin).max(1); let span_z = dim[2].saturating_sub(2 * margin).max(1); for r in 0..rows { for c in 0..cols { // Jitter LCG por celda para que no quede una grilla perfecta. k = k.wrapping_mul(1664525).wrapping_add(1013904223); let jx = (k >> 16) % 9; k = k.wrapping_mul(1664525).wrapping_add(1013904223); let jz = (k >> 16) % 9; let x = margin + span_x * c / (cols - 1) + jx; let z = margin + span_z * r / (rows - 1) + jz; let color = HERD_PALETTE[(r * cols + c) as usize % HERD_PALETTE.len()]; critters.push(Critter::spawn_on(&grid, x.min(dim[0] - 1), z.min(dim[2] - 1), color, k)); } } let hud = Hud::new(device, FMT); let mut world = Self { scene: Scene3d::new(), voxel, monument, grid, dim, player, critters, hud, show_monument: true, mapa: None, origin: [0, 0], origin0: [0, 0], ceniza: Enjambre::ceniza(0x1CE), siembra: Vec::new(), bioma: None, en_sdf: Vec::new(), gente: Vec::new(), reloj: 0.0, }; // Calentar la manada para que arranque desparramada (no en grilla). for _ in 0..150 { world.tick_sin_gpu(1.0 / 30.0); } world } /// Construye el **mundo completo**: TODO lo que se puede crear con el motor, /// junto, para caminarlo — terreno con colinas, **flora** (árboles, arbustos, /// flores, pasto), **minerales** que asoman en los acantilados, un **pueblo /// procedural** (casas de varios estilos + plazas con fuente/bancos/árboles) y /// **fauna** variada (vacas, ovejas, ciervos, zorros, caballos, gallinas, /// mariposas…), todo esparcido por la misma semilla. Sin monumento ni manada /// analítica: la vida está horneada en el propio terreno. pub fn build_todo(device: &wgpu::Device, queue: &wgpu::Queue, dim_xz: u32, seed: u32) -> Self { let dy = (dim_xz * 4 / 10).max(48); let dim = [dim_xz, dy, dim_xz]; // Partimos de la pradera del proyecto de arranque (ya trae flora + // minerales + fauna) y le sumamos un pueblo procedural encima. let project = llimphi_voxel::Project::starter(); let mut bioma = project .biomas .iter() .find(|b| b.name == "pradera") .cloned() .expect("bioma pradera"); // Colinas suaves: hay acantilados donde asoman los minerales, pero se // camina cómodo y las casas encuentran dónde asentarse. bioma.relief = 0.4; bioma.mountains = 0.3; bioma.water_level = 0.0; bioma.rivers = 0.0; // Un pueblo mezclado, en manchas conectadas por rutas (campo abierto en // medio) → se nace en pradera y se camina hacia el pueblo. bioma.densidad_urbana = 0.42; bioma.casas = vec![ CasaUso { tipo: CasaTipo::Cabana, densidad: 0.3 }, CasaUso { tipo: CasaTipo::Moderna, densidad: 0.22 }, CasaUso { tipo: CasaTipo::Granja, densidad: 0.2 }, CasaUso { tipo: CasaTipo::Adobe, densidad: 0.2 }, CasaUso { tipo: CasaTipo::Tienda, densidad: 0.15 }, CasaUso { tipo: CasaTipo::Torre, densidad: 0.1 }, CasaUso { tipo: CasaTipo::Flintstone, densidad: 0.12 }, CasaUso { tipo: CasaTipo::Hobbit, densidad: 0.12 }, CasaUso { tipo: CasaTipo::Duplex, densidad: 0.15 }, ]; // Fauna variada (además de la que ya trae la pradera). Desde que existe el // bestiario SDF, el catálogo del bioma puede ser el catálogo entero: lo que // se ve de cerca es el ser analítico, y lo de lejos su estampa de voxels. bioma.fauna.extend_from_slice(&[ BichoUso { bicho: Bicho::Ciervo, densidad: 0.20 }, BichoUso { bicho: Bicho::Zorro, densidad: 0.12 }, BichoUso { bicho: Bicho::Caballo, densidad: 0.14 }, BichoUso { bicho: Bicho::Gallina, densidad: 0.16 }, BichoUso { bicho: Bicho::Pato, densidad: 0.12 }, BichoUso { bicho: Bicho::Cabra, densidad: 0.14 }, BichoUso { bicho: Bicho::Cerdo, densidad: 0.12 }, BichoUso { bicho: Bicho::Pajaro, densidad: 0.16 }, BichoUso { bicho: Bicho::Perro, densidad: 0.10 }, BichoUso { bicho: Bicho::Gato, densidad: 0.08 }, BichoUso { bicho: Bicho::Burro, densidad: 0.08 }, BichoUso { bicho: Bicho::Llama, densidad: 0.08 }, BichoUso { bicho: Bicho::Conejo, densidad: 0.14 }, BichoUso { bicho: Bicho::Ardilla, densidad: 0.10 }, BichoUso { bicho: Bicho::Mariposa, densidad: 0.14 }, BichoUso { bicho: Bicho::Cardenal, densidad: 0.10 }, BichoUso { bicho: Bicho::Gorrion, densidad: 0.12 }, BichoUso { bicho: Bicho::Cuervo, densidad: 0.08 }, BichoUso { bicho: Bicho::Ganso, densidad: 0.08 }, BichoUso { bicho: Bicho::Serpiente, densidad: 0.06 }, ]); // Y la vegetación. La pradera del catálogo viene MUY tupida (árboles a // 0,008 por columna): con el bosque cerrado no se ve un animal ni a diez // pasos, y la primera toma salió con la cámara metida adentro de un // arbusto. Acá se rehace la lista — menos árboles, y adelante los que la // técnica analítica sabe dibujar bien de cerca (el orden manda: gana el // primero que acierta en la columna). { use llimphi_voxel::{Forma, ObjetoUso}; let hoja = bioma.objetos.first().map(|o| o.material).unwrap_or(0); let flor = bioma.objetos.last().map(|o| o.material).unwrap_or(hoja); bioma.objetos = vec![ ObjetoUso { material: hoja, densidad: 0.0022, forma: Forma::Ficus }, ObjetoUso { material: hoja, densidad: 0.0016, forma: Forma::Sauce }, ObjetoUso { material: hoja, densidad: 0.0018, forma: Forma::Jacaranda }, ObjetoUso { material: hoja, densidad: 0.0035, forma: Forma::Arbol }, ObjetoUso { material: hoja, densidad: 0.0045, forma: Forma::Arbusto }, ObjetoUso { material: flor, densidad: 0.0060, forma: Forma::Girasol }, ObjetoUso { material: flor, densidad: 0.0070, forma: Forma::Margarita }, ObjetoUso { material: flor, densidad: 0.0080, forma: Forma::Lavanda }, ObjetoUso { material: hoja, densidad: 0.0060, forma: Forma::Helecho }, ObjetoUso { material: hoja, densidad: 0.0300, forma: Forma::Pasto }, ]; } let palette = project.bioma_palette(&bioma); // Con siembra: la lista de lo que quedó plantado, para que el anillo SDF // pueda cambiar de técnica a los que tengas al lado (`repartir_sdf`). let (grid, siembra) = bioma.generate_window_con_siembra(seed, &palette, dim, [0, 0]); let mut voxel = VoxelRenderer::new(device, queue, FMT, &grid); voxel.sun_dir = [0.5, 0.52, 0.42]; voxel.atmosphere = Atmosphere { sky_zenith: [72, 124, 198], sky_horizon: [206, 220, 236], fog_density: 0.45 / dim_xz as f32, god_rays: 0.5, }; let monument = Renderer3d::new(device, FMT); // Buscamos una columna ABIERTA (fuera de mancha de pueblo) cerca del centro // para nacer en el campo, no dentro de una casa. Espiral de búsqueda. let (mut sx, mut sz) = (dim[0] / 2, dim[2] / 2); 'find: for r in 0..(dim[0] / 2) { for &(dx, dz) in &[(r, 0i32), (0, r as i32), (r, r as i32), (r, -(r as i32))] { let x = ((dim[0] / 2) as i32 + dx as i32).clamp(2, dim[0] as i32 - 3) as u32; let z = ((dim[2] / 2) as i32 + dz).clamp(2, dim[2] as i32 - 3) as u32; if !bioma.in_town(seed, x as i32, z as i32, dim) { sx = x; sz = z; break 'find; } } } let player = Player::spawn_on(&grid, sx, sz); let hud = Hud::new(device, FMT); // Además de la fauna horneada en el terreno, un puñado de bichos que // DEAMBULAN (misma física que el jugador) para que el mundo tenga // movimiento — se dibujan como cajas en el pase de ray-march. let mut critters = Vec::new(); let mut k: u32 = 0x1234_5678 ^ seed; for i in 0..14u32 { k = k.wrapping_mul(1664525).wrapping_add(1013904223); let x = 20 + (k >> 8) % dim[0].saturating_sub(40).max(1); k = k.wrapping_mul(1664525).wrapping_add(1013904223); let z = 20 + (k >> 8) % dim[2].saturating_sub(40).max(1); let color = HERD_PALETTE[i as usize % HERD_PALETTE.len()]; critters.push(Critter::spawn_on(&grid, x.min(dim[0] - 1), z.min(dim[2] - 1), color, k)); } let mut world = Self { scene: Scene3d::new(), voxel, monument, grid, dim, player, critters, hud, show_monument: false, mapa: None, origin: [0, 0], origin0: [0, 0], ceniza: Enjambre::ceniza(0x1CE), siembra, bioma: Some((bioma, palette)), en_sdf: Vec::new(), // Gente caminando por el campo, cerca de donde se nace: la multitud del // catálogo puesta en el mundo. Se posan sobre el relieve cada cuadro. gente: multitud_caminando( seed ^ 0x6E7E, Self::GENTE, Vec3::new(sx as f32 - 14.0, 0.0, sz as f32 - 18.0), 28.0, 40.0, 0.0, ), reloj: 0.0, }; for _ in 0..120 { world.tick_sin_gpu(1.0 / 30.0); } world } /// Construye un mundo de **desierto** (vía [`WorldRecipe::desert`](llimphi_voxel::WorldRecipe)): /// llano de arena con pocas montañas/ríos y cactus ralos, sin manada (escena /// solemne del corto de nacimiento) y sin el monumento. Atmósfera diurna cálida. /// Pensado para el modo `--born`. pub fn build_desert(device: &wgpu::Device, queue: &wgpu::Queue, dim_xz: u32, seed: u32) -> Self { let dy = (dim_xz * 4 / 10).max(48); let dim = [dim_xz, dy, dim_xz]; // Desierto **llano** (como lo pidió el corto): sin mesas/relieve ni ríos, // sólo una ondulación de dunas mínima. Así el huevo siempre tiene despeje y // el plano de seguimiento (que retrocede detrás del sujeto) ve arena abierta // en vez de meterse dentro de una columna de terreno. // El bioma de desierto del proyecto de arranque, resuelto a su paleta y // aplanado (sin mesas/relieve ni ríos) — la semilla del CLI manda. let project = llimphi_voxel::Project::starter(); let mut bioma = project .biomas .iter() .find(|b| b.name == "desierto") .cloned() .expect("bioma desierto"); bioma.mountains = 0.0; bioma.relief = 0.0; bioma.dune = 0.035; bioma.rivers = 0.0; let palette = project.bioma_palette(&bioma); let grid = bioma.generate_window(seed, &palette, dim, [0, 0]); let mut voxel = VoxelRenderer::new(device, queue, FMT, &grid); voxel.sun_dir = [0.5, 0.62, 0.30]; // sol alto y cálido voxel.atmosphere = Atmosphere { sky_zenith: [108, 152, 206], // azul de mediodía sky_horizon: [226, 212, 178], // horizonte arenoso/caluroso fog_density: 0.18 / dim_xz as f32, // baja: hay tomas aéreas (cámara lejos) god_rays: 0.5, }; let monument = Renderer3d::new(device, FMT); let player = Player::spawn_on(&grid, dim[0] / 2, dim[2] / 2); let hud = Hud::new(device, FMT); Self { scene: Scene3d::new(), voxel, monument, grid, dim, player, critters: Vec::new(), // desierto vacío hud, show_monument: false, mapa: None, origin: [0, 0], origin0: [0, 0], ceniza: Enjambre::ceniza(0x1CE), siembra: Vec::new(), bioma: None, en_sdf: Vec::new(), gente: Vec::new(), reloj: 0.0, } } /// Avanza un frame de vida: deambula la manada y vuelca sus cajas al /// renderer (la capa de entidades del ray-march). Barato (instancing /// analítico) — llamar cada frame, en cualquier modo. pub fn tick(&mut self, queue: &wgpu::Queue, dt: f32) { self.tick_sin_gpu(dt); // El anillo SDF: quién deja de ser voxel y pasa a ser analítico. Toca el // grid, así que necesita la cola para subir los bricks que cambiaron. self.repartir_sdf(queue); } /// La parte del cuadro que no toca la GPU (la usa el constructor para /// «asentar» el mundo antes de que exista un cuadro). pub fn tick_sin_gpu(&mut self, dt: f32) { self.reloj += dt; for c in &mut self.critters { c.step(&self.grid, dt); } self.ceniza.paso(dt); self.voxel.entities.clear(); self.voxel.entities.extend(self.critters.iter().map(|c| c.entity())); // Lo que sobra del cupo de cajas del motor (64) es para la ceniza, lo más // cerca del ojo primero: la que no entra es la que no se ve. let resto = 64usize.saturating_sub(self.voxel.entities.len()); if resto > 0 && !self.ceniza.is_empty() { let ojo = self.player.eye(); self.voxel.entities.extend(self.ceniza.entidades(resto, Some(ojo))); } } /// Cuántas personas caminan por el mundo. const GENTE: usize = 9; /// Hasta dónde llega el anillo SDF de la FAUNA (voxels). Generoso: un animal /// mide un par de metros, así que a cincuenta pasos todavía se le ve la forma. const RADIO_FAUNA: f32 = 62.0; /// Y el de la FLORA. Corto a propósito: los árboles son cientos y a diez metros /// la estampa de voxels ya cuenta lo mismo. El anillo chico es lo que deja el /// presupuesto libre para los animales y la gente, que es donde se nota. const RADIO_FLORA: f32 = 26.0; /// Histéresis: se entra al anillo a `RADIO` y se sale a `RADIO · SALIDA`. Sin /// esto, el que queda justo en el borde alterna de técnica todos los cuadros — /// y cada alternancia re-sube bricks a la GPU. const SALIDA: f32 = 1.18; /// **El reparto entre las dos técnicas de graficación**, cuadro a cuadro. /// /// Lo que el bioma sembró en voxels sigue en voxels **salvo lo que tenés /// cerca**: eso se borra del grid y se dibuja como ser analítico (silueta lisa, /// cara, andar). Al alejarse vuelve a su estampa. El mundo NO se regenera: se /// re-suben los pocos bricks que cambiaron (`VoxelRenderer::sync`). /// /// El presupuesto del uniform (40 seres / 640 primitivas) se reparte por /// cercanía, y el detalle de cada uno baja con la distancia: de cerca la cara, /// de lejos la silueta. fn repartir_sdf(&mut self, queue: &wgpu::Queue) { let Some((bioma, _)) = self.bioma.as_ref() else { return; }; let ojo = self.player.eye(); let dist = |s: &Sembrado| { let c = Vec3::new( s.base[0] as f32 + 0.5, s.base[1] as f32 + s.alto() * 0.5, s.base[2] as f32 + 0.5, ); ojo.distance(c) }; // 1) Quién califica: por clase, por distancia, y con histéresis para los // que ya están adentro. let mut candidatos: Vec<(f32, usize)> = Vec::new(); for (i, s) in self.siembra.iter().enumerate() { let radio = match s.clase { Sembrada::Fauna { .. } => Self::RADIO_FAUNA, Sembrada::Flora { .. } => Self::RADIO_FLORA, }; let radio = if self.en_sdf.contains(&i) { radio * Self::SALIDA } else { radio }; let d = dist(s); if d <= radio { candidatos.push((d, i)); } } // La FAUNA primero y la flora después, cada grupo por cercanía. Con un // orden puramente por distancia, las flores y los arbustos que tenés a dos // pasos se comen el presupuesto y no queda un solo animal analítico en el // cuadro — que es justo lo que se vino a ver. Una toma salió así: un // girasol precioso y ni una vaca. candidatos.sort_by(|a, b| { let clase = |i: usize| match self.siembra[i].clase { Sembrada::Fauna { .. } => 0u8, Sembrada::Flora { .. } => 1, }; clase(a.1).cmp(&clase(b.1)).then(a.0.total_cmp(&b.0)) }); // 2) El presupuesto del uniform. La gente entra primero (es lo que más se // mira) y el resto se reparte por el orden de arriba, bajando el detalle // antes de descartar a nadie. let cupo_gente = self.gente.len().min(llimphi_3d::VOXEL_MAX_SERS / 3); let mut sers: Vec = Vec::with_capacity(llimphi_3d::VOXEL_MAX_SERS); let mut gastado = 0usize; let mut gente: Vec<(f32, usize)> = self .gente .iter() .enumerate() .map(|(i, c)| (ojo.distance(c.pos), i)) .collect(); gente.sort_by(|a, b| a.0.total_cmp(&b.0)); for (d, i) in gente.into_iter().take(cupo_gente) { let c = &self.gente[i]; let detalle = DetalleSer::por_distancia(d, c.persona.altura); if gastado + detalle.costo() > llimphi_3d::VOXEL_MAX_SER_PRIMS { break; } let ser = c.persona.posada(c.clip, c.fase, c.pos, c.yaw, None, detalle); gastado += ser.prims.len(); sers.push(ser); } // 3) Los del anillo: los que YA están sin estampar se dibujan sí o sí (si // no, quedarían de agujero en el mundo); los nuevos entran mientras // alcance el presupuesto. let mut quiero: Vec<(f32, usize, bool)> = candidatos .iter() .map(|&(d, i)| (d, i, self.en_sdf.contains(&i))) .collect(); quiero.sort_by(|a, b| b.2.cmp(&a.2).then(a.0.total_cmp(&b.0))); let mut dibujar: Vec<(f32, usize)> = Vec::new(); for (d, i, ya) in quiero { if sers.len() + dibujar.len() >= llimphi_3d::VOXEL_MAX_SERS { break; } let s = &self.siembra[i]; let mut detalle = DetalleSer::por_distancia(d, s.alto()); while gastado + detalle.costo() > llimphi_3d::VOXEL_MAX_SER_PRIMS { detalle = match detalle { DetalleSer::Hero => DetalleSer::Medio, DetalleSer::Medio => DetalleSer::Lejos, DetalleSer::Lejos => break, }; } if gastado + detalle.costo() > llimphi_3d::VOXEL_MAX_SER_PRIMS { if ya { continue; // sin cupo, pero sigue borrado: se re-estampa abajo } break; } let ser = s.ser_sdf(self.reloj, detalle); gastado += ser.prims.len(); sers.push(ser); dibujar.push((d, i)); } // 4) Cambios de técnica, **a cuentagotas**: `VoxelGrid::take_dirty` devuelve // UNA caja que envuelve todo lo tocado y `sync` re-sube sus bricks. // Cambiando dos sujetos en esquinas opuestas del anillo, esa caja es // media ventana. Con tope por cuadro la caja queda chica, y lo que no // entró pasa al siguiente — gratis, porque la histéresis ya evita que // nadie oscile en el borde. const POR_CUADRO: usize = 3; let elegidos: Vec = dibujar.iter().map(|&(_, i)| i).collect(); let mut cambios = 0usize; let previos = std::mem::take(&mut self.en_sdf); for i in previos { if !elegidos.contains(&i) && cambios < POR_CUADRO { // Se aleja: vuelve a ser su estampa de voxels. bioma.estampar(&mut self.grid, &self.siembra[i], self.dim); cambios += 1; } else { self.en_sdf.push(i); } } for &i in &elegidos { if !self.en_sdf.contains(&i) { if cambios >= POR_CUADRO { // No entra este cuadro: tampoco se dibuja, o saldría encimado // con su propio fantasma de voxels. if let Some(p) = dibujar.iter().position(|&(_, j)| j == i) { sers.remove(cupo_gente.min(sers.len()) + p); dibujar.remove(p); } continue; } bioma.desestampar(&mut self.grid, &self.siembra[i], self.dim); self.en_sdf.push(i); cambios += 1; } } // Y el que sigue borrado pero no entró en el cupo del uniform: se dibuja // igual con el detalle más barato, o quedaría un hueco en el terreno. for &i in &self.en_sdf.clone() { if !dibujar.iter().any(|&(_, j)| j == i) { let s = &self.siembra[i]; sers.push(s.ser_sdf(self.reloj, DetalleSer::Lejos)); } } // Válvula de comparación: `SDF_RING=0` deja el mundo ENTERO en voxels (se // vuelve a estampar lo que se había sacado y no se dibuja ningún ser // analítico). Es para sacar las dos tomas del mismo sitio, con la misma // cámara, y ver de qué se trata el cambio de técnica — el "antes" no se // puede reconstruir de memoria. let anillo = std::env::var("SDF_RING").map(|v| v != "0").unwrap_or(true); if !anillo { for &i in &self.en_sdf { bioma.estampar(&mut self.grid, &self.siembra[i], self.dim); } sers.clear(); cambios += 1; } if cambios > 0 { self.voxel.sync(queue, &mut self.grid); } self.voxel.sers = sers; // 5) La gente sigue caminando y pisando el relieve. self.caminar_gente(); } /// Avanza la multitud: cada uno a su paso, y **posado sobre el terreno** (la /// vereda del catálogo era plana; un campo con lomas, no). fn caminar_gente(&mut self) { for c in &mut self.gente { let vel = c.persona.altura * 0.62; let paso = if c.yaw.abs() < 0.1 { vel } else { -vel }; c.pos.z += paso * (1.0 / 60.0); c.fase += vel * 2.2 / 60.0; // Rebote en los bordes de la ventana (el mundo tiene borde). let margen = 6.0; if c.pos.z < margen || c.pos.z > self.dim[2] as f32 - margen { c.yaw = if c.yaw.abs() < 0.1 { std::f32::consts::PI } else { 0.0 }; c.pos.z = c.pos.z.clamp(margen, self.dim[2] as f32 - margen); } c.pos.x = c.pos.x.clamp(margen, self.dim[0] as f32 - margen); // Pisar el relieve: la altura de la columna de abajo. if let Some(h) = self.grid.height_at(c.pos.x as u32, c.pos.z as u32) { let objetivo = h as f32 + 1.0; // Suavizado: subir de golpe un escalón se ve como un salto. c.pos.y += (objetivo - c.pos.y).clamp(-0.35, 0.35); } } } /// El punto que mirar para VER el catálogo: el centro del ser analítico más /// cercano al ojo que no sea el propio pie del jugador. /// /// **En coordenadas CENTRADAS**, como todo lo que consume [`Camera3d`]: la /// siembra vive en coords de grilla y devolverla cruda dejaba la cámara mirando /// media ventana más allá — la toma salía de puro cielo y no había nada que /// delatara por qué. pub fn mirar_sdf(&self, ojo: Vec3) -> Option { let centro = self.world_center(); // Un animal antes que una planta: es lo que cuenta la historia del cambio // de técnica (una flor también, pero un ciervo se entiende de una). let hay_fauna = self .en_sdf .iter() .any(|i| matches!(self.siembra[*i].clase, Sembrada::Fauna { .. })); self.siembra .iter() .enumerate() .filter(|(i, _)| self.en_sdf.contains(i)) .filter(|(_, s)| !hay_fauna || matches!(s.clase, Sembrada::Fauna { .. })) .map(|(_, s)| { Vec3::new( s.base[0] as f32 + 0.5, s.base[1] as f32 + s.alto() * 0.45, s.base[2] as f32 + 0.5, ) - centro }) .filter(|c| ojo.distance(*c) > 2.5) .min_by(|a, b| ojo.distance(*a).total_cmp(&ojo.distance(*b))) } /// Altura del terreno en una columna, **en coordenadas centradas** (para no /// plantar la cámara adentro de una loma). pub fn altura_en(&self, x: f32, z: f32) -> Option { let centro = self.world_center(); let (gx, gz) = (x + centro.x, z + centro.z); if gx < 0.0 || gz < 0.0 || gx >= self.dim[0] as f32 || gz >= self.dim[2] as f32 { return None; } self.grid .height_at(gx as u32, gz as u32) .map(|h| h as f32 + 1.0 - centro.y) } /// Cuántos seres analíticos hay en el cuadro y cuántas primitivas gastan — el /// número que dice si el anillo entra en el presupuesto. pub fn cuenta_sdf(&self) -> (usize, usize) { ( self.voxel.sers.len(), self.voxel.sers.iter().map(|s| s.prims.len()).sum(), ) } /// **Desmorona** el mundo donde apunta el rayo: arranca un puñado de voxels y /// los pone a subir como ceniza (gravedad al revés). Devuelve `false` si el rayo /// no pegó en nada. pub fn desprender_ceniza(&mut self, queue: &wgpu::Queue, origin: [f32; 3], dir: [f32; 3]) -> bool { let max = self.dim[0] as f32 * 3.0; let Some(hit) = llimphi_voxel::raycast(&self.grid, origin, dir, max) else { return false; }; let [cx, cy, cz] = hit.cell; let centro = Vec3::new(cx as f32 + 0.5, cy as f32 + 0.5, cz as f32 + 0.5); let n = self.ceniza.desprender(&mut self.grid, centro, 3.5, 28); if n > 0 { self.voxel.sync(queue, &mut self.grid); } n > 0 } /// Avanza la física del jugador `dt` segundos con la caminata deseada /// (`wish`, horizontal, espacio de grilla) y `jump`. Devuelve el ojo del /// jugador en **mundo** (centrado en el origen, listo para la cámara). pub fn step_player(&mut self, wish: Vec3, jump: bool, dt: f32) -> Vec3 { self.player.step(&self.grid, wish, jump, dt); self.player.eye() - self.world_center() } /// Ojo del jugador en coords centradas (para la cámara), sin avanzar la física. /// Se usa tras [`follow_mapa`](Self::follow_mapa), que puede reubicar al jugador. pub fn eye(&self) -> Vec3 { self.player.eye() - self.world_center() } /// Niebla base del mapa (la que difumina el borde de la ventana a ras de suelo). fn fog_base(&self) -> f32 { 1.1 / self.dim[0] as f32 } /// Altura del jugador SOBRE el terreno de su columna (voxels), `0` si está al ras. fn altitud(&self) -> f32 { let px = (self.player.pos.x.max(0.0) as u32).min(self.dim[0] - 1); let pz = (self.player.pos.z.max(0.0) as u32).min(self.dim[2] - 1); let suelo = self.grid.height_at(px, pz).unwrap_or(0) as f32; (self.player.pos.y - suelo).max(0.0) } /// **Vuelo libre** (creativo): mueve al jugador en `wish` (con vertical), sin /// gravedad ni colisión. La velocidad **escala con la altitud** (más alto → más /// rápido, para cubrir territorio) y la **niebla se abre** con la altura (más /// alto → ves más lejos, hasta el borde de la ventana). Devuelve el ojo centrado. pub fn fly_player(&mut self, wish: Vec3, dt: f32) -> Vec3 { let alt = self.altitud(); let boost = (1.0 + alt / 14.0).min(16.0); self.player.volar_veloz(wish, dt, boost); // Niebla que se disipa al subir → vista lejana. self.voxel.atmosphere.fog_density = self.fog_base() / (1.0 + alt / 30.0); self.player.eye() - self.world_center() } /// Restaura la niebla base (al volver a caminar tras haber volado alto). pub fn reset_mapa_fog(&mut self) { if self.mapa.is_some() { self.voxel.atmosphere.fog_density = self.fog_base(); } } /// Dimensión `[dx, dy, dz]` de la ventana actual. pub fn dim(&self) -> [u32; 3] { self.dim } /// Origen de la ventana de streaming (columna de mundo de la esquina). Debug. pub fn origin_debug(&self) -> [i32; 2] { self.origin } /// Reposa al jugador sobre la columna central (al entrar a modo explorar, /// por si el terreno cambió por ediciones). pub fn respawn_player(&mut self) { self.player = Player::spawn_on(&self.grid, self.dim[0] / 2, self.dim[2] / 2); } /// Reposa al jugador sobre una columna concreta `(x, z)` (clamp al grid). /// Útil para encuadrar una vista en primera persona desde un mirador. pub fn spawn_player_at(&mut self, x: u32, z: u32) { let x = x.min(self.dim[0] - 1); let z = z.min(self.dim[2] - 1); self.player = Player::spawn_on(&self.grid, x, z); } /// Centro (en **mundo**) del bicho más cercano a `eye_world`, o `None` si no /// hay manada. Para encuadrar un bicho en una toma de primera persona. pub fn nearest_critter(&self, eye_world: Vec3) -> Option { let center = self.world_center(); self.critters .iter() .map(|c| { let e = c.entity(); Vec3::new(e.pos[0], e.pos[1], e.pos[2]) - center }) .min_by(|a, b| { a.distance_squared(eye_world) .total_cmp(&b.distance_squared(eye_world)) }) } /// Medio-`dim`: el offset entre espacio de grilla (`[0,dim]`) y mundo /// (centrado en el origen). `mundo = grilla - centro`. fn world_center(&self) -> Vec3 { Vec3::new(self.dim[0] as f32, self.dim[1] as f32, self.dim[2] as f32) * 0.5 } /// Edita el mundo por **raycast** desde un origen/dirección (espacio de /// grilla = `eye_mundo + dim/2`): `build=false` cava un cráter en el voxel /// golpeado; `build=true` deposita un bloque en la celda vacía adyacente. /// Sube sólo lo tocado (`sync` incremental). Devuelve `true` si pegó algo. /// Se llama desde el hilo GPU (necesita `queue`). pub fn apply_edit(&mut self, queue: &wgpu::Queue, origin: [f32; 3], dir: [f32; 3], build: bool) -> bool { let max = self.dim[0] as f32 * 3.0; let Some(hit) = llimphi_voxel::raycast(&self.grid, origin, dir, max) else { return false; }; if build { let [px, py, pz] = hit.place; let r = 1i32; for dz in -r..=r { for dy in -r..=r { for dx in -r..=r { let (x, y, z) = (px + dx, py + dy, pz + dz); if x >= 0 && y >= 0 && z >= 0 { self.grid.set(x as u32, y as u32, z as u32, [222, 184, 96]); } } } } } else { let [cx, cy, cz] = hit.cell; let r = 4i32; for dz in -r..=r { for dy in -r..=r { for dx in -r..=r { if dx * dx + dy * dy + dz * dz <= r * r { let (x, y, z) = (cx + dx, cy + dy, cz + dz); if x >= 0 && y >= 0 && z >= 0 { self.grid.clear(x as u32, y as u32, z as u32); } } } } } } self.voxel.sync(queue, &mut self.grid); true } /// Posiciona el monumento: flota sobre el centro del mundo y gira con /// `angle` (rad). El mundo voxel ocupa `y ∈ [-dy/2, dy/2]`; lo dejamos a /// ~`0.45·dy` para que asome entre los picos (y se ocluya con ellos — prueba /// la convivencia voxel+malla en vivo). pub fn animate(&mut self, angle: f32) { let dy = self.dim[1] as f32; let size = self.dim[0] as f32 * 0.11; let y = dy * 0.45; let m = Mat4::from_translation(Vec3::new(0.0, y, 0.0)) * Mat4::from_rotation_y(angle) * Mat4::from_scale(Vec3::splat(size)); self.monument.set_model(m); } /// Compone terreno + monumento sobre `target` (depth compartido). Firma /// compatible con la closure de `gpu_paint_with` (más la cámara). pub fn render( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, viewport: (u32, u32), camera: &Camera3d, ) { self.render_with(device, queue, encoder, target, viewport, camera, &[]); } /// Como [`render`](Self::render) pero agrega **mallas extra** al pase (mismo /// depth compartido → se ocluyen con el terreno y el monumento). Es el gancho /// para los **actores** de una escena filmada: la app posa cada actor en su /// propio [`Renderer3d`](llimphi_3d::Renderer3d) y los pasa aquí. #[allow(clippy::too_many_arguments)] pub fn render_with( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, viewport: (u32, u32), camera: &Camera3d, extra: &[&Renderer3d], ) { let mut meshes: Vec<&Renderer3d> = Vec::with_capacity(1 + extra.len()); if self.show_monument { meshes.push(&self.monument); } meshes.extend_from_slice(extra); self.scene.render( device, queue, encoder, target, viewport, camera, Some(&self.voxel), &meshes, ); } /// Enciende/apaga el dibujo del monumento-malla flotante (se apaga en /// `--poses` para no tapar la fila de actores). pub fn show_monument(&mut self, v: bool) { self.show_monument = v; } /// Posición en **mundo** (centrado en el origen) del **suelo** sobre la /// columna de grilla `(gx, gz)`: pies un voxel por encima del terreno (o del /// piso `y=0` si la columna está vacía). Para posar un actor sobre el relieve. pub fn ground_at(&self, gx: u32, gz: u32) -> Vec3 { let gx = gx.min(self.dim[0] - 1); let gz = gz.min(self.dim[2] - 1); let top = self.grid.height_at(gx, gz).map(|y| y as f32 + 1.0).unwrap_or(0.0); Vec3::new(gx as f32 + 0.5, top, gz as f32 + 0.5) - self.world_center() } /// Cota del suelo en un punto de **mundo** (el mismo espacio que devuelve /// [`ground_at`](Self::ground_at)). Es lo que necesita el IK de pies de un actor: /// muestrear bajo cada pie por separado, no bajo el centro del cuerpo. pub fn ground_y(&self, wx: f32, wz: f32) -> f32 { let c = self.world_center(); let gx = (wx + c.x).max(0.0) as u32; let gz = (wz + c.z).max(0.0) as u32; self.ground_at(gx, gz).y } /// Pinta el HUD de primera persona encima de la escena (pase screen-space): /// la **mira** centrada + un panel con el **modo** y las **coordenadas** del /// jugador (lectura del propio `Player`, espacio de grilla). Llamar *después* /// de [`render`](Self::render), en modo explorar. pub fn draw_hud( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, viewport: (u32, u32), ) { let mut quads = HudQuad::crosshair(viewport, 9.0, 2.0, [1.0, 1.0, 1.0, 0.85]).to_vec(); // Texto: modo + coordenadas. La grilla es entera, redondeamos a voxel. let p = self.player.pos; let coords = format!("X {} Y {} Z {}", p.x as i32, p.y as i32, p.z as i32); let px = 2.0; // pixel de glifo (10×14 por carácter) let (tx, ty) = (16.0, 14.0); let lh = 7.0 * px + 6.0; // alto de línea (glifo + interlínea) // Panel de fondo translúcido para legibilidad sobre cualquier terreno. let pw = HudQuad::text_width(&coords, px).max(HudQuad::text_width("EXPLORAR", px)); quads.push(HudQuad { x: tx - 8.0, y: ty - 8.0, w: pw + 16.0, h: lh * 2.0 + 8.0, color: [0.0, 0.0, 0.0, 0.45], }); quads.extend(HudQuad::text("EXPLORAR", tx, ty, px, [0.62, 0.95, 0.78, 0.95])); quads.extend(HudQuad::text(&coords, tx, ty + lh, px, [0.95, 0.97, 1.0, 0.95])); self.hud.render(device, queue, encoder, target, viewport, &quads); } }