//! **El bestiario SDF** — la segunda técnica de graficación aplicada a los animales: //! cada [`Bicho`] armado con primitivas analíticas fundidas por `smin`, igual que el //! ser humanoide de [`ser_sdf`](crate::ser_sdf) y con la misma respuesta de luz. //! //! ## Por qué existe //! //! La fauna del mundo se estampaba en voxels ([`stamp_bicho`](crate::stamp_bicho)): //! cinco o seis cubos por animal, perfectos para poblar un bioma a la distancia y //! **imposibles de mirar de cerca**. Cuando la gente pasó a ser SDF, un rebaño de //! cajas al lado de una persona de silueta lisa se veía como dos juegos distintos //! pegados con cinta. Esto cierra esa brecha: **una vaca cerca es analítica; a //! doscientos metros sigue siendo la estampa voxel** (`Bicho::silueta_voxel`). //! //! ## Cómo se escribe una especie //! //! No se modela: se describe. Toda la geometría sale de la //! [`Receta`](crate::Receta) — proporciones con nombre — y de cinco constructores, //! uno por familia morfológica. Una especie nueva es una fila de la tabla de //! [`receta`](crate::receta), no un archivo nuevo. //! //! ## Convenciones //! //! Espacio LOCAL: patas en `y = 0`, centrado en X/Z, **mirando `+Z`**. El ensamble //! se arma en unidades cómodas para las fórmulas y al final se **asienta y escala** //! a la altura pedida ([`asentar`](crate::sdf_kit::asentar)) — así una jirafa y una //! hormiga se escriben con los mismos rangos de números. use std::f32::consts::PI; use llimphi_3d::glam::Vec3; use llimphi_3d::{SdfPrim, SdfSer}; use crate::bicho::Bicho; use crate::bicho_receta::{ alto_tipico, receta, Ala, AlaBicho, Ave, ColaAve, Cola, Cresta, Cuadrupedo, Cuerno, Insecto, Nadador, Oreja, Patron, Pico, Pinta, Receta, Reptante, }; use crate::sdf_kit::{ asentar, cadena, capsula, decal, decal_linea, elipsoide, esfera, hash01, hash11, mezcla, ojo, rot_x, rot_y, tono, }; use crate::ser_sdf::DetalleSer; /// **Qué está haciendo el bicho.** No es un catálogo de clips como el de la gente /// ([`Clip`](crate::Clip)): un animal tiene menos poses y las que importan son /// las que se ven en un mundo — pastar, caminar, estar alerta. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Andanza { /// Quieto, respirando y moviendo la cola. Quieto, /// Caminando: patas en oposición diagonal. Caminar, /// Al trote/galope: más amplitud, más rebote, cuerpo inclinado. Trotar, /// Pastando/comiendo: la cabeza baja al suelo. Comer, /// Alerta: cabeza y orejas arriba, cuerpo tenso. Alerta, /// En el aire (aves e insectos): alas batiendo, patas recogidas. Volar, /// Nadando (cetáceos, peces, patos): el cuerpo ondula. Nadar, /// Saltando (canguro, rana): patas plegadas y cuerpo despegado. Saltar, } impl Andanza { /// Amplitud del balanceo de las patas y del rebote del cuerpo. fn ritmo(self) -> (f32, f32) { match self { Andanza::Quieto | Andanza::Alerta => (0.0, 0.004), Andanza::Caminar => (0.34, 0.018), Andanza::Trotar => (0.62, 0.045), Andanza::Comer => (0.0, 0.006), Andanza::Volar | Andanza::Nadar => (0.10, 0.020), Andanza::Saltar => (0.0, 0.030), } } } /// El bicho armado como ser SDF, a su [`alto_tipico`], quieto. pub fn bicho_sdf(b: Bicho) -> SdfSer { bicho_sdf_lod(b, alto_tipico(b), Andanza::Quieto, 0.0, DetalleSer::Hero) } /// El bicho a la altura, andanza y fase pedidas, con todo el detalle. pub fn bicho_sdf_con(b: Bicho, alto: f32, andanza: Andanza, fase: f32) -> SdfSer { bicho_sdf_lod(b, alto, andanza, fase, DetalleSer::Hero) } /// El bicho con el nivel de detalle elegido. `Medio` suelta las chispas de los ojos /// y ralea el patrón; `Lejos` se queda con la silueta y dos puntos por ojos — a esa /// distancia lo demás es sub-píxel y las primitivas son el recurso escaso. pub fn bicho_sdf_lod( b: Bicho, alto: f32, andanza: Andanza, fase: f32, detalle: DetalleSer, ) -> SdfSer { let mut prims = match receta(b) { Receta::Cuatro(c) => cuadrupedo(&c, andanza, fase, detalle), Receta::Ave(a) => ave(&a, andanza, fase, detalle), Receta::Reptante(r) => reptante(&r, andanza, fase, detalle), Receta::Nadador(n) => nadador(&n, andanza, fase, detalle), Receta::Insecto(i) => insecto(&i, andanza, fase, detalle), }; asentar(&mut prims, alto.max(0.02)); SdfSer { prims, pos: [0.0; 3], yaw: 0.0 } } // ============================================================================ // Piezas comunes // ============================================================================ /// El ojo de un animal: el iris manda. Si la pinta declara esclera (primates, aves /// de dibujo) va el blanco; si el color de ojo es claro (el ámbar del lobo, el /// amarillo del búho) ese color hace de esclera y la pupila va oscura encima; y si /// es oscuro, un globo negro a secas — que es lo que se ve en un ciervo. fn ojos(centro: Vec3, sep: Vec3, r: f32, p: &Pinta, detalle: DetalleSer, out: &mut Vec) { let lum = p.ojo[0] * 0.3 + p.ojo[1] * 0.6 + p.ojo[2] * 0.1; let chispa = detalle == DetalleSer::Hero; for s in [-1.0f32, 1.0] { let c = centro + Vec3::new(sep.x * s, sep.y, sep.z); let dir = Vec3::new(s * 0.55, 0.10, 0.85); if detalle == DetalleSer::Lejos { out.push(decal(c + dir.normalize() * (r * 0.6), r, [0.10, 0.09, 0.11])); continue; } let (esclera, pupila) = if p.esclera { (Some([0.94, 0.945, 0.955]), [0.16, 0.13, 0.18]) } else if lum > 0.45 { (Some(p.ojo), [0.12, 0.10, 0.12]) } else { (None, p.ojo) }; ojo(c, dir, r, esclera, pupila, chispa, out); } } /// Manchas/rayas/antifaz sobre un cuerpo ya armado. Los decals no aportan /// geometría: repintan la superficie donde caen, así que un patrón no cuesta /// silueta — sólo primitivas. #[allow(clippy::too_many_arguments)] fn patron( p: &Pinta, eje_a: Vec3, eje_b: Vec3, r: f32, semilla: u32, detalle: DetalleSer, out: &mut Vec, ) { if p.patron == Patron::Liso || detalle == DetalleSer::Lejos { return; } let n = match detalle { DetalleSer::Hero => 9, _ => 5, }; let col = p.patron_col; match p.patron { Patron::Manchas | Patron::Montura => { for i in 0..n { let t = (i as f32 + 0.5) / n as f32; let c = eje_a + (eje_b - eje_a) * t; let ang = hash01(semilla, i as u32) * PI * 2.0; let alt = 0.35 + 0.55 * hash01(semilla + 7, i as u32); let d = Vec3::new(ang.cos(), alt, ang.sin() * 0.6).normalize(); out.push(decal(c + d * (r * 0.95), r * (0.30 + 0.22 * hash01(semilla + 11, i as u32)), col)); } } Patron::Lunares => { for i in 0..(n * 2) { let t = (i as f32 + 0.5) / (n * 2) as f32; let c = eje_a + (eje_b - eje_a) * t; let ang = hash01(semilla + 3, i as u32) * PI * 2.0; let d = Vec3::new(ang.cos(), 0.55 + 0.4 * hash01(semilla + 5, i as u32), ang.sin() * 0.5) .normalize(); out.push(decal(c + d * (r * 0.95), r * 0.16, col)); } } Patron::Rayas | Patron::Anillos => { for i in 0..n { let t = (i as f32 + 0.5) / n as f32; let c = eje_a + (eje_b - eje_a) * t; // Franja transversal: cruza el lomo de un flanco al otro. out.push(decal_linea( c + Vec3::new(-r * 1.02, 0.0, 0.0), c + Vec3::new(r * 1.02, r * 0.35, 0.0), r * 0.16, col, )); } } Patron::Rombos => { for i in 0..n { let t = (i as f32 + 0.5) / n as f32; let c = eje_a + (eje_b - eje_a) * t; out.push(decal(c + Vec3::new(0.0, r * 0.92, 0.0), r * 0.42, col)); } } Patron::Antifaz | Patron::Liso => {} } } // ============================================================================ // Cuadrúpedo // ============================================================================ fn cuadrupedo(c: &Cuadrupedo, andanza: Andanza, fase: f32, detalle: DetalleSer) -> Vec { let mut out: Vec = Vec::with_capacity(40); let p = &c.pinta; let (amp, bob_amp) = andanza.ritmo(); let bob = (fase * 2.0).sin() * bob_amp; let lejos = detalle == DetalleSer::Lejos; let l = c.largo; let r = c.cuerpo_r; // Inclinación del tronco: el bípedo (canguro) se para, el que trota se echa // adelante. El eje del cuerpo se arma girado alrededor de la cadera. // Signo: positivo hunde el frente (el que trota se echa adelante), negativo lo // levanta. El bípedo se PARA, así que va negativo — con `+0.95` el canguro // salía olfateando el piso. let tilt = if c.bipedo { -0.95 } else { 0.0 } + if andanza == Andanza::Trotar { 0.06 } else { 0.0 }; let cadera = Vec3::new(0.0, c.pata + r + bob, -l * 0.5); let eje = |t: f32, dy: f32| -> Vec3 { // `t` = 0 en la grupa, 1 en el pecho. El lomo se comba con `lomo`. let base = Vec3::new(0.0, c.lomo * (PI * t).sin() + dy, l * t); cadera + rot_x(base, tilt) }; let grupa = eje(0.0, 0.0); let medio = eje(0.5, 0.0); let pecho = eje(1.0, 0.0); // --- Tronco: dos conos redondeados (grupa→medio→pecho). Un solo elipsoide era // una bola que se tragaba las patas y el cuello (la lección del ser humanoide). out.push(capsula(grupa, r * c.grupa, medio, r, p.cuerpo, r * 0.9)); out.push(capsula(medio, r, pecho, r * c.pecho, p.cuerpo, r * 0.9)); // Vientre: mismo eje, corrido abajo y aclarado. Funde con el tronco y le pinta // la panza sin una sola primitiva de geometría extra en la silueta. if !lejos { out.push(capsula( eje(0.12, -r * 0.42), r * 0.72 * c.grupa, eje(0.88, -r * 0.42), r * 0.72 * c.pecho, p.vientre, r * 0.7, )); } // --- Jorobas. for i in 0..c.jorobas { let t = if c.jorobas == 1 { 0.45 } else { 0.32 + 0.34 * i as f32 }; let cj = eje(t, r * 0.55); out.push(elipsoide( cj, Vec3::new(r * 0.78, c.joroba_alto + r * 0.45, l * 0.20), p.cuerpo, r * 0.6, )); } // --- Patas. Diagonales en oposición: la delantera izquierda va con la trasera // derecha (el paso de cualquier cuadrúpedo). let hip_x = (r * 0.70).max(c.pata_r * 1.6); for (i, (sx, fz)) in [(-1.0f32, 1.0f32), (1.0, 1.0), (-1.0, -1.0), (1.0, -1.0)] .into_iter() .enumerate() { let delantera = fz > 0.0; let desfase = if (sx > 0.0) == delantera { 0.0 } else { PI }; let sw = amp * (fase + desfase).sin(); let t = if delantera { 0.86 } else { 0.14 }; let raiz = eje(t, -r * 0.25) + Vec3::new(sx * hip_x, 0.0, 0.0); // El bípedo lleva los brazos cortos y altos, y las patas traseras enormes. // El largo de la pata sale de DÓNDE ESTÁ LA RAÍZ, no de `c.pata`: la raíz va // enterrada en el tronco (media panza más arriba), así que usar `c.pata` // dejaba al pie a un cuarto de altura del piso. Después `asentar` bajaba // todo el bicho hasta apoyar ESE pie y el animal quedaba aplastado, con // patas de pan y la panza rozando el suelo — así salió la primera hoja // entera del bestiario. let (largo_pata, radio, alto_raiz) = if c.bipedo && delantera { let raiz_alta = raiz + Vec3::new(0.0, r * 0.55, 0.0); (raiz_alta.y * 0.42, c.pata_r * 0.62, raiz_alta) } else { (raiz.y, c.pata_r, raiz) }; let dir = rot_x(Vec3::new(0.0, -largo_pata, 0.0), sw); let rodilla = alto_raiz + dir * 0.52 + Vec3::new(0.0, 0.0, largo_pata * 0.10 * fz); let pie = alto_raiz + dir; if largo_pata > 0.30 { // Pata larga: dos tramos (la rodilla se lee de lejos en un caballo). out.push(capsula(alto_raiz, radio * 1.25, rodilla, radio, p.cuerpo, radio * 0.8)); out.push(capsula(rodilla, radio, pie, radio * 0.85, p.cuerpo, radio * 0.6)); } else { out.push(capsula(alto_raiz, radio * 1.25, pie, radio * 0.9, p.cuerpo, radio * 0.8)); } // Pezuña/garra. out.push(esfera(pie, radio * 0.95, p.extra, radio * 0.5)); let _ = i; } // --- Cola. let cola_base = eje(0.0, r * 0.35) + Vec3::new(0.0, 0.0, -r * 0.35); let vaiven = (fase * 1.3).sin() * 0.22; cola(c, cola_base, vaiven, &mut out); // --- Cuello y cabeza. let ang = match andanza { Andanza::Comer => -0.75, // Levantar la cabeza tiene TECHO: el cuello de la llama ya sale a 1,25 rad // (casi vertical) y sumarle 0,30 no la levanta más — la manda para atrás. // Lo que queda de recorrido hasta la vertical, eso se levanta. Andanza::Alerta => c.cuello_ang + 0.30 * (1.50 - c.cuello_ang).clamp(0.0, 1.0), _ => c.cuello_ang, } + (fase * 2.0).cos() * bob_amp * 2.0; let cuello_base = eje(0.98, r * 0.45); // OJO CON EL SIGNO: `rot_x` gira (0,0,1) hacia −Y con ángulo positivo, así que // pasarle `ang` directo mandaba el cuello AL PISO — la primera hoja tenía a la // vaca, el ciervo, el caballo y el camello pastando todos a la vez, en andanza // «caminar». `arriba(a)` deja el signo del lado en que se lee la receta: // positivo = cuello levantado. let arriba = |a: f32| rot_x(Vec3::new(0.0, 0.0, 1.0), -a); let dir_cuello = arriba(ang); // El cuello arranca EN la superficie del pecho, así que su largo tiene que // pasar el radio del pecho o la cabeza queda medio hundida en el tronco (la // vaca y el caballo de la primera hoja parecían jabalíes sin cuello). let largo_cuello = c.cuello + c.cabeza * 0.55 + r * c.pecho * 0.55; let cabeza_c = cuello_base + dir_cuello * largo_cuello; if c.cuello > 0.01 { // Cuello largo (jirafa, llama): dos tramos para que arquee. if c.cuello > 0.5 { let medio_c = cuello_base + arriba(ang - 0.25) * (largo_cuello * 0.55); out.push(capsula(cuello_base, c.cuello_r * 1.35, medio_c, c.cuello_r, p.cuerpo, c.cuello_r * 0.9)); out.push(capsula(medio_c, c.cuello_r, cabeza_c, c.cuello_r * 0.9, p.cuerpo, c.cuello_r * 0.8)); } else { out.push(capsula(cuello_base, c.cuello_r * 1.3, cabeza_c, c.cuello_r * 0.95, p.cuerpo, c.cuello_r * 0.9)); } } let cab = c.cabeza; out.push(elipsoide( cabeza_c, Vec3::new(cab * c.craneo[0], cab * c.craneo[1], cab * c.craneo[2]), p.cuerpo, cab * 0.45, )); // --- Hocico: cápsula hacia adelante y abajo (la cabeza es un elipsoide alineado // a ejes y no puede rotar; el hocico sí, y es el que da la especie). // El hocico mira adelante y un poco abajo, y acompaña al cuello. let dir_hocico = arriba(ang * 0.45 - 0.30); let punta = cabeza_c + dir_hocico * (cab * (0.55 + c.hocico)); if c.hocico > 0.05 { out.push(capsula( cabeza_c + dir_hocico * (cab * 0.25), cab * c.hocico_r * 1.05, punta, cab * c.hocico_r * 0.82, p.cuerpo, cab * 0.30, )); if !lejos { out.push(decal(punta + dir_hocico * (cab * c.hocico_r * 0.5), cab * c.hocico_r * 0.55, tono(p.detalle, 0.7))); } } // --- Trompa (elefante, tapir): cadena que cae desde el hocico y se enrosca. if c.trompa > 0.01 { let n = 5; let mut pts = Vec::with_capacity(n + 1); for i in 0..=n { let t = i as f32 / n as f32; let caida = -t * c.trompa * cab * 0.95; let curva = (t * t) * cab * c.trompa * 0.30; pts.push(punta + Vec3::new(0.0, caida, curva * 0.7 + t * cab * 0.15)); } cadena(&pts, cab * 0.34, cab * 0.13, p.cuerpo, cab * 0.18, &mut out); } // --- Orejas. orejas(c, cabeza_c, cab, andanza, &mut out); // --- Cuernos, astas, colmillos. cuernos(c, cabeza_c, punta, cab, &mut out); // --- Melena / crin. if c.melena > 0.01 && !lejos { if c.cola == Cola::Crin { // Crin: cresta de pelo a lo largo del cuello (caballo, cebra). let n = 5; for i in 0..=n { let t = i as f32 / n as f32; let cpt = cuello_base + (cabeza_c - cuello_base) * t + Vec3::new(0.0, c.cuello_r * 0.9, 0.0); out.push(elipsoide( cpt, Vec3::new(c.cuello_r * 0.35, c.cuello_r * (0.5 + 0.5 * c.melena), c.cuello_r * 0.8), p.detalle, c.cuello_r * 0.3, )); } } else { // Melena: corona de bultos alrededor del cuello (león). let n = 9; for i in 0..n { let a = i as f32 / n as f32 * PI * 2.0; let rad = cab * (1.05 + 0.25 * c.melena); let cpt = cabeza_c + Vec3::new(a.cos() * rad, a.sin() * rad, -cab * 0.55); out.push(esfera(cpt, cab * 0.42 * c.melena, p.detalle, cab * 0.30)); } } } // --- Púas (erizo, puercoespín): conos cortos por todo el lomo. Es lo mismo // que la lana en el sitio y lo contrario en la forma — y es LA silueta del // erizo, que sin ellas es un pan. if c.puas > 0.01 && !lejos { let n = 14; for i in 0..n { let t = 0.10 + 0.82 * (i as f32 / (n - 1) as f32); let a = hash01(91, i as u32) * PI * 2.0; let dir = Vec3::new(a.cos() * 0.55, 0.82, -0.15).normalize(); // La raíz va sobre la SUPERFICIE del lomo y la púa mide vez y media el // radio del cuerpo: con `0.85·r` desde adentro, el `smin` se las comía // enteras y el erizo seguía siendo un pan. let base = eje(t, 0.0) + dir * (r * 0.72); out.push(capsula( base, r * 0.15, base + dir * (r * 1.35 * c.puas), r * 0.012, p.detalle, r * 0.07, )); } } // --- Lana (oveja, llama, koala): bultos mullidos sobre el tronco. if c.lana > 0.01 && !lejos { let n = 7; for i in 0..n { let t = 0.12 + 0.78 * (i as f32 / (n - 1) as f32); let a = hash01(31, i as u32) * PI * 2.0; let cpt = eje(t, r * 0.35) + Vec3::new(a.cos() * r * 0.55, 0.0, 0.0); out.push(esfera(cpt, r * (0.48 + 0.18 * hash01(57, i as u32)) * c.lana, p.cuerpo, r * 0.55)); } } // --- Patrón (manchas, rayas, lunares) sobre el tronco. patron(p, grupa, pecho, r, 17, detalle, &mut out); if p.patron == Patron::Antifaz && !lejos { for s in [-1.0f32, 1.0] { out.push(decal( cabeza_c + Vec3::new(s * cab * 0.52, cab * 0.20, cab * 0.72), cab * 0.42, p.patron_col, )); } } // --- Ojos. let sep = Vec3::new( cab * (0.30 + 0.50 * c.ojo_lateral), cab * 0.22, cab * (0.86 - 0.55 * c.ojo_lateral), ); ojos(cabeza_c, sep, cab * c.ojo, p, detalle, &mut out); out } fn orejas(c: &Cuadrupedo, cabeza_c: Vec3, cab: f32, andanza: Andanza, out: &mut Vec) { let p = &c.pinta; let t = c.oreja_tam; let alerta = if andanza == Andanza::Alerta { 0.25 } else { 0.0 }; for s in [-1.0f32, 1.0] { let raiz = cabeza_c + Vec3::new(s * cab * 0.58, cab * 0.52, -cab * 0.12); match c.oreja { Oreja::Ninguna => {} Oreja::Redonda => { out.push(elipsoide( raiz + Vec3::new(s * cab * 0.12, cab * 0.10, 0.0), Vec3::new(cab * 0.30 * t, cab * 0.34 * t, cab * 0.14 * t), p.cuerpo, cab * 0.10, )); } Oreja::Punta => { let punta = raiz + Vec3::new(s * cab * 0.28 * t, cab * (0.75 + alerta) * t, -cab * 0.05); out.push(capsula(raiz, cab * 0.20 * t, punta, cab * 0.02, p.cuerpo, cab * 0.10)); } Oreja::Larga => { let punta = raiz + Vec3::new(s * cab * 0.34 * t, cab * (1.35 + alerta) * t, -cab * 0.22); out.push(capsula(raiz, cab * 0.20 * t, punta, cab * 0.08 * t, p.cuerpo, cab * 0.10)); out.push(decal( raiz + (punta - raiz) * 0.55 + Vec3::new(0.0, 0.0, cab * 0.10), cab * 0.13 * t, tono(p.vientre, 1.05), )); } Oreja::Caida => { let punta = raiz + Vec3::new(s * cab * 0.40 * t, -cab * 0.75 * t, -cab * 0.10); out.push(capsula(raiz, cab * 0.22 * t, punta, cab * 0.14 * t, p.detalle, cab * 0.12)); } Oreja::Abanico => { out.push(elipsoide( raiz + Vec3::new(s * cab * 0.55 * t, -cab * 0.20, -cab * 0.35), Vec3::new(cab * 0.12, cab * 0.85 * t, cab * 0.70 * t), p.cuerpo, cab * 0.18, )); } Oreja::Lateral => { let punta = raiz + Vec3::new(s * cab * 0.62 * t, cab * 0.18 * t, -cab * 0.05); out.push(capsula(raiz, cab * 0.18 * t, punta, cab * 0.07 * t, p.cuerpo, cab * 0.10)); } } } } fn cuernos(c: &Cuadrupedo, cabeza_c: Vec3, punta_hocico: Vec3, cab: f32, out: &mut Vec) { let p = &c.pinta; match c.cuerno { Cuerno::Ninguno => {} Cuerno::Cortos | Cuerno::Largos => { let k = if c.cuerno == Cuerno::Largos { 1.9 } else { 1.0 }; for s in [-1.0f32, 1.0] { let raiz = cabeza_c + Vec3::new(s * cab * 0.45, cab * 0.72, -cab * 0.05); let medio = raiz + Vec3::new(s * cab * 0.40 * k, cab * 0.45 * k, -cab * 0.05); let punta = medio + Vec3::new(s * cab * 0.10 * k, cab * 0.42 * k, cab * 0.20 * k); out.push(capsula(raiz, cab * 0.17, medio, cab * 0.11, p.extra, cab * 0.08)); out.push(capsula(medio, cab * 0.11, punta, cab * 0.03, p.extra, cab * 0.05)); } } Cuerno::Astas => { for s in [-1.0f32, 1.0] { let raiz = cabeza_c + Vec3::new(s * cab * 0.42, cab * 0.75, -cab * 0.10); let alto = raiz + Vec3::new(s * cab * 0.55, cab * 1.15, -cab * 0.30); out.push(capsula(raiz, cab * 0.13, alto, cab * 0.07, p.extra, cab * 0.07)); for (i, t) in [0.35f32, 0.72].into_iter().enumerate() { let base = raiz + (alto - raiz) * t; let ramita = base + Vec3::new(s * cab * 0.30, cab * (0.30 + 0.12 * i as f32), cab * 0.34); out.push(capsula(base, cab * 0.08, ramita, cab * 0.03, p.extra, cab * 0.05)); } } } Cuerno::Nasal => { let base = punta_hocico + Vec3::new(0.0, cab * 0.28, -cab * 0.10); out.push(capsula( base, cab * 0.24, base + Vec3::new(0.0, cab * 0.78, cab * 0.28), cab * 0.03, p.extra, cab * 0.10, )); } Cuerno::Colmillos => { for s in [-1.0f32, 1.0] { let raiz = cabeza_c + Vec3::new(s * cab * 0.42, -cab * 0.25, cab * 0.45); let medio = raiz + Vec3::new(s * cab * 0.10, -cab * 0.80, cab * 0.45); let punta = medio + Vec3::new(s * cab * 0.05, cab * 0.10, cab * 0.45); out.push(capsula(raiz, cab * 0.15, medio, cab * 0.10, p.extra, cab * 0.08)); out.push(capsula(medio, cab * 0.10, punta, cab * 0.04, p.extra, cab * 0.05)); } } } } fn cola(c: &Cuadrupedo, base: Vec3, vaiven: f32, out: &mut Vec) { let p = &c.pinta; let r = c.cuerpo_r; let ll = c.cola_len * c.largo; let atras = |dy: f32, dz: f32, dx: f32| base + Vec3::new(dx, dy, -dz); match c.cola { Cola::Ninguna => {} Cola::Muñon => { out.push(esfera(atras(r * 0.15, r * 0.25, 0.0), r * 0.32, p.vientre, r * 0.3)); } Cola::Penacho => { let punta = atras(-ll * 0.55, ll * 0.55, vaiven * ll * 0.35); out.push(capsula(base, r * 0.16, punta, r * 0.07, p.cuerpo, r * 0.2)); out.push(esfera(punta, r * 0.26, p.detalle, r * 0.15)); } Cola::Tupida => { let pts = [ base, atras(ll * 0.18, ll * 0.42, vaiven * ll * 0.2), atras(ll * 0.32, ll * 0.85, vaiven * ll * 0.35), ]; cadena(&pts, r * 0.55, r * 0.30, p.cuerpo, r * 0.5, out); out.push(esfera(pts[2], r * 0.32, p.vientre, r * 0.28)); } Cola::Prensil => { let pts: Vec = (0..=4) .map(|i| { let t = i as f32 / 4.0; atras(ll * 0.55 * (t * PI * 0.7).sin(), ll * t, vaiven * ll * 0.35 * t) }) .collect(); cadena(&pts, r * 0.22, r * 0.08, p.cuerpo, r * 0.18, out); } Cola::Rulo => { let pts: Vec = (0..=4) .map(|i| { let a = i as f32 / 4.0 * PI * 1.6; atras(ll * 0.35 * a.sin() + ll * 0.1, ll * 0.30 * (1.0 - a.cos()), 0.0) }) .collect(); cadena(&pts, r * 0.16, r * 0.10, p.cuerpo, r * 0.14, out); } Cola::Gruesa => { let pts: Vec = (0..=4) .map(|i| { let t = i as f32 / 4.0; atras(-ll * 0.32 * t * t + ll * 0.10, ll * t, vaiven * ll * 0.25 * t) }) .collect(); cadena(&pts, r * 0.70, r * 0.12, p.cuerpo, r * 0.5, out); } Cola::Crin => { let punta = atras(-ll * 0.75, ll * 0.35, vaiven * ll * 0.25); out.push(capsula(base, r * 0.22, punta, r * 0.16, p.detalle, r * 0.2)); } } } // ============================================================================ // Ave // ============================================================================ fn ave(a: &Ave, andanza: Andanza, fase: f32, detalle: DetalleSer) -> Vec { let mut out: Vec = Vec::with_capacity(30); let p = &a.pinta; let lejos = detalle == DetalleSer::Lejos; let vuela = matches!(andanza, Andanza::Volar) || a.ala == Ala::Bate; let bob = (fase * 2.0).sin() * if vuela { 0.02 } else { 0.006 }; // El cuerpo es una CÁPSULA orientada, no un elipsoide: el elipsoide no rota // (semiejes alineados a ejes) y un pájaro posado va inclinado. La cápsula toma // cualquier ángulo y funde igual. let r_cuerpo = (a.cuerpo[0] + a.cuerpo[1]) * 0.5; let largo = a.cuerpo[2]; let centro = Vec3::new(0.0, a.pata + r_cuerpo + bob, 0.0); let dir = Vec3::new(0.0, a.inclina.cos(), a.inclina.sin()); let cola_pt = centro - dir * largo; let pecho = centro + dir * largo; out.push(capsula(cola_pt, r_cuerpo * 0.80, pecho, r_cuerpo * 1.02, p.cuerpo, r_cuerpo * 0.8)); if !lejos { out.push(capsula( cola_pt + Vec3::new(0.0, -r_cuerpo * 0.32, 0.0), r_cuerpo * 0.62, pecho + Vec3::new(0.0, -r_cuerpo * 0.30, 0.0), r_cuerpo * 0.72, p.vientre, r_cuerpo * 0.6, )); } // --- Cuello + cabeza. La S del cisne/garza sale de tres puntos. let cuello_dir = rot_x(dir, 0.55); let cabeza_c = if a.cuello_s > 0.01 { let p1 = pecho + cuello_dir * (a.cuello * 0.45) + Vec3::new(0.0, 0.0, -a.cuello * a.cuello_s * 0.35); let p2 = p1 + Vec3::new(0.0, a.cuello * 0.42, a.cuello * a.cuello_s * 0.30); let p3 = p2 + Vec3::new(0.0, a.cuello * 0.18, a.cuello * (0.25 + a.cuello_s * 0.25)); cadena(&[pecho, p1, p2, p3], a.cuello_r * 1.15, a.cuello_r * 0.85, p.cuerpo, a.cuello_r * 0.9, &mut out); p3 } else { let c = pecho + cuello_dir * (a.cuello + a.cabeza * 0.5); if a.cuello > 0.02 { out.push(capsula(pecho, a.cuello_r * 1.2, c, a.cuello_r * 0.9, p.cuerpo, a.cuello_r)); } c }; out.push(esfera(cabeza_c, a.cabeza, p.cuerpo, a.cabeza * 0.45)); // --- Pico. let dp = Vec3::new(0.0, -0.12, 1.0).normalize(); let base_pico = cabeza_c + dp * (a.cabeza * 0.55); let ll = a.cabeza * a.pico_len * 1.5; let rp = a.cabeza * a.pico_r; match a.pico { Pico::Corto | Pico::Largo | Pico::Aguja => { out.push(capsula(base_pico, rp, base_pico + dp * ll, rp * 0.12, p.extra, rp * 0.25)); } Pico::Curvo => { let medio = base_pico + dp * (ll * 0.6); let punta = medio + Vec3::new(0.0, -ll * 0.55, ll * 0.25); out.push(capsula(base_pico, rp, medio, rp * 0.65, p.extra, rp * 0.3)); out.push(capsula(medio, rp * 0.65, punta, rp * 0.10, p.extra, rp * 0.25)); } Pico::Ancho => { // Tucán/pelícano: el pico ES la especie. Elipsoide alargado + punta. let c = base_pico + dp * (ll * 0.5); out.push(elipsoide(c, Vec3::new(rp * 0.75, rp * 0.95, ll * 0.55), p.extra, rp * 0.3)); out.push(capsula(c + dp * (ll * 0.45), rp * 0.5, base_pico + dp * (ll * 1.05), rp * 0.08, p.extra, rp * 0.2)); if !lejos { out.push(decal_linea(base_pico, base_pico + dp * ll, rp * 0.10, tono(p.extra, 0.55))); } } Pico::Pala => { let c = base_pico + dp * (ll * 0.45); out.push(elipsoide(c, Vec3::new(rp * 1.05, rp * 0.42, ll * 0.55), p.extra, rp * 0.3)); } } // --- Cresta / copete. match a.cresta { Cresta::Ninguna => {} Cresta::Punta => { for i in 0..3 { let s = (i as f32 - 1.0) * 0.35; out.push(capsula( cabeza_c + Vec3::new(0.0, a.cabeza * 0.5, -a.cabeza * 0.1), a.cabeza * 0.22, cabeza_c + Vec3::new(s * a.cabeza * 0.35, a.cabeza * 1.25, -a.cabeza * (0.35 + 0.2 * i as f32)), a.cabeza * 0.05, p.cuerpo, a.cabeza * 0.12, )); } } Cresta::Gallo => { for i in 0..3 { out.push(esfera( cabeza_c + Vec3::new(0.0, a.cabeza * (0.95 + 0.1 * (i as f32 - 1.0).abs()), a.cabeza * (0.35 - 0.35 * i as f32)), a.cabeza * 0.28, p.detalle, a.cabeza * 0.16, )); } // Barbilla. out.push(esfera(cabeza_c + Vec3::new(0.0, -a.cabeza * 0.72, a.cabeza * 0.42), a.cabeza * 0.24, p.detalle, a.cabeza * 0.14)); } Cresta::Penacho => { for i in 0..4 { let s = (i as f32 - 1.5) * 0.28; let punta = cabeza_c + Vec3::new(s * a.cabeza * 0.5, a.cabeza * 1.55, -a.cabeza * 0.9); out.push(capsula(cabeza_c + Vec3::new(0.0, a.cabeza * 0.55, 0.0), a.cabeza * 0.10, punta, a.cabeza * 0.03, p.detalle, a.cabeza * 0.06)); if !lejos { out.push(decal(punta, a.cabeza * 0.16, p.patron_col)); } } } Cresta::Orejas => { for s in [-1.0f32, 1.0] { out.push(capsula( cabeza_c + Vec3::new(s * a.cabeza * 0.45, a.cabeza * 0.55, 0.0), a.cabeza * 0.20, cabeza_c + Vec3::new(s * a.cabeza * 0.62, a.cabeza * 1.05, -a.cabeza * 0.15), a.cabeza * 0.04, p.detalle, a.cabeza * 0.10, )); } } } // --- Alas. let bateo = if a.ala == Ala::Bate || vuela { (fase * 6.0).sin() * 0.85 } else { 0.0 }; // Un águila POSADA pliega las alas. Dejarlas abiertas por receta la convertía // en un avioncito de papel — el ala tendida es la pose de vuelo, no la especie. let ala = match a.ala { Ala::Abierta if !vuela => Ala::Plegada, otra => otra, }; for s in [-1.0f32, 1.0] { let hombro = centro + dir * (largo * 0.25) + Vec3::new(s * r_cuerpo * 0.75, r_cuerpo * 0.25, 0.0); match ala { Ala::Plegada => { out.push(capsula( hombro, r_cuerpo * 0.52, hombro - dir * (largo * 1.15) + Vec3::new(0.0, -r_cuerpo * 0.1, 0.0), r_cuerpo * 0.20, p.detalle, r_cuerpo * 0.35, )); } Ala::Aleta => { out.push(capsula( hombro, r_cuerpo * 0.34, hombro + Vec3::new(s * r_cuerpo * 0.35, -r_cuerpo * 1.25, -r_cuerpo * 0.15), r_cuerpo * 0.14, p.detalle, r_cuerpo * 0.25, )); } Ala::Abierta | Ala::Bate => { let ang = 0.18 + bateo * 0.55; let codo = hombro + Vec3::new(s * r_cuerpo * 1.5, ang * r_cuerpo * 1.6, -r_cuerpo * 0.2); let punta = codo + Vec3::new(s * r_cuerpo * 1.7, ang * r_cuerpo * 1.1, -r_cuerpo * 0.8); out.push(capsula(hombro, r_cuerpo * 0.48, codo, r_cuerpo * 0.36, p.detalle, r_cuerpo * 0.3)); out.push(capsula(codo, r_cuerpo * 0.36, punta, r_cuerpo * 0.10, p.detalle, r_cuerpo * 0.25)); if !lejos { // Plumas primarias: tres dedos en la punta. for i in 0..3 { let d = (i as f32 - 1.0) * 0.3; out.push(capsula( codo, r_cuerpo * 0.14, punta + Vec3::new(s * r_cuerpo * 0.3, d * r_cuerpo * 0.5, -r_cuerpo * (0.4 + 0.2 * i as f32)), r_cuerpo * 0.05, tono(p.detalle, 0.8), r_cuerpo * 0.12, )); } } } } } // --- Cola. let cl = a.cola_len * largo * 2.2; let dir_cola = -rot_x(dir, 0.30); match a.cola { ColaAve::Corta => { out.push(capsula(cola_pt, r_cuerpo * 0.55, cola_pt + dir_cola * cl * 0.5, r_cuerpo * 0.22, p.detalle, r_cuerpo * 0.4)); } ColaAve::Larga => { for s in [-1.0f32, 1.0] { out.push(capsula( cola_pt, r_cuerpo * 0.30, cola_pt + dir_cola * cl + Vec3::new(s * r_cuerpo * 0.30, -cl * 0.15, 0.0), r_cuerpo * 0.07, p.detalle, r_cuerpo * 0.2, )); } } ColaAve::Abanico => { for i in 0..5 { let s = (i as f32 - 2.0) * 0.32; out.push(capsula( cola_pt, r_cuerpo * 0.26, cola_pt + dir_cola * cl + Vec3::new(s * cl * 0.45, s.abs() * cl * 0.18, 0.0), r_cuerpo * 0.08, p.detalle, r_cuerpo * 0.2, )); } } ColaAve::Rueda => { // La rueda del pavo: abanico grande de plumas con ocelos. let n = if lejos { 5 } else { 11 }; for i in 0..n { let t = i as f32 / (n - 1) as f32; let ang = (t - 0.5) * PI * 1.35; let punta = cola_pt + Vec3::new(ang.sin() * cl, ang.cos() * cl * 0.95 + cl * 0.15, -cl * 0.35); out.push(capsula(cola_pt, r_cuerpo * 0.22, punta, r_cuerpo * 0.13, p.detalle, r_cuerpo * 0.18)); if !lejos { out.push(decal(punta, r_cuerpo * 0.30, p.patron_col)); out.push(decal(punta, r_cuerpo * 0.15, tono(p.cuerpo, 0.5))); } } } } // --- Patas. for s in [-1.0f32, 1.0] { let cadera = centro + Vec3::new(s * r_cuerpo * 0.42, -r_cuerpo * 0.55, -largo * 0.15); let pie = Vec3::new(cadera.x, 0.0, cadera.z + a.pata * 0.12); if andanza == Andanza::Volar { // En vuelo las patas se recogen contra el vientre. out.push(capsula(cadera, a.pata_r * 1.2, cadera + Vec3::new(0.0, -a.pata * 0.25, -a.pata * 0.35), a.pata_r, p.extra, a.pata_r)); continue; } let rodilla = cadera + Vec3::new(0.0, -(cadera.y) * 0.55, -a.pata * 0.10); out.push(capsula(cadera, a.pata_r * 1.6, rodilla, a.pata_r, p.extra, a.pata_r)); out.push(capsula(rodilla, a.pata_r, pie, a.pata_r * 0.9, p.extra, a.pata_r * 0.8)); if !lejos { for i in 0..3 { let d = (i as f32 - 1.0) * 0.55; out.push(capsula( pie, a.pata_r * 0.9, pie + Vec3::new(d * a.pata_r * 3.0, 0.0, a.pata_r * 4.2), a.pata_r * 0.5, p.extra, a.pata_r * 0.5, )); } } } // --- Patrón y ojos. patron(p, cola_pt, pecho, r_cuerpo, 23, detalle, &mut out); ojos( cabeza_c, Vec3::new(a.cabeza * 0.72, a.cabeza * 0.18, a.cabeza * 0.42), a.cabeza * a.ojo, p, detalle, &mut out, ); out } // ============================================================================ // Reptante (serpiente, lagarto, cocodrilo, tortuga, rana) // ============================================================================ fn reptante(c: &Reptante, andanza: Andanza, fase: f32, detalle: DetalleSer) -> Vec { let mut out: Vec = Vec::with_capacity(34); let p = &c.pinta; let lejos = detalle == DetalleSer::Lejos; let r = c.grosor; let l = c.largo; let repta = matches!(andanza, Andanza::Caminar | Andanza::Trotar | Andanza::Nadar); let onda_fase = if repta { fase } else { fase * 0.3 }; if c.anfibio { return anfibio(c, fase, detalle); } if c.caparazon > 0.01 { return tortuga(c, andanza, fase, detalle); } // --- Espinazo: polilínea que ondula en X y se levanta adelante si `alza`. let n = 9; let mut pts = Vec::with_capacity(n + 1); for i in 0..=n { let t = i as f32 / n as f32; // 0 = punta de la cola, 1 = cuello // Amplitud en unidades de LARGO, no de grosor: con `amplitud · r` la onda // medía medio cuerpo de ancho y la serpiente salía como un salchichón. let x = (t * PI * 2.0 * c.ondas + onda_fase).sin() * c.amplitud * r * 2.6; let sube = if c.alza > 0.0 { let s = ((t - 0.55) / 0.45).clamp(0.0, 1.0); s * s * c.alza * l * 0.45 } else { 0.0 }; pts.push(Vec3::new(x, r + sube, (t - 0.5) * l)); } // Radio: fino en la cola, grueso al medio, apenas menor en el cuello. 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; let rad = |t: f32| r * (0.28 + 0.90 * (t * PI * 0.92).sin().max(0.10)); out.push(capsula(w[0], rad(t0), w[1], rad(t1), p.cuerpo, r * 0.7)); } if !lejos { // Vientre claro: la misma cadena, corrida abajo. for (i, w) in pts.windows(2).enumerate().step_by(2) { let t = i as f32 / n as f32; let rad = r * (0.20 + 0.66 * (t * PI * 0.92).sin().max(0.10)); out.push(capsula( w[0] - Vec3::new(0.0, r * 0.30, 0.0), rad, w[1] - Vec3::new(0.0, r * 0.30, 0.0), rad, p.vientre, r * 0.5, )); } } let cuello = pts[n]; let dir = (cuello - pts[n - 1]).normalize_or_zero(); let cab = r * c.cabeza * 1.35; // --- Capucha de la cobra: disco fino detrás de la cabeza. if c.capucha > 0.01 { out.push(elipsoide( cuello - dir * (cab * 0.35), Vec3::new(cab * 1.9 * c.capucha, cab * 1.5 * c.capucha, cab * 0.30), p.cuerpo, cab * 0.35, )); } // --- Cabeza: elipsoide chato + hocico (el cocodrilo es casi todo hocico). let cabeza_c = cuello + dir * (cab * 0.6); out.push(elipsoide( cabeza_c, Vec3::new(cab * 0.95, cab * 0.72, cab * 1.15), p.cuerpo, cab * 0.4, )); let punta = cabeza_c + dir * (cab * c.hocico * 1.1); if c.hocico > 0.2 { out.push(capsula(cabeza_c, cab * 0.75, punta, cab * 0.55, p.cuerpo, cab * 0.35)); if !lejos { // Línea de la boca: el cocodrilo se lee por la sonrisa larga. out.push(decal_linea( cabeza_c + Vec3::new(-cab * 0.55, -cab * 0.30, 0.0), punta + Vec3::new(-cab * 0.10, -cab * 0.25, 0.0), cab * 0.10, tono(p.detalle, 0.6), )); out.push(decal_linea( cabeza_c + Vec3::new(cab * 0.55, -cab * 0.30, 0.0), punta + Vec3::new(cab * 0.10, -cab * 0.25, 0.0), cab * 0.10, tono(p.detalle, 0.6), )); } } // Lengua bífida. if c.lengua && !lejos { let base = punta + dir * (cab * 0.35); for s in [-1.0f32, 1.0] { out.push(capsula( punta, cab * 0.07, base + Vec3::new(s * cab * 0.35, -cab * 0.05, 0.0), cab * 0.02, [0.85, 0.20, 0.28], cab * 0.04, )); } } // --- Cresta dorsal de púas (iguana, cocodrilo). if c.cresta > 0.01 && !lejos { for i in 2..n { let t = i as f32 / n as f32; let base = pts[i] + Vec3::new(0.0, r * 0.55, 0.0); out.push(capsula( base, r * 0.16 * c.cresta, base + Vec3::new(0.0, r * 0.55 * c.cresta * (t * PI).sin().max(0.3), 0.0), r * 0.02, p.detalle, r * 0.08, )); } } // --- Cascabel. if c.cascabel { let cola_pt = pts[0]; for i in 0..3 { out.push(esfera( cola_pt - Vec3::new(0.0, 0.0, i as f32 * r * 0.28), r * (0.22 - 0.04 * i as f32), p.detalle, r * 0.1, )); } } // --- Patas de reptil: abiertas hacia afuera (no debajo del cuerpo). if c.patas >= 4 { let paso = if repta { 0.5 } else { 0.0 }; for (i, (sx, t)) in [(-1.0f32, 0.72f32), (1.0, 0.72), (-1.0, 0.28), (1.0, 0.28)] .into_iter() .enumerate() { let idx = (t * n as f32) as usize; let cadera = pts[idx.min(n)] + Vec3::new(sx * r * 0.65, -r * 0.1, 0.0); let sw = paso * ((fase + if i % 3 == 0 { 0.0 } else { PI }).sin()); let codo = cadera + Vec3::new(sx * c.pata * c.abertura * r * 1.4, -r * 0.45, sw * r * 0.5); let pie = Vec3::new(codo.x + sx * r * 0.25, 0.0, codo.z + sw * r * 0.6); out.push(capsula(cadera, r * 0.26, codo, r * 0.20, p.cuerpo, r * 0.2)); out.push(capsula(codo, r * 0.20, pie, r * 0.14, p.cuerpo, r * 0.15)); if !lejos { for k in 0..3 { let d = (k as f32 - 1.0) * 0.5; out.push(capsula( pie, r * 0.10, pie + Vec3::new(sx * r * 0.20 + d * r * 0.22, 0.0, r * 0.38), r * 0.04, p.cuerpo, r * 0.06, )); } } } } patron(p, pts[1], pts[n - 1], r, 41, detalle, &mut out); ojos( cabeza_c, Vec3::new(cab * 0.70, cab * 0.42, cab * 0.35), cab * c.ojo, p, detalle, &mut out, ); out } /// La tortuga: caparazón abovedado, plastrón y cuatro muñones. fn tortuga(c: &Reptante, andanza: Andanza, fase: f32, detalle: DetalleSer) -> Vec { let mut out: Vec = Vec::with_capacity(24); let p = &c.pinta; let lejos = detalle == DetalleSer::Lejos; let r = c.grosor * 1.6; let cuerpo_y = r * 0.55; let centro = Vec3::new(0.0, cuerpo_y, 0.0); out.push(elipsoide(centro, Vec3::new(r * 0.98, r * 0.62, r * 1.15), p.detalle, r * 0.3)); out.push(elipsoide( centro - Vec3::new(0.0, r * 0.42, 0.0), Vec3::new(r * 0.86, r * 0.22, r * 1.02), p.vientre, r * 0.25, )); if !lejos { // Escudos del caparazón. for i in 0..6 { let a = i as f32 / 6.0 * PI * 2.0; out.push(decal( centro + Vec3::new(a.cos() * r * 0.55, r * 0.55, a.sin() * r * 0.62), r * 0.26, p.patron_col, )); } } // Patas. let paso = if andanza == Andanza::Caminar { 0.35 } else { 0.0 }; for (i, (sx, sz)) in [(-1.0f32, 1.0f32), (1.0, 1.0), (-1.0, -1.0), (1.0, -1.0)].into_iter().enumerate() { let sw = paso * (fase + i as f32 * PI * 0.5).sin(); let raiz = centro + Vec3::new(sx * r * 0.6, -r * 0.25, sz * r * 0.72); let pie = Vec3::new(raiz.x + sx * r * 0.35, r * 0.12, raiz.z + sw * r * 0.3); out.push(capsula(raiz, r * 0.24, pie, r * 0.20, p.cuerpo, r * 0.18)); } // Cabeza y cola. let cuello = centro + Vec3::new(0.0, -r * 0.05, r * 1.0); let cabeza_c = cuello + Vec3::new(0.0, r * 0.12, r * c.cabeza * 0.75); out.push(capsula(cuello, r * 0.24, cabeza_c, r * 0.26, p.cuerpo, r * 0.2)); out.push(elipsoide(cabeza_c, Vec3::new(r * 0.28, r * 0.26, r * 0.34), p.cuerpo, r * 0.15)); out.push(capsula( centro + Vec3::new(0.0, -r * 0.1, -r * 1.0), r * 0.14, centro + Vec3::new(0.0, -r * 0.2, -r * 1.35), r * 0.04, p.cuerpo, r * 0.1, )); ojos(cabeza_c, Vec3::new(r * 0.20, r * 0.10, r * 0.22), r * 0.10, p, detalle, &mut out); out } /// La rana: agazapada, ojos arriba y patas traseras plegadas. fn anfibio(c: &Reptante, fase: f32, detalle: DetalleSer) -> Vec { let mut out: Vec = Vec::with_capacity(22); let p = &c.pinta; let lejos = detalle == DetalleSer::Lejos; let r = c.grosor; let respira = (fase * 2.0).sin() * r * 0.03; let centro = Vec3::new(0.0, r * 0.72 + respira, 0.0); out.push(elipsoide(centro, Vec3::new(r * 0.86, r * 0.68, r * 1.05), p.cuerpo, r * 0.35)); out.push(elipsoide( centro - Vec3::new(0.0, r * 0.34, 0.0), Vec3::new(r * 0.72, r * 0.30, r * 0.90), p.vientre, r * 0.3, )); // Patas traseras plegadas en Z (el resorte de la rana). for s in [-1.0f32, 1.0] { let cadera = centro + Vec3::new(s * r * 0.60, -r * 0.15, -r * 0.55); let rodilla = cadera + Vec3::new(s * r * 0.45, r * 0.30, -r * 0.30); let tobillo = rodilla + Vec3::new(s * r * 0.10, -r * 0.62, r * 0.35); let pie = tobillo + Vec3::new(s * r * 0.05, -r * 0.05, r * 0.55); out.push(capsula(cadera, r * 0.28, rodilla, r * 0.22, p.cuerpo, r * 0.2)); out.push(capsula(rodilla, r * 0.22, tobillo, r * 0.16, p.cuerpo, r * 0.16)); out.push(capsula(tobillo, r * 0.16, pie, r * 0.10, p.cuerpo, r * 0.12)); // Manos adelante. let hombro = centro + Vec3::new(s * r * 0.55, -r * 0.25, r * 0.55); let mano = Vec3::new(hombro.x + s * r * 0.12, r * 0.05, hombro.z + r * 0.35); out.push(capsula(hombro, r * 0.16, mano, r * 0.10, p.cuerpo, r * 0.12)); } // Ojos saltones ARRIBA de la cabeza (la marca de la rana). let cabeza_c = centro + Vec3::new(0.0, r * 0.20, r * 0.72); for s in [-1.0f32, 1.0] { let o = cabeza_c + Vec3::new(s * r * 0.42, r * 0.42, r * 0.05); out.push(esfera(o, r * c.ojo * 0.62, p.cuerpo, r * 0.16)); if !lejos { ojo(o + Vec3::new(0.0, r * 0.10, 0.0), Vec3::new(s * 0.4, 0.5, 0.75), r * c.ojo * 0.42, Some([0.95, 0.86, 0.30]), [0.12, 0.10, 0.12], detalle == DetalleSer::Hero, &mut out); } } if !lejos { out.push(decal_linea( cabeza_c + Vec3::new(-r * 0.55, -r * 0.18, r * 0.10), cabeza_c + Vec3::new(r * 0.55, -r * 0.18, r * 0.10), r * 0.09, tono(p.detalle, 0.6), )); } patron(p, centro - Vec3::new(0.0, 0.0, r * 0.6), centro + Vec3::new(0.0, 0.0, r * 0.6), r * 0.9, 61, detalle, &mut out); out } // ============================================================================ // Nadador // ============================================================================ fn nadador(c: &Nadador, andanza: Andanza, fase: f32, detalle: DetalleSer) -> Vec { let mut out: Vec = Vec::with_capacity(20); let p = &c.pinta; let lejos = detalle == DetalleSer::Lejos; let (rx, ry, rz) = (c.cuerpo[0], c.cuerpo[1], c.cuerpo[2]); let coleo = if matches!(andanza, Andanza::Nadar | Andanza::Caminar) { (fase * 2.0).sin() * 0.35 } else { (fase).sin() * 0.08 }; // Un cetáceo no se apoya: flota. Con el centro a `ry·1.25` el delfín quedaba // panza abajo sobre el damero como un pescado en la mesada. let centro = Vec3::new(0.0, ry * 2.6, 0.0); if c.tentaculos > 0 { // Pulpo: manto + tentáculos que ondean. out.push(elipsoide(centro + Vec3::new(0.0, ry * 0.35, 0.0), Vec3::new(rx, ry, rz), p.cuerpo, rx * 0.4)); let n = c.tentaculos as usize; for i in 0..n { let a = i as f32 / n as f32 * PI * 2.0; let base = centro + Vec3::new(a.cos() * rx * 0.72, -ry * 0.25, a.sin() * rz * 0.72); let pts: Vec = (0..=3) .map(|k| { let t = k as f32 / 3.0; base + Vec3::new( a.cos() * rx * 1.5 * t, -ry * 0.9 * t + (t * PI + fase + i as f32).sin() * ry * 0.25 * t, a.sin() * rz * 1.5 * t, ) }) .collect(); cadena(&pts, rx * 0.20, rx * 0.05, p.cuerpo, rx * 0.14, &mut out); } ojos(centro + Vec3::new(0.0, ry * 0.45, rz * 0.55), Vec3::new(rx * 0.55, 0.0, 0.0), rx * c.ojo, p, detalle, &mut out); return out; } // Cuerpo: cápsula fusiforme (hocico → cola), que además ondula al nadar. let morro = Vec3::new(0.0, centro.y, rz); let cola_pt = Vec3::new(coleo * rz * 0.35, centro.y, -rz); out.push(capsula(cola_pt, ry * 0.35, centro, ry, p.cuerpo, ry * 0.7)); out.push(capsula(centro, ry, morro, ry * (0.35 + 0.3 * c.hocico), p.cuerpo, ry * 0.7)); if !lejos { out.push(capsula( cola_pt - Vec3::new(0.0, ry * 0.25, 0.0), ry * 0.22, morro - Vec3::new(0.0, ry * 0.30, 0.0), ry * 0.30, p.vientre, ry * 0.5, )); } if c.hocico > 0.01 { out.push(capsula(morro, ry * 0.42, morro + Vec3::new(0.0, -ry * 0.15, rz * c.hocico), ry * 0.20, p.cuerpo, ry * 0.3)); } if c.boca > 0.01 && !lejos { out.push(decal_linea( morro + Vec3::new(-ry * 0.45, -ry * 0.38, -rz * 0.08), morro + Vec3::new(ry * 0.45, -ry * 0.38, -rz * 0.08), ry * 0.12, [0.72, 0.62, 0.60], )); } // Aleta dorsal. if c.dorsal > 0.01 { out.push(elipsoide( centro + Vec3::new(0.0, ry * (0.85 + c.dorsal * 0.4), -rz * 0.05), Vec3::new(rx * 0.10, ry * c.dorsal * 1.15, rz * c.dorsal * 0.55), p.detalle, ry * 0.2, )); } // Pectorales. if c.pectoral > 0.01 { for s in [-1.0f32, 1.0] { out.push(elipsoide( centro + Vec3::new(s * rx * 1.05, -ry * 0.35, rz * 0.20), Vec3::new(rx * c.pectoral * 1.3, ry * 0.10, rz * c.pectoral * 0.55), p.detalle, ry * 0.15, )); } } // Caudal: vertical en los peces, horizontal en los cetáceos. let cola_c = cola_pt - Vec3::new(0.0, 0.0, rz * c.caudal * 0.55); if c.caudal_vertical { out.push(elipsoide(cola_c, Vec3::new(rx * 0.10, ry * c.caudal * 2.0, rz * c.caudal * 0.7), p.detalle, ry * 0.25)); } else { out.push(elipsoide(cola_c, Vec3::new(rx * c.caudal * 2.6, ry * 0.10, rz * c.caudal * 0.6), p.detalle, ry * 0.25)); } patron(p, cola_pt, morro, ry, 71, detalle, &mut out); ojos( morro + Vec3::new(0.0, ry * 0.30, -rz * 0.12), Vec3::new(ry * 0.55, 0.0, 0.0), ry * c.ojo, p, detalle, &mut out, ); out } // ============================================================================ // Insecto y afines // ============================================================================ fn insecto(c: &Insecto, andanza: Andanza, fase: f32, detalle: DetalleSer) -> Vec { let mut out: Vec = Vec::with_capacity(30); let p = &c.pinta; let lejos = detalle == DetalleSer::Lejos; let l = c.largo; let vuela = matches!(andanza, Andanza::Volar) || c.alas == AlaBicho::Mariposa; let alto_cuerpo = c.segmentos[1] * (1.0 + if c.patas > 0 { c.pata * 0.55 } else { 0.0 }); // El que vuela VUELA: en el suelo, la mariposa era dos discos naranjas tirados // sobre el damero. Se levanta el cuerpo entero un largo y medio. let vuelo = if vuela { l * 1.4 } else { 0.0 }; let cy = alto_cuerpo + vuelo + (fase * 3.0).sin() * if vuela { l * 0.04 } else { l * 0.008 }; // Concha del caracol: espiral logarítmica en el plano Y-Z. if c.concha > 0.01 { let n = 9; for i in 0..n { let t = i as f32 / (n - 1) as f32; let a = t * PI * 3.2; let rad = l * 0.55 * (1.0 - t * 0.72); let dist = l * 0.55 * (0.25 + 0.75 * (1.0 - t)); out.push(esfera( Vec3::new(0.0, cy + a.sin() * dist * 0.55 + l * 0.25, -a.cos() * dist * 0.55 - l * 0.1), rad * 0.62, if i % 2 == 0 { p.detalle } else { p.extra }, rad * 0.35, )); } } // Cuerpo: cabeza (+z), tórax, abdomen (−z). let seg_z = [l * 0.42, l * 0.02, -l * 0.55]; let cabeza_c = Vec3::new(0.0, cy, seg_z[0]); let torax_c = Vec3::new(0.0, cy, seg_z[1]); let abdomen_c = Vec3::new(0.0, cy * 0.95, seg_z[2]); out.push(esfera(abdomen_c, c.segmentos[2], p.cuerpo, c.segmentos[2] * 0.5)); out.push(esfera(torax_c, c.segmentos[1], p.cuerpo, c.segmentos[1] * 0.6)); out.push(esfera(cabeza_c, c.segmentos[0], p.cuerpo, c.segmentos[0] * 0.6)); if c.largo > 1.2 { // Abdomen largo (libélula): cadena hacia atrás. cadena( &[abdomen_c, Vec3::new(0.0, cy * 0.95, -l * 1.15)], c.segmentos[2] * 0.8, c.segmentos[2] * 0.25, p.cuerpo, c.segmentos[2] * 0.4, &mut out, ); } // Patas: arqueadas hacia afuera, en trípode alterno al caminar. let paso = if matches!(andanza, Andanza::Caminar | Andanza::Trotar) { 0.35 } else { 0.0 }; let pares = (c.patas / 2) as usize; for i in 0..pares { let t = if pares <= 1 { 0.0 } else { i as f32 / (pares - 1) as f32 - 0.5 }; for s in [-1.0f32, 1.0] { let raiz = torax_c + Vec3::new(s * c.segmentos[1] * 0.75, -c.segmentos[1] * 0.25, -t * l * 0.75); let sw = paso * (fase + i as f32 * 2.1 + if s > 0.0 { PI } else { 0.0 }).sin(); let codo = raiz + Vec3::new(s * c.pata * c.arco * 0.75, c.pata * 0.42, sw * c.pata * 0.25); let pie = Vec3::new( codo.x + s * c.pata * 0.35, if vuela { codo.y - c.pata * 0.55 } else { 0.0 }, codo.z + sw * c.pata * 0.3, ); let rp = c.segmentos[1] * 0.13; out.push(capsula(raiz, rp, codo, rp * 0.8, p.detalle, rp * 0.7)); out.push(capsula(codo, rp * 0.8, pie, rp * 0.5, p.detalle, rp * 0.6)); } } // Pinzas (escorpión, cangrejo). if c.pinzas > 0.01 { for s in [-1.0f32, 1.0] { let hombro = torax_c + Vec3::new(s * c.segmentos[1] * 0.8, 0.0, c.segmentos[1] * 0.5); let codo = hombro + Vec3::new(s * c.pinzas * l * 0.30, 0.0, c.pinzas * l * 0.28); out.push(capsula(hombro, c.segmentos[1] * 0.20, codo, c.segmentos[1] * 0.16, p.cuerpo, c.segmentos[1] * 0.15)); let punta_a = codo + Vec3::new(s * c.pinzas * l * 0.12, c.segmentos[1] * 0.14, c.pinzas * l * 0.26); let punta_b = codo + Vec3::new(s * c.pinzas * l * 0.16, -c.segmentos[1] * 0.14, c.pinzas * l * 0.24); out.push(capsula(codo, c.segmentos[1] * 0.24, punta_a, c.segmentos[1] * 0.05, p.extra, c.segmentos[1] * 0.12)); out.push(capsula(codo, c.segmentos[1] * 0.20, punta_b, c.segmentos[1] * 0.05, p.extra, c.segmentos[1] * 0.12)); } } // Cola con aguijón (escorpión): arco sobre el lomo. if c.aguijon > 0.01 { let n = 5; let pts: Vec = (0..=n) .map(|i| { let t = i as f32 / n as f32; let a = t * PI * 0.85; abdomen_c + Vec3::new(0.0, a.sin() * c.aguijon * l * 0.85, -a.cos() * c.aguijon * l * 0.45 + c.aguijon * l * 0.45) }) .collect(); cadena(&pts, c.segmentos[2] * 0.55, c.segmentos[2] * 0.28, p.cuerpo, c.segmentos[2] * 0.3, &mut out); let punta = *pts.last().unwrap(); out.push(capsula(punta, c.segmentos[2] * 0.26, punta + Vec3::new(0.0, -c.segmentos[2] * 0.2, c.segmentos[2] * 0.55), c.segmentos[2] * 0.02, p.extra, c.segmentos[2] * 0.1)); } // Alas. let bateo = if vuela { (fase * 8.0).sin() * 0.55 } else { 0.0 }; match c.alas { AlaBicho::Ninguna => {} AlaBicho::Mariposa => { for s in [-1.0f32, 1.0] { let sube = bateo * c.ala_len * 0.25; // Ala delantera grande + trasera chica, casi planas (finas en Y). let a1 = torax_c + Vec3::new(s * c.ala_len * 0.62, sube + c.segmentos[1] * 0.2, c.ala_len * 0.18); out.push(elipsoide(a1, Vec3::new(c.ala_len * 0.62, c.ala_len * 0.05, c.ala_len * 0.48), p.detalle, c.ala_len * 0.08)); let a2 = torax_c + Vec3::new(s * c.ala_len * 0.45, sube * 0.8, -c.ala_len * 0.42); out.push(elipsoide(a2, Vec3::new(c.ala_len * 0.42, c.ala_len * 0.045, c.ala_len * 0.34), p.extra, c.ala_len * 0.08)); if !lejos { out.push(decal(a1 + Vec3::new(s * c.ala_len * 0.22, c.ala_len * 0.05, c.ala_len * 0.10), c.ala_len * 0.20, p.patron_col)); out.push(decal(a2 + Vec3::new(s * c.ala_len * 0.10, c.ala_len * 0.04, 0.0), c.ala_len * 0.13, p.patron_col)); } } } AlaBicho::Membrana => { for s in [-1.0f32, 1.0] { for (k, dz) in [(0usize, 0.25f32), (1, -0.35)] { let a = torax_c + Vec3::new( s * c.ala_len * 0.55, c.segmentos[1] * 0.35 + bateo * c.ala_len * 0.30, c.ala_len * dz, ); out.push(elipsoide( a, Vec3::new(c.ala_len * (0.58 - 0.10 * k as f32), c.ala_len * 0.035, c.ala_len * 0.16), [0.86, 0.90, 0.94], c.ala_len * 0.05, )); } } } AlaBicho::Elitros => { for s in [-1.0f32, 1.0] { out.push(elipsoide( abdomen_c + Vec3::new(s * c.segmentos[2] * 0.45, c.segmentos[2] * 0.25, 0.0), Vec3::new(c.segmentos[2] * 0.62, c.segmentos[2] * 0.55, c.segmentos[2] * 1.05), p.detalle, c.segmentos[2] * 0.25, )); } if !lejos && p.patron == Patron::Lunares { for i in 0..4 { let s = if i % 2 == 0 { -1.0 } else { 1.0f32 }; out.push(decal( abdomen_c + Vec3::new(s * c.segmentos[2] * 0.45, c.segmentos[2] * 0.72, c.segmentos[2] * (0.4 - 0.5 * (i / 2) as f32)), c.segmentos[2] * 0.22, p.patron_col, )); } } } } // Antenas y pedúnculos oculares. if c.antenas > 0.01 { for s in [-1.0f32, 1.0] { let punta = cabeza_c + Vec3::new(s * c.antenas * l * 0.28, c.antenas * l * 0.55, c.antenas * l * 0.25); out.push(capsula(cabeza_c + Vec3::new(0.0, c.segmentos[0] * 0.6, 0.0), c.segmentos[0] * 0.09, punta, c.segmentos[0] * 0.03, p.detalle, c.segmentos[0] * 0.05)); if !lejos { out.push(esfera(punta, c.segmentos[0] * 0.10, p.detalle, c.segmentos[0] * 0.05)); } } } if c.pedunculos && lejos { // De lejos el pedúnculo mide menos de un píxel: quedan los dos puntos. for s in [-1.0f32, 1.0] { out.push(decal( cabeza_c + Vec3::new(s * c.segmentos[0] * 0.5, c.segmentos[0] * 0.8, c.segmentos[0] * 0.3), c.segmentos[0] * c.ojo * 1.2, [0.14, 0.12, 0.14], )); } } else if c.pedunculos { for s in [-1.0f32, 1.0] { let punta = cabeza_c + Vec3::new(s * c.segmentos[0] * 0.55, c.segmentos[0] * 1.15, c.segmentos[0] * 0.25); out.push(capsula(cabeza_c, c.segmentos[0] * 0.12, punta, c.segmentos[0] * 0.08, p.cuerpo, c.segmentos[0] * 0.08)); out.push(esfera(punta, c.segmentos[0] * c.ojo * 1.4, [0.14, 0.12, 0.14], c.segmentos[0] * 0.06)); } } else { ojos( cabeza_c, Vec3::new(c.segmentos[0] * 0.62, c.segmentos[0] * 0.20, c.segmentos[0] * 0.42), c.segmentos[0] * c.ojo, p, detalle, &mut out, ); } // Rayas del abdomen (abeja). if p.patron == Patron::Rayas && !lejos { patron(p, abdomen_c + Vec3::new(0.0, 0.0, -c.segmentos[2] * 0.7), torax_c, c.segmentos[2], 83, detalle, &mut out); } let _ = mezcla; let _ = rot_y; let _ = hash11; out } #[cfg(test)] mod tests { use super::*; use std::collections::HashSet; fn geometria(s: &SdfSer) -> usize { s.prims .iter() .filter(|p| !matches!(p.kind, llimphi_3d::SdfKind::DecalEsfera | llimphi_3d::SdfKind::DecalCapsula)) .count() } /// El catálogo entero se arma: nadie queda vacío, nadie escupe un NaN y nadie /// se pasa del presupuesto del uniform él solo. #[test] fn todo_el_bestiario_se_arma() { for b in Bicho::ALL { let s = bicho_sdf(b); assert!(geometria(&s) >= 6, "{} tiene {} primitivas", b.label(), geometria(&s)); assert!( s.prims.len() <= llimphi_3d::VOXEL_MAX_SER_PRIMS, "{} no entra en el uniform ({} prims)", b.label(), s.prims.len() ); 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}", b.label()); } assert!(p.blend > 0.0, "{}: blend cero funde mal", b.label()); } } } /// Cada bicho apoya en `y = 0` y mide lo que se le pidió: es lo que deja /// sembrarlo sobre el relieve sin que flote ni se entierre. #[test] fn cada_bicho_apoya_en_el_piso_y_mide_lo_pedido() { for b in Bicho::ALL { for alto in [0.2f32, 1.0, 4.0] { let s = bicho_sdf_lod(b, alto, Andanza::Quieto, 0.0, DetalleSer::Hero); let (lo, hi) = crate::sdf_kit::extension_y(&s.prims); assert!(lo.abs() < alto * 1e-3, "{} apoya en {lo}", b.label()); assert!( (hi - alto).abs() < alto * 1e-3, "{} mide {hi} y se pidió {alto}", b.label() ); } } } /// El LOD tiene que BAJAR el costo, no subirlo — es el único motivo por el que /// existe. Y el costo declarado en `DetalleSer::costo` no aplica acá (es el del /// humanoide), así que lo que se fija es la monotonía. #[test] fn el_detalle_baja_con_la_distancia() { for b in Bicho::ALL { let n = |d| bicho_sdf_lod(b, 1.0, Andanza::Quieto, 0.0, d).prims.len(); let (h, m, l) = (n(DetalleSer::Hero), n(DetalleSer::Medio), n(DetalleSer::Lejos)); assert!(h >= m && m >= l, "{}: hero={h} medio={m} lejos={l}", b.label()); assert!(l < h, "{}: el LOD lejano no ahorra nada ({l} vs {h})", b.label()); } } /// Dos especies no pueden dibujar exactamente lo mismo: si pasa, una de las dos /// es una fila de la tabla que no cambió nada. #[test] fn ninguna_especie_es_copia_de_otra() { let mut firmas: Vec<(String, &str)> = Vec::new(); for b in Bicho::ALL { let s = bicho_sdf_lod(b, 1.0, Andanza::Quieto, 0.0, DetalleSer::Hero); let firma = s .prims .iter() .map(|p| format!("{:?}{:?}{:.4}{:?}", p.kind, p.a, p.ra, p.color)) .collect::>() .join("|"); if let Some((_, otro)) = firmas.iter().find(|(f, _)| *f == firma) { panic!("{} dibuja exactamente lo mismo que {}", b.label(), otro); } firmas.push((firma, b.label())); } } /// La andanza tiene que MOVER algo. Un `Caminar` que da la misma pose que /// `Quieto` es una animación que no existe (y no se nota hasta ver el video). #[test] fn caminar_mueve_las_patas() { let mut movidos = 0; for b in Bicho::ALL { let q = bicho_sdf_con(b, 1.0, Andanza::Quieto, 0.0); let c = bicho_sdf_con(b, 1.0, Andanza::Caminar, 1.1); let dif = q .prims .iter() .zip(c.prims.iter()) .filter(|(a, b)| a.a != b.a || a.b != b.b) .count(); if dif > 0 { movidos += 1; } } assert_eq!(movidos, Bicho::ALL.len(), "{movidos} de {} se mueven al caminar", Bicho::ALL.len()); } /// **El hocico baja al comer y sube al estar alerta.** Parece obvio y no lo es: /// `rot_x` gira `+Z` hacia `−Y`, así que el cuello se armó invertido y las /// cincuenta especies de cuatro patas salieron pastando en andanza «caminar». /// Ningún conteo lo veía —la altura total y el apoyo eran correctos— y en la /// hoja de contacto saltaba a la vista. Este test es esa hoja, en números. #[test] fn el_hocico_baja_al_comer_y_sube_al_estar_alerta() { // El hocico es la geometría más adelantada (mayor `z`). let punta_y = |b: Bicho, a: Andanza| -> f32 { let s = bicho_sdf_lod(b, 1.0, a, 0.0, DetalleSer::Lejos); s.prims .iter() .filter(|p| !matches!(p.kind, llimphi_3d::SdfKind::DecalEsfera | llimphi_3d::SdfKind::DecalCapsula)) .max_by(|x, y| x.b[2].total_cmp(&y.b[2])) .map(|p| p.b[1]) .unwrap_or(0.0) }; for b in [Bicho::Vaca, Bicho::Caballo, Bicho::Ciervo, Bicho::Jirafa, Bicho::Llama] { let comiendo = punta_y(b, Andanza::Comer); let quieto = punta_y(b, Andanza::Quieto); let alerta = punta_y(b, Andanza::Alerta); assert!( comiendo < quieto, "{}: comiendo el hocico está a {comiendo} y quieto a {quieto}", b.label() ); // Alerta nunca baja la cabeza de forma apreciable. El margen del 3 % // no es una excusa: el ensamble se normaliza a la altura total, así que // en un cuello YA vertical (llama, jirafa) levantarlo hace al animal // más alto en vez de subir el hocico, y al reescalar el hocico queda un // pelo más abajo en la figura. Donde el cuello tiene recorrido, la // exigencia es estricta (abajo). assert!( alerta >= quieto * 0.97, "{}: alerta el hocico está a {alerta} y quieto a {quieto}", b.label() ); if let crate::Receta::Cuatro(rec) = receta(b) { if rec.cuello_ang < 1.0 { assert!( alerta > quieto + 1e-3, "{}: alerta no levantó la cabeza ({alerta} vs {quieto})", b.label() ); } } // Y quieto, la cabeza va POR ENCIMA de la panza: un cuadrúpedo que // camina no lleva el hocico al ras del piso. assert!(quieto > 0.25, "{}: quieto el hocico está a {quieto} de 1.0", b.label()); } } /// Determinismo: la misma entrada da el mismo bicho (el preview y el export /// tienen que coincidir cuadro a cuadro). #[test] fn el_mismo_bicho_sale_igual_dos_veces() { for b in [Bicho::Vaca, Bicho::Guacamayo, Bicho::Boa, Bicho::Mariposa, Bicho::Delfin] { let a = bicho_sdf_con(b, 1.0, Andanza::Caminar, 0.7); let c = bicho_sdf_con(b, 1.0, Andanza::Caminar, 0.7); assert_eq!(a.prims.len(), c.prims.len()); for (x, y) in a.prims.iter().zip(c.prims.iter()) { assert_eq!(x.a, y.a); assert_eq!(x.color, y.color); } } } /// Las cinco familias están todas representadas y cada especie cae en la suya. #[test] fn las_cinco_familias_estan_pobladas() { let mut vistas: HashSet<&str> = HashSet::new(); for b in Bicho::ALL { vistas.insert(receta(b).familia()); } assert_eq!(vistas.len(), 5, "familias vistas: {vistas:?}"); } /// Un rebaño de veinte animales tiene que entrar en el uniform con LOD — es el /// caso de uso, no una curiosidad. #[test] fn un_rebano_de_veinte_entra_en_el_uniform() { let manada = [ Bicho::Vaca, Bicho::Oveja, Bicho::Caballo, Bicho::Cabra, Bicho::Cerdo, Bicho::Gallina, Bicho::Pato, Bicho::Perro, Bicho::Gato, Bicho::Conejo, ]; let total: usize = manada .iter() .enumerate() .map(|(i, &b)| { let d = if i < 2 { DetalleSer::Hero } else if i < 5 { DetalleSer::Medio } else { DetalleSer::Lejos }; bicho_sdf_lod(b, 1.0, Andanza::Caminar, i as f32, d).prims.len() }) .sum::() * 2; assert!( total <= llimphi_3d::VOXEL_MAX_SER_PRIMS, "veinte animales gastan {total} de {}", llimphi_3d::VOXEL_MAX_SER_PRIMS ); } }