tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0)
1//! **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}