gioser

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 660Markdown 302TOML 157RON 312382 Definiciones

⎇ main llimphi-voxel/src/vox.rs Rust · 445 líneas · 17.7 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos b3f12e136 · Sergio ·

1//! Importación de modelos **MagicaVoxel `.vox`** al motor: convierte un
2//! [`foreign_vox::VoxModel`] (neutral, leído por el puente) en un
3//! [`VoxelGrid`](llimphi_3d::VoxelGrid) del motor, para meter **sets y
4//! personajes diseñados afuera** a una escena/película voxel.
5//!
6//! Ejes: MagicaVoxel usa `z` como **arriba**; el motor usa `y` arriba. La
7//! conversión mapea `vox (x, y, z) → grid (x, z, y)` (la `z` del `.vox` sube a la
8//! `y` del grid, la `y` del `.vox` pasa a la profundidad `z`).
9//!
10//! La capa de juego es la dueña de esto (no el motor ni el puente): el puente
11//! sólo entiende bytes, el motor sólo voxels; aquí se casan (CLAUDE.md regla #4).
12
13use std::fmt;
14use std::path::Path;
15
16use foreign_vox::{Rot, VoxError, VoxModel, Voxel};
17use llimphi_3d::VoxelGrid;
18
19/// Error al cargar un `.vox` desde disco.
20#[derive(Debug)]
21pub enum VoxLoadError {
22    /// Falló la lectura del archivo.
23    Io(std::io::Error),
24    /// El contenido no es un `.vox` válido.
25    Parse(VoxError),
26}
27
28impl fmt::Display for VoxLoadError {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            VoxLoadError::Io(e) => write!(f, "leyendo .vox: {e}"),
32            VoxLoadError::Parse(e) => write!(f, "parseando .vox: {e}"),
33        }
34    }
35}
36
37impl std::error::Error for VoxLoadError {}
38
39/// Convierte un `VoxModel` en un `VoxelGrid` ajustado a su tamaño (con los ejes
40/// remapeados a la convención del motor). El estado dirty queda limpio (el grid
41/// recién hecho se sube entero).
42pub fn model_to_grid(m: &VoxModel) -> VoxelGrid {
43    // dim del grid = [x, z(vox→y), y(vox→z)], mínimo 1 por eje.
44    let dim = [m.size[0].max(1), m.size[2].max(1), m.size[1].max(1)];
45    let mut g = VoxelGrid::new(dim);
46    stamp(&mut g, m, [0, 0, 0]);
47    g.reset_dirty();
48    g
49}
50
51/// **Estampa** los voxels de un modelo dentro de un grid existente, con la
52/// esquina del modelo en `origin` (espacio de grilla del motor). Para componer
53/// *sets*: meter varias piezas `.vox` en un mismo mundo. Voxels transparentes
54/// (`alpha 0`) se omiten; los que caen fuera del grid, también (`set` los ignora).
55pub fn stamp(grid: &mut VoxelGrid, m: &VoxModel, origin: [u32; 3]) {
56    for v in &m.voxels {
57        let c = m.color(v);
58        if c[3] == 0 {
59            continue;
60        }
61        let gx = origin[0] + v.x as u32;
62        let gy = origin[1] + v.z as u32; // z-arriba (vox) → y-arriba (grid)
63        let gz = origin[2] + v.y as u32;
64        grid.set(gx, gy, gz, [c[0], c[1], c[2]]);
65    }
66}
67
68/// Por qué no se pudo exportar un grid al formato ajeno.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum VoxExportError {
71    /// El formato guarda las coordenadas en un byte: ningún eje puede pasar de 256
72    /// voxels. Un mundo grande se exporta por pedazos.
73    DemasiadoGrande([u32; 3]),
74}
75
76impl fmt::Display for VoxExportError {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            VoxExportError::DemasiadoGrande(d) => {
80                write!(f, "el modelo {d:?} pasa los 256 voxels por eje del formato .vox")
81            }
82        }
83    }
84}
85
86impl std::error::Error for VoxExportError {}
87
88/// Cuántos colores distintos admite la paleta del formato (el índice `0` es vacío).
89const PALETA: usize = 255;
90
91/// **Exporta un grid del motor a un modelo `.vox`** — el camino de vuelta de
92/// [`model_to_grid`]: lo que se compuso acá (un ser, un mueble, una casa, un
93/// interior) sale a que lo edite quien quiera en el editor ajeno.
94///
95/// Los ejes se remapean al revés (`grid (x,y,z) → vox (x,z,y)`) y los colores se
96/// **cuantizan** a la paleta de 255: si el grid trae 255 colores o menos, el viaje es
97/// exacto; si trae más, los 255 **más usados** forman la paleta y el resto va al más
98/// parecido (distancia RGB al cuadrado). Cuantizar por frecuencia y no por orden de
99/// aparición importa: un mundo con un cielo de mil azules y una casa roja conserva la
100/// casa.
101pub fn grid_to_model(grid: &VoxelGrid) -> Result<VoxModel, VoxExportError> {
102    let dim = grid.dim();
103    if dim.iter().any(|&d| d > 256) {
104        return Err(VoxExportError::DemasiadoGrande(dim));
105    }
106    // Censo de colores (cuántas celdas tiene cada uno).
107    let mut censo: std::collections::HashMap<[u8; 3], u32> = std::collections::HashMap::new();
108    for x in 0..dim[0] {
109        for y in 0..dim[1] {
110            for z in 0..dim[2] {
111                if let Some(c) = grid.get(x, y, z) {
112                    if c[3] != 0 {
113                        *censo.entry([c[0], c[1], c[2]]).or_insert(0) += 1;
114                    }
115                }
116            }
117        }
118    }
119    // Los más usados primero; con empate, orden de color (determinista).
120    let mut orden: Vec<([u8; 3], u32)> = censo.into_iter().collect();
121    orden.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
122    let paleta: Vec<[u8; 3]> = orden.iter().take(PALETA).map(|(c, _)| *c).collect();
123
124    let mut m = VoxModel::new([dim[0], dim[2], dim[1]]); // vox = [x, y(prof), z(arriba)]
125    for (k, c) in paleta.iter().enumerate() {
126        m.palette[k + 1] = [c[0], c[1], c[2], 255];
127    }
128    let indice_de = |c: [u8; 3]| -> u8 {
129        // Exacto si está en la paleta; si no, el más parecido.
130        let mut mejor = 0usize;
131        let mut mejor_d = u32::MAX;
132        for (k, p) in paleta.iter().enumerate() {
133            let d = (0..3)
134                .map(|a| {
135                    let e = p[a] as i32 - c[a] as i32;
136                    (e * e) as u32
137                })
138                .sum::<u32>();
139            if d == 0 {
140                return k as u8 + 1;
141            }
142            if d < mejor_d {
143                mejor_d = d;
144                mejor = k;
145            }
146        }
147        mejor as u8 + 1
148    };
149    for x in 0..dim[0] {
150        for y in 0..dim[1] {
151            for z in 0..dim[2] {
152                let Some(c) = grid.get(x, y, z) else { continue };
153                if c[3] == 0 {
154                    continue;
155                }
156                m.voxels.push(Voxel {
157                    x: x as u8,
158                    y: z as u8, // profundidad del motor → y del .vox
159                    z: y as u8, // altura del motor → z (arriba) del .vox
160                    i: indice_de([c[0], c[1], c[2]]),
161                });
162            }
163        }
164    }
165    Ok(m)
166}
167
168/// Escribe un grid del motor como archivo `.vox` (ver [`grid_to_model`]).
169pub fn save_grid(path: impl AsRef<Path>, grid: &VoxelGrid) -> Result<(), VoxLoadError> {
170    let m = grid_to_model(grid).map_err(|e| VoxLoadError::Io(std::io::Error::other(e.to_string())))?;
171    std::fs::write(path, foreign_vox::write(&m)).map_err(VoxLoadError::Io)
172}
173
174/// Caja envolvente de un modelo **ya rotado**, en ejes del motor y **relativa al
175/// centro del modelo** (que es el punto que la traslación del grafo ubica).
176/// Devuelve `(mínimo, máximo)` inclusive. Como la rotación de MagicaVoxel es una
177/// permutación con signo, alcanza con mirar las ocho esquinas.
178fn rotated_span(m: &VoxModel, rot: Rot) -> ([i32; 3], [i32; 3]) {
179    let sz = [m.size[0] as i32, m.size[1] as i32, m.size[2] as i32];
180    let c = [sz[0] / 2, sz[1] / 2, sz[2] / 2];
181    let mut lo = [i32::MAX; 3];
182    let mut hi = [i32::MIN; 3];
183    for bx in 0..2 {
184        for by in 0..2 {
185            for bz in 0..2 {
186                let v = [
187                    if bx == 0 { 0 } else { (sz[0] - 1).max(0) },
188                    if by == 0 { 0 } else { (sz[1] - 1).max(0) },
189                    if bz == 0 { 0 } else { (sz[2] - 1).max(0) },
190                ];
191                let w = rot.apply([v[0] - c[0], v[1] - c[1], v[2] - c[2]]);
192                let wg = [w[0], w[2], w[1]]; // vox (z-arriba) → grid (y-arriba)
193                for a in 0..3 {
194                    lo[a] = lo[a].min(wg[a]);
195                    hi[a] = hi[a].max(wg[a]);
196                }
197            }
198        }
199    }
200    (lo, hi)
201}
202
203/// Estampa un modelo **girado** por la rotación del grafo de escena, con la esquina
204/// mínima de su caja ya rotada en `origin`. Es el mismo trabajo que [`stamp`] más el
205/// giro alrededor del centro del modelo — que es donde MagicaVoxel lo ancla.
206pub fn stamp_rotated(grid: &mut VoxelGrid, m: &VoxModel, rot: Rot, origin: [u32; 3]) {
207    if rot == Rot::IDENTITY {
208        stamp(grid, m, origin);
209        return;
210    }
211    let (lo, _) = rotated_span(m, rot);
212    let c = [m.size[0] as i32 / 2, m.size[1] as i32 / 2, m.size[2] as i32 / 2];
213    for v in &m.voxels {
214        let col = m.color(v);
215        if col[3] == 0 {
216            continue;
217        }
218        let w = rot.apply([v.x as i32 - c[0], v.y as i32 - c[1], v.z as i32 - c[2]]);
219        let wg = [w[0], w[2], w[1]];
220        let cell = [
221            origin[0] as i32 + wg[0] - lo[0],
222            origin[1] as i32 + wg[1] - lo[1],
223            origin[2] as i32 + wg[2] - lo[2],
224        ];
225        if cell.iter().any(|&k| k < 0) {
226            continue;
227        }
228        grid.set(cell[0] as u32, cell[1] as u32, cell[2] as u32, [col[0], col[1], col[2]]);
229    }
230}
231
232/// Carga el **primer** modelo de un archivo `.vox` como `VoxelGrid`.
233pub fn load_grid(path: impl AsRef<Path>) -> Result<VoxelGrid, VoxLoadError> {
234    let bytes = std::fs::read(path).map_err(VoxLoadError::Io)?;
235    let models = foreign_vox::parse(&bytes).map_err(VoxLoadError::Parse)?;
236    // `parse` ya garantiza ≥1 modelo (si no, devuelve NoModel).
237    Ok(model_to_grid(&models[0]))
238}
239
240/// Compone una **escena multi-modelo** (`nTRN/nGRP/nSHP`) en un único `VoxelGrid`,
241/// colocando cada modelo en su traslación de mundo resuelta por el grafo. En
242/// MagicaVoxel la traslación `_t` ubica el **centro** del modelo, así que la esquina
243/// va en `t − size/2`. El grid se dimensiona a la caja envolvente de todas las piezas
244/// y se desplaza para que el mínimo caiga en el origen. Si la escena no trae grafo
245/// (`.vox` viejo de un solo modelo), cae a [`model_to_grid`] del primer modelo.
246pub fn scene_to_grid(scene: &foreign_vox::Scene) -> VoxelGrid {
247    if scene.placements.is_empty() || scene.models.is_empty() {
248        return model_to_grid(&scene.models[0]);
249    }
250    // Esquina (espacio grid: [x, z, y]) y tamaño grid de cada colocación, con la
251    // pieza YA girada por el grafo (la rotación gira alrededor del centro, así que
252    // cambia tanto el tamaño de la caja como dónde cae su esquina).
253    let mut corners: Vec<([i32; 3], [i32; 3])> = Vec::with_capacity(scene.placements.len());
254    for p in &scene.placements {
255        let m = &scene.models[p.model];
256        let (lo, hi) = rotated_span(m, p.rotation);
257        let t = p.translation; // vox (z-arriba)
258        let t_grid = [t[0], t[2], t[1]];
259        let corner_grid = [t_grid[0] + lo[0], t_grid[1] + lo[1], t_grid[2] + lo[2]];
260        let sz_grid = [hi[0] - lo[0] + 1, hi[1] - lo[1] + 1, hi[2] - lo[2] + 1];
261        corners.push((corner_grid, sz_grid));
262    }
263    // Caja envolvente.
264    let mut lo = [i32::MAX; 3];
265    let mut hi = [i32::MIN; 3];
266    for (c, s) in &corners {
267        for a in 0..3 {
268            lo[a] = lo[a].min(c[a]);
269            hi[a] = hi[a].max(c[a] + s[a]);
270        }
271    }
272    let dim = [
273        (hi[0] - lo[0]).max(1) as u32,
274        (hi[1] - lo[1]).max(1) as u32,
275        (hi[2] - lo[2]).max(1) as u32,
276    ];
277    let mut g = VoxelGrid::new(dim);
278    for (p, (c, _)) in scene.placements.iter().zip(&corners) {
279        let origin = [(c[0] - lo[0]) as u32, (c[1] - lo[1]) as u32, (c[2] - lo[2]) as u32];
280        stamp_rotated(&mut g, &scene.models[p.model], p.rotation, origin);
281    }
282    g.reset_dirty();
283    g
284}
285
286/// Carga una **escena** `.vox` completa (multi-modelo con grafo) como un único
287/// `VoxelGrid` compuesto. Para un `.vox` de un solo modelo equivale a [`load_grid`].
288pub fn load_scene_grid(path: impl AsRef<Path>) -> Result<VoxelGrid, VoxLoadError> {
289    let bytes = std::fs::read(path).map_err(VoxLoadError::Io)?;
290    let scene = foreign_vox::parse_scene(&bytes).map_err(VoxLoadError::Parse)?;
291    Ok(scene_to_grid(&scene))
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use foreign_vox::Voxel;
298
299    #[test]
300    fn remapea_ejes_z_arriba_a_y_arriba() {
301        let mut m = VoxModel::new([2, 3, 4]); // x=2, y=3, z=4 (z arriba)
302        m.palette[1] = [10, 20, 30, 255];
303        // Voxel en vox (1, 2, 3) → grid (1, 3, 2).
304        m.voxels = vec![Voxel { x: 1, y: 2, z: 3, i: 1 }];
305        let g = model_to_grid(&m);
306        assert_eq!(g.dim(), [2, 4, 3], "dim = [x, z, y]");
307        assert!(g.is_solid(1, 3, 2), "vox(x,y,z) → grid(x,z,y)");
308        assert_eq!(g.get(1, 3, 2), Some([10, 20, 30, 255]));
309    }
310
311    #[test]
312    fn omite_transparentes() {
313        let mut m = VoxModel::new([2, 2, 2]);
314        m.palette[1] = [0, 0, 0, 0]; // transparente
315        m.voxels = vec![Voxel { x: 0, y: 0, z: 0, i: 1 }];
316        let g = model_to_grid(&m);
317        assert!(!g.is_solid(0, 0, 0));
318    }
319
320    #[test]
321    fn el_grid_sale_al_formato_ajeno_y_vuelve_igual() {
322        // Ida y vuelta completa: grid → modelo → grid. Con pocos colores tiene que ser
323        // exacta, celda por celda y color por color.
324        let mut g = VoxelGrid::new([5, 4, 3]);
325        g.set(0, 0, 0, [200, 30, 40]);
326        g.set(4, 3, 2, [10, 220, 60]);
327        g.set(2, 1, 1, [200, 30, 40]);
328        let m = grid_to_model(&g).expect("exporta");
329        assert_eq!(m.size, [5, 3, 4], "vox = [x, profundidad, altura]");
330        assert_eq!(m.voxels.len(), 3);
331        let vuelta = model_to_grid(&m);
332        assert_eq!(vuelta.dim(), g.dim());
333        for x in 0..5 {
334            for y in 0..4 {
335                for z in 0..3 {
336                    assert_eq!(
337                        vuelta.get(x, y, z).map(|c| [c[0], c[1], c[2], c[3]]),
338                        g.get(x, y, z).map(|c| [c[0], c[1], c[2], c[3]]),
339                        "celda ({x},{y},{z})"
340                    );
341                }
342            }
343        }
344    }
345
346    #[test]
347    fn con_mas_colores_que_la_paleta_se_conserva_el_que_manda() {
348        // 300 colores: 255 entran exactos y el resto va al más parecido. El color más
349        // USADO (no el primero) tiene que sobrevivir intacto.
350        let mut g = VoxelGrid::new([20, 20, 1]);
351        let mut k = 0u32;
352        for x in 0..20u32 {
353            for y in 0..20u32 {
354                let c = if x == 0 && y < 10 {
355                    [255, 0, 0] // el mandón: 10 celdas del mismo rojo
356                } else {
357                    k += 1;
358                    [(k % 250) as u8, (k / 7 % 250) as u8, (k / 13 % 250) as u8]
359                };
360                g.set(x, y, 0, c);
361            }
362        }
363        let m = grid_to_model(&g).expect("exporta");
364        assert_eq!(m.voxels.len(), 400, "no se pierde ninguna celda");
365        let vuelta = model_to_grid(&m);
366        for y in 0..10 {
367            assert_eq!(
368                vuelta.get(0, y, 0).map(|c| [c[0], c[1], c[2]]),
369                Some([255, 0, 0]),
370                "el color más usado se conserva exacto"
371            );
372        }
373    }
374
375    #[test]
376    fn un_mundo_grande_no_entra_en_el_formato() {
377        let g = VoxelGrid::new([300, 8, 8]);
378        assert_eq!(
379            grid_to_model(&g).err(),
380            Some(VoxExportError::DemasiadoGrande([300, 8, 8])),
381            "el formato guarda la coordenada en un byte"
382        );
383    }
384
385    #[test]
386    fn la_escena_gira_la_pieza_que_el_grafo_giro() {
387        use foreign_vox::{Placement, Scene};
388        // Barra de 3 voxels a lo largo de la x del .vox.
389        let barra = || {
390            let mut m = VoxModel::new([3, 1, 1]);
391            m.palette[1] = [9, 90, 9, 255];
392            m.voxels = (0..3).map(|x| Voxel { x, y: 0, z: 0, i: 1 }).collect();
393            m
394        };
395        let cuenta = |g: &VoxelGrid| {
396            let d = g.dim();
397            (0..d[0])
398                .flat_map(|x| (0..d[1]).flat_map(move |y| (0..d[2]).map(move |z| (x, y, z))))
399                .filter(|&(x, y, z)| g.is_solid(x as i32, y as i32, z as i32))
400                .count()
401        };
402        let escena = |rot| Scene {
403            models: vec![barra()],
404            placements: vec![Placement { model: 0, translation: [0, 0, 0], rotation: rot }],
405        };
406
407        let derecha = scene_to_grid(&escena(Rot::IDENTITY));
408        assert_eq!(derecha.dim(), [3, 1, 1], "sin girar, la barra corre en x");
409        assert_eq!(cuenta(&derecha), 3);
410
411        // Cuarto de vuelta alrededor de la z del .vox: la barra pasa a correr en la
412        // y del .vox, que es la profundidad (z) del motor.
413        let girada = scene_to_grid(&escena(Rot::from_byte(17)));
414        assert_eq!(girada.dim(), [1, 1, 3], "girada, la barra corre en profundidad");
415        assert_eq!(cuenta(&girada), 3, "no se pierde ni se duplica un voxel al girar");
416        assert_eq!(girada.get(0, 0, 0).map(|c| [c[0], c[1], c[2]]), Some([9, 90, 9]));
417    }
418
419    #[test]
420    fn escena_compone_dos_modelos_con_offset() {
421        use foreign_vox::{Placement, Scene};
422        // Dos cubitos 2³ con un voxel en su esquina; el 2º trasladado +10 en x (vox).
423        let cubo = || {
424            let mut m = VoxModel::new([2, 2, 2]);
425            m.palette[1] = [50, 60, 70, 255];
426            m.voxels = vec![Voxel { x: 0, y: 0, z: 0, i: 1 }];
427            m
428        };
429        let scene = Scene {
430            models: vec![cubo(), cubo()],
431            placements: vec![
432                Placement { model: 0, translation: [0, 0, 0], rotation: Rot::IDENTITY },
433                Placement { model: 1, translation: [10, 0, 0], rotation: Rot::IDENTITY },
434            ],
435        };
436        let g = scene_to_grid(&scene);
437        // Ambos modelos presentes y separados por ~10 en x (centro→esquina = −1 cada uno,
438        // misma resta, así que la separación entre voxels se conserva = 10).
439        let (dx, dy, dz) = (g.dim()[0] as i32, g.dim()[1] as i32, g.dim()[2] as i32);
440        let solid: Vec<i32> =
441            (0..dx).filter(|&x| (0..dy).any(|y| (0..dz).any(|z| g.is_solid(x, y, z)))).collect();
442        assert_eq!(solid.len(), 2, "dos columnas sólidas separadas");
443        assert_eq!(solid[1] - solid[0], 10, "separación de mundo preservada");
444    }
445}