//! **La vegetación SDF** — la segunda técnica de graficación aplicada a las plantas: //! cada [`Forma`] armada con primitivas analíticas fundidas por `smin`, la misma //! receta que el ser humanoide y el bestiario. //! //! ## Por qué una planta no es «un árbol» //! //! El catálogo voxel tenía ocho formas y todas las leñosas terminaban siendo //! *tronco + bola*. De cerca eso no es un árbol: es una paleta. Un ficus se //! reconoce por la **copa ancha y baja con raíces aéreas**, un sauce por las //! **ramas que caen**, una palmera por **cómo se arquea la fronda**, un jacarandá //! porque la flor le tapa la hoja. Nada de eso es más geometría: es la MISMA //! cantidad de primitivas puestas donde va. //! //! ## Cómo se dibuja //! //! Todo sale de cinco ladrillos ([`sdf_kit`](crate::sdf_kit)) y de tres ideas: //! 1. **El tronco es una cadena**, no un cilindro: se curva, se afina y se ramifica. //! 2. **La copa son bultos que se funden** (`blend` grande): tres o cuatro //! elipsoides con `smin` leen como una masa de hojas, no como pelotas. //! 3. **La flor y el fruto son decals**: pintan sobre la copa sin costar silueta — //! un jacarandá en flor cuesta lo mismo que uno sin flor. //! //! El **viento** es un parámetro, no una animación aparte: desplaza la copa y las //! frondas en proporción a la altura, que es como se dobla una planta de verdad. use std::f32::consts::PI; use llimphi_3d::glam::Vec3; use llimphi_3d::{SdfPrim, SdfSer}; use crate::sdf_kit::{ asentar, cadena, capsula, decal, decal_linea, elipsoide, esfera, hash01, hash11, mezcla, rgb, tono, }; use crate::worldgen::Forma; /// Paleta de una planta: madera, hoja, flor y fruto. #[derive(Debug, Clone, Copy)] pub struct Follaje { pub tronco: [f32; 3], pub hoja: [f32; 3], pub flor: [f32; 3], pub fruto: [f32; 3], } impl Follaje { /// La paleta propia de cada forma. El bioma puede pisar la hoja con /// [`flora_sdf_col`] — un bosque otoñal o una selva no se distinguen por la /// silueta sino por el verde. pub fn de(f: Forma) -> Self { let (tronco, hoja, flor, fruto) = match f { Forma::Palmera => (0x8A6A42, 0x3E8A4A, 0xE8D060, 0x9A6A2E), Forma::Conifera | Forma::Cipres => (0x6A4A2E, 0x2A5E3E, 0xC8C0A0, 0x6A5A3A), Forma::Ficus => (0x8A7460, 0x2E7A46, 0xE8E0C0, 0x8A2A3A), Forma::Sauce => (0x7A6448, 0x8AAE58, 0xE8E4C0, 0x8A7A50), Forma::Jacaranda => (0x7A6450, 0x4A8A52, 0x8A6ACC, 0x8A7A50), Forma::Baobab => (0x9A8A70, 0x5E8A46, 0xF0E8D0, 0xC8B070), Forma::Roble => (0x6E5236, 0x3E7A38, 0xE8E4C0, 0x8A6A28), Forma::Platanera => (0x7A7A4A, 0x4EA046, 0xE8D050, 0xE0C830), Forma::Bambu => (0x9AA84A, 0x6EA84E, 0xE8E4C0, 0x8A9A50), Forma::Hongo => (0xE0D8C0, 0xB03A30, 0xF2EEE6, 0xF2EEE6), Forma::Tocon => (0x6E5236, 0x4A7A38, 0xE8E4C0, 0x8A6A28), Forma::Trigo => (0xD8B858, 0xC8A840, 0xE8D880, 0xE0C860), Forma::Junco => (0x6A8A4A, 0x5E8A46, 0x8A6A3A, 0x8A6A3A), Forma::Lavanda => (0x6A7A50, 0x6A8A5A, 0x8A6AC8, 0x8A6AC8), Forma::Girasol => (0x5E8A3E, 0x4A7A34, 0xE8C020, 0x6A4A22), Forma::Margarita => (0x5E8A3E, 0x4A7A34, 0xF4F2EA, 0xE8C830), Forma::Tulipan => (0x4E8A44, 0x3E7A38, 0xD8324A, 0xE8C830), Forma::Rosa => (0x4A7A3E, 0x3A6A34, 0xD03A5A, 0xE8C830), Forma::Amapola => (0x6A8A4A, 0x5A7A3E, 0xD82A22, 0x22201E), Forma::Orquidea => (0x4A7A46, 0x3E6A3E, 0xE080C0, 0xE8D860), Forma::Agave => (0x6A8A5E, 0x7AA070, 0xE8E0A0, 0xC8C060), Forma::Nopal | Forma::Columnar => (0x4A7A3E, 0x4E8A46, 0xE8607A, 0xD84A5A), Forma::Flor => (0x5E8A3E, 0x4A7A34, 0xE05A8A, 0xE8C830), Forma::Liana => (0x6A5A3A, 0x3E8A46, 0xE8D060, 0x8A6A2E), // Las herbáceas no tienen madera: si el tronco queda en el marrón del // default, el pasto sale de palitos secos (se vio en la primera hoja). Forma::Pasto => (0x5E8A3E, 0x6EA84A, 0xC8D880, 0x8A9A50), Forma::Helecho => (0x4A7A38, 0x3E8A42, 0xC8D880, 0x8A9A50), Forma::Arbusto => (0x6A5236, 0x4A8A44, 0xE8E4C0, 0x8A6A28), _ => (0x6E5236, 0x4A8A44, 0xE8E4C0, 0x8A6A28), }; Self { tronco: rgb(tronco), hoja: rgb(hoja), flor: rgb(flor), fruto: rgb(fruto), } } } /// La planta armada como ser SDF, a la altura pedida. `semilla` la varía (ninguna /// planta es igual a la de al lado) y `viento` la dobla. pub fn flora_sdf(f: Forma, alto: f32, semilla: u32, viento: f32) -> SdfSer { flora_sdf_col(f, alto, semilla, viento, None) } /// Igual, con el color de hoja del bioma (`None` = el propio de la especie). pub fn flora_sdf_col( f: Forma, alto: f32, semilla: u32, viento: f32, hoja: Option<[f32; 3]>, ) -> SdfSer { let mut pal = Follaje::de(f); if let Some(h) = hoja { pal.hoja = h; } let mut prims = armar(f, &pal, semilla, viento); asentar(&mut prims, alto.max(0.05)); SdfSer { prims, pos: [0.0; 3], yaw: 0.0 } } /// **Altura típica en metros** de cada forma — para que un ficus no mida lo mismo /// que una margarita cuando el mundo las siembra juntas. pub fn alto_flora(f: Forma) -> f32 { match f { Forma::Baobab => 14.0, Forma::Ficus => 12.0, Forma::Roble | Forma::Conifera => 11.0, Forma::Sauce | Forma::Jacaranda => 9.0, Forma::Cipres => 8.5, Forma::Palmera => 8.0, Forma::Arbol => 7.0, Forma::Bambu => 5.0, Forma::Platanera => 3.2, Forma::Liana => 3.0, Forma::Columnar => 2.6, Forma::Nopal => 1.6, Forma::Junco | Forma::Girasol => 1.5, Forma::Arbusto | Forma::Agave => 1.1, Forma::Trigo => 0.9, Forma::Tocon | Forma::Helecho | Forma::Rosa | Forma::Orquidea => 0.7, Forma::Lavanda | Forma::Tulipan | Forma::Amapola => 0.5, Forma::Margarita | Forma::Flor => 0.4, Forma::Hongo => 0.25, Forma::Pasto => 0.3, } } fn armar(f: Forma, pal: &Follaje, sem: u32, viento: f32) -> Vec { match f { Forma::Arbol => arbol(pal, sem, viento, Copa::Bola, 0.42, 0.0, false), Forma::Roble => arbol(pal, sem, viento, Copa::Ancha, 0.38, 0.0, false), Forma::Ficus => arbol(pal, sem, viento, Copa::Domo, 0.34, 0.0, true), Forma::Jacaranda => arbol(pal, sem, viento, Copa::Sombrilla, 0.46, 1.0, false), Forma::Baobab => baobab(pal, sem, viento), Forma::Sauce => sauce(pal, sem, viento), Forma::Conifera => conifera(pal, sem, viento, 4), Forma::Cipres => cipres(pal, sem, viento), Forma::Palmera => palmera(pal, sem, viento, Fronda::Pinnada), Forma::Platanera => palmera(pal, sem, viento, Fronda::Ancha), Forma::Bambu => bambu(pal, sem, viento), Forma::Liana => liana(pal, sem, viento), Forma::Arbusto => arbusto(pal, sem, viento), Forma::Helecho => helecho(pal, sem, viento), Forma::Pasto => manojo(pal, sem, viento, 11, 0.10, false), Forma::Trigo => manojo(pal, sem, viento, 7, 0.06, true), Forma::Junco => junco(pal, sem, viento), Forma::Lavanda => lavanda(pal, sem, viento), Forma::Hongo => hongo(pal, sem), Forma::Tocon => tocon(pal, sem), Forma::Columnar => saguaro(pal, sem), Forma::Nopal => nopal(pal, sem), Forma::Agave => agave(pal, sem), Forma::Flor => flor(pal, sem, viento, 6, Petalo::Plano, 0.30), Forma::Margarita => flor(pal, sem, viento, 12, Petalo::Plano, 0.26), Forma::Girasol => flor(pal, sem, viento, 16, Petalo::Plano, 0.40), Forma::Amapola => flor(pal, sem, viento, 4, Petalo::Copa, 0.34), Forma::Tulipan => flor(pal, sem, viento, 5, Petalo::Copa, 0.30), Forma::Rosa => rosa(pal, sem, viento), Forma::Orquidea => orquidea(pal, sem, viento), } } // ============================================================================ // Árboles // ============================================================================ /// Forma de la copa: lo que distingue a un roble de un ciprés desde lejos. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Copa { /// Bola clásica. Bola, /// Ancha y algo achatada (roble). Ancha, /// Domo muy ancho y bajo (ficus, higuera). Domo, /// Sombrilla: plana y abierta (acacia, jacarandá). Sombrilla, } /// Tronco que sube curvándose, con `n` tramos. Devuelve los puntos. fn tronco(alto: f32, r: f32, curva: f32, sem: u32, viento: f32, pal: &Follaje, out: &mut Vec) -> Vec { let n = 4; let incl = hash11(sem, 1) * curva; let pts: Vec = (0..=n) .map(|i| { let t = i as f32 / n as f32; Vec3::new( incl * t * t * alto * 0.25 + viento * t * t * alto * 0.10, t * alto, (hash11(sem, 2 + i as u32) * 0.05 + incl * 0.3) * t * t * alto * 0.20, ) }) .collect(); for (i, w) in pts.windows(2).enumerate() { let t0 = i as f32 / n as f32; let t1 = (i + 1) as f32 / n as f32; // La base ensancha (raíz al pie): un tronco de radio constante se ve pegado. let rad = |t: f32| r * (1.0 + 0.55 * (1.0 - t).powi(3) - 0.35 * t); out.push(capsula(w[0], rad(t0), w[1], rad(t1), pal.tronco, r * 0.5)); } pts } /// Bultos de copa fundidos alrededor de un centro. Es LA idea de la vegetación: /// tres o cuatro elipsoides con `blend` grande leen como follaje, uno solo como /// una pelota de plástico. #[allow(clippy::too_many_arguments)] fn copa_bultos( centro: Vec3, rx: f32, ry: f32, n: usize, sem: u32, pal: &Follaje, aplasta: f32, out: &mut Vec, ) { // El bulto central es MENOR que los de alrededor: si domina, la copa vuelve a // ser una pelota lisa y los lóbulos no se leen. out.push(elipsoide(centro, Vec3::new(rx * 0.82, ry * 0.86, rx * 0.82), pal.hoja, rx * 0.35)); for i in 0..n { let a = i as f32 / n as f32 * PI * 2.0 + hash01(sem, i as u32) * 0.8; let d = rx * (0.45 + 0.30 * hash01(sem + 3, i as u32)); let c = centro + Vec3::new(a.cos() * d, (hash11(sem + 5, i as u32) * 0.35 - aplasta * 0.25) * ry, a.sin() * d); let k = 0.55 + 0.28 * hash01(sem + 7, i as u32); out.push(elipsoide( c, Vec3::new(rx * k, ry * k * (1.0 - aplasta * 0.25), rx * k), // Tinte por bulto: dos verdes apenas distintos rompen el plano. mezcla(pal.hoja, tono(pal.hoja, 0.82 + 0.30 * hash01(sem + 11, i as u32)), 0.6), // Blend CORTO (0.35, no 0.60): con la unión ancha los bultos se fundían // en una sola masa lisa y la copa perdía el recorte de follaje. rx * 0.35, )); } } fn arbol(pal: &Follaje, sem: u32, viento: f32, copa: Copa, alto_tronco: f32, flores: f32, raices: bool) -> Vec { let mut out = Vec::with_capacity(30); let h = 1.0; let r = 0.045 + 0.02 * hash01(sem, 21); let ptos = tronco(h * alto_tronco, r, 0.35, sem, viento, pal, &mut out); let top = *ptos.last().unwrap(); // Ramas: salen del tercio alto del tronco hacia afuera y arriba. let nramas = match copa { Copa::Domo | Copa::Sombrilla => 5, _ => 4, }; let (rx, ry, aplasta) = match copa { Copa::Bola => (0.34, 0.32, 0.0), Copa::Ancha => (0.44, 0.30, 0.18), // El domo del ficus era un panqueque: `0.26` de alto contra `0.52` de ancho // y con los bultos aplastados encima, la copa leía como una tapa de hongo. Copa::Domo => (0.50, 0.34, 0.25), Copa::Sombrilla => (0.46, 0.24, 0.40), }; let mut puntas = Vec::with_capacity(nramas); for i in 0..nramas { let a = i as f32 / nramas as f32 * PI * 2.0 + hash01(sem, 30 + i as u32); let largo = h * (0.20 + 0.12 * hash01(sem, 40 + i as u32)); let punta = top + Vec3::new( a.cos() * largo * (1.0 + aplasta), largo * (0.85 - aplasta * 0.55), a.sin() * largo * (1.0 + aplasta), ); 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)); puntas.push(punta); } let centro = top + Vec3::new(viento * 0.08, ry * 0.85 + h * 0.10, 0.0); copa_bultos(centro, rx, ry, nramas, sem, pal, aplasta, &mut out); // Flor (jacarandá): decals sobre la copa. No cuestan silueta. if flores > 0.01 { let n = 22; for i in 0..n { let a = hash01(sem + 13, i as u32) * PI * 2.0; let e = hash01(sem + 17, i as u32); 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)) .normalize(); // El punto va sobre el ELIPSOIDE de la copa (semiejes distintos), no // sobre una esfera de radio `rx`: con la copa aplastada, media flor // caía en el aire y no pintaba nada. let p = centro + Vec3::new(d.x * rx, d.y * ry, d.z * rx) * 0.98; out.push(decal(p, rx * (0.20 + 0.14 * e) * flores, pal.flor)); } } // Raíces aéreas del ficus: cuerdas finas de la copa al suelo. Es LA marca de la // higuera y no cuesta más que cuatro cápsulas. if raices { for i in 0..5 { let a = hash01(sem + 23, i as u32) * PI * 2.0; let d = rx * (0.35 + 0.5 * hash01(sem + 29, i as u32)); let arriba = centro + Vec3::new(a.cos() * d, -ry * 0.55, a.sin() * d); // La punta baja HASTA LA TANGENTE del tronco (`−1.55·r`, que es donde // `asentar` va a poner el piso), no hasta el `y = 0` del espacio de // armado: eso dejaba la raíz colgando medio metro en un ficus de ocho. let abajo = Vec3::new(arriba.x * 1.08, -r * 1.15, arriba.z * 1.08); let medio = (arriba + abajo) * 0.5 + Vec3::new(0.0, 0.0, 0.0); cadena(&[arriba, medio, abajo], r * 0.30, r * 0.42, pal.tronco, r * 0.20, &mut out); } } out } fn baobab(pal: &Follaje, sem: u32, viento: f32) -> Vec { let mut out = Vec::with_capacity(20); // El tronco ES el baobab: un tonel que se afina de golpe arriba. let base = Vec3::ZERO; let cuello = Vec3::new(viento * 0.03, 0.62, 0.0); out.push(capsula(base, 0.30, Vec3::new(0.0, 0.30, 0.0), 0.26, pal.tronco, 0.15)); out.push(capsula(Vec3::new(0.0, 0.30, 0.0), 0.26, cuello, 0.13, pal.tronco, 0.12)); for i in 0..6 { let a = i as f32 / 6.0 * PI * 2.0 + hash01(sem, i as u32) * 0.5; let punta = cuello + Vec3::new(a.cos() * 0.30, 0.26 + 0.10 * hash01(sem + 1, i as u32), a.sin() * 0.30); out.push(capsula(cuello, 0.075, punta, 0.02, pal.tronco, 0.05)); out.push(elipsoide(punta, Vec3::new(0.14, 0.09, 0.14), pal.hoja, 0.10)); } out } fn sauce(pal: &Follaje, sem: u32, viento: f32) -> Vec { let mut out = Vec::with_capacity(34); let ptos = tronco(0.52, 0.055, 0.30, sem, viento, pal, &mut out); let top = *ptos.last().unwrap(); copa_bultos(top + Vec3::new(0.0, 0.16, 0.0), 0.34, 0.16, 4, sem, pal, 0.45, &mut out); // Las ramas que LLORAN: cortinas de cadenas que caen casi hasta el piso. let n = 14; for i in 0..n { let a = i as f32 / n as f32 * PI * 2.0 + hash01(sem + 9, i as u32) * 0.3; let d = 0.32 * (0.55 + 0.45 * hash01(sem + 3, i as u32)); let arriba = top + Vec3::new(a.cos() * d, 0.20, a.sin() * d); let caida = 0.34 + 0.24 * hash01(sem + 5, i as u32); let pts = [ arriba, arriba + Vec3::new(a.cos() * d * 0.35 + viento * 0.05, -caida * 0.5, a.sin() * d * 0.35), arriba + Vec3::new(a.cos() * d * 0.45 + viento * 0.12, -caida, a.sin() * d * 0.45), ]; cadena(&pts, 0.060, 0.028, pal.hoja, 0.045, &mut out); } out } fn conifera(pal: &Follaje, sem: u32, viento: f32, tiers: usize) -> Vec { let mut out = Vec::with_capacity(14); out.push(capsula(Vec3::ZERO, 0.055, Vec3::new(0.0, 0.30, 0.0), 0.04, pal.tronco, 0.03)); for i in 0..tiers { let t = i as f32 / tiers as f32; let y0 = 0.16 + t * 0.66; let alto = 0.30 * (1.0 - t * 0.35); let r = 0.30 * (1.0 - t * 0.72) + 0.03; let dx = viento * (y0 * 0.10); out.push(capsula( Vec3::new(dx, y0, 0.0), r, Vec3::new(dx * 1.4, y0 + alto, 0.0), r * 0.15, mezcla(pal.hoja, tono(pal.hoja, 0.85 + 0.2 * hash01(sem, i as u32)), 0.5), r * 0.25, )); } out } fn cipres(pal: &Follaje, sem: u32, viento: f32) -> Vec { let mut out = Vec::with_capacity(8); out.push(capsula(Vec3::ZERO, 0.05, Vec3::new(0.0, 0.14, 0.0), 0.045, pal.tronco, 0.03)); // Columna: tres elipsoides apilados con blend grande = una llama verde. for i in 0..3 { let t = i as f32 / 3.0; let y = 0.24 + t * 0.52; out.push(elipsoide( Vec3::new(viento * y * 0.10, y, 0.0), Vec3::new(0.16 * (1.0 - t * 0.45), 0.24 * (1.0 - t * 0.12), 0.16 * (1.0 - t * 0.45)), mezcla(pal.hoja, tono(pal.hoja, 0.88 + 0.18 * hash01(sem, i as u32)), 0.5), 0.14, )); } out.push(capsula( Vec3::new(viento * 0.09, 0.80, 0.0), 0.09, Vec3::new(viento * 0.12, 1.0, 0.0), 0.005, pal.hoja, 0.06, )); out } /// Tipo de fronda: la pinnada de la palmera (larga y arqueada) o la hoja ANCHA del /// banano (una lámina entera). #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Fronda { Pinnada, Ancha, } fn palmera(pal: &Follaje, sem: u32, viento: f32, tipo: Fronda) -> Vec { let mut out = Vec::with_capacity(34); // Tronco ALTO: en la primera hoja la palmera leía como una araña sobre un // palito porque la corona pesaba más que el fuste. let alto = if tipo == Fronda::Pinnada { 0.86 } else { 0.34 }; let r = if tipo == Fronda::Pinnada { 0.035 } else { 0.075 }; // Tronco arqueado: la palmera se reconoce por la curva, no por el largo. let curva = (0.10 + 0.10 * hash01(sem, 1)) * if hash01(sem, 2) > 0.5 { 1.0 } else { -1.0 }; let n = 5; let pts: Vec = (0..=n) .map(|i| { let t = i as f32 / n as f32; Vec3::new((curva + viento * 0.12) * t * t, t * alto, 0.0) }) .collect(); for (i, w) in pts.windows(2).enumerate() { let t = i as f32 / n as f32; out.push(capsula(w[0], r * (1.5 - 0.5 * t), w[1], r * (1.4 - 0.5 * t), pal.tronco, r * 0.4)); } let top = *pts.last().unwrap(); if tipo == Fronda::Pinnada { // Anillos del tronco: decals baratos que cuentan la especie. for i in 1..6 { let t = i as f32 / 6.0; let c = Vec3::new((curva + viento * 0.12) * t * t, t * alto, 0.0); 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))); } // Cocos. for i in 0..3 { let a = i as f32 * 2.1; 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)); } } let nf = if tipo == Fronda::Pinnada { 10 } else { 6 }; for i in 0..nf { let a = i as f32 / nf as f32 * PI * 2.0 + hash01(sem, 50 + i as u32) * 0.4; let largo = if tipo == Fronda::Pinnada { 0.40 } else { 0.46 }; // Frondas ANCHAS: una fronda de 0.022 de radio es un alambre, y diez // alambres son una araña. La hoja del banano es una lámina entera. let ancho = if tipo == Fronda::Pinnada { 0.040 } else { 0.115 }; let dir = Vec3::new(a.cos(), 0.0, a.sin()); // Dos tramos: sube y se arquea hacia abajo. Una fronda recta parece un palo. let medio = top + dir * (largo * 0.55) + Vec3::new(viento * 0.05, largo * 0.36, 0.0); 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); let col = mezcla(pal.hoja, tono(pal.hoja, 0.85 + 0.25 * hash01(sem, 70 + i as u32)), 0.5); out.push(capsula(top, ancho * 1.2, medio, ancho, col, ancho * 0.8)); out.push(capsula(medio, ancho, punta, ancho * 0.5, col, ancho * 0.8)); if tipo == Fronda::Pinnada { // Folíolos: dos líneas pintadas a lo largo — la fronda se lee peinada. out.push(decal_linea(medio, punta, ancho * 1.9, tono(col, 0.86))); } } out } fn bambu(pal: &Follaje, sem: u32, viento: f32) -> Vec { let mut out = Vec::with_capacity(30); for c in 0..4 { let dx = (hash11(sem, c) * 0.09, hash11(sem + 1, c) * 0.09); let alto = 0.72 + 0.28 * hash01(sem + 2, c); let r = 0.020 + 0.008 * hash01(sem + 3, c); let base = Vec3::new(dx.0, 0.0, dx.1); let punta = base + Vec3::new(viento * 0.12 * alto, alto, 0.0); out.push(capsula(base, r, punta, r * 0.8, pal.tronco, r * 0.3)); // Nudos. for k in 1..5 { let t = k as f32 / 5.0; out.push(esfera(base + (punta - base) * t, r * 1.25, tono(pal.tronco, 0.8), r * 0.4)); } // Hojas: lanzas cortas cerca de la punta. for k in 0..3 { let t = 0.62 + 0.14 * k as f32; let a = hash01(sem + 5, c * 8 + k) * PI * 2.0; let p0 = base + (punta - base) * t; out.push(capsula( p0, r * 0.8, p0 + Vec3::new(a.cos() * 0.11, 0.05, a.sin() * 0.11), r * 0.15, pal.hoja, r * 0.5, )); } } out } fn liana(pal: &Follaje, sem: u32, viento: f32) -> Vec { let mut out = Vec::with_capacity(22); // Cuelga desde arriba: se arma de 1 a 0 y `asentar` la deja apoyada. let n = 7; let pts: Vec = (0..=n) .map(|i| { let t = i as f32 / n as f32; Vec3::new( (t * PI * 1.6).sin() * 0.06 + viento * (1.0 - t) * 0.10, t, (t * PI * 1.1).cos() * 0.04, ) }) .collect(); cadena(&pts, 0.028, 0.042, pal.tronco, 0.018, &mut out); for i in 1..n { let p0 = pts[i]; for k in 0..2 { let a = hash01(sem, i as u32 * 2 + k) * PI * 2.0; out.push(elipsoide( p0 + Vec3::new(a.cos() * 0.10, -0.02, a.sin() * 0.10), Vec3::new(0.085, 0.016, 0.062), pal.hoja, 0.04, )); } } out } // ============================================================================ // Matas, cañas y hongos // ============================================================================ fn arbusto(pal: &Follaje, sem: u32, viento: f32) -> Vec { let mut out = Vec::with_capacity(10); copa_bultos( Vec3::new(viento * 0.05, 0.45, 0.0), 0.42, 0.40, 5, sem, pal, 0.15, &mut out, ); // Ramitas asomando abajo. for i in 0..3 { let a = i as f32 * 2.1 + hash01(sem, 9); out.push(capsula( Vec3::new(a.cos() * 0.05, 0.0, a.sin() * 0.05), 0.020, Vec3::new(a.cos() * 0.10, 0.28, a.sin() * 0.10), 0.012, pal.tronco, 0.012, )); } out } fn helecho(pal: &Follaje, sem: u32, viento: f32) -> Vec { let mut out = Vec::with_capacity(24); let n = 7; for i in 0..n { let a = i as f32 / n as f32 * PI * 2.0 + hash01(sem, i as u32) * 0.3; let largo = 0.55 + 0.30 * hash01(sem + 1, i as u32); let dir = Vec3::new(a.cos(), 0.0, a.sin()); let medio = dir * (largo * 0.42) + Vec3::new(viento * 0.04, largo * 0.68, 0.0); let punta = dir * (largo * 0.92) + Vec3::new(viento * 0.09, largo * 0.42, 0.0); let col = mezcla(pal.hoja, tono(pal.hoja, 0.82 + 0.3 * hash01(sem + 2, i as u32)), 0.5); out.push(capsula(Vec3::new(0.0, 0.04, 0.0), 0.022, medio, 0.030, col, 0.02)); out.push(capsula(medio, 0.030, punta, 0.006, col, 0.02)); // Folíolos pintados: el peine de la fronda sin geometría. out.push(decal_linea(medio, punta, 0.045, tono(col, 0.88))); } out } fn manojo(pal: &Follaje, sem: u32, viento: f32, n: usize, r: f32, espiga: bool) -> Vec { let mut out = Vec::with_capacity(n * 2); // La hoja de pasto es VERDE; la caña del trigo, dorada. El color de la brizna // sale del rol, no de «tronco» — que en una herbácea no existe. let brizna = if espiga { pal.tronco } else { pal.hoja }; for i in 0..n { let a = i as f32 / n as f32 * PI * 2.0 + hash01(sem, i as u32); let d = 0.10 * hash01(sem + 1, i as u32); let base = Vec3::new(a.cos() * d, 0.0, a.sin() * d); let alto = 0.65 + 0.35 * hash01(sem + 2, i as u32); let punta = base + Vec3::new(a.cos() * 0.16 + viento * 0.14, alto, a.sin() * 0.16); out.push(capsula(base, r, punta, r * 0.4, brizna, r * 0.4)); if espiga { // Espiga: fina y alargada, no un globo. La primera versión ponía // `r*1.8` de ancho y el trigal parecía un ramo de piruletas. let tope = punta + Vec3::new(0.0, alto * 0.10, 0.0); out.push(capsula(punta, r * 1.15, tope, r * 0.45, pal.flor, r * 0.5)); for k in 0..3 { out.push(decal( punta + Vec3::new(0.0, alto * 0.035 * k as f32, r * 1.0), r * 0.55, tono(pal.flor, 0.78), )); } } } out } fn junco(pal: &Follaje, sem: u32, viento: f32) -> Vec { let mut out = Vec::with_capacity(16); for i in 0..6 { let a = i as f32 * 1.05 + hash01(sem, i); let d = 0.09 * hash01(sem + 1, i); let base = Vec3::new(a.cos() * d, 0.0, a.sin() * d); let alto = 0.70 + 0.30 * hash01(sem + 2, i); let punta = base + Vec3::new(viento * 0.12 + a.cos() * 0.05, alto, a.sin() * 0.05); out.push(capsula(base, 0.016, punta, 0.010, pal.tronco, 0.01)); if i % 2 == 0 { // Espadaña marrón. 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)); } } out } fn lavanda(pal: &Follaje, sem: u32, viento: f32) -> Vec { let mut out = Vec::with_capacity(24); for i in 0..7 { let a = i as f32 * 0.9 + hash01(sem, i); let d = 0.12 * hash01(sem + 1, i); let base = Vec3::new(a.cos() * d, 0.0, a.sin() * d); let alto = 0.62 + 0.38 * hash01(sem + 2, i); let punta = base + Vec3::new(viento * 0.10 + a.cos() * 0.06, alto, a.sin() * 0.06); out.push(capsula(base, 0.012, punta, 0.008, pal.tronco, 0.008)); // Espiga de flores: bolitas apiladas. for k in 0..4 { let t = 0.72 + 0.09 * k as f32; out.push(esfera(base + (punta - base) * t, 0.030 - 0.004 * k as f32, pal.flor, 0.018)); } } out } fn hongo(pal: &Follaje, sem: u32) -> Vec { let mut out = Vec::with_capacity(8); let alto = 0.55; out.push(capsula(Vec3::ZERO, 0.10, Vec3::new(0.0, alto, 0.0), 0.085, pal.tronco, 0.04)); out.push(elipsoide(Vec3::new(0.0, alto, 0.0), Vec3::new(0.34, 0.24, 0.34), pal.hoja, 0.12)); // Laminillas debajo del sombrero. 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)); for i in 0..5 { let a = i as f32 / 5.0 * PI * 2.0 + hash01(sem, i); out.push(decal( Vec3::new(a.cos() * 0.18, alto + 0.16, a.sin() * 0.18), 0.075, pal.fruto, )); } out } fn tocon(pal: &Follaje, sem: u32) -> Vec { let mut out = Vec::with_capacity(8); out.push(capsula(Vec3::ZERO, 0.34, Vec3::new(0.02, 0.62, 0.0), 0.30, pal.tronco, 0.12)); // Anillos del corte + musgo. for i in 1..4 { let r = 0.28 * i as f32 / 4.0; 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))); 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))); } for i in 0..3 { let a = hash01(sem, i) * PI * 2.0; 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)); } out } // ============================================================================ // Suculentas // ============================================================================ fn saguaro(pal: &Follaje, sem: u32) -> Vec { let mut out = Vec::with_capacity(12); let alto = 1.0; out.push(capsula(Vec3::ZERO, 0.13, Vec3::new(0.0, alto, 0.0), 0.115, pal.hoja, 0.06)); // Brazos: suben en L (el saguaro no tiene ramas, tiene codos). for (i, s) in [-1.0f32, 1.0].into_iter().enumerate() { if hash01(sem, i as u32) < 0.25 { continue; } let y = 0.35 + 0.22 * hash01(sem + 1, i as u32); let codo = Vec3::new(s * 0.24, y, 0.0); out.push(capsula(Vec3::new(s * 0.05, y, 0.0), 0.075, codo, 0.070, pal.hoja, 0.05)); 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)); } // Costillas y espinas: decals a lo largo. for i in 0..8 { let t = 0.10 + 0.10 * i as f32; out.push(decal(Vec3::new(0.0, t * alto, 0.125), 0.030, tono(pal.hoja, 0.75))); } // Flor en la corona. out.push(esfera(Vec3::new(0.0, alto + 0.02, 0.0), 0.055, pal.flor, 0.03)); out } fn nopal(pal: &Follaje, sem: u32) -> Vec { let mut out = Vec::with_capacity(12); // Palas: elipsoides finos en Z, apilados en abanico. let base = Vec3::new(0.0, 0.30, 0.0); out.push(elipsoide(base, Vec3::new(0.26, 0.30, 0.075), pal.hoja, 0.10)); for i in 0..3 { let a = (i as f32 - 1.0) * 0.9 + hash11(sem, i) * 0.2; let c = base + Vec3::new(a.sin() * 0.30, 0.34 + 0.10 * hash01(sem + 1, i), 0.0); out.push(elipsoide(c, Vec3::new(0.20, 0.24, 0.065), pal.hoja, 0.09)); // Tunas en el borde. out.push(esfera(c + Vec3::new(0.0, 0.24, 0.0), 0.045, pal.fruto, 0.02)); } for i in 0..10 { let a = hash01(sem + 3, i) * PI * 2.0; out.push(decal( base + Vec3::new(a.cos() * 0.18, a.sin() * 0.20, 0.078), 0.018, [0.94, 0.92, 0.80], )); } out } fn agave(pal: &Follaje, sem: u32) -> Vec { let mut out = Vec::with_capacity(14); let n = 11; for i in 0..n { let a = i as f32 / n as f32 * PI * 2.0; let alto = 0.75 + 0.25 * hash01(sem, i as u32); let abre = 0.32 + 0.22 * hash01(sem + 1, i as u32); let punta = Vec3::new(a.cos() * abre, alto, a.sin() * abre); out.push(capsula( Vec3::new(a.cos() * 0.04, 0.02, a.sin() * 0.04), 0.070, punta, 0.008, mezcla(pal.hoja, tono(pal.hoja, 0.85 + 0.25 * hash01(sem + 2, i as u32)), 0.5), 0.05, )); out.push(decal(punta, 0.030, tono(pal.tronco, 0.6))); } out } // ============================================================================ // Flores // ============================================================================ /// Cómo se abre el pétalo: plano (margarita, girasol) o en copa (tulipán, amapola). #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Petalo { Plano, Copa, } fn tallo(alto: f32, r: f32, viento: f32, sem: u32, pal: &Follaje, hojas: usize, out: &mut Vec) -> Vec3 { let curva = hash11(sem, 3) * 0.06; let medio = Vec3::new(curva * 0.5 + viento * alto * 0.06, alto * 0.5, 0.0); let top = Vec3::new(curva + viento * alto * 0.16, alto, 0.0); out.push(capsula(Vec3::ZERO, r * 1.3, medio, r, pal.tronco, r * 0.5)); out.push(capsula(medio, r, top, r * 0.9, pal.tronco, r * 0.5)); for i in 0..hojas { let s = if i % 2 == 0 { 1.0 } else { -1.0f32 }; let t = 0.25 + 0.30 * i as f32; let base = Vec3::new(0.0, alto * t, 0.0); out.push(elipsoide( base + Vec3::new(s * alto * 0.16, alto * 0.04, 0.0), Vec3::new(alto * 0.17, r * 1.1, alto * 0.07), pal.hoja, r * 1.5, )); } top } fn flor(pal: &Follaje, sem: u32, viento: f32, petalos: usize, forma: Petalo, corola: f32) -> Vec { let mut out = Vec::with_capacity(petalos + 8); let alto = 0.62; let r = 0.022; let top = tallo(alto, r, viento, sem, pal, 2, &mut out); // Centro. let centro = top + Vec3::new(0.0, corola * 0.12, 0.0); out.push(elipsoide(centro, Vec3::new(corola * 0.34, corola * 0.20, corola * 0.34), pal.fruto, corola * 0.10)); for i in 0..petalos { let a = i as f32 / petalos as f32 * PI * 2.0; let dir = Vec3::new(a.cos(), 0.0, a.sin()); match forma { Petalo::Plano => { // Pétalo tendido: cápsula chata que sale del centro hacia afuera. let punta = centro + dir * (corola * 1.05) + Vec3::new(0.0, corola * 0.10, 0.0); out.push(capsula( centro + dir * (corola * 0.28), corola * 0.16, punta, corola * 0.10, pal.flor, corola * 0.10, )); } Petalo::Copa => { // Pétalo parado: forma la copa cerrada del tulipán. let punta = centro + dir * (corola * 0.55) + Vec3::new(0.0, corola * 0.95, 0.0); out.push(capsula( centro + dir * (corola * 0.22) - Vec3::new(0.0, corola * 0.10, 0.0), corola * 0.22, punta, corola * 0.16, pal.flor, corola * 0.16, )); } } } if forma == Petalo::Plano { // Semillas del disco (girasol): decals sobre el centro. for i in 0..5 { let a = hash01(sem, i) * PI * 2.0; out.push(decal( centro + Vec3::new(a.cos() * corola * 0.18, corola * 0.20, a.sin() * corola * 0.18), corola * 0.09, tono(pal.fruto, 0.7), )); } } out } fn rosa(pal: &Follaje, sem: u32, viento: f32) -> Vec { let mut out = Vec::with_capacity(18); let alto = 0.60; let r = 0.024; let top = tallo(alto, r, viento, sem, pal, 2, &mut out); // Corola: tres anillos de pétalos que se cierran hacia el centro. let c = top + Vec3::new(0.0, 0.05, 0.0); out.push(esfera(c, 0.055, tono(pal.flor, 0.85), 0.03)); for anillo in 0..3 { let k = anillo as f32; let n = 5 + anillo * 2; let rad = 0.055 + k * 0.045; for i in 0..n { let a = i as f32 / n as f32 * PI * 2.0 + k * 0.6; out.push(elipsoide( c + Vec3::new(a.cos() * rad, -k * 0.012, a.sin() * rad), Vec3::new(0.050, 0.038 + 0.010 * k, 0.030), mezcla(pal.flor, tono(pal.flor, 1.15 - 0.12 * k), 0.5), 0.028, )); } } // Espinas. for i in 0..4 { let t = 0.20 + 0.18 * i as f32; let a = hash01(sem, i) * PI * 2.0; out.push(capsula( Vec3::new(0.0, alto * t, 0.0), r * 0.5, Vec3::new(a.cos() * r * 2.4, alto * t + r, a.sin() * r * 2.4), r * 0.1, pal.tronco, r * 0.2, )); } out } fn orquidea(pal: &Follaje, sem: u32, viento: f32) -> Vec { let mut out = Vec::with_capacity(22); let alto = 0.85; // Vara arqueada: la orquídea cuelga, no se para. let n = 4; let pts: Vec = (0..=n) .map(|i| { let t = i as f32 / n as f32; Vec3::new((t * t) * 0.30 + viento * t * 0.08, alto * (t * 0.9 + 0.1 - t * t * 0.25), 0.0) }) .collect(); cadena(&pts, 0.018, 0.010, pal.tronco, 0.010, &mut out); // Hojas anchas en la base. for s in [-1.0f32, 1.0] { out.push(elipsoide( Vec3::new(s * 0.12, 0.09, 0.0), Vec3::new(0.14, 0.028, 0.055), pal.hoja, 0.03, )); } // Flores a lo largo de la vara: tres pétalos + labelo. for i in 1..=3 { let p = pts[i]; let lado = if i % 2 == 0 { 1.0 } else { -1.0f32 }; let c = p + Vec3::new(lado * 0.035, -0.02, 0.0); for k in 0..4 { let a = k as f32 / 4.0 * PI * 2.0 + hash01(sem, i as u32) * 0.6; out.push(elipsoide( c + Vec3::new(a.cos() * 0.045, a.sin() * 0.045 * 0.6, 0.01), Vec3::new(0.040, 0.032, 0.014), pal.flor, 0.020, )); } out.push(esfera(c + Vec3::new(0.0, 0.0, 0.022), 0.020, pal.fruto, 0.010)); } out } #[cfg(test)] mod tests { use super::*; fn geometria(s: &SdfSer) -> usize { s.prims .iter() .filter(|p| !matches!(p.kind, llimphi_3d::SdfKind::DecalEsfera | llimphi_3d::SdfKind::DecalCapsula)) .count() } #[test] fn toda_la_vegetacion_se_arma() { for f in Forma::ALL { let s = flora_sdf(f, alto_flora(f), 7, 0.0); assert!(geometria(&s) >= 3, "{} tiene {} primitivas", f.label(), geometria(&s)); assert!( s.prims.len() <= llimphi_3d::VOXEL_MAX_SER_PRIMS, "{} no entra en el uniform", f.label() ); for p in &s.prims { for v in p.a.iter().chain(p.b.iter()).chain([p.ra, p.rb, p.blend].iter()) { assert!(v.is_finite(), "{}: primitiva con {v}", f.label()); } } } } #[test] fn cada_planta_apoya_en_el_piso_y_mide_lo_pedido() { for f in Forma::ALL { for alto in [0.3f32, 2.0, 12.0] { let s = flora_sdf(f, alto, 3, 0.0); let (lo, hi) = crate::sdf_kit::extension_y(&s.prims); assert!(lo.abs() < alto * 1e-3, "{} apoya en {lo}", f.label()); assert!((hi - alto).abs() < alto * 1e-3, "{} mide {hi} de {alto}", f.label()); } } } /// La semilla tiene que variar la planta: un bosque de árboles idénticos se ve /// como papel pintado. (Las que no varían son las que no deben: el hongo y el /// tocón son casi enteros por forma.) #[test] fn la_semilla_varia_la_planta() { let mut varian = 0; for f in Forma::ALL { let a = flora_sdf(f, 1.0, 1, 0.0); let b = flora_sdf(f, 1.0, 2, 0.0); if a.prims.iter().zip(b.prims.iter()).any(|(x, y)| x.a != y.a || x.ra != y.ra) { varian += 1; } } assert!(varian >= Forma::ALL.len() - 4, "sólo {varian} de {} varían", Forma::ALL.len()); } /// El viento tiene que doblar la planta, y más arriba que abajo (la base no se /// mueve: está enterrada). #[test] fn el_viento_dobla_la_copa_y_no_la_base() { for f in [Forma::Palmera, Forma::Arbol, Forma::Trigo, Forma::Bambu, Forma::Cipres] { let quieto = flora_sdf(f, 4.0, 5, 0.0); let soplado = flora_sdf(f, 4.0, 5, 1.0); let corrido = |s: &SdfSer, alto: bool| -> f32 { s.prims .iter() .filter(|p| if alto { p.a[1] > 2.5 } else { p.a[1] < 0.6 }) .map(|p| p.a[0]) .fold(0.0f32, |acc, x| acc + x) }; let dif_arriba = (corrido(&soplado, true) - corrido(&quieto, true)).abs(); let dif_abajo = (corrido(&soplado, false) - corrido(&quieto, false)).abs(); assert!(dif_arriba > 1e-3, "{}: el viento no la dobla", f.label()); assert!( dif_arriba > dif_abajo, "{}: se dobla más abajo que arriba ({dif_abajo} vs {dif_arriba})", f.label() ); } } /// Las leñosas tienen tronco (color de madera abajo) y las herbáceas no: es la /// misma propiedad que ya vigila el catálogo voxel, del lado SDF. #[test] fn las_lenosas_tienen_tronco() { for f in [Forma::Arbol, Forma::Roble, Forma::Ficus, Forma::Palmera, Forma::Cipres, Forma::Baobab] { let s = flora_sdf(f, 6.0, 2, 0.0); let pal = Follaje::de(f); let madera = s .prims .iter() .filter(|p| p.a[1] < 1.5 && p.color == pal.tronco) .count(); assert!(madera >= 1, "{} no tiene tronco leñoso al pie", f.label()); } } /// El ficus se distingue de un árbol cualquiera por las raíces aéreas: si el /// dibujo es el mismo, el catálogo tiene una forma de más. #[test] fn el_ficus_no_es_el_arbol_generico() { let ficus = flora_sdf(Forma::Ficus, 8.0, 4, 0.0); let arbol = flora_sdf(Forma::Arbol, 8.0, 4, 0.0); assert!( ficus.prims.len() > arbol.prims.len(), "ficus {} vs árbol {}", ficus.prims.len(), arbol.prims.len() ); // Raíces aéreas: cápsulas de madera que llegan al piso lejos del tronco. let pal = Follaje::de(Forma::Ficus); let raices = ficus .prims .iter() .filter(|p| p.color == pal.tronco && p.b[1] < 0.4 && p.b[0].abs() > 0.8) .count(); assert!(raices >= 3, "sólo {raices} raíces aéreas"); } #[test] fn la_misma_semilla_da_la_misma_planta() { for f in [Forma::Palmera, Forma::Ficus, Forma::Girasol, Forma::Agave] { let a = flora_sdf(f, 3.0, 42, 0.3); let b = flora_sdf(f, 3.0, 42, 0.3); assert_eq!(a.prims.len(), b.prims.len()); for (x, y) in a.prims.iter().zip(b.prims.iter()) { assert_eq!(x.a, y.a); } } } }