//! Importación de modelos **MagicaVoxel `.vox`** al motor: convierte un //! [`foreign_vox::VoxModel`] (neutral, leído por el puente) en un //! [`VoxelGrid`](llimphi_3d::VoxelGrid) del motor, para meter **sets y //! personajes diseñados afuera** a una escena/película voxel. //! //! Ejes: MagicaVoxel usa `z` como **arriba**; el motor usa `y` arriba. La //! conversión mapea `vox (x, y, z) → grid (x, z, y)` (la `z` del `.vox` sube a la //! `y` del grid, la `y` del `.vox` pasa a la profundidad `z`). //! //! La capa de juego es la dueña de esto (no el motor ni el puente): el puente //! sólo entiende bytes, el motor sólo voxels; aquí se casan (CLAUDE.md regla #4). use std::fmt; use std::path::Path; use foreign_vox::{Rot, VoxError, VoxModel, Voxel}; use llimphi_3d::VoxelGrid; /// Error al cargar un `.vox` desde disco. #[derive(Debug)] pub enum VoxLoadError { /// Falló la lectura del archivo. Io(std::io::Error), /// El contenido no es un `.vox` válido. Parse(VoxError), } impl fmt::Display for VoxLoadError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { VoxLoadError::Io(e) => write!(f, "leyendo .vox: {e}"), VoxLoadError::Parse(e) => write!(f, "parseando .vox: {e}"), } } } impl std::error::Error for VoxLoadError {} /// Convierte un `VoxModel` en un `VoxelGrid` ajustado a su tamaño (con los ejes /// remapeados a la convención del motor). El estado dirty queda limpio (el grid /// recién hecho se sube entero). pub fn model_to_grid(m: &VoxModel) -> VoxelGrid { // dim del grid = [x, z(vox→y), y(vox→z)], mínimo 1 por eje. let dim = [m.size[0].max(1), m.size[2].max(1), m.size[1].max(1)]; let mut g = VoxelGrid::new(dim); stamp(&mut g, m, [0, 0, 0]); g.reset_dirty(); g } /// **Estampa** los voxels de un modelo dentro de un grid existente, con la /// esquina del modelo en `origin` (espacio de grilla del motor). Para componer /// *sets*: meter varias piezas `.vox` en un mismo mundo. Voxels transparentes /// (`alpha 0`) se omiten; los que caen fuera del grid, también (`set` los ignora). pub fn stamp(grid: &mut VoxelGrid, m: &VoxModel, origin: [u32; 3]) { for v in &m.voxels { let c = m.color(v); if c[3] == 0 { continue; } let gx = origin[0] + v.x as u32; let gy = origin[1] + v.z as u32; // z-arriba (vox) → y-arriba (grid) let gz = origin[2] + v.y as u32; grid.set(gx, gy, gz, [c[0], c[1], c[2]]); } } /// Por qué no se pudo exportar un grid al formato ajeno. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum VoxExportError { /// El formato guarda las coordenadas en un byte: ningún eje puede pasar de 256 /// voxels. Un mundo grande se exporta por pedazos. DemasiadoGrande([u32; 3]), } impl fmt::Display for VoxExportError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { VoxExportError::DemasiadoGrande(d) => { write!(f, "el modelo {d:?} pasa los 256 voxels por eje del formato .vox") } } } } impl std::error::Error for VoxExportError {} /// Cuántos colores distintos admite la paleta del formato (el índice `0` es vacío). const PALETA: usize = 255; /// **Exporta un grid del motor a un modelo `.vox`** — el camino de vuelta de /// [`model_to_grid`]: lo que se compuso acá (un ser, un mueble, una casa, un /// interior) sale a que lo edite quien quiera en el editor ajeno. /// /// Los ejes se remapean al revés (`grid (x,y,z) → vox (x,z,y)`) y los colores se /// **cuantizan** a la paleta de 255: si el grid trae 255 colores o menos, el viaje es /// exacto; si trae más, los 255 **más usados** forman la paleta y el resto va al más /// parecido (distancia RGB al cuadrado). Cuantizar por frecuencia y no por orden de /// aparición importa: un mundo con un cielo de mil azules y una casa roja conserva la /// casa. pub fn grid_to_model(grid: &VoxelGrid) -> Result { let dim = grid.dim(); if dim.iter().any(|&d| d > 256) { return Err(VoxExportError::DemasiadoGrande(dim)); } // Censo de colores (cuántas celdas tiene cada uno). let mut censo: std::collections::HashMap<[u8; 3], u32> = std::collections::HashMap::new(); for x in 0..dim[0] { for y in 0..dim[1] { for z in 0..dim[2] { if let Some(c) = grid.get(x, y, z) { if c[3] != 0 { *censo.entry([c[0], c[1], c[2]]).or_insert(0) += 1; } } } } } // Los más usados primero; con empate, orden de color (determinista). let mut orden: Vec<([u8; 3], u32)> = censo.into_iter().collect(); orden.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0))); let paleta: Vec<[u8; 3]> = orden.iter().take(PALETA).map(|(c, _)| *c).collect(); let mut m = VoxModel::new([dim[0], dim[2], dim[1]]); // vox = [x, y(prof), z(arriba)] for (k, c) in paleta.iter().enumerate() { m.palette[k + 1] = [c[0], c[1], c[2], 255]; } let indice_de = |c: [u8; 3]| -> u8 { // Exacto si está en la paleta; si no, el más parecido. let mut mejor = 0usize; let mut mejor_d = u32::MAX; for (k, p) in paleta.iter().enumerate() { let d = (0..3) .map(|a| { let e = p[a] as i32 - c[a] as i32; (e * e) as u32 }) .sum::(); if d == 0 { return k as u8 + 1; } if d < mejor_d { mejor_d = d; mejor = k; } } mejor as u8 + 1 }; for x in 0..dim[0] { for y in 0..dim[1] { for z in 0..dim[2] { let Some(c) = grid.get(x, y, z) else { continue }; if c[3] == 0 { continue; } m.voxels.push(Voxel { x: x as u8, y: z as u8, // profundidad del motor → y del .vox z: y as u8, // altura del motor → z (arriba) del .vox i: indice_de([c[0], c[1], c[2]]), }); } } } Ok(m) } /// Escribe un grid del motor como archivo `.vox` (ver [`grid_to_model`]). pub fn save_grid(path: impl AsRef, grid: &VoxelGrid) -> Result<(), VoxLoadError> { let m = grid_to_model(grid).map_err(|e| VoxLoadError::Io(std::io::Error::other(e.to_string())))?; std::fs::write(path, foreign_vox::write(&m)).map_err(VoxLoadError::Io) } /// Caja envolvente de un modelo **ya rotado**, en ejes del motor y **relativa al /// centro del modelo** (que es el punto que la traslación del grafo ubica). /// Devuelve `(mínimo, máximo)` inclusive. Como la rotación de MagicaVoxel es una /// permutación con signo, alcanza con mirar las ocho esquinas. fn rotated_span(m: &VoxModel, rot: Rot) -> ([i32; 3], [i32; 3]) { let sz = [m.size[0] as i32, m.size[1] as i32, m.size[2] as i32]; let c = [sz[0] / 2, sz[1] / 2, sz[2] / 2]; let mut lo = [i32::MAX; 3]; let mut hi = [i32::MIN; 3]; for bx in 0..2 { for by in 0..2 { for bz in 0..2 { let v = [ if bx == 0 { 0 } else { (sz[0] - 1).max(0) }, if by == 0 { 0 } else { (sz[1] - 1).max(0) }, if bz == 0 { 0 } else { (sz[2] - 1).max(0) }, ]; let w = rot.apply([v[0] - c[0], v[1] - c[1], v[2] - c[2]]); let wg = [w[0], w[2], w[1]]; // vox (z-arriba) → grid (y-arriba) for a in 0..3 { lo[a] = lo[a].min(wg[a]); hi[a] = hi[a].max(wg[a]); } } } } (lo, hi) } /// Estampa un modelo **girado** por la rotación del grafo de escena, con la esquina /// mínima de su caja ya rotada en `origin`. Es el mismo trabajo que [`stamp`] más el /// giro alrededor del centro del modelo — que es donde MagicaVoxel lo ancla. pub fn stamp_rotated(grid: &mut VoxelGrid, m: &VoxModel, rot: Rot, origin: [u32; 3]) { if rot == Rot::IDENTITY { stamp(grid, m, origin); return; } let (lo, _) = rotated_span(m, rot); let c = [m.size[0] as i32 / 2, m.size[1] as i32 / 2, m.size[2] as i32 / 2]; for v in &m.voxels { let col = m.color(v); if col[3] == 0 { continue; } let w = rot.apply([v.x as i32 - c[0], v.y as i32 - c[1], v.z as i32 - c[2]]); let wg = [w[0], w[2], w[1]]; let cell = [ origin[0] as i32 + wg[0] - lo[0], origin[1] as i32 + wg[1] - lo[1], origin[2] as i32 + wg[2] - lo[2], ]; if cell.iter().any(|&k| k < 0) { continue; } grid.set(cell[0] as u32, cell[1] as u32, cell[2] as u32, [col[0], col[1], col[2]]); } } /// Carga el **primer** modelo de un archivo `.vox` como `VoxelGrid`. pub fn load_grid(path: impl AsRef) -> Result { let bytes = std::fs::read(path).map_err(VoxLoadError::Io)?; let models = foreign_vox::parse(&bytes).map_err(VoxLoadError::Parse)?; // `parse` ya garantiza ≥1 modelo (si no, devuelve NoModel). Ok(model_to_grid(&models[0])) } /// Compone una **escena multi-modelo** (`nTRN/nGRP/nSHP`) en un único `VoxelGrid`, /// colocando cada modelo en su traslación de mundo resuelta por el grafo. En /// MagicaVoxel la traslación `_t` ubica el **centro** del modelo, así que la esquina /// va en `t − size/2`. El grid se dimensiona a la caja envolvente de todas las piezas /// y se desplaza para que el mínimo caiga en el origen. Si la escena no trae grafo /// (`.vox` viejo de un solo modelo), cae a [`model_to_grid`] del primer modelo. pub fn scene_to_grid(scene: &foreign_vox::Scene) -> VoxelGrid { if scene.placements.is_empty() || scene.models.is_empty() { return model_to_grid(&scene.models[0]); } // Esquina (espacio grid: [x, z, y]) y tamaño grid de cada colocación, con la // pieza YA girada por el grafo (la rotación gira alrededor del centro, así que // cambia tanto el tamaño de la caja como dónde cae su esquina). let mut corners: Vec<([i32; 3], [i32; 3])> = Vec::with_capacity(scene.placements.len()); for p in &scene.placements { let m = &scene.models[p.model]; let (lo, hi) = rotated_span(m, p.rotation); let t = p.translation; // vox (z-arriba) let t_grid = [t[0], t[2], t[1]]; let corner_grid = [t_grid[0] + lo[0], t_grid[1] + lo[1], t_grid[2] + lo[2]]; let sz_grid = [hi[0] - lo[0] + 1, hi[1] - lo[1] + 1, hi[2] - lo[2] + 1]; corners.push((corner_grid, sz_grid)); } // Caja envolvente. let mut lo = [i32::MAX; 3]; let mut hi = [i32::MIN; 3]; for (c, s) in &corners { for a in 0..3 { lo[a] = lo[a].min(c[a]); hi[a] = hi[a].max(c[a] + s[a]); } } let dim = [ (hi[0] - lo[0]).max(1) as u32, (hi[1] - lo[1]).max(1) as u32, (hi[2] - lo[2]).max(1) as u32, ]; let mut g = VoxelGrid::new(dim); for (p, (c, _)) in scene.placements.iter().zip(&corners) { let origin = [(c[0] - lo[0]) as u32, (c[1] - lo[1]) as u32, (c[2] - lo[2]) as u32]; stamp_rotated(&mut g, &scene.models[p.model], p.rotation, origin); } g.reset_dirty(); g } /// Carga una **escena** `.vox` completa (multi-modelo con grafo) como un único /// `VoxelGrid` compuesto. Para un `.vox` de un solo modelo equivale a [`load_grid`]. pub fn load_scene_grid(path: impl AsRef) -> Result { let bytes = std::fs::read(path).map_err(VoxLoadError::Io)?; let scene = foreign_vox::parse_scene(&bytes).map_err(VoxLoadError::Parse)?; Ok(scene_to_grid(&scene)) } #[cfg(test)] mod tests { use super::*; use foreign_vox::Voxel; #[test] fn remapea_ejes_z_arriba_a_y_arriba() { let mut m = VoxModel::new([2, 3, 4]); // x=2, y=3, z=4 (z arriba) m.palette[1] = [10, 20, 30, 255]; // Voxel en vox (1, 2, 3) → grid (1, 3, 2). m.voxels = vec![Voxel { x: 1, y: 2, z: 3, i: 1 }]; let g = model_to_grid(&m); assert_eq!(g.dim(), [2, 4, 3], "dim = [x, z, y]"); assert!(g.is_solid(1, 3, 2), "vox(x,y,z) → grid(x,z,y)"); assert_eq!(g.get(1, 3, 2), Some([10, 20, 30, 255])); } #[test] fn omite_transparentes() { let mut m = VoxModel::new([2, 2, 2]); m.palette[1] = [0, 0, 0, 0]; // transparente m.voxels = vec![Voxel { x: 0, y: 0, z: 0, i: 1 }]; let g = model_to_grid(&m); assert!(!g.is_solid(0, 0, 0)); } #[test] fn el_grid_sale_al_formato_ajeno_y_vuelve_igual() { // Ida y vuelta completa: grid → modelo → grid. Con pocos colores tiene que ser // exacta, celda por celda y color por color. let mut g = VoxelGrid::new([5, 4, 3]); g.set(0, 0, 0, [200, 30, 40]); g.set(4, 3, 2, [10, 220, 60]); g.set(2, 1, 1, [200, 30, 40]); let m = grid_to_model(&g).expect("exporta"); assert_eq!(m.size, [5, 3, 4], "vox = [x, profundidad, altura]"); assert_eq!(m.voxels.len(), 3); let vuelta = model_to_grid(&m); assert_eq!(vuelta.dim(), g.dim()); for x in 0..5 { for y in 0..4 { for z in 0..3 { assert_eq!( vuelta.get(x, y, z).map(|c| [c[0], c[1], c[2], c[3]]), g.get(x, y, z).map(|c| [c[0], c[1], c[2], c[3]]), "celda ({x},{y},{z})" ); } } } } #[test] fn con_mas_colores_que_la_paleta_se_conserva_el_que_manda() { // 300 colores: 255 entran exactos y el resto va al más parecido. El color más // USADO (no el primero) tiene que sobrevivir intacto. let mut g = VoxelGrid::new([20, 20, 1]); let mut k = 0u32; for x in 0..20u32 { for y in 0..20u32 { let c = if x == 0 && y < 10 { [255, 0, 0] // el mandón: 10 celdas del mismo rojo } else { k += 1; [(k % 250) as u8, (k / 7 % 250) as u8, (k / 13 % 250) as u8] }; g.set(x, y, 0, c); } } let m = grid_to_model(&g).expect("exporta"); assert_eq!(m.voxels.len(), 400, "no se pierde ninguna celda"); let vuelta = model_to_grid(&m); for y in 0..10 { assert_eq!( vuelta.get(0, y, 0).map(|c| [c[0], c[1], c[2]]), Some([255, 0, 0]), "el color más usado se conserva exacto" ); } } #[test] fn un_mundo_grande_no_entra_en_el_formato() { let g = VoxelGrid::new([300, 8, 8]); assert_eq!( grid_to_model(&g).err(), Some(VoxExportError::DemasiadoGrande([300, 8, 8])), "el formato guarda la coordenada en un byte" ); } #[test] fn la_escena_gira_la_pieza_que_el_grafo_giro() { use foreign_vox::{Placement, Scene}; // Barra de 3 voxels a lo largo de la x del .vox. let barra = || { let mut m = VoxModel::new([3, 1, 1]); m.palette[1] = [9, 90, 9, 255]; m.voxels = (0..3).map(|x| Voxel { x, y: 0, z: 0, i: 1 }).collect(); m }; let cuenta = |g: &VoxelGrid| { let d = g.dim(); (0..d[0]) .flat_map(|x| (0..d[1]).flat_map(move |y| (0..d[2]).map(move |z| (x, y, z)))) .filter(|&(x, y, z)| g.is_solid(x as i32, y as i32, z as i32)) .count() }; let escena = |rot| Scene { models: vec![barra()], placements: vec![Placement { model: 0, translation: [0, 0, 0], rotation: rot }], }; let derecha = scene_to_grid(&escena(Rot::IDENTITY)); assert_eq!(derecha.dim(), [3, 1, 1], "sin girar, la barra corre en x"); assert_eq!(cuenta(&derecha), 3); // Cuarto de vuelta alrededor de la z del .vox: la barra pasa a correr en la // y del .vox, que es la profundidad (z) del motor. let girada = scene_to_grid(&escena(Rot::from_byte(17))); assert_eq!(girada.dim(), [1, 1, 3], "girada, la barra corre en profundidad"); assert_eq!(cuenta(&girada), 3, "no se pierde ni se duplica un voxel al girar"); assert_eq!(girada.get(0, 0, 0).map(|c| [c[0], c[1], c[2]]), Some([9, 90, 9])); } #[test] fn escena_compone_dos_modelos_con_offset() { use foreign_vox::{Placement, Scene}; // Dos cubitos 2³ con un voxel en su esquina; el 2º trasladado +10 en x (vox). let cubo = || { let mut m = VoxModel::new([2, 2, 2]); m.palette[1] = [50, 60, 70, 255]; m.voxels = vec![Voxel { x: 0, y: 0, z: 0, i: 1 }]; m }; let scene = Scene { models: vec![cubo(), cubo()], placements: vec![ Placement { model: 0, translation: [0, 0, 0], rotation: Rot::IDENTITY }, Placement { model: 1, translation: [10, 0, 0], rotation: Rot::IDENTITY }, ], }; let g = scene_to_grid(&scene); // Ambos modelos presentes y separados por ~10 en x (centro→esquina = −1 cada uno, // misma resta, así que la separación entre voxels se conserva = 10). let (dx, dy, dz) = (g.dim()[0] as i32, g.dim()[1] as i32, g.dim()[2] as i32); let solid: Vec = (0..dx).filter(|&x| (0..dy).any(|y| (0..dz).any(|z| g.is_solid(x, y, z)))).collect(); assert_eq!(solid.len(), 2, "dos columnas sólidas separadas"); assert_eq!(solid[1] - solid[0], 10, "separación de mundo preservada"); } }