tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0)
1//! **La vegetación SDF** — la segunda técnica de graficación aplicada a las plantas:
2//! cada [`Forma`] armada con primitivas analíticas fundidas por `smin`, la misma
3//! receta que el ser humanoide y el bestiario.
4//!
5//! ## Por qué una planta no es «un árbol»
6//!
7//! El catálogo voxel tenía ocho formas y todas las leñosas terminaban siendo
8//! *tronco + bola*. De cerca eso no es un árbol: es una paleta. Un ficus se
9//! reconoce por la **copa ancha y baja con raíces aéreas**, un sauce por las
10//! **ramas que caen**, una palmera por **cómo se arquea la fronda**, un jacarandá
11//! porque la flor le tapa la hoja. Nada de eso es más geometría: es la MISMA
12//! cantidad de primitivas puestas donde va.
13//!
14//! ## Cómo se dibuja
15//!
16//! Todo sale de cinco ladrillos ([`sdf_kit`](crate::sdf_kit)) y de tres ideas:
17//! 1. **El tronco es una cadena**, no un cilindro: se curva, se afina y se ramifica.
18//! 2. **La copa son bultos que se funden** (`blend` grande): tres o cuatro
19//! elipsoides con `smin` leen como una masa de hojas, no como pelotas.
20//! 3. **La flor y el fruto son decals**: pintan sobre la copa sin costar silueta —
21//! un jacarandá en flor cuesta lo mismo que uno sin flor.
22//!
23//! El **viento** es un parámetro, no una animación aparte: desplaza la copa y las
24//! frondas en proporción a la altura, que es como se dobla una planta de verdad.
25
26use std::f32::consts::PI;
27
28use llimphi_3d::glam::Vec3;
29use llimphi_3d::{SdfPrim, SdfSer};
30
31use crate::sdf_kit::{
32 asentar, cadena, capsula, decal, decal_linea, elipsoide, esfera, hash01, hash11, mezcla, rgb,
33 tono,
34};
35use crate::worldgen::Forma;
36
37/// Paleta de una planta: madera, hoja, flor y fruto.
38#[derive(Debug, Clone, Copy)]
39pub struct Follaje {
40 pub tronco: [f32; 3],
41 pub hoja: [f32; 3],
42 pub flor: [f32; 3],
43 pub fruto: [f32; 3],
44}
45
46impl Follaje {
47 /// La paleta propia de cada forma. El bioma puede pisar la hoja con
48 /// [`flora_sdf_col`] — un bosque otoñal o una selva no se distinguen por la
49 /// silueta sino por el verde.
50 pub fn de(f: Forma) -> Self {
51 let (tronco, hoja, flor, fruto) = match f {
52 Forma::Palmera => (0x8A6A42, 0x3E8A4A, 0xE8D060, 0x9A6A2E),
53 Forma::Conifera | Forma::Cipres => (0x6A4A2E, 0x2A5E3E, 0xC8C0A0, 0x6A5A3A),
54 Forma::Ficus => (0x8A7460, 0x2E7A46, 0xE8E0C0, 0x8A2A3A),
55 Forma::Sauce => (0x7A6448, 0x8AAE58, 0xE8E4C0, 0x8A7A50),
56 Forma::Jacaranda => (0x7A6450, 0x4A8A52, 0x8A6ACC, 0x8A7A50),
57 Forma::Baobab => (0x9A8A70, 0x5E8A46, 0xF0E8D0, 0xC8B070),
58 Forma::Roble => (0x6E5236, 0x3E7A38, 0xE8E4C0, 0x8A6A28),
59 Forma::Platanera => (0x7A7A4A, 0x4EA046, 0xE8D050, 0xE0C830),
60 Forma::Bambu => (0x9AA84A, 0x6EA84E, 0xE8E4C0, 0x8A9A50),
61 Forma::Hongo => (0xE0D8C0, 0xB03A30, 0xF2EEE6, 0xF2EEE6),
62 Forma::Tocon => (0x6E5236, 0x4A7A38, 0xE8E4C0, 0x8A6A28),
63 Forma::Trigo => (0xD8B858, 0xC8A840, 0xE8D880, 0xE0C860),
64 Forma::Junco => (0x6A8A4A, 0x5E8A46, 0x8A6A3A, 0x8A6A3A),
65 Forma::Lavanda => (0x6A7A50, 0x6A8A5A, 0x8A6AC8, 0x8A6AC8),
66 Forma::Girasol => (0x5E8A3E, 0x4A7A34, 0xE8C020, 0x6A4A22),
67 Forma::Margarita => (0x5E8A3E, 0x4A7A34, 0xF4F2EA, 0xE8C830),
68 Forma::Tulipan => (0x4E8A44, 0x3E7A38, 0xD8324A, 0xE8C830),
69 Forma::Rosa => (0x4A7A3E, 0x3A6A34, 0xD03A5A, 0xE8C830),
70 Forma::Amapola => (0x6A8A4A, 0x5A7A3E, 0xD82A22, 0x22201E),
71 Forma::Orquidea => (0x4A7A46, 0x3E6A3E, 0xE080C0, 0xE8D860),
72 Forma::Agave => (0x6A8A5E, 0x7AA070, 0xE8E0A0, 0xC8C060),
73 Forma::Nopal | Forma::Columnar => (0x4A7A3E, 0x4E8A46, 0xE8607A, 0xD84A5A),
74 Forma::Flor => (0x5E8A3E, 0x4A7A34, 0xE05A8A, 0xE8C830),
75 Forma::Liana => (0x6A5A3A, 0x3E8A46, 0xE8D060, 0x8A6A2E),
76 // Las herbáceas no tienen madera: si el tronco queda en el marrón del
77 // default, el pasto sale de palitos secos (se vio en la primera hoja).
78 Forma::Pasto => (0x5E8A3E, 0x6EA84A, 0xC8D880, 0x8A9A50),
79 Forma::Helecho => (0x4A7A38, 0x3E8A42, 0xC8D880, 0x8A9A50),
80 Forma::Arbusto => (0x6A5236, 0x4A8A44, 0xE8E4C0, 0x8A6A28),
81 _ => (0x6E5236, 0x4A8A44, 0xE8E4C0, 0x8A6A28),
82 };
83 Self {
84 tronco: rgb(tronco),
85 hoja: rgb(hoja),
86 flor: rgb(flor),
87 fruto: rgb(fruto),
88 }
89 }
90}
91
92/// La planta armada como ser SDF, a la altura pedida. `semilla` la varía (ninguna
93/// planta es igual a la de al lado) y `viento` la dobla.
94pub fn flora_sdf(f: Forma, alto: f32, semilla: u32, viento: f32) -> SdfSer {
95 flora_sdf_col(f, alto, semilla, viento, None)
96}
97
98/// Igual, con el color de hoja del bioma (`None` = el propio de la especie).
99pub fn flora_sdf_col(
100 f: Forma,
101 alto: f32,
102 semilla: u32,
103 viento: f32,
104 hoja: Option<[f32; 3]>,
105) -> SdfSer {
106 let mut pal = Follaje::de(f);
107 if let Some(h) = hoja {
108 pal.hoja = h;
109 }
110 let mut prims = armar(f, &pal, semilla, viento);
111 asentar(&mut prims, alto.max(0.05));
112 SdfSer { prims, pos: [0.0; 3], yaw: 0.0 }
113}
114
115/// **Altura típica en metros** de cada forma — para que un ficus no mida lo mismo
116/// que una margarita cuando el mundo las siembra juntas.
117pub fn alto_flora(f: Forma) -> f32 {
118 match f {
119 Forma::Baobab => 14.0,
120 Forma::Ficus => 12.0,
121 Forma::Roble | Forma::Conifera => 11.0,
122 Forma::Sauce | Forma::Jacaranda => 9.0,
123 Forma::Cipres => 8.5,
124 Forma::Palmera => 8.0,
125 Forma::Arbol => 7.0,
126 Forma::Bambu => 5.0,
127 Forma::Platanera => 3.2,
128 Forma::Liana => 3.0,
129 Forma::Columnar => 2.6,
130 Forma::Nopal => 1.6,
131 Forma::Junco | Forma::Girasol => 1.5,
132 Forma::Arbusto | Forma::Agave => 1.1,
133 Forma::Trigo => 0.9,
134 Forma::Tocon | Forma::Helecho | Forma::Rosa | Forma::Orquidea => 0.7,
135 Forma::Lavanda | Forma::Tulipan | Forma::Amapola => 0.5,
136 Forma::Margarita | Forma::Flor => 0.4,
137 Forma::Hongo => 0.25,
138 Forma::Pasto => 0.3,
139 }
140}
141
142fn armar(f: Forma, pal: &Follaje, sem: u32, viento: f32) -> Vec<SdfPrim> {
143 match f {
144 Forma::Arbol => arbol(pal, sem, viento, Copa::Bola, 0.42, 0.0, false),
145 Forma::Roble => arbol(pal, sem, viento, Copa::Ancha, 0.38, 0.0, false),
146 Forma::Ficus => arbol(pal, sem, viento, Copa::Domo, 0.34, 0.0, true),
147 Forma::Jacaranda => arbol(pal, sem, viento, Copa::Sombrilla, 0.46, 1.0, false),
148 Forma::Baobab => baobab(pal, sem, viento),
149 Forma::Sauce => sauce(pal, sem, viento),
150 Forma::Conifera => conifera(pal, sem, viento, 4),
151 Forma::Cipres => cipres(pal, sem, viento),
152 Forma::Palmera => palmera(pal, sem, viento, Fronda::Pinnada),
153 Forma::Platanera => palmera(pal, sem, viento, Fronda::Ancha),
154 Forma::Bambu => bambu(pal, sem, viento),
155 Forma::Liana => liana(pal, sem, viento),
156 Forma::Arbusto => arbusto(pal, sem, viento),
157 Forma::Helecho => helecho(pal, sem, viento),
158 Forma::Pasto => manojo(pal, sem, viento, 11, 0.10, false),
159 Forma::Trigo => manojo(pal, sem, viento, 7, 0.06, true),
160 Forma::Junco => junco(pal, sem, viento),
161 Forma::Lavanda => lavanda(pal, sem, viento),
162 Forma::Hongo => hongo(pal, sem),
163 Forma::Tocon => tocon(pal, sem),
164 Forma::Columnar => saguaro(pal, sem),
165 Forma::Nopal => nopal(pal, sem),
166 Forma::Agave => agave(pal, sem),
167 Forma::Flor => flor(pal, sem, viento, 6, Petalo::Plano, 0.30),
168 Forma::Margarita => flor(pal, sem, viento, 12, Petalo::Plano, 0.26),
169 Forma::Girasol => flor(pal, sem, viento, 16, Petalo::Plano, 0.40),
170 Forma::Amapola => flor(pal, sem, viento, 4, Petalo::Copa, 0.34),
171 Forma::Tulipan => flor(pal, sem, viento, 5, Petalo::Copa, 0.30),
172 Forma::Rosa => rosa(pal, sem, viento),
173 Forma::Orquidea => orquidea(pal, sem, viento),
174 }
175}
176
177// ============================================================================
178// Árboles
179// ============================================================================
180
181/// Forma de la copa: lo que distingue a un roble de un ciprés desde lejos.
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183enum Copa {
184 /// Bola clásica.
185 Bola,
186 /// Ancha y algo achatada (roble).
187 Ancha,
188 /// Domo muy ancho y bajo (ficus, higuera).
189 Domo,
190 /// Sombrilla: plana y abierta (acacia, jacarandá).
191 Sombrilla,
192}
193
194/// Tronco que sube curvándose, con `n` tramos. Devuelve los puntos.
195fn tronco(alto: f32, r: f32, curva: f32, sem: u32, viento: f32, pal: &Follaje, out: &mut Vec<SdfPrim>) -> Vec<Vec3> {
196 let n = 4;
197 let incl = hash11(sem, 1) * curva;
198 let pts: Vec<Vec3> = (0..=n)
199 .map(|i| {
200 let t = i as f32 / n as f32;
201 Vec3::new(
202 incl * t * t * alto * 0.25 + viento * t * t * alto * 0.10,
203 t * alto,
204 (hash11(sem, 2 + i as u32) * 0.05 + incl * 0.3) * t * t * alto * 0.20,
205 )
206 })
207 .collect();
208 for (i, w) in pts.windows(2).enumerate() {
209 let t0 = i as f32 / n as f32;
210 let t1 = (i + 1) as f32 / n as f32;
211 // La base ensancha (raíz al pie): un tronco de radio constante se ve pegado.
212 let rad = |t: f32| r * (1.0 + 0.55 * (1.0 - t).powi(3) - 0.35 * t);
213 out.push(capsula(w[0], rad(t0), w[1], rad(t1), pal.tronco, r * 0.5));
214 }
215 pts
216}
217
218/// Bultos de copa fundidos alrededor de un centro. Es LA idea de la vegetación:
219/// tres o cuatro elipsoides con `blend` grande leen como follaje, uno solo como
220/// una pelota de plástico.
221#[allow(clippy::too_many_arguments)]
222fn copa_bultos(
223 centro: Vec3,
224 rx: f32,
225 ry: f32,
226 n: usize,
227 sem: u32,
228 pal: &Follaje,
229 aplasta: f32,
230 out: &mut Vec<SdfPrim>,
231) {
232 // El bulto central es MENOR que los de alrededor: si domina, la copa vuelve a
233 // ser una pelota lisa y los lóbulos no se leen.
234 out.push(elipsoide(centro, Vec3::new(rx * 0.82, ry * 0.86, rx * 0.82), pal.hoja, rx * 0.35));
235 for i in 0..n {
236 let a = i as f32 / n as f32 * PI * 2.0 + hash01(sem, i as u32) * 0.8;
237 let d = rx * (0.45 + 0.30 * hash01(sem + 3, i as u32));
238 let c = centro
239 + Vec3::new(a.cos() * d, (hash11(sem + 5, i as u32) * 0.35 - aplasta * 0.25) * ry, a.sin() * d);
240 let k = 0.55 + 0.28 * hash01(sem + 7, i as u32);
241 out.push(elipsoide(
242 c,
243 Vec3::new(rx * k, ry * k * (1.0 - aplasta * 0.25), rx * k),
244 // Tinte por bulto: dos verdes apenas distintos rompen el plano.
245 mezcla(pal.hoja, tono(pal.hoja, 0.82 + 0.30 * hash01(sem + 11, i as u32)), 0.6),
246 // Blend CORTO (0.35, no 0.60): con la unión ancha los bultos se fundían
247 // en una sola masa lisa y la copa perdía el recorte de follaje.
248 rx * 0.35,
249 ));
250 }
251}
252
253fn arbol(pal: &Follaje, sem: u32, viento: f32, copa: Copa, alto_tronco: f32, flores: f32, raices: bool) -> Vec<SdfPrim> {
254 let mut out = Vec::with_capacity(30);
255 let h = 1.0;
256 let r = 0.045 + 0.02 * hash01(sem, 21);
257 let ptos = tronco(h * alto_tronco, r, 0.35, sem, viento, pal, &mut out);
258 let top = *ptos.last().unwrap();
259
260 // Ramas: salen del tercio alto del tronco hacia afuera y arriba.
261 let nramas = match copa {
262 Copa::Domo | Copa::Sombrilla => 5,
263 _ => 4,
264 };
265 let (rx, ry, aplasta) = match copa {
266 Copa::Bola => (0.34, 0.32, 0.0),
267 Copa::Ancha => (0.44, 0.30, 0.18),
268 // El domo del ficus era un panqueque: `0.26` de alto contra `0.52` de ancho
269 // y con los bultos aplastados encima, la copa leía como una tapa de hongo.
270 Copa::Domo => (0.50, 0.34, 0.25),
271 Copa::Sombrilla => (0.46, 0.24, 0.40),
272 };
273 let mut puntas = Vec::with_capacity(nramas);
274 for i in 0..nramas {
275 let a = i as f32 / nramas as f32 * PI * 2.0 + hash01(sem, 30 + i as u32);
276 let largo = h * (0.20 + 0.12 * hash01(sem, 40 + i as u32));
277 let punta = top
278 + Vec3::new(
279 a.cos() * largo * (1.0 + aplasta),
280 largo * (0.85 - aplasta * 0.55),
281 a.sin() * largo * (1.0 + aplasta),
282 );
283 out.push(capsula(top - Vec3::new(0.0, largo * 0.25, 0.0), r * 0.85, punta, r * 0.35, pal.tronco, r * 0.5));
284 puntas.push(punta);
285 }
286 let centro = top + Vec3::new(viento * 0.08, ry * 0.85 + h * 0.10, 0.0);
287 copa_bultos(centro, rx, ry, nramas, sem, pal, aplasta, &mut out);
288
289 // Flor (jacarandá): decals sobre la copa. No cuestan silueta.
290 if flores > 0.01 {
291 let n = 22;
292 for i in 0..n {
293 let a = hash01(sem + 13, i as u32) * PI * 2.0;
294 let e = hash01(sem + 17, i as u32);
295 let d = Vec3::new(a.cos() * (0.4 + 0.6 * e), 0.35 + 0.6 * hash01(sem + 19, i as u32), a.sin() * (0.4 + 0.6 * e))
296 .normalize();
297 // El punto va sobre el ELIPSOIDE de la copa (semiejes distintos), no
298 // sobre una esfera de radio `rx`: con la copa aplastada, media flor
299 // caía en el aire y no pintaba nada.
300 let p = centro + Vec3::new(d.x * rx, d.y * ry, d.z * rx) * 0.98;
301 out.push(decal(p, rx * (0.20 + 0.14 * e) * flores, pal.flor));
302 }
303 }
304
305 // Raíces aéreas del ficus: cuerdas finas de la copa al suelo. Es LA marca de la
306 // higuera y no cuesta más que cuatro cápsulas.
307 if raices {
308 for i in 0..5 {
309 let a = hash01(sem + 23, i as u32) * PI * 2.0;
310 let d = rx * (0.35 + 0.5 * hash01(sem + 29, i as u32));
311 let arriba = centro + Vec3::new(a.cos() * d, -ry * 0.55, a.sin() * d);
312 // La punta baja HASTA LA TANGENTE del tronco (`−1.55·r`, que es donde
313 // `asentar` va a poner el piso), no hasta el `y = 0` del espacio de
314 // armado: eso dejaba la raíz colgando medio metro en un ficus de ocho.
315 let abajo = Vec3::new(arriba.x * 1.08, -r * 1.15, arriba.z * 1.08);
316 let medio = (arriba + abajo) * 0.5 + Vec3::new(0.0, 0.0, 0.0);
317 cadena(&[arriba, medio, abajo], r * 0.30, r * 0.42, pal.tronco, r * 0.20, &mut out);
318 }
319 }
320 out
321}
322
323fn baobab(pal: &Follaje, sem: u32, viento: f32) -> Vec<SdfPrim> {
324 let mut out = Vec::with_capacity(20);
325 // El tronco ES el baobab: un tonel que se afina de golpe arriba.
326 let base = Vec3::ZERO;
327 let cuello = Vec3::new(viento * 0.03, 0.62, 0.0);
328 out.push(capsula(base, 0.30, Vec3::new(0.0, 0.30, 0.0), 0.26, pal.tronco, 0.15));
329 out.push(capsula(Vec3::new(0.0, 0.30, 0.0), 0.26, cuello, 0.13, pal.tronco, 0.12));
330 for i in 0..6 {
331 let a = i as f32 / 6.0 * PI * 2.0 + hash01(sem, i as u32) * 0.5;
332 let punta = cuello + Vec3::new(a.cos() * 0.30, 0.26 + 0.10 * hash01(sem + 1, i as u32), a.sin() * 0.30);
333 out.push(capsula(cuello, 0.075, punta, 0.02, pal.tronco, 0.05));
334 out.push(elipsoide(punta, Vec3::new(0.14, 0.09, 0.14), pal.hoja, 0.10));
335 }
336 out
337}
338
339fn sauce(pal: &Follaje, sem: u32, viento: f32) -> Vec<SdfPrim> {
340 let mut out = Vec::with_capacity(34);
341 let ptos = tronco(0.52, 0.055, 0.30, sem, viento, pal, &mut out);
342 let top = *ptos.last().unwrap();
343 copa_bultos(top + Vec3::new(0.0, 0.16, 0.0), 0.34, 0.16, 4, sem, pal, 0.45, &mut out);
344 // Las ramas que LLORAN: cortinas de cadenas que caen casi hasta el piso.
345 let n = 14;
346 for i in 0..n {
347 let a = i as f32 / n as f32 * PI * 2.0 + hash01(sem + 9, i as u32) * 0.3;
348 let d = 0.32 * (0.55 + 0.45 * hash01(sem + 3, i as u32));
349 let arriba = top + Vec3::new(a.cos() * d, 0.20, a.sin() * d);
350 let caida = 0.34 + 0.24 * hash01(sem + 5, i as u32);
351 let pts = [
352 arriba,
353 arriba + Vec3::new(a.cos() * d * 0.35 + viento * 0.05, -caida * 0.5, a.sin() * d * 0.35),
354 arriba + Vec3::new(a.cos() * d * 0.45 + viento * 0.12, -caida, a.sin() * d * 0.45),
355 ];
356 cadena(&pts, 0.060, 0.028, pal.hoja, 0.045, &mut out);
357 }
358 out
359}
360
361fn conifera(pal: &Follaje, sem: u32, viento: f32, tiers: usize) -> Vec<SdfPrim> {
362 let mut out = Vec::with_capacity(14);
363 out.push(capsula(Vec3::ZERO, 0.055, Vec3::new(0.0, 0.30, 0.0), 0.04, pal.tronco, 0.03));
364 for i in 0..tiers {
365 let t = i as f32 / tiers as f32;
366 let y0 = 0.16 + t * 0.66;
367 let alto = 0.30 * (1.0 - t * 0.35);
368 let r = 0.30 * (1.0 - t * 0.72) + 0.03;
369 let dx = viento * (y0 * 0.10);
370 out.push(capsula(
371 Vec3::new(dx, y0, 0.0),
372 r,
373 Vec3::new(dx * 1.4, y0 + alto, 0.0),
374 r * 0.15,
375 mezcla(pal.hoja, tono(pal.hoja, 0.85 + 0.2 * hash01(sem, i as u32)), 0.5),
376 r * 0.25,
377 ));
378 }
379 out
380}
381
382fn cipres(pal: &Follaje, sem: u32, viento: f32) -> Vec<SdfPrim> {
383 let mut out = Vec::with_capacity(8);
384 out.push(capsula(Vec3::ZERO, 0.05, Vec3::new(0.0, 0.14, 0.0), 0.045, pal.tronco, 0.03));
385 // Columna: tres elipsoides apilados con blend grande = una llama verde.
386 for i in 0..3 {
387 let t = i as f32 / 3.0;
388 let y = 0.24 + t * 0.52;
389 out.push(elipsoide(
390 Vec3::new(viento * y * 0.10, y, 0.0),
391 Vec3::new(0.16 * (1.0 - t * 0.45), 0.24 * (1.0 - t * 0.12), 0.16 * (1.0 - t * 0.45)),
392 mezcla(pal.hoja, tono(pal.hoja, 0.88 + 0.18 * hash01(sem, i as u32)), 0.5),
393 0.14,
394 ));
395 }
396 out.push(capsula(
397 Vec3::new(viento * 0.09, 0.80, 0.0),
398 0.09,
399 Vec3::new(viento * 0.12, 1.0, 0.0),
400 0.005,
401 pal.hoja,
402 0.06,
403 ));
404 out
405}
406
407/// Tipo de fronda: la pinnada de la palmera (larga y arqueada) o la hoja ANCHA del
408/// banano (una lámina entera).
409#[derive(Debug, Clone, Copy, PartialEq, Eq)]
410enum Fronda {
411 Pinnada,
412 Ancha,
413}
414
415fn palmera(pal: &Follaje, sem: u32, viento: f32, tipo: Fronda) -> Vec<SdfPrim> {
416 let mut out = Vec::with_capacity(34);
417 // Tronco ALTO: en la primera hoja la palmera leía como una araña sobre un
418 // palito porque la corona pesaba más que el fuste.
419 let alto = if tipo == Fronda::Pinnada { 0.86 } else { 0.34 };
420 let r = if tipo == Fronda::Pinnada { 0.035 } else { 0.075 };
421 // Tronco arqueado: la palmera se reconoce por la curva, no por el largo.
422 let curva = (0.10 + 0.10 * hash01(sem, 1)) * if hash01(sem, 2) > 0.5 { 1.0 } else { -1.0 };
423 let n = 5;
424 let pts: Vec<Vec3> = (0..=n)
425 .map(|i| {
426 let t = i as f32 / n as f32;
427 Vec3::new((curva + viento * 0.12) * t * t, t * alto, 0.0)
428 })
429 .collect();
430 for (i, w) in pts.windows(2).enumerate() {
431 let t = i as f32 / n as f32;
432 out.push(capsula(w[0], r * (1.5 - 0.5 * t), w[1], r * (1.4 - 0.5 * t), pal.tronco, r * 0.4));
433 }
434 let top = *pts.last().unwrap();
435 if tipo == Fronda::Pinnada {
436 // Anillos del tronco: decals baratos que cuentan la especie.
437 for i in 1..6 {
438 let t = i as f32 / 6.0;
439 let c = Vec3::new((curva + viento * 0.12) * t * t, t * alto, 0.0);
440 out.push(decal_linea(c - Vec3::new(r * 1.3, 0.0, 0.0), c + Vec3::new(r * 1.3, 0.0, 0.0), r * 0.45, tono(pal.tronco, 0.78)));
441 }
442 // Cocos.
443 for i in 0..3 {
444 let a = i as f32 * 2.1;
445 out.push(esfera(top + Vec3::new(a.cos() * r * 2.2, -r * 1.6, a.sin() * r * 2.2), r * 1.5, pal.fruto, r * 0.6));
446 }
447 }
448 let nf = if tipo == Fronda::Pinnada { 10 } else { 6 };
449 for i in 0..nf {
450 let a = i as f32 / nf as f32 * PI * 2.0 + hash01(sem, 50 + i as u32) * 0.4;
451 let largo = if tipo == Fronda::Pinnada { 0.40 } else { 0.46 };
452 // Frondas ANCHAS: una fronda de 0.022 de radio es un alambre, y diez
453 // alambres son una araña. La hoja del banano es una lámina entera.
454 let ancho = if tipo == Fronda::Pinnada { 0.040 } else { 0.115 };
455 let dir = Vec3::new(a.cos(), 0.0, a.sin());
456 // Dos tramos: sube y se arquea hacia abajo. Una fronda recta parece un palo.
457 let medio = top + dir * (largo * 0.55) + Vec3::new(viento * 0.05, largo * 0.36, 0.0);
458 let punta = top + dir * (largo * 1.12) + Vec3::new(viento * 0.12, -largo * (0.10 + 0.35 * hash01(sem, 60 + i as u32)), 0.0);
459 let col = mezcla(pal.hoja, tono(pal.hoja, 0.85 + 0.25 * hash01(sem, 70 + i as u32)), 0.5);
460 out.push(capsula(top, ancho * 1.2, medio, ancho, col, ancho * 0.8));
461 out.push(capsula(medio, ancho, punta, ancho * 0.5, col, ancho * 0.8));
462 if tipo == Fronda::Pinnada {
463 // Folíolos: dos líneas pintadas a lo largo — la fronda se lee peinada.
464 out.push(decal_linea(medio, punta, ancho * 1.9, tono(col, 0.86)));
465 }
466 }
467 out
468}
469
470fn bambu(pal: &Follaje, sem: u32, viento: f32) -> Vec<SdfPrim> {
471 let mut out = Vec::with_capacity(30);
472 for c in 0..4 {
473 let dx = (hash11(sem, c) * 0.09, hash11(sem + 1, c) * 0.09);
474 let alto = 0.72 + 0.28 * hash01(sem + 2, c);
475 let r = 0.020 + 0.008 * hash01(sem + 3, c);
476 let base = Vec3::new(dx.0, 0.0, dx.1);
477 let punta = base + Vec3::new(viento * 0.12 * alto, alto, 0.0);
478 out.push(capsula(base, r, punta, r * 0.8, pal.tronco, r * 0.3));
479 // Nudos.
480 for k in 1..5 {
481 let t = k as f32 / 5.0;
482 out.push(esfera(base + (punta - base) * t, r * 1.25, tono(pal.tronco, 0.8), r * 0.4));
483 }
484 // Hojas: lanzas cortas cerca de la punta.
485 for k in 0..3 {
486 let t = 0.62 + 0.14 * k as f32;
487 let a = hash01(sem + 5, c * 8 + k) * PI * 2.0;
488 let p0 = base + (punta - base) * t;
489 out.push(capsula(
490 p0,
491 r * 0.8,
492 p0 + Vec3::new(a.cos() * 0.11, 0.05, a.sin() * 0.11),
493 r * 0.15,
494 pal.hoja,
495 r * 0.5,
496 ));
497 }
498 }
499 out
500}
501
502fn liana(pal: &Follaje, sem: u32, viento: f32) -> Vec<SdfPrim> {
503 let mut out = Vec::with_capacity(22);
504 // Cuelga desde arriba: se arma de 1 a 0 y `asentar` la deja apoyada.
505 let n = 7;
506 let pts: Vec<Vec3> = (0..=n)
507 .map(|i| {
508 let t = i as f32 / n as f32;
509 Vec3::new(
510 (t * PI * 1.6).sin() * 0.06 + viento * (1.0 - t) * 0.10,
511 t,
512 (t * PI * 1.1).cos() * 0.04,
513 )
514 })
515 .collect();
516 cadena(&pts, 0.028, 0.042, pal.tronco, 0.018, &mut out);
517 for i in 1..n {
518 let p0 = pts[i];
519 for k in 0..2 {
520 let a = hash01(sem, i as u32 * 2 + k) * PI * 2.0;
521 out.push(elipsoide(
522 p0 + Vec3::new(a.cos() * 0.10, -0.02, a.sin() * 0.10),
523 Vec3::new(0.085, 0.016, 0.062),
524 pal.hoja,
525 0.04,
526 ));
527 }
528 }
529 out
530}
531
532// ============================================================================
533// Matas, cañas y hongos
534// ============================================================================
535
536fn arbusto(pal: &Follaje, sem: u32, viento: f32) -> Vec<SdfPrim> {
537 let mut out = Vec::with_capacity(10);
538 copa_bultos(
539 Vec3::new(viento * 0.05, 0.45, 0.0),
540 0.42,
541 0.40,
542 5,
543 sem,
544 pal,
545 0.15,
546 &mut out,
547 );
548 // Ramitas asomando abajo.
549 for i in 0..3 {
550 let a = i as f32 * 2.1 + hash01(sem, 9);
551 out.push(capsula(
552 Vec3::new(a.cos() * 0.05, 0.0, a.sin() * 0.05),
553 0.020,
554 Vec3::new(a.cos() * 0.10, 0.28, a.sin() * 0.10),
555 0.012,
556 pal.tronco,
557 0.012,
558 ));
559 }
560 out
561}
562
563fn helecho(pal: &Follaje, sem: u32, viento: f32) -> Vec<SdfPrim> {
564 let mut out = Vec::with_capacity(24);
565 let n = 7;
566 for i in 0..n {
567 let a = i as f32 / n as f32 * PI * 2.0 + hash01(sem, i as u32) * 0.3;
568 let largo = 0.55 + 0.30 * hash01(sem + 1, i as u32);
569 let dir = Vec3::new(a.cos(), 0.0, a.sin());
570 let medio = dir * (largo * 0.42) + Vec3::new(viento * 0.04, largo * 0.68, 0.0);
571 let punta = dir * (largo * 0.92) + Vec3::new(viento * 0.09, largo * 0.42, 0.0);
572 let col = mezcla(pal.hoja, tono(pal.hoja, 0.82 + 0.3 * hash01(sem + 2, i as u32)), 0.5);
573 out.push(capsula(Vec3::new(0.0, 0.04, 0.0), 0.022, medio, 0.030, col, 0.02));
574 out.push(capsula(medio, 0.030, punta, 0.006, col, 0.02));
575 // Folíolos pintados: el peine de la fronda sin geometría.
576 out.push(decal_linea(medio, punta, 0.045, tono(col, 0.88)));
577 }
578 out
579}
580
581fn manojo(pal: &Follaje, sem: u32, viento: f32, n: usize, r: f32, espiga: bool) -> Vec<SdfPrim> {
582 let mut out = Vec::with_capacity(n * 2);
583 // La hoja de pasto es VERDE; la caña del trigo, dorada. El color de la brizna
584 // sale del rol, no de «tronco» — que en una herbácea no existe.
585 let brizna = if espiga { pal.tronco } else { pal.hoja };
586 for i in 0..n {
587 let a = i as f32 / n as f32 * PI * 2.0 + hash01(sem, i as u32);
588 let d = 0.10 * hash01(sem + 1, i as u32);
589 let base = Vec3::new(a.cos() * d, 0.0, a.sin() * d);
590 let alto = 0.65 + 0.35 * hash01(sem + 2, i as u32);
591 let punta = base + Vec3::new(a.cos() * 0.16 + viento * 0.14, alto, a.sin() * 0.16);
592 out.push(capsula(base, r, punta, r * 0.4, brizna, r * 0.4));
593 if espiga {
594 // Espiga: fina y alargada, no un globo. La primera versión ponía
595 // `r*1.8` de ancho y el trigal parecía un ramo de piruletas.
596 let tope = punta + Vec3::new(0.0, alto * 0.10, 0.0);
597 out.push(capsula(punta, r * 1.15, tope, r * 0.45, pal.flor, r * 0.5));
598 for k in 0..3 {
599 out.push(decal(
600 punta + Vec3::new(0.0, alto * 0.035 * k as f32, r * 1.0),
601 r * 0.55,
602 tono(pal.flor, 0.78),
603 ));
604 }
605 }
606 }
607 out
608}
609
610fn junco(pal: &Follaje, sem: u32, viento: f32) -> Vec<SdfPrim> {
611 let mut out = Vec::with_capacity(16);
612 for i in 0..6 {
613 let a = i as f32 * 1.05 + hash01(sem, i);
614 let d = 0.09 * hash01(sem + 1, i);
615 let base = Vec3::new(a.cos() * d, 0.0, a.sin() * d);
616 let alto = 0.70 + 0.30 * hash01(sem + 2, i);
617 let punta = base + Vec3::new(viento * 0.12 + a.cos() * 0.05, alto, a.sin() * 0.05);
618 out.push(capsula(base, 0.016, punta, 0.010, pal.tronco, 0.01));
619 if i % 2 == 0 {
620 // Espadaña marrón.
621 out.push(elipsoide(punta - Vec3::new(0.0, alto * 0.10, 0.0), Vec3::new(0.026, alto * 0.11, 0.026), pal.flor, 0.015));
622 }
623 }
624 out
625}
626
627fn lavanda(pal: &Follaje, sem: u32, viento: f32) -> Vec<SdfPrim> {
628 let mut out = Vec::with_capacity(24);
629 for i in 0..7 {
630 let a = i as f32 * 0.9 + hash01(sem, i);
631 let d = 0.12 * hash01(sem + 1, i);
632 let base = Vec3::new(a.cos() * d, 0.0, a.sin() * d);
633 let alto = 0.62 + 0.38 * hash01(sem + 2, i);
634 let punta = base + Vec3::new(viento * 0.10 + a.cos() * 0.06, alto, a.sin() * 0.06);
635 out.push(capsula(base, 0.012, punta, 0.008, pal.tronco, 0.008));
636 // Espiga de flores: bolitas apiladas.
637 for k in 0..4 {
638 let t = 0.72 + 0.09 * k as f32;
639 out.push(esfera(base + (punta - base) * t, 0.030 - 0.004 * k as f32, pal.flor, 0.018));
640 }
641 }
642 out
643}
644
645fn hongo(pal: &Follaje, sem: u32) -> Vec<SdfPrim> {
646 let mut out = Vec::with_capacity(8);
647 let alto = 0.55;
648 out.push(capsula(Vec3::ZERO, 0.10, Vec3::new(0.0, alto, 0.0), 0.085, pal.tronco, 0.04));
649 out.push(elipsoide(Vec3::new(0.0, alto, 0.0), Vec3::new(0.34, 0.24, 0.34), pal.hoja, 0.12));
650 // Laminillas debajo del sombrero.
651 out.push(elipsoide(Vec3::new(0.0, alto - 0.10, 0.0), Vec3::new(0.24, 0.05, 0.24), tono(pal.tronco, 0.92), 0.06));
652 for i in 0..5 {
653 let a = i as f32 / 5.0 * PI * 2.0 + hash01(sem, i);
654 out.push(decal(
655 Vec3::new(a.cos() * 0.18, alto + 0.16, a.sin() * 0.18),
656 0.075,
657 pal.fruto,
658 ));
659 }
660 out
661}
662
663fn tocon(pal: &Follaje, sem: u32) -> Vec<SdfPrim> {
664 let mut out = Vec::with_capacity(8);
665 out.push(capsula(Vec3::ZERO, 0.34, Vec3::new(0.02, 0.62, 0.0), 0.30, pal.tronco, 0.12));
666 // Anillos del corte + musgo.
667 for i in 1..4 {
668 let r = 0.28 * i as f32 / 4.0;
669 out.push(decal(Vec3::new(0.02, 0.62, 0.0) + Vec3::new(r, 0.0, 0.0), 0.045, tono(pal.tronco, 0.75)));
670 out.push(decal(Vec3::new(0.02, 0.62, 0.0) - Vec3::new(r, 0.0, 0.0), 0.045, tono(pal.tronco, 0.75)));
671 }
672 for i in 0..3 {
673 let a = hash01(sem, i) * PI * 2.0;
674 out.push(esfera(Vec3::new(a.cos() * 0.30, 0.42 + 0.12 * hash01(sem + 1, i), a.sin() * 0.30), 0.11, pal.hoja, 0.06));
675 }
676 out
677}
678
679// ============================================================================
680// Suculentas
681// ============================================================================
682
683fn saguaro(pal: &Follaje, sem: u32) -> Vec<SdfPrim> {
684 let mut out = Vec::with_capacity(12);
685 let alto = 1.0;
686 out.push(capsula(Vec3::ZERO, 0.13, Vec3::new(0.0, alto, 0.0), 0.115, pal.hoja, 0.06));
687 // Brazos: suben en L (el saguaro no tiene ramas, tiene codos).
688 for (i, s) in [-1.0f32, 1.0].into_iter().enumerate() {
689 if hash01(sem, i as u32) < 0.25 {
690 continue;
691 }
692 let y = 0.35 + 0.22 * hash01(sem + 1, i as u32);
693 let codo = Vec3::new(s * 0.24, y, 0.0);
694 out.push(capsula(Vec3::new(s * 0.05, y, 0.0), 0.075, codo, 0.070, pal.hoja, 0.05));
695 out.push(capsula(codo, 0.070, codo + Vec3::new(0.0, 0.26 + 0.14 * hash01(sem + 2, i as u32), 0.0), 0.065, pal.hoja, 0.05));
696 }
697 // Costillas y espinas: decals a lo largo.
698 for i in 0..8 {
699 let t = 0.10 + 0.10 * i as f32;
700 out.push(decal(Vec3::new(0.0, t * alto, 0.125), 0.030, tono(pal.hoja, 0.75)));
701 }
702 // Flor en la corona.
703 out.push(esfera(Vec3::new(0.0, alto + 0.02, 0.0), 0.055, pal.flor, 0.03));
704 out
705}
706
707fn nopal(pal: &Follaje, sem: u32) -> Vec<SdfPrim> {
708 let mut out = Vec::with_capacity(12);
709 // Palas: elipsoides finos en Z, apilados en abanico.
710 let base = Vec3::new(0.0, 0.30, 0.0);
711 out.push(elipsoide(base, Vec3::new(0.26, 0.30, 0.075), pal.hoja, 0.10));
712 for i in 0..3 {
713 let a = (i as f32 - 1.0) * 0.9 + hash11(sem, i) * 0.2;
714 let c = base + Vec3::new(a.sin() * 0.30, 0.34 + 0.10 * hash01(sem + 1, i), 0.0);
715 out.push(elipsoide(c, Vec3::new(0.20, 0.24, 0.065), pal.hoja, 0.09));
716 // Tunas en el borde.
717 out.push(esfera(c + Vec3::new(0.0, 0.24, 0.0), 0.045, pal.fruto, 0.02));
718 }
719 for i in 0..10 {
720 let a = hash01(sem + 3, i) * PI * 2.0;
721 out.push(decal(
722 base + Vec3::new(a.cos() * 0.18, a.sin() * 0.20, 0.078),
723 0.018,
724 [0.94, 0.92, 0.80],
725 ));
726 }
727 out
728}
729
730fn agave(pal: &Follaje, sem: u32) -> Vec<SdfPrim> {
731 let mut out = Vec::with_capacity(14);
732 let n = 11;
733 for i in 0..n {
734 let a = i as f32 / n as f32 * PI * 2.0;
735 let alto = 0.75 + 0.25 * hash01(sem, i as u32);
736 let abre = 0.32 + 0.22 * hash01(sem + 1, i as u32);
737 let punta = Vec3::new(a.cos() * abre, alto, a.sin() * abre);
738 out.push(capsula(
739 Vec3::new(a.cos() * 0.04, 0.02, a.sin() * 0.04),
740 0.070,
741 punta,
742 0.008,
743 mezcla(pal.hoja, tono(pal.hoja, 0.85 + 0.25 * hash01(sem + 2, i as u32)), 0.5),
744 0.05,
745 ));
746 out.push(decal(punta, 0.030, tono(pal.tronco, 0.6)));
747 }
748 out
749}
750
751// ============================================================================
752// Flores
753// ============================================================================
754
755/// Cómo se abre el pétalo: plano (margarita, girasol) o en copa (tulipán, amapola).
756#[derive(Debug, Clone, Copy, PartialEq, Eq)]
757enum Petalo {
758 Plano,
759 Copa,
760}
761
762fn tallo(alto: f32, r: f32, viento: f32, sem: u32, pal: &Follaje, hojas: usize, out: &mut Vec<SdfPrim>) -> Vec3 {
763 let curva = hash11(sem, 3) * 0.06;
764 let medio = Vec3::new(curva * 0.5 + viento * alto * 0.06, alto * 0.5, 0.0);
765 let top = Vec3::new(curva + viento * alto * 0.16, alto, 0.0);
766 out.push(capsula(Vec3::ZERO, r * 1.3, medio, r, pal.tronco, r * 0.5));
767 out.push(capsula(medio, r, top, r * 0.9, pal.tronco, r * 0.5));
768 for i in 0..hojas {
769 let s = if i % 2 == 0 { 1.0 } else { -1.0f32 };
770 let t = 0.25 + 0.30 * i as f32;
771 let base = Vec3::new(0.0, alto * t, 0.0);
772 out.push(elipsoide(
773 base + Vec3::new(s * alto * 0.16, alto * 0.04, 0.0),
774 Vec3::new(alto * 0.17, r * 1.1, alto * 0.07),
775 pal.hoja,
776 r * 1.5,
777 ));
778 }
779 top
780}
781
782fn flor(pal: &Follaje, sem: u32, viento: f32, petalos: usize, forma: Petalo, corola: f32) -> Vec<SdfPrim> {
783 let mut out = Vec::with_capacity(petalos + 8);
784 let alto = 0.62;
785 let r = 0.022;
786 let top = tallo(alto, r, viento, sem, pal, 2, &mut out);
787 // Centro.
788 let centro = top + Vec3::new(0.0, corola * 0.12, 0.0);
789 out.push(elipsoide(centro, Vec3::new(corola * 0.34, corola * 0.20, corola * 0.34), pal.fruto, corola * 0.10));
790 for i in 0..petalos {
791 let a = i as f32 / petalos as f32 * PI * 2.0;
792 let dir = Vec3::new(a.cos(), 0.0, a.sin());
793 match forma {
794 Petalo::Plano => {
795 // Pétalo tendido: cápsula chata que sale del centro hacia afuera.
796 let punta = centro + dir * (corola * 1.05) + Vec3::new(0.0, corola * 0.10, 0.0);
797 out.push(capsula(
798 centro + dir * (corola * 0.28),
799 corola * 0.16,
800 punta,
801 corola * 0.10,
802 pal.flor,
803 corola * 0.10,
804 ));
805 }
806 Petalo::Copa => {
807 // Pétalo parado: forma la copa cerrada del tulipán.
808 let punta = centro + dir * (corola * 0.55) + Vec3::new(0.0, corola * 0.95, 0.0);
809 out.push(capsula(
810 centro + dir * (corola * 0.22) - Vec3::new(0.0, corola * 0.10, 0.0),
811 corola * 0.22,
812 punta,
813 corola * 0.16,
814 pal.flor,
815 corola * 0.16,
816 ));
817 }
818 }
819 }
820 if forma == Petalo::Plano {
821 // Semillas del disco (girasol): decals sobre el centro.
822 for i in 0..5 {
823 let a = hash01(sem, i) * PI * 2.0;
824 out.push(decal(
825 centro + Vec3::new(a.cos() * corola * 0.18, corola * 0.20, a.sin() * corola * 0.18),
826 corola * 0.09,
827 tono(pal.fruto, 0.7),
828 ));
829 }
830 }
831 out
832}
833
834fn rosa(pal: &Follaje, sem: u32, viento: f32) -> Vec<SdfPrim> {
835 let mut out = Vec::with_capacity(18);
836 let alto = 0.60;
837 let r = 0.024;
838 let top = tallo(alto, r, viento, sem, pal, 2, &mut out);
839 // Corola: tres anillos de pétalos que se cierran hacia el centro.
840 let c = top + Vec3::new(0.0, 0.05, 0.0);
841 out.push(esfera(c, 0.055, tono(pal.flor, 0.85), 0.03));
842 for anillo in 0..3 {
843 let k = anillo as f32;
844 let n = 5 + anillo * 2;
845 let rad = 0.055 + k * 0.045;
846 for i in 0..n {
847 let a = i as f32 / n as f32 * PI * 2.0 + k * 0.6;
848 out.push(elipsoide(
849 c + Vec3::new(a.cos() * rad, -k * 0.012, a.sin() * rad),
850 Vec3::new(0.050, 0.038 + 0.010 * k, 0.030),
851 mezcla(pal.flor, tono(pal.flor, 1.15 - 0.12 * k), 0.5),
852 0.028,
853 ));
854 }
855 }
856 // Espinas.
857 for i in 0..4 {
858 let t = 0.20 + 0.18 * i as f32;
859 let a = hash01(sem, i) * PI * 2.0;
860 out.push(capsula(
861 Vec3::new(0.0, alto * t, 0.0),
862 r * 0.5,
863 Vec3::new(a.cos() * r * 2.4, alto * t + r, a.sin() * r * 2.4),
864 r * 0.1,
865 pal.tronco,
866 r * 0.2,
867 ));
868 }
869 out
870}
871
872fn orquidea(pal: &Follaje, sem: u32, viento: f32) -> Vec<SdfPrim> {
873 let mut out = Vec::with_capacity(22);
874 let alto = 0.85;
875 // Vara arqueada: la orquídea cuelga, no se para.
876 let n = 4;
877 let pts: Vec<Vec3> = (0..=n)
878 .map(|i| {
879 let t = i as f32 / n as f32;
880 Vec3::new((t * t) * 0.30 + viento * t * 0.08, alto * (t * 0.9 + 0.1 - t * t * 0.25), 0.0)
881 })
882 .collect();
883 cadena(&pts, 0.018, 0.010, pal.tronco, 0.010, &mut out);
884 // Hojas anchas en la base.
885 for s in [-1.0f32, 1.0] {
886 out.push(elipsoide(
887 Vec3::new(s * 0.12, 0.09, 0.0),
888 Vec3::new(0.14, 0.028, 0.055),
889 pal.hoja,
890 0.03,
891 ));
892 }
893 // Flores a lo largo de la vara: tres pétalos + labelo.
894 for i in 1..=3 {
895 let p = pts[i];
896 let lado = if i % 2 == 0 { 1.0 } else { -1.0f32 };
897 let c = p + Vec3::new(lado * 0.035, -0.02, 0.0);
898 for k in 0..4 {
899 let a = k as f32 / 4.0 * PI * 2.0 + hash01(sem, i as u32) * 0.6;
900 out.push(elipsoide(
901 c + Vec3::new(a.cos() * 0.045, a.sin() * 0.045 * 0.6, 0.01),
902 Vec3::new(0.040, 0.032, 0.014),
903 pal.flor,
904 0.020,
905 ));
906 }
907 out.push(esfera(c + Vec3::new(0.0, 0.0, 0.022), 0.020, pal.fruto, 0.010));
908 }
909 out
910}
911
912#[cfg(test)]
913mod tests {
914 use super::*;
915
916 fn geometria(s: &SdfSer) -> usize {
917 s.prims
918 .iter()
919 .filter(|p| !matches!(p.kind, llimphi_3d::SdfKind::DecalEsfera | llimphi_3d::SdfKind::DecalCapsula))
920 .count()
921 }
922
923 #[test]
924 fn toda_la_vegetacion_se_arma() {
925 for f in Forma::ALL {
926 let s = flora_sdf(f, alto_flora(f), 7, 0.0);
927 assert!(geometria(&s) >= 3, "{} tiene {} primitivas", f.label(), geometria(&s));
928 assert!(
929 s.prims.len() <= llimphi_3d::VOXEL_MAX_SER_PRIMS,
930 "{} no entra en el uniform",
931 f.label()
932 );
933 for p in &s.prims {
934 for v in p.a.iter().chain(p.b.iter()).chain([p.ra, p.rb, p.blend].iter()) {
935 assert!(v.is_finite(), "{}: primitiva con {v}", f.label());
936 }
937 }
938 }
939 }
940
941 #[test]
942 fn cada_planta_apoya_en_el_piso_y_mide_lo_pedido() {
943 for f in Forma::ALL {
944 for alto in [0.3f32, 2.0, 12.0] {
945 let s = flora_sdf(f, alto, 3, 0.0);
946 let (lo, hi) = crate::sdf_kit::extension_y(&s.prims);
947 assert!(lo.abs() < alto * 1e-3, "{} apoya en {lo}", f.label());
948 assert!((hi - alto).abs() < alto * 1e-3, "{} mide {hi} de {alto}", f.label());
949 }
950 }
951 }
952
953 /// La semilla tiene que variar la planta: un bosque de árboles idénticos se ve
954 /// como papel pintado. (Las que no varían son las que no deben: el hongo y el
955 /// tocón son casi enteros por forma.)
956 #[test]
957 fn la_semilla_varia_la_planta() {
958 let mut varian = 0;
959 for f in Forma::ALL {
960 let a = flora_sdf(f, 1.0, 1, 0.0);
961 let b = flora_sdf(f, 1.0, 2, 0.0);
962 if a.prims.iter().zip(b.prims.iter()).any(|(x, y)| x.a != y.a || x.ra != y.ra) {
963 varian += 1;
964 }
965 }
966 assert!(varian >= Forma::ALL.len() - 4, "sólo {varian} de {} varían", Forma::ALL.len());
967 }
968
969 /// El viento tiene que doblar la planta, y más arriba que abajo (la base no se
970 /// mueve: está enterrada).
971 #[test]
972 fn el_viento_dobla_la_copa_y_no_la_base() {
973 for f in [Forma::Palmera, Forma::Arbol, Forma::Trigo, Forma::Bambu, Forma::Cipres] {
974 let quieto = flora_sdf(f, 4.0, 5, 0.0);
975 let soplado = flora_sdf(f, 4.0, 5, 1.0);
976 let corrido = |s: &SdfSer, alto: bool| -> f32 {
977 s.prims
978 .iter()
979 .filter(|p| if alto { p.a[1] > 2.5 } else { p.a[1] < 0.6 })
980 .map(|p| p.a[0])
981 .fold(0.0f32, |acc, x| acc + x)
982 };
983 let dif_arriba = (corrido(&soplado, true) - corrido(&quieto, true)).abs();
984 let dif_abajo = (corrido(&soplado, false) - corrido(&quieto, false)).abs();
985 assert!(dif_arriba > 1e-3, "{}: el viento no la dobla", f.label());
986 assert!(
987 dif_arriba > dif_abajo,
988 "{}: se dobla más abajo que arriba ({dif_abajo} vs {dif_arriba})",
989 f.label()
990 );
991 }
992 }
993
994 /// Las leñosas tienen tronco (color de madera abajo) y las herbáceas no: es la
995 /// misma propiedad que ya vigila el catálogo voxel, del lado SDF.
996 #[test]
997 fn las_lenosas_tienen_tronco() {
998 for f in [Forma::Arbol, Forma::Roble, Forma::Ficus, Forma::Palmera, Forma::Cipres, Forma::Baobab] {
999 let s = flora_sdf(f, 6.0, 2, 0.0);
1000 let pal = Follaje::de(f);
1001 let madera = s
1002 .prims
1003 .iter()
1004 .filter(|p| p.a[1] < 1.5 && p.color == pal.tronco)
1005 .count();
1006 assert!(madera >= 1, "{} no tiene tronco leñoso al pie", f.label());
1007 }
1008 }
1009
1010 /// El ficus se distingue de un árbol cualquiera por las raíces aéreas: si el
1011 /// dibujo es el mismo, el catálogo tiene una forma de más.
1012 #[test]
1013 fn el_ficus_no_es_el_arbol_generico() {
1014 let ficus = flora_sdf(Forma::Ficus, 8.0, 4, 0.0);
1015 let arbol = flora_sdf(Forma::Arbol, 8.0, 4, 0.0);
1016 assert!(
1017 ficus.prims.len() > arbol.prims.len(),
1018 "ficus {} vs árbol {}",
1019 ficus.prims.len(),
1020 arbol.prims.len()
1021 );
1022 // Raíces aéreas: cápsulas de madera que llegan al piso lejos del tronco.
1023 let pal = Follaje::de(Forma::Ficus);
1024 let raices = ficus
1025 .prims
1026 .iter()
1027 .filter(|p| p.color == pal.tronco && p.b[1] < 0.4 && p.b[0].abs() > 0.8)
1028 .count();
1029 assert!(raices >= 3, "sólo {raices} raíces aéreas");
1030 }
1031
1032 #[test]
1033 fn la_misma_semilla_da_la_misma_planta() {
1034 for f in [Forma::Palmera, Forma::Ficus, Forma::Girasol, Forma::Agave] {
1035 let a = flora_sdf(f, 3.0, 42, 0.3);
1036 let b = flora_sdf(f, 3.0, 42, 0.3);
1037 assert_eq!(a.prims.len(), b.prims.len());
1038 for (x, y) in a.prims.iter().zip(b.prims.iter()) {
1039 assert_eq!(x.a, y.a);
1040 }
1041 }
1042 }
1043}