tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
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}