//! `VoxelGrid` — grid de voxels denso y acotado (CPU side). Cada voxel es //! RGBA8: `rgb` = color, `a` = ocupación (`0` vacío, `>0` sólido). Se sube a //! una textura 3D de GPU que el shader ray-march recorre por DDA. //! //! M1 es **denso** a propósito (lo más simple que funciona). El salto a sparse //! (SVO/brickmap, saltar el aire) es M2 — ver `MOTOR-VOXEL.md` §11.2. //! //! M3 agrega **dirty tracking**: cada `set`/`clear` expande una caja AABB de la //! región cambiada. `VoxelRenderer::sync` sube sólo esa sub-caja (fina + bricks //! gruesos afectados) — la actualización incremental que reemplaza al re-mesh. /// Caja AABB de voxels cambiados desde el último `take_dirty`: `[xmin, ymin, /// zmin, xmax, ymax, zmax]` inclusiva. pub type DirtyBox = [u32; 6]; /// Grid denso de voxels RGBA8. Índice lineal `x + y*dx + z*dx*dy` (x contiguo), /// que es justo el layout que espera `queue.write_texture` (filas en x, luego y, /// luego capas en z). #[derive(Clone)] pub struct VoxelGrid { dim: [u32; 3], data: Vec<[u8; 4]>, /// **Detalle sub-voxel por voxel** (Rg8): `[rugosidad, bisel]` codificados con /// **offset +1** para distinguir "sin autor" (`[0,0]`) de "autor puso 0" /// (`[1,1]`). `rug`/`bisel` autorados en `[0,1]` (rug re-escalado a amplitud /// `0..1.5` en el shader). Todo `0` = el voxel usa el **look global** del /// ray-march (`VoxelRenderer::{rugosidad_amp, bisel}`) → retrocompat byte a /// byte cuando nadie lo toca. Lo llena [`set_detail`](Self::set_detail); el /// renderer lo sube a un atlas paralelo (`dpool`) que espeja los slots del /// pool de color. Es una propiedad de **construcción** del material, no un /// canal editado en vivo (`sync`/`scroll_to` no lo re-suben). detail: Vec<[u8; 2]>, /// **Pincel de detalle** activo (bytes ya codificados con offset +1, o `None`). /// Cuando está puesto, cada [`set`](Self::set)/[`set_rgba`](Self::set_rgba) /// estampa también ESTE detalle en el voxel — así los estampadores profundos /// (`place_objeto`, `stamp_bicho`…) heredan el detalle del material/ser sin /// tener que threadearlo por cada `put`. Se prende con /// [`set_detail_brush`](Self::set_detail_brush) y se apaga con /// [`clear_detail_brush`](Self::clear_detail_brush). `None` = comportamiento /// clásico (el voxel queda "sin autor" salvo `set_detail` explícito). detail_brush: Option<[u8; 2]>, /// AABB de voxels mutados desde el último `take_dirty`. `None` = sin cambios. dirty: Option, } impl VoxelGrid { /// Grid vacío de `dim = [dx, dy, dz]` voxels. pub fn new(dim: [u32; 3]) -> Self { let n = (dim[0] * dim[1] * dim[2]) as usize; Self { dim, data: vec![[0, 0, 0, 0]; n], detail: vec![[0, 0]; n], detail_brush: None, dirty: None, } } /// Dimensiones `[dx, dy, dz]`. pub fn dim(&self) -> [u32; 3] { self.dim } #[inline] fn idx(&self, x: u32, y: u32, z: u32) -> usize { (x + y * self.dim[0] + z * self.dim[0] * self.dim[1]) as usize } #[inline] fn mark_dirty(&mut self, x: u32, y: u32, z: u32) { match &mut self.dirty { None => self.dirty = Some([x, y, z, x, y, z]), Some(d) => { d[0] = d[0].min(x); d[1] = d[1].min(y); d[2] = d[2].min(z); d[3] = d[3].max(x); d[4] = d[4].max(y); d[5] = d[5].max(z); } } } /// Toma y limpia la caja de cambios pendientes. `VoxelRenderer::sync` la usa /// para subir sólo lo mutado. `None` si no hubo cambios desde la última toma. pub fn take_dirty(&mut self) -> Option { self.dirty.take() } /// Descarta los cambios pendientes sin subirlos (tras un upload completo, el /// estado inicial ya está en GPU). pub fn reset_dirty(&mut self) { self.dirty = None; } /// Marca **todo** el grid como sucio (caja = grid entero) → el próximo /// [`VoxelRenderer::sync`](crate::VoxelRenderer::sync) re-sube la ventana completa. /// Útil al **regenerar** el grid por streaming (la ventana cambió de origen). pub fn mark_all_dirty(&mut self) { self.dirty = Some([0, 0, 0, self.dim[0] - 1, self.dim[1] - 1, self.dim[2] - 1]); } /// Marca un voxel sólido con color `rgb` (alpha = 255). Fuera de rango: no-op. pub fn set(&mut self, x: u32, y: u32, z: u32, rgb: [u8; 3]) { if x < self.dim[0] && y < self.dim[1] && z < self.dim[2] { let i = self.idx(x, y, z); self.data[i] = [rgb[0], rgb[1], rgb[2], 255]; if let Some(d) = self.detail_brush { self.detail[i] = d; } self.mark_dirty(x, y, z); } } /// Marca un voxel con color `rgb` y **alpha** explícito. El alpha codifica /// ocupación **y** brillo: `>127` = sólido; el shader lee el brillo como /// `(1 - a/255)·2` (matte `255` → 0 brillo, `130` → lustre pleno). Pasar /// alpha `≤127` lo dejaría "vacío" para el shader — usa `[130, 255]` para /// voxels lustrosos. Fuera de rango: no-op. pub fn set_rgba(&mut self, x: u32, y: u32, z: u32, rgb: [u8; 3], a: u8) { if x < self.dim[0] && y < self.dim[1] && z < self.dim[2] { let i = self.idx(x, y, z); self.data[i] = [rgb[0], rgb[1], rgb[2], a]; if let Some(d) = self.detail_brush { self.detail[i] = d; } self.mark_dirty(x, y, z); } } /// Convierte un factor de **brillo** `[0,1]` al alpha de voxel correspondiente /// (`0` → `255` matte, `1` → `130` lustre pleno), manteniéndolo siempre sólido. #[inline] pub fn shine_to_alpha(brillo: f32) -> u8 { 255 - (brillo.clamp(0.0, 1.0) * 125.0) as u8 } /// Amplitud de rugosidad `[0,1.5]` que el shader mapea desde el byte de detalle /// (espeja `(byte-1)/254·1.5`). Expuesto para que el caller razone en la misma /// escala que `VoxelRenderer::rugosidad_amp`. pub const DETALLE_RUG_MAX: f32 = 1.5; /// Codifica un factor `[0,1]` al byte de detalle con **offset +1** (`0` autorado /// → `1`, distinguible del `0` "sin autor"). `norm` ya normalizado a `[0,1]`. #[inline] fn detail_byte(norm: f32) -> u8 { 1 + (norm.clamp(0.0, 1.0) * 254.0).round() as u8 } /// **Detalle sub-voxel de este voxel** (capa 3 del plan): `rugosidad` en /// amplitud `[0, DETALLE_RUG_MAX]` y `bisel` en `[0,1]`, autorados por voxel. /// Sobrescribe el look **global** del ray-march sólo para este voxel (dos /// materiales en la misma escena pueden lucir distinto a la vez — lo que el /// escalar global no podía). No cambia la ocupación; pon el voxel sólido con /// `set`/`set_rgba` aparte. Fuera de rango: no-op. No marca dirty: el detalle /// es propiedad de construcción (el renderer lo hornea al armar/rearmar). pub fn set_detail(&mut self, x: u32, y: u32, z: u32, rugosidad: f32, bisel: f32) { if x < self.dim[0] && y < self.dim[1] && z < self.dim[2] { let i = self.idx(x, y, z); self.detail[i] = [ Self::detail_byte(rugosidad / Self::DETALLE_RUG_MAX), Self::detail_byte(bisel), ]; } } /// **Prende el pincel de detalle**: a partir de aquí cada `set`/`set_rgba` /// estampa también este `(rugosidad, bisel)` en el voxel escrito (misma escala /// que [`set_detail`](Self::set_detail)). Pensado para que los estampadores /// profundos (flora en `place_objeto`, seres en `stamp_bicho`, muebles…) hereden /// el detalle del material/ser sin threadearlo por cada `put`. Acuérdate de /// apagarlo con [`clear_detail_brush`](Self::clear_detail_brush) al terminar el /// objeto, o teñirás de más. No toca voxels ya escritos. pub fn set_detail_brush(&mut self, rugosidad: f32, bisel: f32) { self.detail_brush = Some([ Self::detail_byte(rugosidad / Self::DETALLE_RUG_MAX), Self::detail_byte(bisel), ]); } /// Apaga el pincel de detalle (vuelve al comportamiento clásico: los `set` /// posteriores no estampan detalle salvo `set_detail` explícito). pub fn clear_detail_brush(&mut self) { self.detail_brush = None; } /// Detalle codificado `[rug, bisel]` (bytes con offset +1; `[0,0]` = sin autor) /// del voxel `(x,y,z)`, o `None` fuera de rango. pub fn detail_at(&self, x: u32, y: u32, z: u32) -> Option<[u8; 2]> { (x < self.dim[0] && y < self.dim[1] && z < self.dim[2]) .then(|| self.detail[self.idx(x, y, z)]) } /// Vacía **todos** los voxels y marca el grid entero como dirty (la próxima /// `VoxelRenderer::sync` re-sube todo). Para regenerar el contenido de una /// ventana de *streaming* in-place sin reconstruir el renderer. pub fn clear_all(&mut self) { for px in &mut self.data { *px = [0, 0, 0, 0]; } for d in &mut self.detail { *d = [0, 0]; } self.dirty = Some([0, 0, 0, self.dim[0] - 1, self.dim[1] - 1, self.dim[2] - 1]); } /// Vacía un voxel. pub fn clear(&mut self, x: u32, y: u32, z: u32) { if x < self.dim[0] && y < self.dim[1] && z < self.dim[2] { let i = self.idx(x, y, z); self.data[i] = [0, 0, 0, 0]; self.detail[i] = [0, 0]; self.mark_dirty(x, y, z); } } #[inline] fn solid(&self, x: u32, y: u32, z: u32) -> bool { self.data[self.idx(x, y, z)][3] > 0 } /// `true` si el voxel `(x,y,z)` es sólido. Fuera de rango → `false` (el /// "afuera" del mundo es aire). Lo usa el raycast de `llimphi-voxel` para /// picking/edición (mirar → bloque). #[inline] pub fn is_solid(&self, x: i32, y: i32, z: i32) -> bool { if x < 0 || y < 0 || z < 0 { return false; } let (x, y, z) = (x as u32, y as u32, z as u32); x < self.dim[0] && y < self.dim[1] && z < self.dim[2] && self.solid(x, y, z) } /// Color RGBA del voxel `(x,y,z)`, o `None` fuera de rango. `a = 0` = aire. pub fn get(&self, x: u32, y: u32, z: u32) -> Option<[u8; 4]> { (x < self.dim[0] && y < self.dim[1] && z < self.dim[2]).then(|| self.data[self.idx(x, y, z)]) } /// Altura del voxel sólido más alto en la columna `(x, z)` (escaneando de /// arriba hacia abajo), o `None` si la columna está vacía. Útil para posar /// una cámara/entidad sobre el terreno sin meterla dentro de la roca. pub fn height_at(&self, x: u32, z: u32) -> Option { if x >= self.dim[0] || z >= self.dim[2] { return None; } (0..self.dim[1]).rev().find(|&y| self.solid(x, y, z)) } /// Mapa de ocupación grueso por *bricks* de `brick³` voxels (M2): un texel /// por brick, `255` si el brick contiene algún voxel sólido, `0` si está /// todo vacío. El shader marcha primero esta grilla gruesa y se salta los /// bricks vacíos enteros en un paso (empty-space skipping). Devuelve /// `(dim_grueso, bytes R8)` con índice `cx + cy*cdx + cz*cdx*cdy`. pub fn coarse_occupancy(&self, brick: u32) -> ([u32; 3], Vec) { let b = brick.max(1); let cdim = [ self.dim[0].div_ceil(b), self.dim[1].div_ceil(b), self.dim[2].div_ceil(b), ]; let mut out = vec![0u8; (cdim[0] * cdim[1] * cdim[2]) as usize]; for z in 0..self.dim[2] { for y in 0..self.dim[1] { for x in 0..self.dim[0] { if self.solid(x, y, z) { let (cx, cy, cz) = (x / b, y / b, z / b); out[(cx + cy * cdim[0] + cz * cdim[0] * cdim[1]) as usize] = 255; } } } } (cdim, out) } /// `255` si el brick `(cx,cy,cz)` (tamaño `b`) tiene algún voxel sólido, /// `0` si está todo vacío. Lo usa el brick pool para decidir si un brick /// necesita slot. pub fn brick_occupied(&self, b: u32, cx: u32, cy: u32, cz: u32) -> u8 { let (x0, y0, z0) = (cx * b, cy * b, cz * b); for z in z0..(z0 + b).min(self.dim[2]) { for y in y0..(y0 + b).min(self.dim[1]) { for x in x0..(x0 + b).min(self.dim[0]) { if self.solid(x, y, z) { return 255; } } } } 0 } /// Extrae los voxels de un brick `(cx,cy,cz)` de lado `brick` como RGBA /// plano (`brick³` voxels, x contiguo), padeando con vacío los voxels fuera /// del grid (bricks de borde cuando `dim` no es múltiplo de `brick`). Es la /// unidad de subida al *pool* sparse (un slot del atlas = un brick). pub fn extract_brick(&self, brick: u32, cx: u32, cy: u32, cz: u32) -> Vec { let b = brick; let mut out = vec![0u8; (b * b * b * 4) as usize]; for lz in 0..b { for ly in 0..b { for lx in 0..b { let (x, y, z) = (cx * b + lx, cy * b + ly, cz * b + lz); if x < self.dim[0] && y < self.dim[1] && z < self.dim[2] { let px = self.data[self.idx(x, y, z)]; let o = ((lx + ly * b + lz * b * b) * 4) as usize; out[o..o + 4].copy_from_slice(&px); } } } } out } /// Extrae el **detalle sub-voxel** de un brick `(cx,cy,cz)` de lado `brick` /// como Rg8 plano (`brick³` voxels × 2 bytes, x contiguo), padeando con `[0,0]` /// (sin autor → look global) los voxels fuera del grid. Espeja `extract_brick` /// para el atlas paralelo `dpool` del renderer. pub fn extract_brick_detail(&self, brick: u32, cx: u32, cy: u32, cz: u32) -> Vec { let b = brick; let mut out = vec![0u8; (b * b * b * 2) as usize]; for lz in 0..b { for ly in 0..b { for lx in 0..b { let (x, y, z) = (cx * b + lx, cy * b + ly, cz * b + lz); if x < self.dim[0] && y < self.dim[1] && z < self.dim[2] { let d = self.detail[self.idx(x, y, z)]; let o = ((lx + ly * b + lz * b * b) * 2) as usize; out[o..o + 2].copy_from_slice(&d); } } } } out } /// Extrae una sub-caja RGBA contigua `[origin, origin+ext)` para subirla con /// `queue.write_texture` (M3: upload incremental de la región fina mutada). pub fn extract_fine(&self, origin: [u32; 3], ext: [u32; 3]) -> Vec { let mut out = Vec::with_capacity((ext[0] * ext[1] * ext[2] * 4) as usize); for z in origin[2]..origin[2] + ext[2] { for y in origin[1]..origin[1] + ext[1] { let row = self.idx(origin[0], y, z); for i in 0..ext[0] as usize { out.extend_from_slice(&self.data[row + i]); } } } out } /// Recalcula la ocupación gruesa de la caja de bricks `[cmin, cmin+cext)` y /// la devuelve contigua (R8) para subir sólo esos bricks (M3). pub fn coarse_region(&self, brick: u32, cmin: [u32; 3], cext: [u32; 3]) -> Vec { let b = brick.max(1); let mut out = Vec::with_capacity((cext[0] * cext[1] * cext[2]) as usize); for cz in cmin[2]..cmin[2] + cext[2] { for cy in cmin[1]..cmin[1] + cext[1] { for cx in cmin[0]..cmin[0] + cext[0] { out.push(self.brick_occupied(b, cx, cy, cz)); } } } out } /// Bytes RGBA planos listos para `queue.write_texture`. pub fn bytes(&self) -> &[u8] { // `[u8;4]` es contiguo: reinterpretamos el Vec como bytes planos. // SAFETY: `[u8;4]` no tiene padding; len*4 bytes válidos. unsafe { std::slice::from_raw_parts(self.data.as_ptr() as *const u8, self.data.len() * 4) } } /// Escena de prueba para M1: un piso de 2 capas + una esfera coloreada por /// posición flotando en el centro. Pone a prueba el DDA (atraviesa aire, /// pega en piso y en esfera) y el sombreado por normal de cara. pub fn demo_scene(dim: [u32; 3]) -> Self { let mut g = Self::new(dim); let [dx, dy, dz] = dim; // Piso: 2 capas grises abajo, con un leve damero para leer la perspectiva. for z in 0..dz { for x in 0..dx { let chk = ((x / 4 + z / 4) % 2) == 0; let base = if chk { 70 } else { 95 }; for y in 0..2 { g.set(x, y, z, [base, base + 8, base + 16]); } } } // Esfera centrada, color por posición normalizada. let cx = dx as f32 / 2.0; let cy = dy as f32 * 0.55; let cz = dz as f32 / 2.0; let r = (dx.min(dy).min(dz) as f32) * 0.3; for z in 0..dz { for y in 0..dy { for x in 0..dx { let (fx, fy, fz) = (x as f32 + 0.5, y as f32 + 0.5, z as f32 + 0.5); let d = ((fx - cx).powi(2) + (fy - cy).powi(2) + (fz - cz).powi(2)).sqrt(); if d <= r { let rr = (fx / dx as f32 * 255.0) as u8; let gg = (fy / dy as f32 * 255.0) as u8; let bb = (fz / dz as f32 * 255.0) as u8; g.set(x, y, z, [rr, gg, bb]); } } } } // Pilares: dan rincones para el AO y proyectan/reciben sombras. let pillars: [(u32, u32, u32, [u8; 3]); 3] = [ (dx / 5, dz / 4, dy * 7 / 10, [200, 120, 90]), (dx * 4 / 5, dz / 3, dy / 2, [110, 170, 120]), (dx / 3, dz * 4 / 5, dy * 3 / 5, [120, 130, 210]), ]; for (px, pz, ph, col) in pillars { for y in 2..(2 + ph).min(dy) { for dxx in 0..3u32 { for dzz in 0..3u32 { g.set(px + dxx, y, pz + dzz, col); } } } } // Estado inicial: el upload completo lo cubre, no es "mutación". g.reset_dirty(); g } } #[cfg(test)] mod tests { use super::*; #[test] fn detalle_sin_autor_es_cero() { // Un grid recién nacido no tiene detalle → todo `[0,0]` (look global). let g = VoxelGrid::new([4, 4, 4]); assert_eq!(g.detail_at(1, 1, 1), Some([0, 0])); assert_eq!(g.detail_at(9, 0, 0), None, "fuera de rango"); } #[test] fn detalle_offset_distingue_cero_autorado() { let mut g = VoxelGrid::new([4, 4, 4]); // Autor pone 0 en ambos → bytes `[1,1]`, DISTINTOS del `[0,0]` sin autor. g.set_detail(0, 0, 0, 0.0, 0.0); assert_eq!(g.detail_at(0, 0, 0), Some([1, 1]), "0 autorado ≠ sin autor"); // Máximos → 255 (1 + 254). g.set_detail(1, 0, 0, VoxelGrid::DETALLE_RUG_MAX, 1.0); assert_eq!(g.detail_at(1, 0, 0), Some([255, 255])); // Un valor intermedio queda por encima del piso. g.set_detail(2, 0, 0, VoxelGrid::DETALLE_RUG_MAX * 0.5, 0.5); let d = g.detail_at(2, 0, 0).unwrap(); assert!(d[0] > 120 && d[0] < 135, "rug medio ≈ 128: {}", d[0]); assert!(d[1] > 120 && d[1] < 135, "bisel medio ≈ 128: {}", d[1]); } #[test] fn pincel_de_detalle_tine_los_set_posteriores() { let mut g = VoxelGrid::new([4, 4, 4]); // Sin pincel: `set` deja el voxel "sin autor". g.set(0, 0, 0, [10, 20, 30]); assert_eq!(g.detail_at(0, 0, 0), Some([0, 0]), "sin pincel = sin autor"); // Con pincel: `set` y `set_rgba` heredan el detalle activo. g.set_detail_brush(VoxelGrid::DETALLE_RUG_MAX, 1.0); g.set(1, 0, 0, [10, 20, 30]); g.set_rgba(2, 0, 0, [10, 20, 30], 130); assert_eq!(g.detail_at(1, 0, 0), Some([255, 255]), "set hereda el pincel"); assert_eq!(g.detail_at(2, 0, 0), Some([255, 255]), "set_rgba hereda el pincel"); // Apagado: vuelve a "sin autor". g.clear_detail_brush(); g.set(3, 0, 0, [10, 20, 30]); assert_eq!(g.detail_at(3, 0, 0), Some([0, 0]), "pincel apagado = sin autor"); } #[test] fn extract_brick_detail_ubica_y_padea() { let mut g = VoxelGrid::new([8, 8, 8]); g.set_detail(1, 2, 3, VoxelGrid::DETALLE_RUG_MAX, 1.0); let d = g.extract_brick_detail(8, 0, 0, 0); assert_eq!(d.len(), 8 * 8 * 8 * 2); // Índice del voxel (1,2,3) en el brick, ×2 bytes. let o = (1 + 2 * 8 + 3 * 8 * 8) * 2; assert_eq!(&d[o..o + 2], &[255, 255], "detalle en su lugar"); // El resto padeado en 0 (sin autor). assert_eq!(&d[0..2], &[0, 0]); } #[test] fn clear_borra_detalle() { let mut g = VoxelGrid::new([4, 4, 4]); g.set(0, 0, 0, [200, 100, 50]); g.set_detail(0, 0, 0, 1.0, 0.8); g.clear(0, 0, 0); assert_eq!(g.detail_at(0, 0, 0), Some([0, 0]), "clear también zera el detalle"); } }