gioser

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/bicho_sdf.rs Rust · 1719 líneas · 71.0 KB Historia Quién tocó qué crudo

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

1//! **El bestiario SDF** — la segunda técnica de graficación aplicada a los animales:
2//! cada [`Bicho`] armado con primitivas analíticas fundidas por `smin`, igual que el
3//! ser humanoide de [`ser_sdf`](crate::ser_sdf) y con la misma respuesta de luz.
4//!
5//! ## Por qué existe
6//!
7//! La fauna del mundo se estampaba en voxels ([`stamp_bicho`](crate::stamp_bicho)):
8//! cinco o seis cubos por animal, perfectos para poblar un bioma a la distancia y
9//! **imposibles de mirar de cerca**. Cuando la gente pasó a ser SDF, un rebaño de
10//! cajas al lado de una persona de silueta lisa se veía como dos juegos distintos
11//! pegados con cinta. Esto cierra esa brecha: **una vaca cerca es analítica; a
12//! doscientos metros sigue siendo la estampa voxel** (`Bicho::silueta_voxel`).
13//!
14//! ## Cómo se escribe una especie
15//!
16//! No se modela: se describe. Toda la geometría sale de la
17//! [`Receta`](crate::Receta) — proporciones con nombre — y de cinco constructores,
18//! uno por familia morfológica. Una especie nueva es una fila de la tabla de
19//! [`receta`](crate::receta), no un archivo nuevo.
20//!
21//! ## Convenciones
22//!
23//! Espacio LOCAL: patas en `y = 0`, centrado en X/Z, **mirando `+Z`**. El ensamble
24//! se arma en unidades cómodas para las fórmulas y al final se **asienta y escala**
25//! a la altura pedida ([`asentar`](crate::sdf_kit::asentar)) — así una jirafa y una
26//! hormiga se escriben con los mismos rangos de números.
27
28use std::f32::consts::PI;
29
30use llimphi_3d::glam::Vec3;
31use llimphi_3d::{SdfPrim, SdfSer};
32
33use crate::bicho::Bicho;
34use crate::bicho_receta::{
35    alto_tipico, receta, Ala, AlaBicho, Ave, ColaAve, Cola, Cresta, Cuadrupedo, Cuerno, Insecto,
36    Nadador, Oreja, Patron, Pico, Pinta, Receta, Reptante,
37};
38use crate::sdf_kit::{
39    asentar, cadena, capsula, decal, decal_linea, elipsoide, esfera, hash01, hash11, mezcla, ojo,
40    rot_x, rot_y, tono,
41};
42use crate::ser_sdf::DetalleSer;
43
44/// **Qué está haciendo el bicho.** No es un catálogo de clips como el de la gente
45/// ([`Clip`](crate::Clip)): un animal tiene menos poses y las que importan son
46/// las que se ven en un mundo — pastar, caminar, estar alerta.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum Andanza {
49    /// Quieto, respirando y moviendo la cola.
50    Quieto,
51    /// Caminando: patas en oposición diagonal.
52    Caminar,
53    /// Al trote/galope: más amplitud, más rebote, cuerpo inclinado.
54    Trotar,
55    /// Pastando/comiendo: la cabeza baja al suelo.
56    Comer,
57    /// Alerta: cabeza y orejas arriba, cuerpo tenso.
58    Alerta,
59    /// En el aire (aves e insectos): alas batiendo, patas recogidas.
60    Volar,
61    /// Nadando (cetáceos, peces, patos): el cuerpo ondula.
62    Nadar,
63    /// Saltando (canguro, rana): patas plegadas y cuerpo despegado.
64    Saltar,
65}
66
67impl Andanza {
68    /// Amplitud del balanceo de las patas y del rebote del cuerpo.
69    fn ritmo(self) -> (f32, f32) {
70        match self {
71            Andanza::Quieto | Andanza::Alerta => (0.0, 0.004),
72            Andanza::Caminar => (0.34, 0.018),
73            Andanza::Trotar => (0.62, 0.045),
74            Andanza::Comer => (0.0, 0.006),
75            Andanza::Volar | Andanza::Nadar => (0.10, 0.020),
76            Andanza::Saltar => (0.0, 0.030),
77        }
78    }
79}
80
81/// El bicho armado como ser SDF, a su [`alto_tipico`], quieto.
82pub fn bicho_sdf(b: Bicho) -> SdfSer {
83    bicho_sdf_lod(b, alto_tipico(b), Andanza::Quieto, 0.0, DetalleSer::Hero)
84}
85
86/// El bicho a la altura, andanza y fase pedidas, con todo el detalle.
87pub fn bicho_sdf_con(b: Bicho, alto: f32, andanza: Andanza, fase: f32) -> SdfSer {
88    bicho_sdf_lod(b, alto, andanza, fase, DetalleSer::Hero)
89}
90
91/// El bicho con el nivel de detalle elegido. `Medio` suelta las chispas de los ojos
92/// y ralea el patrón; `Lejos` se queda con la silueta y dos puntos por ojos — a esa
93/// distancia lo demás es sub-píxel y las primitivas son el recurso escaso.
94pub fn bicho_sdf_lod(
95    b: Bicho,
96    alto: f32,
97    andanza: Andanza,
98    fase: f32,
99    detalle: DetalleSer,
100) -> SdfSer {
101    let mut prims = match receta(b) {
102        Receta::Cuatro(c) => cuadrupedo(&c, andanza, fase, detalle),
103        Receta::Ave(a) => ave(&a, andanza, fase, detalle),
104        Receta::Reptante(r) => reptante(&r, andanza, fase, detalle),
105        Receta::Nadador(n) => nadador(&n, andanza, fase, detalle),
106        Receta::Insecto(i) => insecto(&i, andanza, fase, detalle),
107    };
108    asentar(&mut prims, alto.max(0.02));
109    SdfSer { prims, pos: [0.0; 3], yaw: 0.0 }
110}
111
112// ============================================================================
113//  Piezas comunes
114// ============================================================================
115
116/// El ojo de un animal: el iris manda. Si la pinta declara esclera (primates, aves
117/// de dibujo) va el blanco; si el color de ojo es claro (el ámbar del lobo, el
118/// amarillo del búho) ese color hace de esclera y la pupila va oscura encima; y si
119/// es oscuro, un globo negro a secas — que es lo que se ve en un ciervo.
120fn ojos(centro: Vec3, sep: Vec3, r: f32, p: &Pinta, detalle: DetalleSer, out: &mut Vec<SdfPrim>) {
121    let lum = p.ojo[0] * 0.3 + p.ojo[1] * 0.6 + p.ojo[2] * 0.1;
122    let chispa = detalle == DetalleSer::Hero;
123    for s in [-1.0f32, 1.0] {
124        let c = centro + Vec3::new(sep.x * s, sep.y, sep.z);
125        let dir = Vec3::new(s * 0.55, 0.10, 0.85);
126        if detalle == DetalleSer::Lejos {
127            out.push(decal(c + dir.normalize() * (r * 0.6), r, [0.10, 0.09, 0.11]));
128            continue;
129        }
130        let (esclera, pupila) = if p.esclera {
131            (Some([0.94, 0.945, 0.955]), [0.16, 0.13, 0.18])
132        } else if lum > 0.45 {
133            (Some(p.ojo), [0.12, 0.10, 0.12])
134        } else {
135            (None, p.ojo)
136        };
137        ojo(c, dir, r, esclera, pupila, chispa, out);
138    }
139}
140
141/// Manchas/rayas/antifaz sobre un cuerpo ya armado. Los decals no aportan
142/// geometría: repintan la superficie donde caen, así que un patrón no cuesta
143/// silueta — sólo primitivas.
144#[allow(clippy::too_many_arguments)]
145fn patron(
146    p: &Pinta,
147    eje_a: Vec3,
148    eje_b: Vec3,
149    r: f32,
150    semilla: u32,
151    detalle: DetalleSer,
152    out: &mut Vec<SdfPrim>,
153) {
154    if p.patron == Patron::Liso || detalle == DetalleSer::Lejos {
155        return;
156    }
157    let n = match detalle {
158        DetalleSer::Hero => 9,
159        _ => 5,
160        };
161    let col = p.patron_col;
162    match p.patron {
163        Patron::Manchas | Patron::Montura => {
164            for i in 0..n {
165                let t = (i as f32 + 0.5) / n as f32;
166                let c = eje_a + (eje_b - eje_a) * t;
167                let ang = hash01(semilla, i as u32) * PI * 2.0;
168                let alt = 0.35 + 0.55 * hash01(semilla + 7, i as u32);
169                let d = Vec3::new(ang.cos(), alt, ang.sin() * 0.6).normalize();
170                out.push(decal(c + d * (r * 0.95), r * (0.30 + 0.22 * hash01(semilla + 11, i as u32)), col));
171            }
172        }
173        Patron::Lunares => {
174            for i in 0..(n * 2) {
175                let t = (i as f32 + 0.5) / (n * 2) as f32;
176                let c = eje_a + (eje_b - eje_a) * t;
177                let ang = hash01(semilla + 3, i as u32) * PI * 2.0;
178                let d = Vec3::new(ang.cos(), 0.55 + 0.4 * hash01(semilla + 5, i as u32), ang.sin() * 0.5)
179                    .normalize();
180                out.push(decal(c + d * (r * 0.95), r * 0.16, col));
181            }
182        }
183        Patron::Rayas | Patron::Anillos => {
184            for i in 0..n {
185                let t = (i as f32 + 0.5) / n as f32;
186                let c = eje_a + (eje_b - eje_a) * t;
187                // Franja transversal: cruza el lomo de un flanco al otro.
188                out.push(decal_linea(
189                    c + Vec3::new(-r * 1.02, 0.0, 0.0),
190                    c + Vec3::new(r * 1.02, r * 0.35, 0.0),
191                    r * 0.16,
192                    col,
193                ));
194            }
195        }
196        Patron::Rombos => {
197            for i in 0..n {
198                let t = (i as f32 + 0.5) / n as f32;
199                let c = eje_a + (eje_b - eje_a) * t;
200                out.push(decal(c + Vec3::new(0.0, r * 0.92, 0.0), r * 0.42, col));
201            }
202        }
203        Patron::Antifaz | Patron::Liso => {}
204    }
205}
206
207// ============================================================================
208//  Cuadrúpedo
209// ============================================================================
210
211fn cuadrupedo(c: &Cuadrupedo, andanza: Andanza, fase: f32, detalle: DetalleSer) -> Vec<SdfPrim> {
212    let mut out: Vec<SdfPrim> = Vec::with_capacity(40);
213    let p = &c.pinta;
214    let (amp, bob_amp) = andanza.ritmo();
215    let bob = (fase * 2.0).sin() * bob_amp;
216    let lejos = detalle == DetalleSer::Lejos;
217
218    let l = c.largo;
219    let r = c.cuerpo_r;
220    // Inclinación del tronco: el bípedo (canguro) se para, el que trota se echa
221    // adelante. El eje del cuerpo se arma girado alrededor de la cadera.
222    // Signo: positivo hunde el frente (el que trota se echa adelante), negativo lo
223    // levanta. El bípedo se PARA, así que va negativo — con `+0.95` el canguro
224    // salía olfateando el piso.
225    let tilt = if c.bipedo { -0.95 } else { 0.0 }
226        + if andanza == Andanza::Trotar { 0.06 } else { 0.0 };
227    let cadera = Vec3::new(0.0, c.pata + r + bob, -l * 0.5);
228    let eje = |t: f32, dy: f32| -> Vec3 {
229        // `t` = 0 en la grupa, 1 en el pecho. El lomo se comba con `lomo`.
230        let base = Vec3::new(0.0, c.lomo * (PI * t).sin() + dy, l * t);
231        cadera + rot_x(base, tilt)
232    };
233
234    let grupa = eje(0.0, 0.0);
235    let medio = eje(0.5, 0.0);
236    let pecho = eje(1.0, 0.0);
237
238    // --- Tronco: dos conos redondeados (grupa→medio→pecho). Un solo elipsoide era
239    // una bola que se tragaba las patas y el cuello (la lección del ser humanoide).
240    out.push(capsula(grupa, r * c.grupa, medio, r, p.cuerpo, r * 0.9));
241    out.push(capsula(medio, r, pecho, r * c.pecho, p.cuerpo, r * 0.9));
242    // Vientre: mismo eje, corrido abajo y aclarado. Funde con el tronco y le pinta
243    // la panza sin una sola primitiva de geometría extra en la silueta.
244    if !lejos {
245        out.push(capsula(
246            eje(0.12, -r * 0.42),
247            r * 0.72 * c.grupa,
248            eje(0.88, -r * 0.42),
249            r * 0.72 * c.pecho,
250            p.vientre,
251            r * 0.7,
252        ));
253    }
254
255    // --- Jorobas.
256    for i in 0..c.jorobas {
257        let t = if c.jorobas == 1 { 0.45 } else { 0.32 + 0.34 * i as f32 };
258        let cj = eje(t, r * 0.55);
259        out.push(elipsoide(
260            cj,
261            Vec3::new(r * 0.78, c.joroba_alto + r * 0.45, l * 0.20),
262            p.cuerpo,
263            r * 0.6,
264        ));
265    }
266
267    // --- Patas. Diagonales en oposición: la delantera izquierda va con la trasera
268    // derecha (el paso de cualquier cuadrúpedo).
269    let hip_x = (r * 0.70).max(c.pata_r * 1.6);
270    for (i, (sx, fz)) in [(-1.0f32, 1.0f32), (1.0, 1.0), (-1.0, -1.0), (1.0, -1.0)]
271        .into_iter()
272        .enumerate()
273    {
274        let delantera = fz > 0.0;
275        let desfase = if (sx > 0.0) == delantera { 0.0 } else { PI };
276        let sw = amp * (fase + desfase).sin();
277        let t = if delantera { 0.86 } else { 0.14 };
278        let raiz = eje(t, -r * 0.25) + Vec3::new(sx * hip_x, 0.0, 0.0);
279        // El bípedo lleva los brazos cortos y altos, y las patas traseras enormes.
280        // El largo de la pata sale de DÓNDE ESTÁ LA RAÍZ, no de `c.pata`: la raíz va
281        // enterrada en el tronco (media panza más arriba), así que usar `c.pata`
282        // dejaba al pie a un cuarto de altura del piso. Después `asentar` bajaba
283        // todo el bicho hasta apoyar ESE pie y el animal quedaba aplastado, con
284        // patas de pan y la panza rozando el suelo — así salió la primera hoja
285        // entera del bestiario.
286        let (largo_pata, radio, alto_raiz) = if c.bipedo && delantera {
287            let raiz_alta = raiz + Vec3::new(0.0, r * 0.55, 0.0);
288            (raiz_alta.y * 0.42, c.pata_r * 0.62, raiz_alta)
289        } else {
290            (raiz.y, c.pata_r, raiz)
291        };
292        let dir = rot_x(Vec3::new(0.0, -largo_pata, 0.0), sw);
293        let rodilla = alto_raiz + dir * 0.52 + Vec3::new(0.0, 0.0, largo_pata * 0.10 * fz);
294        let pie = alto_raiz + dir;
295        if largo_pata > 0.30 {
296            // Pata larga: dos tramos (la rodilla se lee de lejos en un caballo).
297            out.push(capsula(alto_raiz, radio * 1.25, rodilla, radio, p.cuerpo, radio * 0.8));
298            out.push(capsula(rodilla, radio, pie, radio * 0.85, p.cuerpo, radio * 0.6));
299        } else {
300            out.push(capsula(alto_raiz, radio * 1.25, pie, radio * 0.9, p.cuerpo, radio * 0.8));
301        }
302        // Pezuña/garra.
303        out.push(esfera(pie, radio * 0.95, p.extra, radio * 0.5));
304        let _ = i;
305    }
306
307    // --- Cola.
308    let cola_base = eje(0.0, r * 0.35) + Vec3::new(0.0, 0.0, -r * 0.35);
309    let vaiven = (fase * 1.3).sin() * 0.22;
310    cola(c, cola_base, vaiven, &mut out);
311
312    // --- Cuello y cabeza.
313    let ang = match andanza {
314        Andanza::Comer => -0.75,
315        // Levantar la cabeza tiene TECHO: el cuello de la llama ya sale a 1,25 rad
316        // (casi vertical) y sumarle 0,30 no la levanta más — la manda para atrás.
317        // Lo que queda de recorrido hasta la vertical, eso se levanta.
318        Andanza::Alerta => c.cuello_ang + 0.30 * (1.50 - c.cuello_ang).clamp(0.0, 1.0),
319        _ => c.cuello_ang,
320    } + (fase * 2.0).cos() * bob_amp * 2.0;
321    let cuello_base = eje(0.98, r * 0.45);
322    // OJO CON EL SIGNO: `rot_x` gira (0,0,1) hacia −Y con ángulo positivo, así que
323    // pasarle `ang` directo mandaba el cuello AL PISO — la primera hoja tenía a la
324    // vaca, el ciervo, el caballo y el camello pastando todos a la vez, en andanza
325    // «caminar». `arriba(a)` deja el signo del lado en que se lee la receta:
326    // positivo = cuello levantado.
327    let arriba = |a: f32| rot_x(Vec3::new(0.0, 0.0, 1.0), -a);
328    let dir_cuello = arriba(ang);
329    // El cuello arranca EN la superficie del pecho, así que su largo tiene que
330    // pasar el radio del pecho o la cabeza queda medio hundida en el tronco (la
331    // vaca y el caballo de la primera hoja parecían jabalíes sin cuello).
332    let largo_cuello = c.cuello + c.cabeza * 0.55 + r * c.pecho * 0.55;
333    let cabeza_c = cuello_base + dir_cuello * largo_cuello;
334    if c.cuello > 0.01 {
335        // Cuello largo (jirafa, llama): dos tramos para que arquee.
336        if c.cuello > 0.5 {
337            let medio_c = cuello_base + arriba(ang - 0.25) * (largo_cuello * 0.55);
338            out.push(capsula(cuello_base, c.cuello_r * 1.35, medio_c, c.cuello_r, p.cuerpo, c.cuello_r * 0.9));
339            out.push(capsula(medio_c, c.cuello_r, cabeza_c, c.cuello_r * 0.9, p.cuerpo, c.cuello_r * 0.8));
340        } else {
341            out.push(capsula(cuello_base, c.cuello_r * 1.3, cabeza_c, c.cuello_r * 0.95, p.cuerpo, c.cuello_r * 0.9));
342        }
343    }
344    let cab = c.cabeza;
345    out.push(elipsoide(
346        cabeza_c,
347        Vec3::new(cab * c.craneo[0], cab * c.craneo[1], cab * c.craneo[2]),
348        p.cuerpo,
349        cab * 0.45,
350    ));
351
352    // --- Hocico: cápsula hacia adelante y abajo (la cabeza es un elipsoide alineado
353    // a ejes y no puede rotar; el hocico sí, y es el que da la especie).
354    // El hocico mira adelante y un poco abajo, y acompaña al cuello.
355    let dir_hocico = arriba(ang * 0.45 - 0.30);
356    let punta = cabeza_c + dir_hocico * (cab * (0.55 + c.hocico));
357    if c.hocico > 0.05 {
358        out.push(capsula(
359            cabeza_c + dir_hocico * (cab * 0.25),
360            cab * c.hocico_r * 1.05,
361            punta,
362            cab * c.hocico_r * 0.82,
363            p.cuerpo,
364            cab * 0.30,
365        ));
366        if !lejos {
367            out.push(decal(punta + dir_hocico * (cab * c.hocico_r * 0.5), cab * c.hocico_r * 0.55, tono(p.detalle, 0.7)));
368        }
369    }
370
371    // --- Trompa (elefante, tapir): cadena que cae desde el hocico y se enrosca.
372    if c.trompa > 0.01 {
373        let n = 5;
374        let mut pts = Vec::with_capacity(n + 1);
375        for i in 0..=n {
376            let t = i as f32 / n as f32;
377            let caida = -t * c.trompa * cab * 0.95;
378            let curva = (t * t) * cab * c.trompa * 0.30;
379            pts.push(punta + Vec3::new(0.0, caida, curva * 0.7 + t * cab * 0.15));
380        }
381        cadena(&pts, cab * 0.34, cab * 0.13, p.cuerpo, cab * 0.18, &mut out);
382    }
383
384    // --- Orejas.
385    orejas(c, cabeza_c, cab, andanza, &mut out);
386    // --- Cuernos, astas, colmillos.
387    cuernos(c, cabeza_c, punta, cab, &mut out);
388
389    // --- Melena / crin.
390    if c.melena > 0.01 && !lejos {
391        if c.cola == Cola::Crin {
392            // Crin: cresta de pelo a lo largo del cuello (caballo, cebra).
393            let n = 5;
394            for i in 0..=n {
395                let t = i as f32 / n as f32;
396                let cpt = cuello_base + (cabeza_c - cuello_base) * t + Vec3::new(0.0, c.cuello_r * 0.9, 0.0);
397                out.push(elipsoide(
398                    cpt,
399                    Vec3::new(c.cuello_r * 0.35, c.cuello_r * (0.5 + 0.5 * c.melena), c.cuello_r * 0.8),
400                    p.detalle,
401                    c.cuello_r * 0.3,
402                ));
403            }
404        } else {
405            // Melena: corona de bultos alrededor del cuello (león).
406            let n = 9;
407            for i in 0..n {
408                let a = i as f32 / n as f32 * PI * 2.0;
409                let rad = cab * (1.05 + 0.25 * c.melena);
410                let cpt = cabeza_c
411                    + Vec3::new(a.cos() * rad, a.sin() * rad, -cab * 0.55);
412                out.push(esfera(cpt, cab * 0.42 * c.melena, p.detalle, cab * 0.30));
413            }
414        }
415    }
416
417    // --- Púas (erizo, puercoespín): conos cortos por todo el lomo. Es lo mismo
418    // que la lana en el sitio y lo contrario en la forma — y es LA silueta del
419    // erizo, que sin ellas es un pan.
420    if c.puas > 0.01 && !lejos {
421        let n = 14;
422        for i in 0..n {
423            let t = 0.10 + 0.82 * (i as f32 / (n - 1) as f32);
424            let a = hash01(91, i as u32) * PI * 2.0;
425            let dir = Vec3::new(a.cos() * 0.55, 0.82, -0.15).normalize();
426            // La raíz va sobre la SUPERFICIE del lomo y la púa mide vez y media el
427            // radio del cuerpo: con `0.85·r` desde adentro, el `smin` se las comía
428            // enteras y el erizo seguía siendo un pan.
429            let base = eje(t, 0.0) + dir * (r * 0.72);
430            out.push(capsula(
431                base,
432                r * 0.15,
433                base + dir * (r * 1.35 * c.puas),
434                r * 0.012,
435                p.detalle,
436                r * 0.07,
437            ));
438        }
439    }
440
441    // --- Lana (oveja, llama, koala): bultos mullidos sobre el tronco.
442    if c.lana > 0.01 && !lejos {
443        let n = 7;
444        for i in 0..n {
445            let t = 0.12 + 0.78 * (i as f32 / (n - 1) as f32);
446            let a = hash01(31, i as u32) * PI * 2.0;
447            let cpt = eje(t, r * 0.35) + Vec3::new(a.cos() * r * 0.55, 0.0, 0.0);
448            out.push(esfera(cpt, r * (0.48 + 0.18 * hash01(57, i as u32)) * c.lana, p.cuerpo, r * 0.55));
449        }
450    }
451
452    // --- Patrón (manchas, rayas, lunares) sobre el tronco.
453    patron(p, grupa, pecho, r, 17, detalle, &mut out);
454    if p.patron == Patron::Antifaz && !lejos {
455        for s in [-1.0f32, 1.0] {
456            out.push(decal(
457                cabeza_c + Vec3::new(s * cab * 0.52, cab * 0.20, cab * 0.72),
458                cab * 0.42,
459                p.patron_col,
460            ));
461        }
462    }
463
464    // --- Ojos.
465    let sep = Vec3::new(
466        cab * (0.30 + 0.50 * c.ojo_lateral),
467        cab * 0.22,
468        cab * (0.86 - 0.55 * c.ojo_lateral),
469    );
470    ojos(cabeza_c, sep, cab * c.ojo, p, detalle, &mut out);
471
472    out
473}
474
475fn orejas(c: &Cuadrupedo, cabeza_c: Vec3, cab: f32, andanza: Andanza, out: &mut Vec<SdfPrim>) {
476    let p = &c.pinta;
477    let t = c.oreja_tam;
478    let alerta = if andanza == Andanza::Alerta { 0.25 } else { 0.0 };
479    for s in [-1.0f32, 1.0] {
480        let raiz = cabeza_c + Vec3::new(s * cab * 0.58, cab * 0.52, -cab * 0.12);
481        match c.oreja {
482            Oreja::Ninguna => {}
483            Oreja::Redonda => {
484                out.push(elipsoide(
485                    raiz + Vec3::new(s * cab * 0.12, cab * 0.10, 0.0),
486                    Vec3::new(cab * 0.30 * t, cab * 0.34 * t, cab * 0.14 * t),
487                    p.cuerpo,
488                    cab * 0.10,
489                ));
490            }
491            Oreja::Punta => {
492                let punta = raiz + Vec3::new(s * cab * 0.28 * t, cab * (0.75 + alerta) * t, -cab * 0.05);
493                out.push(capsula(raiz, cab * 0.20 * t, punta, cab * 0.02, p.cuerpo, cab * 0.10));
494            }
495            Oreja::Larga => {
496                let punta = raiz + Vec3::new(s * cab * 0.34 * t, cab * (1.35 + alerta) * t, -cab * 0.22);
497                out.push(capsula(raiz, cab * 0.20 * t, punta, cab * 0.08 * t, p.cuerpo, cab * 0.10));
498                out.push(decal(
499                    raiz + (punta - raiz) * 0.55 + Vec3::new(0.0, 0.0, cab * 0.10),
500                    cab * 0.13 * t,
501                    tono(p.vientre, 1.05),
502                ));
503            }
504            Oreja::Caida => {
505                let punta = raiz + Vec3::new(s * cab * 0.40 * t, -cab * 0.75 * t, -cab * 0.10);
506                out.push(capsula(raiz, cab * 0.22 * t, punta, cab * 0.14 * t, p.detalle, cab * 0.12));
507            }
508            Oreja::Abanico => {
509                out.push(elipsoide(
510                    raiz + Vec3::new(s * cab * 0.55 * t, -cab * 0.20, -cab * 0.35),
511                    Vec3::new(cab * 0.12, cab * 0.85 * t, cab * 0.70 * t),
512                    p.cuerpo,
513                    cab * 0.18,
514                ));
515            }
516            Oreja::Lateral => {
517                let punta = raiz + Vec3::new(s * cab * 0.62 * t, cab * 0.18 * t, -cab * 0.05);
518                out.push(capsula(raiz, cab * 0.18 * t, punta, cab * 0.07 * t, p.cuerpo, cab * 0.10));
519            }
520        }
521    }
522}
523
524fn cuernos(c: &Cuadrupedo, cabeza_c: Vec3, punta_hocico: Vec3, cab: f32, out: &mut Vec<SdfPrim>) {
525    let p = &c.pinta;
526    match c.cuerno {
527        Cuerno::Ninguno => {}
528        Cuerno::Cortos | Cuerno::Largos => {
529            let k = if c.cuerno == Cuerno::Largos { 1.9 } else { 1.0 };
530            for s in [-1.0f32, 1.0] {
531                let raiz = cabeza_c + Vec3::new(s * cab * 0.45, cab * 0.72, -cab * 0.05);
532                let medio = raiz + Vec3::new(s * cab * 0.40 * k, cab * 0.45 * k, -cab * 0.05);
533                let punta = medio + Vec3::new(s * cab * 0.10 * k, cab * 0.42 * k, cab * 0.20 * k);
534                out.push(capsula(raiz, cab * 0.17, medio, cab * 0.11, p.extra, cab * 0.08));
535                out.push(capsula(medio, cab * 0.11, punta, cab * 0.03, p.extra, cab * 0.05));
536            }
537        }
538        Cuerno::Astas => {
539            for s in [-1.0f32, 1.0] {
540                let raiz = cabeza_c + Vec3::new(s * cab * 0.42, cab * 0.75, -cab * 0.10);
541                let alto = raiz + Vec3::new(s * cab * 0.55, cab * 1.15, -cab * 0.30);
542                out.push(capsula(raiz, cab * 0.13, alto, cab * 0.07, p.extra, cab * 0.07));
543                for (i, t) in [0.35f32, 0.72].into_iter().enumerate() {
544                    let base = raiz + (alto - raiz) * t;
545                    let ramita = base
546                        + Vec3::new(s * cab * 0.30, cab * (0.30 + 0.12 * i as f32), cab * 0.34);
547                    out.push(capsula(base, cab * 0.08, ramita, cab * 0.03, p.extra, cab * 0.05));
548                }
549            }
550        }
551        Cuerno::Nasal => {
552            let base = punta_hocico + Vec3::new(0.0, cab * 0.28, -cab * 0.10);
553            out.push(capsula(
554                base,
555                cab * 0.24,
556                base + Vec3::new(0.0, cab * 0.78, cab * 0.28),
557                cab * 0.03,
558                p.extra,
559                cab * 0.10,
560            ));
561        }
562        Cuerno::Colmillos => {
563            for s in [-1.0f32, 1.0] {
564                let raiz = cabeza_c + Vec3::new(s * cab * 0.42, -cab * 0.25, cab * 0.45);
565                let medio = raiz + Vec3::new(s * cab * 0.10, -cab * 0.80, cab * 0.45);
566                let punta = medio + Vec3::new(s * cab * 0.05, cab * 0.10, cab * 0.45);
567                out.push(capsula(raiz, cab * 0.15, medio, cab * 0.10, p.extra, cab * 0.08));
568                out.push(capsula(medio, cab * 0.10, punta, cab * 0.04, p.extra, cab * 0.05));
569            }
570        }
571    }
572}
573
574fn cola(c: &Cuadrupedo, base: Vec3, vaiven: f32, out: &mut Vec<SdfPrim>) {
575    let p = &c.pinta;
576    let r = c.cuerpo_r;
577    let ll = c.cola_len * c.largo;
578    let atras = |dy: f32, dz: f32, dx: f32| base + Vec3::new(dx, dy, -dz);
579    match c.cola {
580        Cola::Ninguna => {}
581        Cola::Muñon => {
582            out.push(esfera(atras(r * 0.15, r * 0.25, 0.0), r * 0.32, p.vientre, r * 0.3));
583        }
584        Cola::Penacho => {
585            let punta = atras(-ll * 0.55, ll * 0.55, vaiven * ll * 0.35);
586            out.push(capsula(base, r * 0.16, punta, r * 0.07, p.cuerpo, r * 0.2));
587            out.push(esfera(punta, r * 0.26, p.detalle, r * 0.15));
588        }
589        Cola::Tupida => {
590            let pts = [
591                base,
592                atras(ll * 0.18, ll * 0.42, vaiven * ll * 0.2),
593                atras(ll * 0.32, ll * 0.85, vaiven * ll * 0.35),
594            ];
595            cadena(&pts, r * 0.55, r * 0.30, p.cuerpo, r * 0.5, out);
596            out.push(esfera(pts[2], r * 0.32, p.vientre, r * 0.28));
597        }
598        Cola::Prensil => {
599            let pts: Vec<Vec3> = (0..=4)
600                .map(|i| {
601                    let t = i as f32 / 4.0;
602                    atras(ll * 0.55 * (t * PI * 0.7).sin(), ll * t, vaiven * ll * 0.35 * t)
603                })
604                .collect();
605            cadena(&pts, r * 0.22, r * 0.08, p.cuerpo, r * 0.18, out);
606        }
607        Cola::Rulo => {
608            let pts: Vec<Vec3> = (0..=4)
609                .map(|i| {
610                    let a = i as f32 / 4.0 * PI * 1.6;
611                    atras(ll * 0.35 * a.sin() + ll * 0.1, ll * 0.30 * (1.0 - a.cos()), 0.0)
612                })
613                .collect();
614            cadena(&pts, r * 0.16, r * 0.10, p.cuerpo, r * 0.14, out);
615        }
616        Cola::Gruesa => {
617            let pts: Vec<Vec3> = (0..=4)
618                .map(|i| {
619                    let t = i as f32 / 4.0;
620                    atras(-ll * 0.32 * t * t + ll * 0.10, ll * t, vaiven * ll * 0.25 * t)
621                })
622                .collect();
623            cadena(&pts, r * 0.70, r * 0.12, p.cuerpo, r * 0.5, out);
624        }
625        Cola::Crin => {
626            let punta = atras(-ll * 0.75, ll * 0.35, vaiven * ll * 0.25);
627            out.push(capsula(base, r * 0.22, punta, r * 0.16, p.detalle, r * 0.2));
628        }
629    }
630}
631
632// ============================================================================
633//  Ave
634// ============================================================================
635
636fn ave(a: &Ave, andanza: Andanza, fase: f32, detalle: DetalleSer) -> Vec<SdfPrim> {
637    let mut out: Vec<SdfPrim> = Vec::with_capacity(30);
638    let p = &a.pinta;
639    let lejos = detalle == DetalleSer::Lejos;
640    let vuela = matches!(andanza, Andanza::Volar) || a.ala == Ala::Bate;
641    let bob = (fase * 2.0).sin() * if vuela { 0.02 } else { 0.006 };
642
643    // El cuerpo es una CÁPSULA orientada, no un elipsoide: el elipsoide no rota
644    // (semiejes alineados a ejes) y un pájaro posado va inclinado. La cápsula toma
645    // cualquier ángulo y funde igual.
646    let r_cuerpo = (a.cuerpo[0] + a.cuerpo[1]) * 0.5;
647    let largo = a.cuerpo[2];
648    let centro = Vec3::new(0.0, a.pata + r_cuerpo + bob, 0.0);
649    let dir = Vec3::new(0.0, a.inclina.cos(), a.inclina.sin());
650    let cola_pt = centro - dir * largo;
651    let pecho = centro + dir * largo;
652    out.push(capsula(cola_pt, r_cuerpo * 0.80, pecho, r_cuerpo * 1.02, p.cuerpo, r_cuerpo * 0.8));
653    if !lejos {
654        out.push(capsula(
655            cola_pt + Vec3::new(0.0, -r_cuerpo * 0.32, 0.0),
656            r_cuerpo * 0.62,
657            pecho + Vec3::new(0.0, -r_cuerpo * 0.30, 0.0),
658            r_cuerpo * 0.72,
659            p.vientre,
660            r_cuerpo * 0.6,
661        ));
662    }
663
664    // --- Cuello + cabeza. La S del cisne/garza sale de tres puntos.
665    let cuello_dir = rot_x(dir, 0.55);
666    let cabeza_c = if a.cuello_s > 0.01 {
667        let p1 = pecho + cuello_dir * (a.cuello * 0.45) + Vec3::new(0.0, 0.0, -a.cuello * a.cuello_s * 0.35);
668        let p2 = p1 + Vec3::new(0.0, a.cuello * 0.42, a.cuello * a.cuello_s * 0.30);
669        let p3 = p2 + Vec3::new(0.0, a.cuello * 0.18, a.cuello * (0.25 + a.cuello_s * 0.25));
670        cadena(&[pecho, p1, p2, p3], a.cuello_r * 1.15, a.cuello_r * 0.85, p.cuerpo, a.cuello_r * 0.9, &mut out);
671        p3
672    } else {
673        let c = pecho + cuello_dir * (a.cuello + a.cabeza * 0.5);
674        if a.cuello > 0.02 {
675            out.push(capsula(pecho, a.cuello_r * 1.2, c, a.cuello_r * 0.9, p.cuerpo, a.cuello_r));
676        }
677        c
678    };
679    out.push(esfera(cabeza_c, a.cabeza, p.cuerpo, a.cabeza * 0.45));
680
681    // --- Pico.
682    let dp = Vec3::new(0.0, -0.12, 1.0).normalize();
683    let base_pico = cabeza_c + dp * (a.cabeza * 0.55);
684    let ll = a.cabeza * a.pico_len * 1.5;
685    let rp = a.cabeza * a.pico_r;
686    match a.pico {
687        Pico::Corto | Pico::Largo | Pico::Aguja => {
688            out.push(capsula(base_pico, rp, base_pico + dp * ll, rp * 0.12, p.extra, rp * 0.25));
689        }
690        Pico::Curvo => {
691            let medio = base_pico + dp * (ll * 0.6);
692            let punta = medio + Vec3::new(0.0, -ll * 0.55, ll * 0.25);
693            out.push(capsula(base_pico, rp, medio, rp * 0.65, p.extra, rp * 0.3));
694            out.push(capsula(medio, rp * 0.65, punta, rp * 0.10, p.extra, rp * 0.25));
695        }
696        Pico::Ancho => {
697            // Tucán/pelícano: el pico ES la especie. Elipsoide alargado + punta.
698            let c = base_pico + dp * (ll * 0.5);
699            out.push(elipsoide(c, Vec3::new(rp * 0.75, rp * 0.95, ll * 0.55), p.extra, rp * 0.3));
700            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));
701            if !lejos {
702                out.push(decal_linea(base_pico, base_pico + dp * ll, rp * 0.10, tono(p.extra, 0.55)));
703            }
704        }
705        Pico::Pala => {
706            let c = base_pico + dp * (ll * 0.45);
707            out.push(elipsoide(c, Vec3::new(rp * 1.05, rp * 0.42, ll * 0.55), p.extra, rp * 0.3));
708        }
709    }
710
711    // --- Cresta / copete.
712    match a.cresta {
713        Cresta::Ninguna => {}
714        Cresta::Punta => {
715            for i in 0..3 {
716                let s = (i as f32 - 1.0) * 0.35;
717                out.push(capsula(
718                    cabeza_c + Vec3::new(0.0, a.cabeza * 0.5, -a.cabeza * 0.1),
719                    a.cabeza * 0.22,
720                    cabeza_c + Vec3::new(s * a.cabeza * 0.35, a.cabeza * 1.25, -a.cabeza * (0.35 + 0.2 * i as f32)),
721                    a.cabeza * 0.05,
722                    p.cuerpo,
723                    a.cabeza * 0.12,
724                ));
725            }
726        }
727        Cresta::Gallo => {
728            for i in 0..3 {
729                out.push(esfera(
730                    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)),
731                    a.cabeza * 0.28,
732                    p.detalle,
733                    a.cabeza * 0.16,
734                ));
735            }
736            // Barbilla.
737            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));
738        }
739        Cresta::Penacho => {
740            for i in 0..4 {
741                let s = (i as f32 - 1.5) * 0.28;
742                let punta = cabeza_c + Vec3::new(s * a.cabeza * 0.5, a.cabeza * 1.55, -a.cabeza * 0.9);
743                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));
744                if !lejos {
745                    out.push(decal(punta, a.cabeza * 0.16, p.patron_col));
746                }
747            }
748        }
749        Cresta::Orejas => {
750            for s in [-1.0f32, 1.0] {
751                out.push(capsula(
752                    cabeza_c + Vec3::new(s * a.cabeza * 0.45, a.cabeza * 0.55, 0.0),
753                    a.cabeza * 0.20,
754                    cabeza_c + Vec3::new(s * a.cabeza * 0.62, a.cabeza * 1.05, -a.cabeza * 0.15),
755                    a.cabeza * 0.04,
756                    p.detalle,
757                    a.cabeza * 0.10,
758                ));
759            }
760        }
761    }
762
763    // --- Alas.
764    let bateo = if a.ala == Ala::Bate || vuela { (fase * 6.0).sin() * 0.85 } else { 0.0 };
765    // Un águila POSADA pliega las alas. Dejarlas abiertas por receta la convertía
766    // en un avioncito de papel — el ala tendida es la pose de vuelo, no la especie.
767    let ala = match a.ala {
768        Ala::Abierta if !vuela => Ala::Plegada,
769        otra => otra,
770    };
771    for s in [-1.0f32, 1.0] {
772        let hombro = centro + dir * (largo * 0.25) + Vec3::new(s * r_cuerpo * 0.75, r_cuerpo * 0.25, 0.0);
773        match ala {
774            Ala::Plegada => {
775                out.push(capsula(
776                    hombro,
777                    r_cuerpo * 0.52,
778                    hombro - dir * (largo * 1.15) + Vec3::new(0.0, -r_cuerpo * 0.1, 0.0),
779                    r_cuerpo * 0.20,
780                    p.detalle,
781                    r_cuerpo * 0.35,
782                ));
783            }
784            Ala::Aleta => {
785                out.push(capsula(
786                    hombro,
787                    r_cuerpo * 0.34,
788                    hombro + Vec3::new(s * r_cuerpo * 0.35, -r_cuerpo * 1.25, -r_cuerpo * 0.15),
789                    r_cuerpo * 0.14,
790                    p.detalle,
791                    r_cuerpo * 0.25,
792                ));
793            }
794            Ala::Abierta | Ala::Bate => {
795                let ang = 0.18 + bateo * 0.55;
796                let codo = hombro + Vec3::new(s * r_cuerpo * 1.5, ang * r_cuerpo * 1.6, -r_cuerpo * 0.2);
797                let punta = codo + Vec3::new(s * r_cuerpo * 1.7, ang * r_cuerpo * 1.1, -r_cuerpo * 0.8);
798                out.push(capsula(hombro, r_cuerpo * 0.48, codo, r_cuerpo * 0.36, p.detalle, r_cuerpo * 0.3));
799                out.push(capsula(codo, r_cuerpo * 0.36, punta, r_cuerpo * 0.10, p.detalle, r_cuerpo * 0.25));
800                if !lejos {
801                    // Plumas primarias: tres dedos en la punta.
802                    for i in 0..3 {
803                        let d = (i as f32 - 1.0) * 0.3;
804                        out.push(capsula(
805                            codo,
806                            r_cuerpo * 0.14,
807                            punta + Vec3::new(s * r_cuerpo * 0.3, d * r_cuerpo * 0.5, -r_cuerpo * (0.4 + 0.2 * i as f32)),
808                            r_cuerpo * 0.05,
809                            tono(p.detalle, 0.8),
810                            r_cuerpo * 0.12,
811                        ));
812                    }
813                }
814            }
815        }
816    }
817
818    // --- Cola.
819    let cl = a.cola_len * largo * 2.2;
820    let dir_cola = -rot_x(dir, 0.30);
821    match a.cola {
822        ColaAve::Corta => {
823            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));
824        }
825        ColaAve::Larga => {
826            for s in [-1.0f32, 1.0] {
827                out.push(capsula(
828                    cola_pt,
829                    r_cuerpo * 0.30,
830                    cola_pt + dir_cola * cl + Vec3::new(s * r_cuerpo * 0.30, -cl * 0.15, 0.0),
831                    r_cuerpo * 0.07,
832                    p.detalle,
833                    r_cuerpo * 0.2,
834                ));
835            }
836        }
837        ColaAve::Abanico => {
838            for i in 0..5 {
839                let s = (i as f32 - 2.0) * 0.32;
840                out.push(capsula(
841                    cola_pt,
842                    r_cuerpo * 0.26,
843                    cola_pt + dir_cola * cl + Vec3::new(s * cl * 0.45, s.abs() * cl * 0.18, 0.0),
844                    r_cuerpo * 0.08,
845                    p.detalle,
846                    r_cuerpo * 0.2,
847                ));
848            }
849        }
850        ColaAve::Rueda => {
851            // La rueda del pavo: abanico grande de plumas con ocelos.
852            let n = if lejos { 5 } else { 11 };
853            for i in 0..n {
854                let t = i as f32 / (n - 1) as f32;
855                let ang = (t - 0.5) * PI * 1.35;
856                let punta = cola_pt
857                    + Vec3::new(ang.sin() * cl, ang.cos() * cl * 0.95 + cl * 0.15, -cl * 0.35);
858                out.push(capsula(cola_pt, r_cuerpo * 0.22, punta, r_cuerpo * 0.13, p.detalle, r_cuerpo * 0.18));
859                if !lejos {
860                    out.push(decal(punta, r_cuerpo * 0.30, p.patron_col));
861                    out.push(decal(punta, r_cuerpo * 0.15, tono(p.cuerpo, 0.5)));
862                }
863            }
864        }
865    }
866
867    // --- Patas.
868    for s in [-1.0f32, 1.0] {
869        let cadera = centro + Vec3::new(s * r_cuerpo * 0.42, -r_cuerpo * 0.55, -largo * 0.15);
870        let pie = Vec3::new(cadera.x, 0.0, cadera.z + a.pata * 0.12);
871        if andanza == Andanza::Volar {
872            // En vuelo las patas se recogen contra el vientre.
873            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));
874            continue;
875        }
876        let rodilla = cadera + Vec3::new(0.0, -(cadera.y) * 0.55, -a.pata * 0.10);
877        out.push(capsula(cadera, a.pata_r * 1.6, rodilla, a.pata_r, p.extra, a.pata_r));
878        out.push(capsula(rodilla, a.pata_r, pie, a.pata_r * 0.9, p.extra, a.pata_r * 0.8));
879        if !lejos {
880            for i in 0..3 {
881                let d = (i as f32 - 1.0) * 0.55;
882                out.push(capsula(
883                    pie,
884                    a.pata_r * 0.9,
885                    pie + Vec3::new(d * a.pata_r * 3.0, 0.0, a.pata_r * 4.2),
886                    a.pata_r * 0.5,
887                    p.extra,
888                    a.pata_r * 0.5,
889                ));
890            }
891        }
892    }
893
894    // --- Patrón y ojos.
895    patron(p, cola_pt, pecho, r_cuerpo, 23, detalle, &mut out);
896    ojos(
897        cabeza_c,
898        Vec3::new(a.cabeza * 0.72, a.cabeza * 0.18, a.cabeza * 0.42),
899        a.cabeza * a.ojo,
900        p,
901        detalle,
902        &mut out,
903    );
904    out
905}
906
907// ============================================================================
908//  Reptante (serpiente, lagarto, cocodrilo, tortuga, rana)
909// ============================================================================
910
911fn reptante(c: &Reptante, andanza: Andanza, fase: f32, detalle: DetalleSer) -> Vec<SdfPrim> {
912    let mut out: Vec<SdfPrim> = Vec::with_capacity(34);
913    let p = &c.pinta;
914    let lejos = detalle == DetalleSer::Lejos;
915    let r = c.grosor;
916    let l = c.largo;
917    let repta = matches!(andanza, Andanza::Caminar | Andanza::Trotar | Andanza::Nadar);
918    let onda_fase = if repta { fase } else { fase * 0.3 };
919
920    if c.anfibio {
921        return anfibio(c, fase, detalle);
922    }
923    if c.caparazon > 0.01 {
924        return tortuga(c, andanza, fase, detalle);
925    }
926
927    // --- Espinazo: polilínea que ondula en X y se levanta adelante si `alza`.
928    let n = 9;
929    let mut pts = Vec::with_capacity(n + 1);
930    for i in 0..=n {
931        let t = i as f32 / n as f32; // 0 = punta de la cola, 1 = cuello
932        // Amplitud en unidades de LARGO, no de grosor: con `amplitud · r` la onda
933        // medía medio cuerpo de ancho y la serpiente salía como un salchichón.
934        let x = (t * PI * 2.0 * c.ondas + onda_fase).sin() * c.amplitud * r * 2.6;
935        let sube = if c.alza > 0.0 {
936            let s = ((t - 0.55) / 0.45).clamp(0.0, 1.0);
937            s * s * c.alza * l * 0.45
938        } else {
939            0.0
940        };
941        pts.push(Vec3::new(x, r + sube, (t - 0.5) * l));
942    }
943    // Radio: fino en la cola, grueso al medio, apenas menor en el cuello.
944    for (i, w) in pts.windows(2).enumerate() {
945        let t0 = i as f32 / n as f32;
946        let t1 = (i + 1) as f32 / n as f32;
947        let rad = |t: f32| r * (0.28 + 0.90 * (t * PI * 0.92).sin().max(0.10));
948        out.push(capsula(w[0], rad(t0), w[1], rad(t1), p.cuerpo, r * 0.7));
949    }
950    if !lejos {
951        // Vientre claro: la misma cadena, corrida abajo.
952        for (i, w) in pts.windows(2).enumerate().step_by(2) {
953            let t = i as f32 / n as f32;
954            let rad = r * (0.20 + 0.66 * (t * PI * 0.92).sin().max(0.10));
955            out.push(capsula(
956                w[0] - Vec3::new(0.0, r * 0.30, 0.0),
957                rad,
958                w[1] - Vec3::new(0.0, r * 0.30, 0.0),
959                rad,
960                p.vientre,
961                r * 0.5,
962            ));
963        }
964    }
965
966    let cuello = pts[n];
967    let dir = (cuello - pts[n - 1]).normalize_or_zero();
968    let cab = r * c.cabeza * 1.35;
969
970    // --- Capucha de la cobra: disco fino detrás de la cabeza.
971    if c.capucha > 0.01 {
972        out.push(elipsoide(
973            cuello - dir * (cab * 0.35),
974            Vec3::new(cab * 1.9 * c.capucha, cab * 1.5 * c.capucha, cab * 0.30),
975            p.cuerpo,
976            cab * 0.35,
977        ));
978    }
979
980    // --- Cabeza: elipsoide chato + hocico (el cocodrilo es casi todo hocico).
981    let cabeza_c = cuello + dir * (cab * 0.6);
982    out.push(elipsoide(
983        cabeza_c,
984        Vec3::new(cab * 0.95, cab * 0.72, cab * 1.15),
985        p.cuerpo,
986        cab * 0.4,
987    ));
988    let punta = cabeza_c + dir * (cab * c.hocico * 1.1);
989    if c.hocico > 0.2 {
990        out.push(capsula(cabeza_c, cab * 0.75, punta, cab * 0.55, p.cuerpo, cab * 0.35));
991        if !lejos {
992            // Línea de la boca: el cocodrilo se lee por la sonrisa larga.
993            out.push(decal_linea(
994                cabeza_c + Vec3::new(-cab * 0.55, -cab * 0.30, 0.0),
995                punta + Vec3::new(-cab * 0.10, -cab * 0.25, 0.0),
996                cab * 0.10,
997                tono(p.detalle, 0.6),
998            ));
999            out.push(decal_linea(
1000                cabeza_c + Vec3::new(cab * 0.55, -cab * 0.30, 0.0),
1001                punta + Vec3::new(cab * 0.10, -cab * 0.25, 0.0),
1002                cab * 0.10,
1003                tono(p.detalle, 0.6),
1004            ));
1005        }
1006    }
1007    // Lengua bífida.
1008    if c.lengua && !lejos {
1009        let base = punta + dir * (cab * 0.35);
1010        for s in [-1.0f32, 1.0] {
1011            out.push(capsula(
1012                punta,
1013                cab * 0.07,
1014                base + Vec3::new(s * cab * 0.35, -cab * 0.05, 0.0),
1015                cab * 0.02,
1016                [0.85, 0.20, 0.28],
1017                cab * 0.04,
1018            ));
1019        }
1020    }
1021
1022    // --- Cresta dorsal de púas (iguana, cocodrilo).
1023    if c.cresta > 0.01 && !lejos {
1024        for i in 2..n {
1025            let t = i as f32 / n as f32;
1026            let base = pts[i] + Vec3::new(0.0, r * 0.55, 0.0);
1027            out.push(capsula(
1028                base,
1029                r * 0.16 * c.cresta,
1030                base + Vec3::new(0.0, r * 0.55 * c.cresta * (t * PI).sin().max(0.3), 0.0),
1031                r * 0.02,
1032                p.detalle,
1033                r * 0.08,
1034            ));
1035        }
1036    }
1037
1038    // --- Cascabel.
1039    if c.cascabel {
1040        let cola_pt = pts[0];
1041        for i in 0..3 {
1042            out.push(esfera(
1043                cola_pt - Vec3::new(0.0, 0.0, i as f32 * r * 0.28),
1044                r * (0.22 - 0.04 * i as f32),
1045                p.detalle,
1046                r * 0.1,
1047            ));
1048        }
1049    }
1050
1051    // --- Patas de reptil: abiertas hacia afuera (no debajo del cuerpo).
1052    if c.patas >= 4 {
1053        let paso = if repta { 0.5 } else { 0.0 };
1054        for (i, (sx, t)) in [(-1.0f32, 0.72f32), (1.0, 0.72), (-1.0, 0.28), (1.0, 0.28)]
1055            .into_iter()
1056            .enumerate()
1057        {
1058            let idx = (t * n as f32) as usize;
1059            let cadera = pts[idx.min(n)] + Vec3::new(sx * r * 0.65, -r * 0.1, 0.0);
1060            let sw = paso * ((fase + if i % 3 == 0 { 0.0 } else { PI }).sin());
1061            let codo = cadera + Vec3::new(sx * c.pata * c.abertura * r * 1.4, -r * 0.45, sw * r * 0.5);
1062            let pie = Vec3::new(codo.x + sx * r * 0.25, 0.0, codo.z + sw * r * 0.6);
1063            out.push(capsula(cadera, r * 0.26, codo, r * 0.20, p.cuerpo, r * 0.2));
1064            out.push(capsula(codo, r * 0.20, pie, r * 0.14, p.cuerpo, r * 0.15));
1065            if !lejos {
1066                for k in 0..3 {
1067                    let d = (k as f32 - 1.0) * 0.5;
1068                    out.push(capsula(
1069                        pie,
1070                        r * 0.10,
1071                        pie + Vec3::new(sx * r * 0.20 + d * r * 0.22, 0.0, r * 0.38),
1072                        r * 0.04,
1073                        p.cuerpo,
1074                        r * 0.06,
1075                    ));
1076                }
1077            }
1078        }
1079    }
1080
1081    patron(p, pts[1], pts[n - 1], r, 41, detalle, &mut out);
1082    ojos(
1083        cabeza_c,
1084        Vec3::new(cab * 0.70, cab * 0.42, cab * 0.35),
1085        cab * c.ojo,
1086        p,
1087        detalle,
1088        &mut out,
1089    );
1090    out
1091}
1092
1093/// La tortuga: caparazón abovedado, plastrón y cuatro muñones.
1094fn tortuga(c: &Reptante, andanza: Andanza, fase: f32, detalle: DetalleSer) -> Vec<SdfPrim> {
1095    let mut out: Vec<SdfPrim> = Vec::with_capacity(24);
1096    let p = &c.pinta;
1097    let lejos = detalle == DetalleSer::Lejos;
1098    let r = c.grosor * 1.6;
1099    let cuerpo_y = r * 0.55;
1100    let centro = Vec3::new(0.0, cuerpo_y, 0.0);
1101    out.push(elipsoide(centro, Vec3::new(r * 0.98, r * 0.62, r * 1.15), p.detalle, r * 0.3));
1102    out.push(elipsoide(
1103        centro - Vec3::new(0.0, r * 0.42, 0.0),
1104        Vec3::new(r * 0.86, r * 0.22, r * 1.02),
1105        p.vientre,
1106        r * 0.25,
1107    ));
1108    if !lejos {
1109        // Escudos del caparazón.
1110        for i in 0..6 {
1111            let a = i as f32 / 6.0 * PI * 2.0;
1112            out.push(decal(
1113                centro + Vec3::new(a.cos() * r * 0.55, r * 0.55, a.sin() * r * 0.62),
1114                r * 0.26,
1115                p.patron_col,
1116            ));
1117        }
1118    }
1119    // Patas.
1120    let paso = if andanza == Andanza::Caminar { 0.35 } else { 0.0 };
1121    for (i, (sx, sz)) in [(-1.0f32, 1.0f32), (1.0, 1.0), (-1.0, -1.0), (1.0, -1.0)].into_iter().enumerate() {
1122        let sw = paso * (fase + i as f32 * PI * 0.5).sin();
1123        let raiz = centro + Vec3::new(sx * r * 0.6, -r * 0.25, sz * r * 0.72);
1124        let pie = Vec3::new(raiz.x + sx * r * 0.35, r * 0.12, raiz.z + sw * r * 0.3);
1125        out.push(capsula(raiz, r * 0.24, pie, r * 0.20, p.cuerpo, r * 0.18));
1126    }
1127    // Cabeza y cola.
1128    let cuello = centro + Vec3::new(0.0, -r * 0.05, r * 1.0);
1129    let cabeza_c = cuello + Vec3::new(0.0, r * 0.12, r * c.cabeza * 0.75);
1130    out.push(capsula(cuello, r * 0.24, cabeza_c, r * 0.26, p.cuerpo, r * 0.2));
1131    out.push(elipsoide(cabeza_c, Vec3::new(r * 0.28, r * 0.26, r * 0.34), p.cuerpo, r * 0.15));
1132    out.push(capsula(
1133        centro + Vec3::new(0.0, -r * 0.1, -r * 1.0),
1134        r * 0.14,
1135        centro + Vec3::new(0.0, -r * 0.2, -r * 1.35),
1136        r * 0.04,
1137        p.cuerpo,
1138        r * 0.1,
1139    ));
1140    ojos(cabeza_c, Vec3::new(r * 0.20, r * 0.10, r * 0.22), r * 0.10, p, detalle, &mut out);
1141    out
1142}
1143
1144/// La rana: agazapada, ojos arriba y patas traseras plegadas.
1145fn anfibio(c: &Reptante, fase: f32, detalle: DetalleSer) -> Vec<SdfPrim> {
1146    let mut out: Vec<SdfPrim> = Vec::with_capacity(22);
1147    let p = &c.pinta;
1148    let lejos = detalle == DetalleSer::Lejos;
1149    let r = c.grosor;
1150    let respira = (fase * 2.0).sin() * r * 0.03;
1151    let centro = Vec3::new(0.0, r * 0.72 + respira, 0.0);
1152    out.push(elipsoide(centro, Vec3::new(r * 0.86, r * 0.68, r * 1.05), p.cuerpo, r * 0.35));
1153    out.push(elipsoide(
1154        centro - Vec3::new(0.0, r * 0.34, 0.0),
1155        Vec3::new(r * 0.72, r * 0.30, r * 0.90),
1156        p.vientre,
1157        r * 0.3,
1158    ));
1159    // Patas traseras plegadas en Z (el resorte de la rana).
1160    for s in [-1.0f32, 1.0] {
1161        let cadera = centro + Vec3::new(s * r * 0.60, -r * 0.15, -r * 0.55);
1162        let rodilla = cadera + Vec3::new(s * r * 0.45, r * 0.30, -r * 0.30);
1163        let tobillo = rodilla + Vec3::new(s * r * 0.10, -r * 0.62, r * 0.35);
1164        let pie = tobillo + Vec3::new(s * r * 0.05, -r * 0.05, r * 0.55);
1165        out.push(capsula(cadera, r * 0.28, rodilla, r * 0.22, p.cuerpo, r * 0.2));
1166        out.push(capsula(rodilla, r * 0.22, tobillo, r * 0.16, p.cuerpo, r * 0.16));
1167        out.push(capsula(tobillo, r * 0.16, pie, r * 0.10, p.cuerpo, r * 0.12));
1168        // Manos adelante.
1169        let hombro = centro + Vec3::new(s * r * 0.55, -r * 0.25, r * 0.55);
1170        let mano = Vec3::new(hombro.x + s * r * 0.12, r * 0.05, hombro.z + r * 0.35);
1171        out.push(capsula(hombro, r * 0.16, mano, r * 0.10, p.cuerpo, r * 0.12));
1172    }
1173    // Ojos saltones ARRIBA de la cabeza (la marca de la rana).
1174    let cabeza_c = centro + Vec3::new(0.0, r * 0.20, r * 0.72);
1175    for s in [-1.0f32, 1.0] {
1176        let o = cabeza_c + Vec3::new(s * r * 0.42, r * 0.42, r * 0.05);
1177        out.push(esfera(o, r * c.ojo * 0.62, p.cuerpo, r * 0.16));
1178        if !lejos {
1179            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);
1180        }
1181    }
1182    if !lejos {
1183        out.push(decal_linea(
1184            cabeza_c + Vec3::new(-r * 0.55, -r * 0.18, r * 0.10),
1185            cabeza_c + Vec3::new(r * 0.55, -r * 0.18, r * 0.10),
1186            r * 0.09,
1187            tono(p.detalle, 0.6),
1188        ));
1189    }
1190    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);
1191    out
1192}
1193
1194// ============================================================================
1195//  Nadador
1196// ============================================================================
1197
1198fn nadador(c: &Nadador, andanza: Andanza, fase: f32, detalle: DetalleSer) -> Vec<SdfPrim> {
1199    let mut out: Vec<SdfPrim> = Vec::with_capacity(20);
1200    let p = &c.pinta;
1201    let lejos = detalle == DetalleSer::Lejos;
1202    let (rx, ry, rz) = (c.cuerpo[0], c.cuerpo[1], c.cuerpo[2]);
1203    let coleo = if matches!(andanza, Andanza::Nadar | Andanza::Caminar) {
1204        (fase * 2.0).sin() * 0.35
1205    } else {
1206        (fase).sin() * 0.08
1207    };
1208    // Un cetáceo no se apoya: flota. Con el centro a `ry·1.25` el delfín quedaba
1209    // panza abajo sobre el damero como un pescado en la mesada.
1210    let centro = Vec3::new(0.0, ry * 2.6, 0.0);
1211
1212    if c.tentaculos > 0 {
1213        // Pulpo: manto + tentáculos que ondean.
1214        out.push(elipsoide(centro + Vec3::new(0.0, ry * 0.35, 0.0), Vec3::new(rx, ry, rz), p.cuerpo, rx * 0.4));
1215        let n = c.tentaculos as usize;
1216        for i in 0..n {
1217            let a = i as f32 / n as f32 * PI * 2.0;
1218            let base = centro + Vec3::new(a.cos() * rx * 0.72, -ry * 0.25, a.sin() * rz * 0.72);
1219            let pts: Vec<Vec3> = (0..=3)
1220                .map(|k| {
1221                    let t = k as f32 / 3.0;
1222                    base + Vec3::new(
1223                        a.cos() * rx * 1.5 * t,
1224                        -ry * 0.9 * t + (t * PI + fase + i as f32).sin() * ry * 0.25 * t,
1225                        a.sin() * rz * 1.5 * t,
1226                    )
1227                })
1228                .collect();
1229            cadena(&pts, rx * 0.20, rx * 0.05, p.cuerpo, rx * 0.14, &mut out);
1230        }
1231        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);
1232        return out;
1233    }
1234
1235    // Cuerpo: cápsula fusiforme (hocico → cola), que además ondula al nadar.
1236    let morro = Vec3::new(0.0, centro.y, rz);
1237    let cola_pt = Vec3::new(coleo * rz * 0.35, centro.y, -rz);
1238    out.push(capsula(cola_pt, ry * 0.35, centro, ry, p.cuerpo, ry * 0.7));
1239    out.push(capsula(centro, ry, morro, ry * (0.35 + 0.3 * c.hocico), p.cuerpo, ry * 0.7));
1240    if !lejos {
1241        out.push(capsula(
1242            cola_pt - Vec3::new(0.0, ry * 0.25, 0.0),
1243            ry * 0.22,
1244            morro - Vec3::new(0.0, ry * 0.30, 0.0),
1245            ry * 0.30,
1246            p.vientre,
1247            ry * 0.5,
1248        ));
1249    }
1250    if c.hocico > 0.01 {
1251        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));
1252    }
1253    if c.boca > 0.01 && !lejos {
1254        out.push(decal_linea(
1255            morro + Vec3::new(-ry * 0.45, -ry * 0.38, -rz * 0.08),
1256            morro + Vec3::new(ry * 0.45, -ry * 0.38, -rz * 0.08),
1257            ry * 0.12,
1258            [0.72, 0.62, 0.60],
1259        ));
1260    }
1261    // Aleta dorsal.
1262    if c.dorsal > 0.01 {
1263        out.push(elipsoide(
1264            centro + Vec3::new(0.0, ry * (0.85 + c.dorsal * 0.4), -rz * 0.05),
1265            Vec3::new(rx * 0.10, ry * c.dorsal * 1.15, rz * c.dorsal * 0.55),
1266            p.detalle,
1267            ry * 0.2,
1268        ));
1269    }
1270    // Pectorales.
1271    if c.pectoral > 0.01 {
1272        for s in [-1.0f32, 1.0] {
1273            out.push(elipsoide(
1274                centro + Vec3::new(s * rx * 1.05, -ry * 0.35, rz * 0.20),
1275                Vec3::new(rx * c.pectoral * 1.3, ry * 0.10, rz * c.pectoral * 0.55),
1276                p.detalle,
1277                ry * 0.15,
1278            ));
1279        }
1280    }
1281    // Caudal: vertical en los peces, horizontal en los cetáceos.
1282    let cola_c = cola_pt - Vec3::new(0.0, 0.0, rz * c.caudal * 0.55);
1283    if c.caudal_vertical {
1284        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));
1285    } else {
1286        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));
1287    }
1288    patron(p, cola_pt, morro, ry, 71, detalle, &mut out);
1289    ojos(
1290        morro + Vec3::new(0.0, ry * 0.30, -rz * 0.12),
1291        Vec3::new(ry * 0.55, 0.0, 0.0),
1292        ry * c.ojo,
1293        p,
1294        detalle,
1295        &mut out,
1296    );
1297    out
1298}
1299
1300// ============================================================================
1301//  Insecto y afines
1302// ============================================================================
1303
1304fn insecto(c: &Insecto, andanza: Andanza, fase: f32, detalle: DetalleSer) -> Vec<SdfPrim> {
1305    let mut out: Vec<SdfPrim> = Vec::with_capacity(30);
1306    let p = &c.pinta;
1307    let lejos = detalle == DetalleSer::Lejos;
1308    let l = c.largo;
1309    let vuela = matches!(andanza, Andanza::Volar) || c.alas == AlaBicho::Mariposa;
1310    let alto_cuerpo = c.segmentos[1] * (1.0 + if c.patas > 0 { c.pata * 0.55 } else { 0.0 });
1311    // El que vuela VUELA: en el suelo, la mariposa era dos discos naranjas tirados
1312    // sobre el damero. Se levanta el cuerpo entero un largo y medio.
1313    let vuelo = if vuela { l * 1.4 } else { 0.0 };
1314    let cy = alto_cuerpo + vuelo + (fase * 3.0).sin() * if vuela { l * 0.04 } else { l * 0.008 };
1315
1316    // Concha del caracol: espiral logarítmica en el plano Y-Z.
1317    if c.concha > 0.01 {
1318        let n = 9;
1319        for i in 0..n {
1320            let t = i as f32 / (n - 1) as f32;
1321            let a = t * PI * 3.2;
1322            let rad = l * 0.55 * (1.0 - t * 0.72);
1323            let dist = l * 0.55 * (0.25 + 0.75 * (1.0 - t));
1324            out.push(esfera(
1325                Vec3::new(0.0, cy + a.sin() * dist * 0.55 + l * 0.25, -a.cos() * dist * 0.55 - l * 0.1),
1326                rad * 0.62,
1327                if i % 2 == 0 { p.detalle } else { p.extra },
1328                rad * 0.35,
1329            ));
1330        }
1331    }
1332
1333    // Cuerpo: cabeza (+z), tórax, abdomen (−z).
1334    let seg_z = [l * 0.42, l * 0.02, -l * 0.55];
1335    let cabeza_c = Vec3::new(0.0, cy, seg_z[0]);
1336    let torax_c = Vec3::new(0.0, cy, seg_z[1]);
1337    let abdomen_c = Vec3::new(0.0, cy * 0.95, seg_z[2]);
1338    out.push(esfera(abdomen_c, c.segmentos[2], p.cuerpo, c.segmentos[2] * 0.5));
1339    out.push(esfera(torax_c, c.segmentos[1], p.cuerpo, c.segmentos[1] * 0.6));
1340    out.push(esfera(cabeza_c, c.segmentos[0], p.cuerpo, c.segmentos[0] * 0.6));
1341    if c.largo > 1.2 {
1342        // Abdomen largo (libélula): cadena hacia atrás.
1343        cadena(
1344            &[abdomen_c, Vec3::new(0.0, cy * 0.95, -l * 1.15)],
1345            c.segmentos[2] * 0.8,
1346            c.segmentos[2] * 0.25,
1347            p.cuerpo,
1348            c.segmentos[2] * 0.4,
1349            &mut out,
1350        );
1351    }
1352
1353    // Patas: arqueadas hacia afuera, en trípode alterno al caminar.
1354    let paso = if matches!(andanza, Andanza::Caminar | Andanza::Trotar) { 0.35 } else { 0.0 };
1355    let pares = (c.patas / 2) as usize;
1356    for i in 0..pares {
1357        let t = if pares <= 1 { 0.0 } else { i as f32 / (pares - 1) as f32 - 0.5 };
1358        for s in [-1.0f32, 1.0] {
1359            let raiz = torax_c + Vec3::new(s * c.segmentos[1] * 0.75, -c.segmentos[1] * 0.25, -t * l * 0.75);
1360            let sw = paso * (fase + i as f32 * 2.1 + if s > 0.0 { PI } else { 0.0 }).sin();
1361            let codo = raiz
1362                + Vec3::new(s * c.pata * c.arco * 0.75, c.pata * 0.42, sw * c.pata * 0.25);
1363            let pie = Vec3::new(
1364                codo.x + s * c.pata * 0.35,
1365                if vuela { codo.y - c.pata * 0.55 } else { 0.0 },
1366                codo.z + sw * c.pata * 0.3,
1367            );
1368            let rp = c.segmentos[1] * 0.13;
1369            out.push(capsula(raiz, rp, codo, rp * 0.8, p.detalle, rp * 0.7));
1370            out.push(capsula(codo, rp * 0.8, pie, rp * 0.5, p.detalle, rp * 0.6));
1371        }
1372    }
1373
1374    // Pinzas (escorpión, cangrejo).
1375    if c.pinzas > 0.01 {
1376        for s in [-1.0f32, 1.0] {
1377            let hombro = torax_c + Vec3::new(s * c.segmentos[1] * 0.8, 0.0, c.segmentos[1] * 0.5);
1378            let codo = hombro + Vec3::new(s * c.pinzas * l * 0.30, 0.0, c.pinzas * l * 0.28);
1379            out.push(capsula(hombro, c.segmentos[1] * 0.20, codo, c.segmentos[1] * 0.16, p.cuerpo, c.segmentos[1] * 0.15));
1380            let punta_a = codo + Vec3::new(s * c.pinzas * l * 0.12, c.segmentos[1] * 0.14, c.pinzas * l * 0.26);
1381            let punta_b = codo + Vec3::new(s * c.pinzas * l * 0.16, -c.segmentos[1] * 0.14, c.pinzas * l * 0.24);
1382            out.push(capsula(codo, c.segmentos[1] * 0.24, punta_a, c.segmentos[1] * 0.05, p.extra, c.segmentos[1] * 0.12));
1383            out.push(capsula(codo, c.segmentos[1] * 0.20, punta_b, c.segmentos[1] * 0.05, p.extra, c.segmentos[1] * 0.12));
1384        }
1385    }
1386
1387    // Cola con aguijón (escorpión): arco sobre el lomo.
1388    if c.aguijon > 0.01 {
1389        let n = 5;
1390        let pts: Vec<Vec3> = (0..=n)
1391            .map(|i| {
1392                let t = i as f32 / n as f32;
1393                let a = t * PI * 0.85;
1394                abdomen_c
1395                    + Vec3::new(0.0, a.sin() * c.aguijon * l * 0.85, -a.cos() * c.aguijon * l * 0.45 + c.aguijon * l * 0.45)
1396            })
1397            .collect();
1398        cadena(&pts, c.segmentos[2] * 0.55, c.segmentos[2] * 0.28, p.cuerpo, c.segmentos[2] * 0.3, &mut out);
1399        let punta = *pts.last().unwrap();
1400        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));
1401    }
1402
1403    // Alas.
1404    let bateo = if vuela { (fase * 8.0).sin() * 0.55 } else { 0.0 };
1405    match c.alas {
1406        AlaBicho::Ninguna => {}
1407        AlaBicho::Mariposa => {
1408            for s in [-1.0f32, 1.0] {
1409                let sube = bateo * c.ala_len * 0.25;
1410                // Ala delantera grande + trasera chica, casi planas (finas en Y).
1411                let a1 = torax_c + Vec3::new(s * c.ala_len * 0.62, sube + c.segmentos[1] * 0.2, c.ala_len * 0.18);
1412                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));
1413                let a2 = torax_c + Vec3::new(s * c.ala_len * 0.45, sube * 0.8, -c.ala_len * 0.42);
1414                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));
1415                if !lejos {
1416                    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));
1417                    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));
1418                }
1419            }
1420        }
1421        AlaBicho::Membrana => {
1422            for s in [-1.0f32, 1.0] {
1423                for (k, dz) in [(0usize, 0.25f32), (1, -0.35)] {
1424                    let a = torax_c
1425                        + Vec3::new(
1426                            s * c.ala_len * 0.55,
1427                            c.segmentos[1] * 0.35 + bateo * c.ala_len * 0.30,
1428                            c.ala_len * dz,
1429                        );
1430                    out.push(elipsoide(
1431                        a,
1432                        Vec3::new(c.ala_len * (0.58 - 0.10 * k as f32), c.ala_len * 0.035, c.ala_len * 0.16),
1433                        [0.86, 0.90, 0.94],
1434                        c.ala_len * 0.05,
1435                    ));
1436                }
1437            }
1438        }
1439        AlaBicho::Elitros => {
1440            for s in [-1.0f32, 1.0] {
1441                out.push(elipsoide(
1442                    abdomen_c + Vec3::new(s * c.segmentos[2] * 0.45, c.segmentos[2] * 0.25, 0.0),
1443                    Vec3::new(c.segmentos[2] * 0.62, c.segmentos[2] * 0.55, c.segmentos[2] * 1.05),
1444                    p.detalle,
1445                    c.segmentos[2] * 0.25,
1446                ));
1447            }
1448            if !lejos && p.patron == Patron::Lunares {
1449                for i in 0..4 {
1450                    let s = if i % 2 == 0 { -1.0 } else { 1.0f32 };
1451                    out.push(decal(
1452                        abdomen_c
1453                            + Vec3::new(s * c.segmentos[2] * 0.45, c.segmentos[2] * 0.72, c.segmentos[2] * (0.4 - 0.5 * (i / 2) as f32)),
1454                        c.segmentos[2] * 0.22,
1455                        p.patron_col,
1456                    ));
1457                }
1458            }
1459        }
1460    }
1461
1462    // Antenas y pedúnculos oculares.
1463    if c.antenas > 0.01 {
1464        for s in [-1.0f32, 1.0] {
1465            let punta = cabeza_c + Vec3::new(s * c.antenas * l * 0.28, c.antenas * l * 0.55, c.antenas * l * 0.25);
1466            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));
1467            if !lejos {
1468                out.push(esfera(punta, c.segmentos[0] * 0.10, p.detalle, c.segmentos[0] * 0.05));
1469            }
1470        }
1471    }
1472    if c.pedunculos && lejos {
1473        // De lejos el pedúnculo mide menos de un píxel: quedan los dos puntos.
1474        for s in [-1.0f32, 1.0] {
1475            out.push(decal(
1476                cabeza_c + Vec3::new(s * c.segmentos[0] * 0.5, c.segmentos[0] * 0.8, c.segmentos[0] * 0.3),
1477                c.segmentos[0] * c.ojo * 1.2,
1478                [0.14, 0.12, 0.14],
1479            ));
1480        }
1481    } else if c.pedunculos {
1482        for s in [-1.0f32, 1.0] {
1483            let punta = cabeza_c + Vec3::new(s * c.segmentos[0] * 0.55, c.segmentos[0] * 1.15, c.segmentos[0] * 0.25);
1484            out.push(capsula(cabeza_c, c.segmentos[0] * 0.12, punta, c.segmentos[0] * 0.08, p.cuerpo, c.segmentos[0] * 0.08));
1485            out.push(esfera(punta, c.segmentos[0] * c.ojo * 1.4, [0.14, 0.12, 0.14], c.segmentos[0] * 0.06));
1486        }
1487    } else {
1488        ojos(
1489            cabeza_c,
1490            Vec3::new(c.segmentos[0] * 0.62, c.segmentos[0] * 0.20, c.segmentos[0] * 0.42),
1491            c.segmentos[0] * c.ojo,
1492            p,
1493            detalle,
1494            &mut out,
1495        );
1496    }
1497    // Rayas del abdomen (abeja).
1498    if p.patron == Patron::Rayas && !lejos {
1499        patron(p, abdomen_c + Vec3::new(0.0, 0.0, -c.segmentos[2] * 0.7), torax_c, c.segmentos[2], 83, detalle, &mut out);
1500    }
1501    let _ = mezcla;
1502    let _ = rot_y;
1503    let _ = hash11;
1504    out
1505}
1506
1507#[cfg(test)]
1508mod tests {
1509    use super::*;
1510    use std::collections::HashSet;
1511
1512    fn geometria(s: &SdfSer) -> usize {
1513        s.prims
1514            .iter()
1515            .filter(|p| !matches!(p.kind, llimphi_3d::SdfKind::DecalEsfera | llimphi_3d::SdfKind::DecalCapsula))
1516            .count()
1517    }
1518
1519    /// El catálogo entero se arma: nadie queda vacío, nadie escupe un NaN y nadie
1520    /// se pasa del presupuesto del uniform él solo.
1521    #[test]
1522    fn todo_el_bestiario_se_arma() {
1523        for b in Bicho::ALL {
1524            let s = bicho_sdf(b);
1525            assert!(geometria(&s) >= 6, "{} tiene {} primitivas", b.label(), geometria(&s));
1526            assert!(
1527                s.prims.len() <= llimphi_3d::VOXEL_MAX_SER_PRIMS,
1528                "{} no entra en el uniform ({} prims)",
1529                b.label(),
1530                s.prims.len()
1531            );
1532            for p in &s.prims {
1533                for v in p.a.iter().chain(p.b.iter()).chain([p.ra, p.rb, p.blend].iter()) {
1534                    assert!(v.is_finite(), "{}: primitiva con {v}", b.label());
1535                }
1536                assert!(p.blend > 0.0, "{}: blend cero funde mal", b.label());
1537            }
1538        }
1539    }
1540
1541    /// Cada bicho apoya en `y = 0` y mide lo que se le pidió: es lo que deja
1542    /// sembrarlo sobre el relieve sin que flote ni se entierre.
1543    #[test]
1544    fn cada_bicho_apoya_en_el_piso_y_mide_lo_pedido() {
1545        for b in Bicho::ALL {
1546            for alto in [0.2f32, 1.0, 4.0] {
1547                let s = bicho_sdf_lod(b, alto, Andanza::Quieto, 0.0, DetalleSer::Hero);
1548                let (lo, hi) = crate::sdf_kit::extension_y(&s.prims);
1549                assert!(lo.abs() < alto * 1e-3, "{} apoya en {lo}", b.label());
1550                assert!(
1551                    (hi - alto).abs() < alto * 1e-3,
1552                    "{} mide {hi} y se pidió {alto}",
1553                    b.label()
1554                );
1555            }
1556        }
1557    }
1558
1559    /// El LOD tiene que BAJAR el costo, no subirlo — es el único motivo por el que
1560    /// existe. Y el costo declarado en `DetalleSer::costo` no aplica acá (es el del
1561    /// humanoide), así que lo que se fija es la monotonía.
1562    #[test]
1563    fn el_detalle_baja_con_la_distancia() {
1564        for b in Bicho::ALL {
1565            let n = |d| bicho_sdf_lod(b, 1.0, Andanza::Quieto, 0.0, d).prims.len();
1566            let (h, m, l) = (n(DetalleSer::Hero), n(DetalleSer::Medio), n(DetalleSer::Lejos));
1567            assert!(h >= m && m >= l, "{}: hero={h} medio={m} lejos={l}", b.label());
1568            assert!(l < h, "{}: el LOD lejano no ahorra nada ({l} vs {h})", b.label());
1569        }
1570    }
1571
1572    /// Dos especies no pueden dibujar exactamente lo mismo: si pasa, una de las dos
1573    /// es una fila de la tabla que no cambió nada.
1574    #[test]
1575    fn ninguna_especie_es_copia_de_otra() {
1576        let mut firmas: Vec<(String, &str)> = Vec::new();
1577        for b in Bicho::ALL {
1578            let s = bicho_sdf_lod(b, 1.0, Andanza::Quieto, 0.0, DetalleSer::Hero);
1579            let firma = s
1580                .prims
1581                .iter()
1582                .map(|p| format!("{:?}{:?}{:.4}{:?}", p.kind, p.a, p.ra, p.color))
1583                .collect::<Vec<_>>()
1584                .join("|");
1585            if let Some((_, otro)) = firmas.iter().find(|(f, _)| *f == firma) {
1586                panic!("{} dibuja exactamente lo mismo que {}", b.label(), otro);
1587            }
1588            firmas.push((firma, b.label()));
1589        }
1590    }
1591
1592    /// La andanza tiene que MOVER algo. Un `Caminar` que da la misma pose que
1593    /// `Quieto` es una animación que no existe (y no se nota hasta ver el video).
1594    #[test]
1595    fn caminar_mueve_las_patas() {
1596        let mut movidos = 0;
1597        for b in Bicho::ALL {
1598            let q = bicho_sdf_con(b, 1.0, Andanza::Quieto, 0.0);
1599            let c = bicho_sdf_con(b, 1.0, Andanza::Caminar, 1.1);
1600            let dif = q
1601                .prims
1602                .iter()
1603                .zip(c.prims.iter())
1604                .filter(|(a, b)| a.a != b.a || a.b != b.b)
1605                .count();
1606            if dif > 0 {
1607                movidos += 1;
1608            }
1609        }
1610        assert_eq!(movidos, Bicho::ALL.len(), "{movidos} de {} se mueven al caminar", Bicho::ALL.len());
1611    }
1612
1613    /// **El hocico baja al comer y sube al estar alerta.** Parece obvio y no lo es:
1614    /// `rot_x` gira `+Z` hacia `−Y`, así que el cuello se armó invertido y las
1615    /// cincuenta especies de cuatro patas salieron pastando en andanza «caminar».
1616    /// Ningún conteo lo veía —la altura total y el apoyo eran correctos— y en la
1617    /// hoja de contacto saltaba a la vista. Este test es esa hoja, en números.
1618    #[test]
1619    fn el_hocico_baja_al_comer_y_sube_al_estar_alerta() {
1620        // El hocico es la geometría más adelantada (mayor `z`).
1621        let punta_y = |b: Bicho, a: Andanza| -> f32 {
1622            let s = bicho_sdf_lod(b, 1.0, a, 0.0, DetalleSer::Lejos);
1623            s.prims
1624                .iter()
1625                .filter(|p| !matches!(p.kind, llimphi_3d::SdfKind::DecalEsfera | llimphi_3d::SdfKind::DecalCapsula))
1626                .max_by(|x, y| x.b[2].total_cmp(&y.b[2]))
1627                .map(|p| p.b[1])
1628                .unwrap_or(0.0)
1629        };
1630        for b in [Bicho::Vaca, Bicho::Caballo, Bicho::Ciervo, Bicho::Jirafa, Bicho::Llama] {
1631            let comiendo = punta_y(b, Andanza::Comer);
1632            let quieto = punta_y(b, Andanza::Quieto);
1633            let alerta = punta_y(b, Andanza::Alerta);
1634            assert!(
1635                comiendo < quieto,
1636                "{}: comiendo el hocico está a {comiendo} y quieto a {quieto}",
1637                b.label()
1638            );
1639            // Alerta nunca baja la cabeza de forma apreciable. El margen del 3 %
1640            // no es una excusa: el ensamble se normaliza a la altura total, así que
1641            // en un cuello YA vertical (llama, jirafa) levantarlo hace al animal
1642            // más alto en vez de subir el hocico, y al reescalar el hocico queda un
1643            // pelo más abajo en la figura. Donde el cuello tiene recorrido, la
1644            // exigencia es estricta (abajo).
1645            assert!(
1646                alerta >= quieto * 0.97,
1647                "{}: alerta el hocico está a {alerta} y quieto a {quieto}",
1648                b.label()
1649            );
1650            if let crate::Receta::Cuatro(rec) = receta(b) {
1651                if rec.cuello_ang < 1.0 {
1652                    assert!(
1653                        alerta > quieto + 1e-3,
1654                        "{}: alerta no levantó la cabeza ({alerta} vs {quieto})",
1655                        b.label()
1656                    );
1657                }
1658            }
1659            // Y quieto, la cabeza va POR ENCIMA de la panza: un cuadrúpedo que
1660            // camina no lleva el hocico al ras del piso.
1661            assert!(quieto > 0.25, "{}: quieto el hocico está a {quieto} de 1.0", b.label());
1662        }
1663    }
1664
1665    /// Determinismo: la misma entrada da el mismo bicho (el preview y el export
1666    /// tienen que coincidir cuadro a cuadro).
1667    #[test]
1668    fn el_mismo_bicho_sale_igual_dos_veces() {
1669        for b in [Bicho::Vaca, Bicho::Guacamayo, Bicho::Boa, Bicho::Mariposa, Bicho::Delfin] {
1670            let a = bicho_sdf_con(b, 1.0, Andanza::Caminar, 0.7);
1671            let c = bicho_sdf_con(b, 1.0, Andanza::Caminar, 0.7);
1672            assert_eq!(a.prims.len(), c.prims.len());
1673            for (x, y) in a.prims.iter().zip(c.prims.iter()) {
1674                assert_eq!(x.a, y.a);
1675                assert_eq!(x.color, y.color);
1676            }
1677        }
1678    }
1679
1680    /// Las cinco familias están todas representadas y cada especie cae en la suya.
1681    #[test]
1682    fn las_cinco_familias_estan_pobladas() {
1683        let mut vistas: HashSet<&str> = HashSet::new();
1684        for b in Bicho::ALL {
1685            vistas.insert(receta(b).familia());
1686        }
1687        assert_eq!(vistas.len(), 5, "familias vistas: {vistas:?}");
1688    }
1689
1690    /// Un rebaño de veinte animales tiene que entrar en el uniform con LOD — es el
1691    /// caso de uso, no una curiosidad.
1692    #[test]
1693    fn un_rebano_de_veinte_entra_en_el_uniform() {
1694        let manada = [
1695            Bicho::Vaca, Bicho::Oveja, Bicho::Caballo, Bicho::Cabra, Bicho::Cerdo,
1696            Bicho::Gallina, Bicho::Pato, Bicho::Perro, Bicho::Gato, Bicho::Conejo,
1697        ];
1698        let total: usize = manada
1699            .iter()
1700            .enumerate()
1701            .map(|(i, &b)| {
1702                let d = if i < 2 {
1703                    DetalleSer::Hero
1704                } else if i < 5 {
1705                    DetalleSer::Medio
1706                } else {
1707                    DetalleSer::Lejos
1708                };
1709                bicho_sdf_lod(b, 1.0, Andanza::Caminar, i as f32, d).prims.len()
1710            })
1711            .sum::<usize>()
1712            * 2;
1713        assert!(
1714            total <= llimphi_3d::VOXEL_MAX_SER_PRIMS,
1715            "veinte animales gastan {total} de {}",
1716            llimphi_3d::VOXEL_MAX_SER_PRIMS
1717        );
1718    }
1719}