tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ main llimphi-voxel/src/decorado.rs Rust · 821 siq'ikuna · 38.0 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

1//! **Decorado**: las construcciones que una escena pone a mano en su mundo — el
2//! palacio del rey, el kiosko del jardín — en un punto elegido, no sembradas por
3//! la semilla como las casas de un pueblo.
4//!
5//! Se llama decorado y no «lugar» porque en `ende` un lugar ya es otra cosa (el
6//! sitio 2.5D que filma la técnica `lugar`). Acá es lo de teatro: lo que se
7//! arma en el escenario para que la escena ocurra en algún sitio.
8//!
9//! El mundo de una escena es una ventana que sigue al reparto y se regenera al
10//! cruzar un paso; el decorado se estampa **cada vez** que la ventana se arma,
11//! en coords de mundo, así que queda en su sitio aunque la ventana se mueva.
12
13use serde::{Deserialize, Serialize};
14
15use crate::casa::{plantar_casa_girada, CasaTipo};
16use crate::worldgen::{Bioma, Forma};
17use llimphi_3d::{SdfSer, VoxelGrid};
18
19/// Una construcción puesta en una escena.
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct Decorado {
22    pub casa: CasaTipo,
23    /// Dónde va su centro, en columnas de **mundo** `[gx, gz]` (las de los
24    /// guiones de los actores).
25    pub pos: [f32; 2],
26    /// Variación dentro del estilo (la misma semilla da la misma casa).
27    #[serde(default)]
28    pub semilla: u32,
29    /// **Hacia dónde da el frente**, en cuartos de vuelta: `0` = a `+Z` (como
30    /// sale del generador), `2` = a `-Z`, que es de donde miran el establecedor,
31    /// el primer plano y el frontal. Sin esto la película mostraba la espalda
32    /// del palacio: un muro liso, sin portal. Va al final y se escribe siempre
33    /// (postcard).
34    #[serde(default)]
35    pub giro: u8,
36}
37
38impl Decorado {
39    pub fn en(casa: CasaTipo, gx: f32, gz: f32) -> Self {
40        Self { casa, pos: [gx, gz], semilla: 0, giro: 0 }
41    }
42
43    /// La misma construcción, con el frente girado `giro` cuartos de vuelta.
44    pub fn girado(mut self, giro: u8) -> Self {
45        self.giro = giro % 4;
46        self
47    }
48}
49
50/// Estampa el `decorado` sobre una ventana del mundo cuya esquina está en
51/// `origin` (columnas de mundo). Lo que cae fuera de la ventana se saltea; lo
52/// que cae en el borde sale cortado, igual que el relieve. Devuelve cuántas
53/// construcciones plantó.
54pub fn estampar_decorado(g: &mut VoxelGrid, origin: [i32; 2], decorado: &[Decorado]) -> usize {
55    let dim = g.dim();
56    let mut n = 0;
57    for d in decorado {
58        let lx = d.pos[0].round() as i32 - origin[0];
59        let lz = d.pos[1].round() as i32 - origin[1];
60        let r = d.casa.huella();
61        // Ni la huella toca la ventana: no hay nada que estampar.
62        if lx + r < 0 || lz + r < 0 || lx - r >= dim[0] as i32 || lz - r >= dim[2] as i32 {
63            continue;
64        }
65        // El centro fuera de la ventana: se planta en el borde más cercano para
66        // tener suelo, pero corrido no — mejor no plantarla que plantarla mal.
67        if lx < 0 || lz < 0 || lx >= dim[0] as i32 || lz >= dim[2] as i32 {
68            continue;
69        }
70        if plantar_casa_girada(g, d.casa, lx as u32, lz as u32, d.semilla, d.giro).is_some() {
71            n += 1;
72        }
73    }
74    n
75}
76
77/// **El jardín** de una escena: una explanada llana de pasto, caminos de tierra
78/// sobre ella y una orla de flores y plantas alrededor. Es el suelo que pide un
79/// palacio de cuento: el relieve natural con sus matas sueltas, filmado de
80/// cerca, se lee como escombros verdes y tapa a los actores.
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct Jardin {
83    /// La explanada: rectángulo `[x0, z0, x1, z1]` en columnas de mundo. Se
84    /// nivela a una sola cota y queda de pasto, sin una mata. `None` = el
85    /// relieve tal cual (una playa con matorrales detrás, por ejemplo).
86    pub explanada: Option<[f32; 4]>,
87    /// Caminos de tierra: polilíneas en columnas de mundo.
88    #[serde(default)]
89    pub caminos: Vec<Vec<[f32; 2]>>,
90    /// Ancho de los caminos, en voxels.
91    pub ancho_camino: f32,
92    /// Ancho de la orla de flores y plantas que rodea la explanada.
93    pub orla: f32,
94    /// Cuántas columnas de la orla llevan algo, `0..1`.
95    pub flores: f32,
96    /// **Canteros**: ancho de la franja de flores a cada lado de cada camino
97    /// (`0` = sin). Empiezan donde termina la tierra, nunca encima.
98    #[serde(default)]
99    pub cantero: f32,
100    /// **Matorrales**: rectángulos `[x0, z0, x1, z1]` de vegetación espesa
101    /// —palmeras, arbustos y flores— sobre el relieve seco. Es lo que hace
102    /// frondoso un fondo.
103    #[serde(default)]
104    pub matorrales: Vec<[f32; 4]>,
105}
106
107/// **Todo lo que una escena le pone al mundo** encima del relieve: el jardín,
108/// los corredores por donde caminan los actores y el decorado. Es la clave con
109/// la que la ventana sabe si tiene que rearmarse.
110#[derive(Debug, Clone, Default, PartialEq)]
111pub struct Puesta {
112    pub jardin: Option<Jardin>,
113    /// Las trayectorias del reparto (las `(gx, gz)` de sus keys, en orden).
114    pub andares: Vec<Vec<[f32; 2]>>,
115    pub decorado: Vec<Decorado>,
116}
117
118/// Medio ancho del corredor que se nivela debajo de quien camina. Tres voxels a
119/// cada lado alcanza para un elefante, que es lo más ancho del reparto.
120const CORREDOR: f32 = 3.0;
121const PASTO: [u8; 3] = [92, 146, 66];
122const TIERRA: [u8; 3] = [150, 112, 74];
123const ARENA_MOJADA: [u8; 3] = [190, 172, 126];
124/// Las corolas de la orla: rojo, amarillo, blanco, lila, rosa.
125const COROLAS: [[u8; 3]; 5] = [[214, 52, 58], [236, 196, 48], [240, 236, 226], [150, 96, 200], [232, 120, 168]];
126
127fn hash(x: i32, z: i32, sal: u32) -> f32 {
128    let mut h = (x as u32).wrapping_mul(0x9E37_79B9) ^ (z as u32).wrapping_mul(0x85EB_CA77) ^ sal.wrapping_mul(0xC2B2_AE3D);
129    h ^= h >> 15;
130    h = h.wrapping_mul(0x2C1B_3C6D);
131    h ^= h >> 12;
132    (h & 0xFFFF) as f32 / 65536.0
133}
134
135/// Un color con un poco de grano por columna, para que el pasto llano no sea
136/// una plancha de un solo verde.
137fn granulado(c: [u8; 3], x: i32, z: i32) -> [u8; 3] {
138    let k = 0.92 + hash(x, z, 0x6A) * 0.16;
139    c.map(|v| (v as f32 * k).clamp(0.0, 255.0) as u8)
140}
141
142fn dist_a_segmento(p: [f32; 2], a: [f32; 2], b: [f32; 2]) -> f32 {
143    let (vx, vz) = (b[0] - a[0], b[1] - a[1]);
144    let l2 = vx * vx + vz * vz;
145    let t = if l2 > 0.0 { (((p[0] - a[0]) * vx + (p[1] - a[1]) * vz) / l2).clamp(0.0, 1.0) } else { 0.0 };
146    let (qx, qz) = (a[0] + vx * t - p[0], a[1] + vz * t - p[1]);
147    (qx * qx + qz * qz).sqrt()
148}
149
150fn dist_a_polilinea(p: [f32; 2], l: &[[f32; 2]]) -> f32 {
151    match l {
152        [] => f32::INFINITY,
153        [a] => dist_a_segmento(p, *a, *a),
154        _ => l.windows(2).map(|w| dist_a_segmento(p, w[0], w[1])).fold(f32::INFINITY, f32::min),
155    }
156}
157
158/// Deja la columna `(lx, lz)` con el tope sólido exactamente en `nivel`:
159/// despeja lo de arriba (matas, agua) y rellena lo que falte con `relleno`; el
160/// voxel del tope queda de `tope`.
161fn nivelar_columna(g: &mut VoxelGrid, lx: u32, lz: u32, nivel: u32, relleno: [u8; 3], tope: [u8; 3]) {
162    let dim = g.dim();
163    let nivel = nivel.min(dim[1] - 1);
164    for y in (nivel + 1)..dim[1] {
165        g.clear(lx, y, lz);
166    }
167    let desde = g.height_at(lx, lz).map(|y| y + 1).unwrap_or(0).min(nivel);
168    for y in desde..nivel {
169        g.set(lx, y, lz, relleno);
170    }
171    g.set(lx, nivel, lz, tope);
172}
173
174/// **Estampa la puesta** sobre una ventana recién generada: la explanada, los
175/// corredores del reparto, los caminos, la orla y el decorado, en ese orden.
176///
177/// **Por donde alguien camina el suelo es llano** — regla del operador, para
178/// toda escena y no sólo la del jardín. El cuerpo se apoya en la altura de la
179/// columna que pisa; sobre el relieve natural eso es un escalón de un voxel
180/// cada dos o tres pasos, y con las matas de pasto uno de dos. Visto en la
181/// película: el reparto sube y baja a tirones, y un elefante metido en una mata
182/// parece atascado. Así que debajo de cada trayectoria se nivela un corredor
183/// a la altura del terreno **suavizada a lo largo del camino** (las matas no
184/// cuentan: la altura sale de [`Bioma::column_height`], que es el relieve
185/// sin vegetación), y bajo el agua se rellena hasta la superficie — el desfile
186/// pasa por la orilla, no se hunde ni flota.
187///
188/// Devuelve las **florcitas** de los canteros, que NO se estampan: van como
189/// seres ([`macizos`]). Un cubo de corola a dos pasos de la cámara se lee como
190/// un juguete de bloques, no como una flor.
191pub fn estampar_puesta(g: &mut VoxelGrid, origin: [i32; 2], bioma: &Bioma, seed: u32, puesta: &Puesta) -> Vec<Florcita> {
192    let mut florcitas = Vec::new();
193    let dim = g.dim();
194    let agua = bioma.water_y(dim);
195    let hay_agua = bioma.water_level > 0.0;
196    let natural = |wx: i32, wz: i32| bioma.column_height(seed, wx, wz, dim).min(dim[1] - 1);
197    let local = |wx: i32, wz: i32| -> Option<(u32, u32)> {
198        let (lx, lz) = (wx - origin[0], wz - origin[1]);
199        (lx >= 0 && lz >= 0 && (lx as u32) < dim[0] && (lz as u32) < dim[2]).then_some((lx as u32, lz as u32))
200    };
201
202    // 1. La explanada, a la cota de su centro.
203    let explanada = puesta.jardin.as_ref().and_then(|j| j.explanada).map(|[x0, z0, x1, z1]| {
204        let (x0, x1) = (x0.min(x1).floor() as i32, x0.max(x1).ceil() as i32);
205        let (z0, z1) = (z0.min(z1).floor() as i32, z0.max(z1).ceil() as i32);
206        let cota = natural((x0 + x1) / 2, (z0 + z1) / 2).max(if hay_agua { agua } else { 0 });
207        (x0, z0, x1, z1, cota)
208    });
209    let en_explanada = |wx: i32, wz: i32| {
210        explanada.and_then(|(x0, z0, x1, z1, cota)| (wx >= x0 && wx <= x1 && wz >= z0 && wz <= z1).then_some(cota))
211    };
212    if let Some((x0, z0, x1, z1, cota)) = explanada {
213        for wz in z0..=z1 {
214            for wx in x0..=x1 {
215                if let Some((lx, lz)) = local(wx, wz) {
216                    nivelar_columna(g, lx, lz, cota, TIERRA, granulado(PASTO, wx, wz));
217                }
218            }
219        }
220    }
221
222    // 2. Los corredores: del reparto y de los caminos. La cota de cada punto es
223    // la de la explanada si cae adentro, si no la del relieve (o la del agua).
224    let suelo = |wx: i32, wz: i32| -> f32 {
225        en_explanada(wx, wz).unwrap_or_else(|| {
226            let n = natural(wx, wz);
227            if hay_agua { n.max(agua) } else { n }
228        }) as f32
229    };
230    let caminos: &[Vec<[f32; 2]>] = puesta.jardin.as_ref().map(|j| j.caminos.as_slice()).unwrap_or(&[]);
231    for linea in puesta.andares.iter().chain(caminos) {
232        // Muestras cada medio voxel a lo largo de la línea.
233        let mut pts: Vec<[f32; 2]> = Vec::new();
234        for w in linea.windows(2) {
235            let (a, b) = (w[0], w[1]);
236            let largo = ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
237            let n = (largo * 2.0).ceil().max(1.0) as usize;
238            pts.extend((0..n).map(|i| {
239                let t = i as f32 / n as f32;
240                [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]
241            }));
242        }
243        if let Some(&ultimo) = linea.last() {
244            pts.push(ultimo);
245        }
246        let crudo: Vec<f32> = pts.iter().map(|p| suelo(p[0].round() as i32, p[1].round() as i32)).collect();
247        // Suavizado a lo largo del camino: ventana de ±4 voxels (8 muestras).
248        let cotas: Vec<u32> = (0..crudo.len())
249            .map(|i| {
250                let (a, b) = (i.saturating_sub(8), (i + 8).min(crudo.len() - 1));
251                (crudo[a..=b].iter().sum::<f32>() / (b - a + 1) as f32).round() as u32
252            })
253            .collect();
254        for (p, &cota) in pts.iter().zip(&cotas) {
255            let r = CORREDOR.ceil() as i32;
256            for dz in -r..=r {
257                for dx in -r..=r {
258                    if (dx * dx + dz * dz) as f32 > CORREDOR * CORREDOR {
259                        continue;
260                    }
261                    let (wx, wz) = (p[0].round() as i32 + dx, p[1].round() as i32 + dz);
262                    let Some((lx, lz)) = local(wx, wz) else { continue };
263                    // Dentro de la explanada ya está llano; afuera, la columna
264                    // conserva su color de superficie (o arena, si era agua).
265                    if en_explanada(wx, wz).is_some() {
266                        continue;
267                    }
268                    let n = natural(wx, wz);
269                    // El color del tope ACTUAL, no el de la altura natural: si
270                    // otro tramo del corredor ya pasó por esta columna, la altura
271                    // natural puede haber quedado despejada, y leerla daba aire
272                    // —negro— (medido: manchas negras bajo la fila del desfile).
273                    let piel = if hay_agua && n < agua {
274                        ARENA_MOJADA
275                    } else {
276                        g.height_at(lx, lz)
277                            .and_then(|y| g.get(lx, y.min(n), lz))
278                            .filter(|c| c[3] > 0)
279                            .map(|c| [c[0], c[1], c[2]])
280                            .unwrap_or(PASTO)
281                    };
282                    nivelar_columna(g, lx, lz, cota, piel, piel);
283                }
284            }
285        }
286    }
287
288    // 3. Los caminos se pintan de tierra (ya están nivelados).
289    if let Some(j) = &puesta.jardin {
290        let medio = j.ancho_camino.max(1.0) * 0.5;
291        for linea in &j.caminos {
292            let (mut x0, mut z0, mut x1, mut z1) = (i32::MAX, i32::MAX, i32::MIN, i32::MIN);
293            for p in linea {
294                x0 = x0.min((p[0] - medio).floor() as i32);
295                z0 = z0.min((p[1] - medio).floor() as i32);
296                x1 = x1.max((p[0] + medio).ceil() as i32);
297                z1 = z1.max((p[1] + medio).ceil() as i32);
298            }
299            for wz in z0..=z1 {
300                for wx in x0..=x1 {
301                    if dist_a_polilinea([wx as f32, wz as f32], linea) > medio {
302                        continue;
303                    }
304                    if let Some((lx, lz)) = local(wx, wz) {
305                        if let Some(y) = g.height_at(lx, lz) {
306                            g.set(lx, y, lz, granulado(TIERRA, wx, wz));
307                        }
308                    }
309                }
310            }
311        }
312
313        // 4. Lo que se siembra: la orla alrededor de la explanada, los canteros
314        // a los lados de los caminos y los matorrales. Primero se junta todo,
315        // después se despeja y recién ahí se planta: si se despejara planta por
316        // planta, cada una le cortaría los pétalos a la vecina ya plantada.
317        //
318        // Nunca sobre un corredor ni sobre un camino: por donde alguien
319        // camina no crece nada.
320        let medio = j.ancho_camino.max(1.0) * 0.5;
321        // `margen`: cuánto más allá del corredor de quien camina hay que
322        // quedarse. `en_la_explanada`: si se puede plantar sobre el pasto
323        // llano (los canteros sí; la orla y los matorrales no).
324        let libre = |wx: i32, wz: i32, margen: f32, en_la_explanada: bool| {
325            let p = [wx as f32, wz as f32];
326            (en_la_explanada || en_explanada(wx, wz).is_none())
327                && !puesta.andares.iter().any(|l| dist_a_polilinea(p, l) <= CORREDOR + margen)
328                && !j.caminos.iter().any(|l| dist_a_polilinea(p, l) <= medio + 1.5)
329        };
330        // `None` = una **florcita** de cantero: tallo y corola, dos voxels. Una
331        // flor del bioma mide tres o cuatro con la corola en cruz —tanto como
332        // el rey— y al lado del camino, que es por donde entra la cámara,
333        // tapaba el cuadro.
334        let mut siembra: Vec<(i32, i32, Option<Forma>)> = Vec::new();
335        let flor_o_lavanda = |wx: i32, wz: i32| if hash(wx, wz, 0x1A) < 0.3 { Forma::Lavanda } else { Forma::Flor };
336
337        // 4a. La orla: flores cerca de la explanada, plantas más afuera.
338        if let Some((x0, z0, x1, z1, _)) = explanada {
339            let o = j.orla.max(0.0).ceil() as i32;
340            for wz in (z0 - o)..=(z1 + o) {
341                for wx in (x0 - o)..=(x1 + o) {
342                    let fuera = (x0 - wx).max(wx - x1).max(z0 - wz).max(wz - z1) as f32;
343                    // Desde el tercer voxel: un arbusto tiene radio 2, y plantado
344                    // al borde metía el follaje en la explanada.
345                    if fuera < 3.0 || fuera > j.orla || hash(wx, wz, 0xF10) > j.flores || !libre(wx, wz, 2.0, false) {
346                        continue;
347                    }
348                    let forma = if fuera < 3.0 + (j.orla - 3.0) * 0.5 {
349                        flor_o_lavanda(wx, wz)
350                    } else if hash(wx, wz, 0xA8) < 0.5 {
351                        Forma::Arbusto
352                    } else {
353                        Forma::Flor
354                    };
355                    siembra.push((wx, wz, Some(forma)));
356                }
357            }
358        }
359
360        // 4b. Los canteros: una flor cada dos columnas, en tresbolillo, a los
361        // dos lados de cada camino. Adentro de la explanada también — es
362        // justamente donde se ven.
363        if j.cantero > 0.0 {
364            for linea in &j.caminos {
365                let r = medio + 1.5 + j.cantero;
366                let (mut x0, mut z0, mut x1, mut z1) = (i32::MAX, i32::MAX, i32::MIN, i32::MIN);
367                for p in linea {
368                    x0 = x0.min((p[0] - r).floor() as i32);
369                    z0 = z0.min((p[1] - r).floor() as i32);
370                    x1 = x1.max((p[0] + r).ceil() as i32);
371                    z1 = z1.max((p[1] + r).ceil() as i32);
372                }
373                for wz in z0..=z1 {
374                    for wx in x0..=x1 {
375                        let d = dist_a_polilinea([wx as f32, wz as f32], linea);
376                        if d <= medio + 1.5 || d > r || (wx + wz).rem_euclid(2) != 0 || !libre(wx, wz, -1.5, true) {
377                            continue;
378                        }
379                        // Margen −1,5: una florcita mide un voxel, y con el
380                        // corredor entero de quien camina por el camino (radio
381                        // 3) el cantero de su lado quedaba pelado.
382                        siembra.push((wx, wz, None));
383                    }
384                }
385            }
386        }
387
388        // 4c. Los matorrales: espesos. Una palmera de vez en cuando, y entre
389        // ellas arbustos y flores.
390        for m in &j.matorrales {
391            let (x0, x1) = (m[0].min(m[2]).floor() as i32, m[0].max(m[2]).ceil() as i32);
392            let (z0, z1) = (m[1].min(m[3]).floor() as i32, m[1].max(m[3]).ceil() as i32);
393            for wz in z0..=z1 {
394                for wx in x0..=x1 {
395                    if !libre(wx, wz, 2.0, false) {
396                        continue;
397                    }
398                    let h = hash(wx, wz, 0x3A7);
399                    let forma = if h < 0.012 {
400                        Forma::Palmera
401                    } else if h < 0.09 {
402                        Forma::Arbusto
403                    } else if h < 0.30 {
404                        flor_o_lavanda(wx, wz)
405                    } else {
406                        continue;
407                    };
408                    siembra.push((wx, wz, Some(forma)));
409                }
410            }
411        }
412
413        // Cada planta, sobre su suelo: la cota de la explanada adentro, el
414        // relieve sin matas afuera. Nada en el agua.
415        let mut plantas: Vec<(u32, u32, u32, Option<Forma>, i32, i32)> = Vec::new();
416        for (wx, wz, forma) in siembra {
417            let Some((lx, lz)) = local(wx, wz) else { continue };
418            let base = match en_explanada(wx, wz) {
419                Some(cota) => cota,
420                None => {
421                    let n = natural(wx, wz);
422                    if hay_agua && n <= agua {
423                        continue;
424                    }
425                    n
426                }
427            };
428            plantas.push((lx, lz, base, forma, wx, wz));
429        }
430        for &(lx, lz, base, ..) in &plantas {
431            for y in (base + 1)..dim[1] {
432                g.clear(lx, y, lz);
433            }
434        }
435        for (lx, lz, base, forma, wx, wz) in plantas {
436            let corola = COROLAS[(hash(wx, wz, 0xC0) * COROLAS.len() as f32) as usize % COROLAS.len()];
437            let Some(forma) = forma else {
438                florcitas.push(Florcita {
439                    pos: [lx as f32 + 0.5, (base + 1) as f32, lz as f32 + 0.5],
440                    mundo: [wx, wz],
441                    color: corola,
442                });
443                continue;
444            };
445            let col = match forma {
446                Forma::Arbusto => [70, 126, 58],
447                Forma::Palmera => [64, 128, 60],
448                Forma::Lavanda => [150, 110, 210],
449                _ => corola,
450            };
451            bioma.stamp_flora(g, forma, col, lx, base + 1, lz, dim);
452        }
453    }
454
455    // 5. El decorado, al final: se planta sobre el suelo ya llano.
456    estampar_decorado(g, origin, &puesta.decorado);
457    florcitas
458}
459
460/// Una florcita de cantero, lista para dibujarse como ser.
461#[derive(Debug, Clone, Copy, PartialEq)]
462pub struct Florcita {
463    /// Dónde nace el tallo, en coords de la VENTANA (`[0, dim]`).
464    pub pos: [f32; 3],
465    /// La columna de mundo: de ahí sale su variación (la misma flor en la misma
466    /// columna, en cualquier ventana).
467    pub mundo: [i32; 2],
468    pub color: [u8; 3],
469}
470
471/// Lado de la celda con que se agrupan las florcitas en macizos (voxels). Cada
472/// macizo es UN ser: el renderer admite 40 por cuadro y un cantero tiene más de
473/// cien flores. Chico, porque la esfera envolvente del ser es la que decide
474/// cuántos rayos lo evalúan. Era 4 y el cantero del palacio daba 37 macizos:
475/// con los cuatro actores, 41 seres, y el más lejano quedaba afuera.
476pub const MACIZO: f32 = 6.0;
477
478/// Primitivas que cuesta una florcita: tallo, corola y centro.
479pub const PRIMS_POR_FLOR: usize = 3;
480
481/// **Las florcitas como seres**: agrupadas en macizos de [`MACIZO`]×[`MACIZO`]
482/// columnas, cada uno un [`SdfSer`] con tallo, corola y centro por flor, sin
483/// fundirse entre ellas (`blend` 0). Medio metro de alto y treinta centímetros
484/// de corola: en este motor el voxel es el metro y una persona mide 1,7.
485pub fn macizos(florcitas: &[Florcita]) -> Vec<SdfSer> {
486    use crate::sdf_kit::{capsula, elipsoide, esfera};
487    use llimphi_3d::glam::Vec3;
488    let mut celdas: std::collections::BTreeMap<(i32, i32), Vec<&Florcita>> = Default::default();
489    for f in florcitas {
490        let k = ((f.pos[0] / MACIZO).floor() as i32, (f.pos[2] / MACIZO).floor() as i32);
491        celdas.entry(k).or_default().push(f);
492    }
493    let tallo = [0.30, 0.52, 0.22];
494    celdas
495        .into_values()
496        .map(|fs| {
497            let o = Vec3::from(fs[0].pos);
498            let mut prims = Vec::with_capacity(fs.len() * PRIMS_POR_FLOR);
499            for f in fs {
500                let (hx, hz) = (f.mundo[0], f.mundo[1]);
501                let pie = Vec3::from(f.pos) - o;
502                let alto = 0.42 + hash(hx, hz, 0x7A) * 0.2;
503                // Un poco inclinada: un cantero de tallos a plomo es un peine.
504                let lean = Vec3::new(hash(hx, hz, 0x7B) - 0.5, 0.0, hash(hx, hz, 0x7C) - 0.5) * 0.16;
505                let cabeza = pie + Vec3::new(0.0, alto, 0.0) + lean;
506                let corola = f.color.map(|c| c as f32 / 255.0);
507                // El centro contrasta: amarillo en las flores, ocre en las amarillas.
508                let centro = if f.color[0] > 200 && f.color[1] > 170 && f.color[2] < 120 {
509                    [0.62, 0.38, 0.12]
510                } else {
511                    [0.95, 0.80, 0.25]
512                };
513                prims.push(capsula(pie, 0.035, cabeza, 0.025, tallo, 0.0));
514                prims.push(elipsoide(cabeza, Vec3::new(0.16, 0.05, 0.16), corola, 0.0));
515                prims.push(esfera(cabeza + Vec3::new(0.0, 0.045, 0.0), 0.055, centro, 0.0));
516            }
517            SdfSer { prims, pos: o.into(), yaw: 0.0 }
518        })
519        .collect()
520}
521
522/// Suma los `macizos` a los `seres` del cuadro **con lo que el reparto dejó**:
523/// se agregan después de los actores y la utilería, así nunca le sacan la cara
524/// a nadie. Van primero los más cercanos al `ojo` (coords de grilla) que estén
525/// adelante de la cámara (`mira`); los que no entran no se dibujan, y se
526/// devuelve cuántos quedaron afuera.
527pub fn sumar_macizos(seres: &mut Vec<SdfSer>, macizos: &[SdfSer], ojo: [f32; 3], mira: [f32; 3]) -> usize {
528    use llimphi_3d::{VOXEL_MAX_SERS, VOXEL_MAX_SER_PRIMS};
529    let mut gastado: usize = seres.iter().map(|s| s.prims.len()).sum();
530    let delante = |m: &SdfSer| {
531        let d = [m.pos[0] - ojo[0], m.pos[1] - ojo[1], m.pos[2] - ojo[2]];
532        (d[0] * mira[0] + d[1] * mira[1] + d[2] * mira[2] > -2.0, d[0] * d[0] + d[1] * d[1] + d[2] * d[2])
533    };
534    let mut orden: Vec<(f32, &SdfSer)> = macizos
535        .iter()
536        .filter_map(|m| {
537            let (ok, d2) = delante(m);
538            ok.then_some((d2, m))
539        })
540        .collect();
541    orden.sort_by(|a, b| a.0.total_cmp(&b.0));
542    let mut afuera = macizos.len() - orden.len();
543    for (_, m) in orden {
544        if seres.len() < VOXEL_MAX_SERS && gastado + m.prims.len() <= VOXEL_MAX_SER_PRIMS {
545            gastado += m.prims.len();
546            seres.push(m.clone());
547        } else {
548            afuera += 1;
549        }
550    }
551    afuera
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557
558    fn llano(dim: [u32; 3], alto: u32) -> VoxelGrid {
559        let mut g = VoxelGrid::new(dim);
560        for x in 0..dim[0] {
561            for z in 0..dim[2] {
562                for y in 0..alto {
563                    g.set(x, y, z, [90, 140, 70]);
564                }
565            }
566        }
567        g
568    }
569
570    fn solidos(g: &VoxelGrid) -> usize {
571        let d = g.dim();
572        (0..d[0]).flat_map(|x| (0..d[1]).flat_map(move |y| (0..d[2]).map(move |z| (x, y, z))))
573            .filter(|&(x, y, z)| g.get(x, y, z).is_some_and(|c| c[3] > 0))
574            .count()
575    }
576
577    /// El palacio y el kiosko se levantan sobre el suelo, en coords de MUNDO: con
578    /// la ventana corrida, el mismo decorado cae corrido en la grilla.
579    #[test]
580    fn el_decorado_se_planta_en_su_lugar_del_mundo() {
581        let dim = [64, 48, 64];
582        for casa in [CasaTipo::Palacio, CasaTipo::Kiosko] {
583            let mut g = llano(dim, 10);
584            let antes = solidos(&g);
585            let dec = [Decorado::en(casa, 132.0, 140.0)];
586            assert_eq!(estampar_decorado(&mut g, [100, 100], &dec), 1, "{casa:?}");
587            assert!(solidos(&g) > antes + 50, "{casa:?}: casi nada estampado");
588            // Algo se levanta arriba del suelo en su centro (columna 32, 40).
589            assert!(g.height_at(32, 40).unwrap() > 12, "{casa:?}: no se levanta en su lugar");
590            // Y nada lejos de su huella.
591            assert_eq!(g.height_at(32 + casa.huella() as u32 + 4, 40), Some(9), "{casa:?}: se derramó");
592        }
593        // Fuera de la ventana no se planta nada.
594        let mut g = llano(dim, 10);
595        assert_eq!(estampar_decorado(&mut g, [100, 100], &[Decorado::en(CasaTipo::Palacio, 20.0, 20.0)]), 0);
596    }
597
598    /// El palacio es de diamante y oro, no sólo de mármol: el cuento lo nombra así.
599    #[test]
600    fn el_palacio_es_de_diamantes_y_el_kiosko_de_malaquita() {
601        use crate::mineral::Mineral;
602        let dim = [64, 48, 64];
603        let cuenta = |g: &VoxelGrid, c: [u8; 3]| {
604            let d = g.dim();
605            (0..d[0]).flat_map(|x| (0..d[1]).flat_map(move |y| (0..d[2]).map(move |z| (x, y, z))))
606                .filter(|&(x, y, z)| g.get(x, y, z).is_some_and(|v| [v[0], v[1], v[2]] == c))
607                .count()
608        };
609        let mut g = llano(dim, 10);
610        estampar_decorado(&mut g, [0, 0], &[Decorado::en(CasaTipo::Palacio, 32.0, 32.0)]);
611        assert!(cuenta(&g, Mineral::Diamante.color()) > 100, "palacio sin diamantes");
612        assert!(cuenta(&g, Mineral::Oro.color()) > 10, "palacio sin oro");
613        let mut g = llano(dim, 10);
614        estampar_decorado(&mut g, [0, 0], &[Decorado::en(CasaTipo::Kiosko, 32.0, 32.0)]);
615        assert!(cuenta(&g, Mineral::Cobre.color()) > 10 && cuenta(&g, Mineral::Jade.color()) > 10, "kiosko sin malaquita");
616    }
617
618    /// El perfil de alturas a lo largo de una línea recta en `z`, voxel a voxel.
619    fn perfil(g: &VoxelGrid, z: u32, x0: u32, x1: u32) -> Vec<u32> {
620        (x0..=x1).map(|x| g.height_at(x, z).unwrap_or(0)).collect()
621    }
622
623    /// **Por donde alguien camina el suelo es llano**: sobre el mundo «mar»
624    /// —relieve, matas y agua— la caminata no tiene escalones de más de un
625    /// voxel, y sobre el agua pasa a la altura de la superficie, no por el
626    /// fondo. El brazo de control es la MISMA línea sin la puesta: tiene que
627    /// tener lo que la puesta arregla, o el test no mide nada.
628    #[test]
629    fn por_donde_se_camina_el_suelo_es_llano() {
630        let p = crate::Project::starter();
631        let m = p.mundos.iter().find(|m| m.name == "mar").expect("el arranque trae el mar");
632        let mr = p.render_mundo(m.id).expect("el mar se pinta");
633        let dim = crate::world_dim(128);
634        let crudo = mr.bioma.generate_window(mr.seed, &mr.palette, dim, [0, 0]);
635        let mut g = crudo.clone();
636        let puesta = Puesta { andares: vec![vec![[8.0, 64.0], [120.0, 64.0]]], ..Puesta::default() };
637        estampar_puesta(&mut g, [0, 0], &mr.bioma, mr.seed, &puesta);
638
639        let saltos = |v: &[u32]| v.windows(2).map(|w| w[0].abs_diff(w[1])).max().unwrap_or(0);
640        let cambios = |v: &[u32]| v.windows(2).filter(|w| w[0] != w[1]).count();
641        let (antes, despues) = (perfil(&crudo, 64, 8, 120), perfil(&g, 64, 8, 120));
642        assert!(
643            saltos(&antes) >= 2 && cambios(&antes) > cambios(&despues),
644            "brazo de control: el mar crudo tenía que tener escalones (saltos {}, cambios {} → {})",
645            saltos(&antes),
646            cambios(&antes),
647            cambios(&despues)
648        );
649        assert!(saltos(&despues) <= 1, "quedó un escalón de {} voxels: {despues:?}", saltos(&despues));
650        // A los costados del centro (dentro del corredor) la misma altura: nada
651        // que tropiece ni que tape las patas.
652        for x in 8..=120u32 {
653            for dz in [-2i32, -1, 1, 2] {
654                let lado = g.height_at(x, (64 + dz) as u32).unwrap_or(0);
655                assert!(lado.abs_diff(g.height_at(x, 64).unwrap_or(0)) <= 1, "x={x} dz={dz}: el corredor no es llano");
656            }
657        }
658        // Ningún voxel negro en la piel del corredor: dos tramos que se pisan
659        // no pueden dejar aire pintado (el defecto del primer desfile). La línea
660        // va y vuelve para que se pise a sí misma.
661        let mut g2 = crudo.clone();
662        let ida_y_vuelta = Puesta {
663            andares: vec![vec![[8.0, 64.0], [120.0, 64.0]], vec![[120.0, 65.0], [8.0, 65.0]]],
664            ..Puesta::default()
665        };
666        estampar_puesta(&mut g2, [0, 0], &mr.bioma, mr.seed, &ida_y_vuelta);
667        for x in 8..=120u32 {
668            for z in 61..=68u32 {
669                let y = g2.height_at(x, z).unwrap();
670                let c = g2.get(x, y, z).unwrap();
671                assert!(c[0] as u32 + c[1] as u32 + c[2] as u32 > 60, "voxel negro en ({x}, {y}, {z}): {c:?}");
672            }
673        }
674        // Nunca por debajo del agua.
675        let agua = mr.bioma.water_y(dim);
676        assert!(despues.iter().all(|&y| y >= agua), "el camino se hunde bajo el agua: {despues:?}");
677    }
678
679    /// La explanada queda a una sola cota, los caminos de tierra, y la orla
680    /// con flores — pero nunca sobre un corredor.
681    #[test]
682    fn el_jardin_es_llano_con_camino_y_flores_alrededor() {
683        let p = crate::Project::starter();
684        let m = p.mundos.iter().find(|m| m.name != "mar" && m.name != "luna").expect("un mundo de tierra");
685        let mr = p.render_mundo(m.id).expect("se pinta");
686        let dim = crate::world_dim(128);
687        let mut g = mr.bioma.generate_window(mr.seed, &mr.palette, dim, [0, 0]);
688        let jardin = Jardin {
689            explanada: Some([30.0, 30.0, 98.0, 98.0]),
690            caminos: vec![vec![[64.0, 40.0], [64.0, 90.0]]],
691            ancho_camino: 5.0,
692            orla: 10.0,
693            flores: 0.4,
694            cantero: 0.0,
695            matorrales: Vec::new(),
696        };
697        let antes = g.clone();
698        let puesta = Puesta { jardin: Some(jardin), ..Puesta::default() };
699        estampar_puesta(&mut g, [0, 0], &mr.bioma, mr.seed, &puesta);
700        let cotas: std::collections::BTreeSet<u32> =
701            (30..=98).flat_map(|x| (30..=98).map(move |z| (x, z))).filter_map(|(x, z)| g.height_at(x, z)).collect();
702        assert_eq!(cotas.len(), 1, "la explanada tiene que quedar a una sola cota: {cotas:?}");
703        let y = *cotas.iter().next().unwrap();
704        let c = g.get(64, y, 60).unwrap();
705        // El grano mueve cada canal ±8 %: se compara con esa holgura.
706        let cerca = |a: u8, b: u8| (a as f32 - b as f32).abs() <= b as f32 * 0.12;
707        assert!((0..3).all(|i| cerca(c[i], TIERRA[i])), "el camino no es de tierra: {c:?}");
708        let c = g.get(40, y, 60).unwrap();
709        assert!(c[1] > c[0] && c[1] > c[2], "fuera del camino la explanada es pasto: {c:?}");
710        // La orla: alguna corola de las nuestras en el anillo de afuera.
711        let es_corola = |v: [u8; 4]| COROLAS.iter().any(|c| *c == [v[0], v[1], v[2]]);
712        let flores = (20..30u32)
713            .flat_map(|x| (20..108u32).map(move |z| (x, z)))
714            .filter(|&(x, z)| (0..dim[1]).any(|yy| g.get(x, yy, z).is_some_and(es_corola)))
715            .count();
716        let flores_antes = (20..30u32)
717            .flat_map(|x| (20..108u32).map(move |z| (x, z)))
718            .filter(|&(x, z)| (0..dim[1]).any(|yy| antes.get(x, yy, z).is_some_and(es_corola)))
719            .count();
720        assert!(flores > 20 && flores_antes == 0, "la orla: {flores_antes} → {flores} columnas con flor");
721    }
722
723    /// Los canteros florecen a los DOS lados del camino y nunca sobre la
724    /// tierra; las florcitas no pasan de dos voxels. Y un matorral sin
725    /// explanada siembra en lo seco, nada en el agua.
726    #[test]
727    fn canteros_a_los_lados_del_camino_y_matorral_en_lo_seco() {
728        let p = crate::Project::starter();
729        let m = p.mundos.iter().find(|m| m.name != "mar" && m.name != "luna").expect("un mundo de tierra");
730        let mr = p.render_mundo(m.id).expect("se pinta");
731        let dim = crate::world_dim(128);
732        let mut g = mr.bioma.generate_window(mr.seed, &mr.palette, dim, [0, 0]);
733        let jardin = Jardin {
734            explanada: Some([30.0, 30.0, 98.0, 98.0]),
735            caminos: vec![vec![[64.0, 40.0], [64.0, 90.0]]],
736            ancho_camino: 5.0,
737            orla: 0.0,
738            flores: 0.0,
739            cantero: 2.5,
740            matorrales: Vec::new(),
741        };
742        let puesta = Puesta { jardin: Some(jardin), ..Puesta::default() };
743        let florcitas = estampar_puesta(&mut g, [0, 0], &mr.bioma, mr.seed, &puesta);
744        let cota = g.height_at(40, 40).unwrap();
745        // Las florcitas no se estampan: van como seres. El camino y sus costados
746        // quedan a la cota del pasto, sin un cubo de corola.
747        for x in 56..=72u32 {
748            for z in 40..90u32 {
749                assert_eq!(g.height_at(x, z).unwrap(), cota, "({x}, {z}): quedó un voxel de flor");
750            }
751        }
752        // Sólo a lo largo del tramo: alrededor de las puntas el cantero da la
753        // vuelta en semicírculo, y ahí `x = 64` no es el camino.
754        let lado = |a: f32, b: f32| {
755            florcitas.iter().filter(|f| (a..=b).contains(&f.pos[0]) && (40.0..=90.0).contains(&f.pos[2])).count()
756        };
757        let (izq, der) = (lado(57.0, 61.0), lado(68.0, 72.0));
758        assert!(izq > 20 && der > 20, "canteros: {izq} a la izquierda, {der} a la derecha");
759        assert_eq!(lado(62.0, 67.0), 0, "una flor sobre el camino");
760        assert!(florcitas.iter().all(|f| f.pos[1] == (cota + 1) as f32), "una flor no nace sobre el pasto");
761        // Como seres: agrupadas en macizos, tres primitivas por flor, y todas
762        // caben en el uniform con lugar de sobra para el reparto.
763        let m = macizos(&florcitas);
764        let prims: usize = m.iter().map(|s| s.prims.len()).sum();
765        assert_eq!(prims, florcitas.len() * PRIMS_POR_FLOR);
766        assert!(m.len() < florcitas.len() / 3, "{} macizos para {} flores: no agrupa", m.len(), florcitas.len());
767        // Con el uniform casi lleno, entran los cercanos y el resto se cuenta.
768        let mut seres = vec![SdfSer { prims: m[0].prims.repeat(1), pos: [0.0; 3], yaw: 0.0 }; 38];
769        let afuera = sumar_macizos(&mut seres, &m, [64.0, cota as f32 + 5.0, 30.0], [0.0, 0.0, 1.0]);
770        assert_eq!(seres.len(), 40, "los dos lugares libres se llenan");
771        assert_eq!(afuera, m.len() - 2);
772
773        // Matorral sobre el mar: sólo en lo seco.
774        let mar = p.mundos.iter().find(|m| m.name == "mar").unwrap();
775        let mr = p.render_mundo(mar.id).unwrap();
776        let crudo = mr.bioma.generate_window(mr.seed, &mr.palette, dim, [0, 0]);
777        let mut g = crudo.clone();
778        let jardin = Jardin {
779            explanada: None,
780            caminos: Vec::new(),
781            ancho_camino: 0.0,
782            orla: 0.0,
783            flores: 0.0,
784            cantero: 0.0,
785            matorrales: vec![[0.0, 0.0, 127.0, 127.0]],
786        };
787        estampar_puesta(&mut g, [0, 0], &mr.bioma, mr.seed, &Puesta { jardin: Some(jardin), ..Puesta::default() });
788        let agua = mr.bioma.water_y(dim);
789        let mut sembradas = 0;
790        for x in 0..128u32 {
791            for z in 0..128u32 {
792                let n = mr.bioma.column_height(mr.seed, x as i32, z as i32, dim);
793                if g.height_at(x, z) != crudo.height_at(x, z) && n > agua {
794                    sembradas += 1;
795                }
796                if n + 2 <= agua {
797                    // Mar adentro nada ARRAIGA: el voxel de arriba del agua sigue
798                    // vacío. (La copa de una palmera de la orilla sí puede
799                    // asomar más arriba, como asoman las de verdad.)
800                    assert!(
801                        g.get(x, agua + 1, z).is_none_or(|c| c[3] == 0),
802                        "({x}, {z}): algo arraigó en el agua"
803                    );
804                }
805            }
806        }
807        assert!(sembradas > 200, "el matorral apenas sembró {sembradas} columnas");
808    }
809
810    /// Un árbol en la huella se despeja: no queda nada atravesando la casa.
811    #[test]
812    fn plantar_despeja_lo_que_habia() {
813        let dim = [64, 48, 64];
814        let mut g = llano(dim, 10);
815        for y in 10..30 {
816            g.set(30, y, 30, [40, 90, 30]); // un tronco altísimo dentro de la huella
817        }
818        estampar_decorado(&mut g, [0, 0], &[Decorado::en(CasaTipo::Kiosko, 32.0, 32.0)]);
819        assert!(g.get(30, 29, 30).is_none_or(|c| c[3] == 0), "el tronco sigue ahí");
820    }
821}