tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ main llimphi-voxel/src/flora_sdf.rs Rust · 1043 líneas · 41.8 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0) b575c63f1 · Sergio ·

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}