gioser

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ main llimphi-voxel/src/ser_sdf.rs Rust · 1674 siq'ikuna · 66.2 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

1//! **Ser SDF** — el mismo diseño de personaje de [`humano`](crate::humano)
2//! (Kindchenschema, canon de cabezas, shape language) emitido como **ensamble de
3//! primitivas analíticas** ([`SdfSer`]) en vez de voxelizado: el motor lo traza por
4//! *sphere tracing* con unión suave (`llimphi_3d::ser`) → silueta matemáticamente
5//! lisa, normales exactas, sin anillos ni bultos de reconstrucción.
6//!
7//! ## Relación con `humanoide_voxel`
8//!
9//! MISMAS proporciones y perillas ([`EstiloSer`]), MISMA [`Pose`] (el catálogo de
10//! [`Clip`](crate::Clip) anima a los dos): este es el camino **hero/cerca** (la
11//! personalidad importa) y el voxel queda como LOD lejano o para cuando el ser deba
12//! ser materia editable del mundo.
13//!
14//! ## Cara = decals 2D sobre la cabeza 3D
15//!
16//! Ojos (esclera + pupila + chispa), sonrisa y **rubor** van como primitivas
17//! `Decal*`: re-pintan el color de la superficie sin geometría — el truco de
18//! *Animal Crossing*/*Wind Waker*. Expresión/parpadeo = regenerar los decals
19//! (son datos por frame, no assets).
20//!
21//! El ser se construye en espacio LOCAL (pies en `y≈0`, centrado en X/Z, mirando
22//! `+Z`); se posa en el mundo con [`SdfSer::posado`] (coords de voxel + yaw).
23
24use llimphi_3d::glam::Vec3;
25use llimphi_3d::{SdfKind, SdfPrim, SdfSer};
26use serde::{Deserialize, Serialize};
27
28use crate::actor::Pose;
29use crate::gesto::Gesto;
30use crate::humano::{rot_x, rot_y, rot_z, Colores, EstiloSer};
31
32#[inline]
33fn v3(v: Vec3) -> [f32; 3] {
34    [v.x, v.y, v.z]
35}
36
37/// Alto (desde los pies) al que quedan los OJOS de un ser de `altura` con `estilo` —
38/// el punto desde el que se mide un look-at. Calco de las proporciones de
39/// [`ser_sdf`] (centro de cabeza + altura del ojo en la cara).
40pub fn altura_ojos(estilo: &EstiloSer, altura: f32) -> f32 {
41    let h = altura.max(0.5);
42    let cabezas = estilo.cabezas.clamp(1.6, 8.0);
43    let head_r = h / (2.0 * cabezas);
44    let head_ry = head_r * (1.0 + 0.2 * (estilo.craneo - 1.0)).clamp(0.94, 1.06);
45    h - head_ry * 1.02 + estilo.ojo_altura * head_r
46}
47
48/// **Construye el ser SDF** de `altura` voxels de alto (referencia), con el `estilo`
49/// dado, en la `pose` dada, coloreado por `col`, con la cara NEUTRA. Pies en `y≈0`,
50/// centrado en X/Z, mirando a `+Z`. Puro y determinista; el render lo hace
51/// `VoxelRenderer.sers`. Para que la cara viva (parpadeo, boca, mirada) usá
52/// [`ser_sdf_con`] con un [`Gesto`].
53pub fn ser_sdf(estilo: &EstiloSer, pose: &Pose, altura: f32, col: Colores) -> SdfSer {
54    ser_sdf_con(estilo, pose, altura, col, &Gesto::neutro())
55}
56
57/// **Nivel de detalle de la cara.** El cuerpo se traza igual en los tres (es la
58/// silueta, y es analítica: no hay nada que simplificar); lo que se cae con la
59/// distancia son los rasgos, que a pocos píxeles de cabeza no se distinguen pero
60/// siguen costando primitivas del uniform — y las primitivas son el recurso escaso
61/// (240 para todo el reparto). Ver [`reparto_sdf`](crate::reparto_sdf).
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum DetalleSer {
64    /// Primer plano: cara completa (domos, pupilas, chispas, párpados, rubor, boca).
65    /// ~22 primitivas (30 parpadeando).
66    Hero,
67    /// Plano medio: sin chispa de brillo, sin rubor y con la boca en 2 tramos.
68    /// ~16 primitivas.
69    Medio,
70    /// Fondo: sin cara — sólo el cuerpo y dos puntos oscuros por ojos. 12 primitivas.
71    Lejos,
72}
73
74impl DetalleSer {
75    /// Detalle que le toca a un ser de `altura` visto desde `dist`. Los cortes están
76    /// en **tamaño angular**, no en distancia absoluta: lo que importa es cuántos
77    /// píxeles mide la cabeza, y eso es la razón `dist/altura`.
78    pub fn por_distancia(dist: f32, altura: f32) -> Self {
79        let r = dist / altura.max(1e-3);
80        if r < 6.0 {
81            DetalleSer::Hero
82        } else if r < 16.0 {
83            DetalleSer::Medio
84        } else {
85            DetalleSer::Lejos
86        }
87    }
88
89    /// Cuántas primitivas cuesta un ser a este detalle (cota superior, con párpados
90    /// y boca abierta) — lo usa el reparto para no pasarse del presupuesto.
91    pub fn costo(self) -> usize {
92        match self {
93            DetalleSer::Hero => 31,
94            DetalleSer::Medio => 23,
95            DetalleSer::Lejos => 12,
96        }
97    }
98}
99
100/// Igual que [`ser_sdf`] pero con la cara del cuadro: párpados, boca, pupilas y giro
101/// de cabeza salen del [`Gesto`] (dato por cuadro, no asset — la cara son decals).
102pub fn ser_sdf_con(
103    estilo: &EstiloSer,
104    pose: &Pose,
105    altura: f32,
106    col: Colores,
107    gesto: &Gesto,
108) -> SdfSer {
109    ser_sdf_lod(estilo, pose, altura, col, gesto, DetalleSer::Hero)
110}
111
112/// Igual que [`ser_sdf_con`] pero eligiendo el [`DetalleSer`] de la cara.
113pub fn ser_sdf_lod(
114    estilo: &EstiloSer,
115    pose: &Pose,
116    altura: f32,
117    col: Colores,
118    gesto: &Gesto,
119    detalle: DetalleSer,
120) -> SdfSer {
121    ser_sdf_atuendo(estilo, pose, altura, col, gesto, detalle, &Atuendo::default())
122}
123
124/// **Dónde está la mano derecha** del ser en esta pose, en su espacio LOCAL (pies en
125/// el origen, mirando `+Z`, sin posar): el centro de la esfera de piel que
126/// [`ser_sdf_atuendo`] dibuja al final del brazo `arm_r`. Es lo que necesita quien
127/// le pone algo en la mano —la estrella de la princesa— para que lo lleve de
128/// verdad cuando saluda o señala, y no flotando al costado.
129///
130/// Repite la cuenta del brazo de [`ser_sdf_atuendo`] en vez de compartirla: la de
131/// allá vive entre treinta proporciones que sólo usa el cuerpo. Las ata el test
132/// `la_mano_derecha_es_la_esfera_que_se_dibuja`, en cada clip y en cada edad.
133pub fn mano_derecha(estilo: &EstiloSer, pose: &Pose, altura: f32) -> Vec3 {
134    let h = altura.max(0.5);
135    let cabezas = estilo.cabezas.clamp(1.6, 8.0);
136    let head_r = h / (2.0 * cabezas);
137    let head_ry = head_r * (1.0 + 0.2 * (estilo.craneo - 1.0)).clamp(0.94, 1.06);
138    let head_cy = h - head_ry * 1.02;
139    let neck = head_r * (0.14 + 0.13 * (cabezas - 2.6).clamp(0.0, 2.2));
140    let shoulder_y = (head_cy - head_ry - neck).max(head_r);
141    let leg_frac = (0.28 + 0.055 * (cabezas - 2.0)).clamp(0.28, 0.52);
142    let hip_y = shoulder_y * leg_frac;
143    let torso_rx = h * (0.13 + 0.05 * estilo.redondez);
144    let limb_r = h * estilo.extremidad;
145    let ang = estilo.angulosidad.clamp(0.0, 1.0);
146    let chest_r = torso_rx * (0.62 + 0.30 * ang);
147    let belly_r = torso_rx * (0.72 + 0.28 * estilo.panza.clamp(0.8, 1.4)) * (1.0 - 0.22 * ang);
148    let shoulder_x = chest_r + limb_r * (0.45 + 0.55 * ang);
149    let arm_len = (shoulder_y - hip_y) * 1.02;
150    let bob = pose.bob * h * 0.5;
151    let rel = Vec3::new(shoulder_x, shoulder_y - hip_y, limb_r * 0.3);
152    let r = rot_x(rel, pose.lean);
153    let shoulder = Vec3::new(r.x, hip_y + r.y + bob, r.z);
154    let base_out = 0.16 + ((belly_r / chest_r - 1.0) * 0.30).clamp(0.0, 0.24);
155    let mut dir = rot_x(Vec3::new(0.0, -arm_len, 0.0), pose.arm_r);
156    dir = rot_z(dir, pose.arm_r_out + base_out);
157    dir = rot_x(dir, pose.lean);
158    let mano = shoulder + dir;
159    // El aplastamiento del rebote, que allá se aplica al final a todo el ensamble.
160    if pose.squash.abs() > 1e-4 {
161        let s = pose.squash.clamp(-0.45, 0.45);
162        let (ky, kx) = (1.0 - s, 1.0 + 0.55 * s);
163        return Vec3::new(mano.x * kx, mano.y * ky, mano.z * kx);
164    }
165    mano
166}
167
168/// Igual que [`ser_sdf_lod`] pero **vestido**: peinado, barba, sombrero, anteojos y
169/// falda ([`Atuendo`]).
170///
171/// Nació el 2026-09-06 para la multitud: con un solo casquete de pelo, una vereda
172/// con veinte personas eran veinte veces la misma persona con la remera de otro
173/// color. El [`Atuendo::default()`] reproduce **exactamente** el ser de antes (mismo
174/// casquete, misma lista de primitivas) — lo fija el test `el_atuendo_por_defecto_no_cambia_nada`.
175pub fn ser_sdf_atuendo(
176    estilo: &EstiloSer,
177    pose: &Pose,
178    altura: f32,
179    col: Colores,
180    gesto: &Gesto,
181    detalle: DetalleSer,
182    atuendo: &Atuendo,
183) -> SdfSer {
184    // El SDF es analítico → funciona a CUALQUIER escala (un actor de escena mide
185    // ~1.8; el demo usa 72). Sólo se guarda de degenerados.
186    let h = altura.max(0.5);
187    let cabezas = estilo.cabezas.clamp(1.6, 8.0);
188
189    // --- Proporciones: calco de `humanoide_voxel` (canon de cabezas + baby-schema) ---
190    let head_r = h / (2.0 * cabezas);
191    let head_ry = head_r * (1.0 + 0.2 * (estilo.craneo - 1.0)).clamp(0.94, 1.06);
192    let head_cy = h - head_ry * 1.02;
193    // El cuello CRECE con el canon: pegado en el chibi (0.14), visible en el adulto
194    // (~0.40) — sin esto la cabeza de 3.6+ cabezas quedaba hundida hasta el mentón
195    // en el torso ("muñeco dentro de una bola").
196    let neck = head_r * (0.14 + 0.13 * (cabezas - 2.6).clamp(0.0, 2.2));
197    let shoulder_y = (head_cy - head_ry - neck).max(head_r);
198
199    let leg_frac = (0.28 + 0.055 * (cabezas - 2.0)).clamp(0.28, 0.52);
200    let hip_y = shoulder_y * leg_frac;
201    let torso_rx = h * (0.13 + 0.05 * estilo.redondez);
202
203    let limb_r = h * estilo.extremidad;
204    let ang = estilo.angulosidad.clamp(0.0, 1.0);
205    // Cuerpo PERA (cono redondeado vertical): pecho angosto arriba, panza ancha
206    // abajo — la silueta clásica de personaje tierno. El elipsoide único de antes
207    // era casi una esfera que se tragaba hombros y mentón. La angulosidad invierte
208    // la pera (pecho ancho, cadera angosta = atlético).
209    let chest_r = torso_rx * (0.62 + 0.30 * ang);
210    let belly_r = torso_rx * (0.72 + 0.28 * estilo.panza.clamp(0.8, 1.4)) * (1.0 - 0.22 * ang);
211    let chest_y = shoulder_y - chest_r * 0.55;
212    let belly_y = hip_y + belly_r * 0.35;
213    let shoulder_x = chest_r + limb_r * (0.45 + 0.55 * ang);
214    let hip_x = (belly_r * 0.52).min(torso_rx * (0.55 - 0.18 * ang) + limb_r * 0.3);
215    let leg_len = hip_y;
216    let arm_len = (shoulder_y - hip_y) * 1.02;
217    let head_rx = head_r * estilo.mejilla;
218    // Cara más PLANA que ancha (0.88, no esférica): en una cabeza-esfera los rasgos
219    // quedan a ±30° del frente y el ojo lejano se pierde en el escorzo del 3/4 —
220    // los cartoon aplanan la cara para que ambos ojos sobrevivan la vista 3/4.
221    let head_rz = head_rx * 0.88;
222
223    // Transform del cuerpo superior: rebote `bob` + inclinación `lean` sobre la cadera.
224    let bob = pose.bob * h * 0.5;
225    let upper = |local_y: f32, off: Vec3| -> Vec3 {
226        let rel = Vec3::new(off.x, local_y - hip_y, off.z);
227        let r = rot_x(rel, pose.lean);
228        Vec3::new(r.x, hip_y + r.y + bob, r.z)
229    };
230
231    let mut prims: Vec<SdfPrim> = Vec::with_capacity(24);
232    let geo = |kind: SdfKind, a: Vec3, ra: f32, b: Vec3, rb: f32, color: [f32; 3], blend: f32| SdfPrim {
233        kind,
234        a: v3(a),
235        ra,
236        b: v3(b),
237        rb,
238        color,
239        blend,
240    };
241
242    // --- Piernas: conos redondeados (cadera gorda → tobillo), rematan a `limb_r` del
243    // piso para que la tapa esférica APOYE en y=0 (no se entierre). El **apoyo** de
244    // la pose (IK de pies) acorta la pierna del pie que pisa más alto — la cadera se
245    // queda donde está y el pie sube, como en una ladera.
246    for (sx, swing, pie) in
247        [(-1.0f32, pose.leg_l, pose.pie_l), (1.0, pose.leg_r, pose.pie_r)]
248    {
249        let hip = Vec3::new(sx * hip_x, hip_y, 0.0);
250        let dir = rot_x(Vec3::new(0.0, -leg_len, 0.0), swing);
251        let largo = ((leg_len - limb_r) * (1.0 - pie.clamp(0.0, 0.85))).max(0.1);
252        let foot = hip + dir * (largo / leg_len.max(1e-3));
253        prims.push(geo(
254            SdfKind::Capsula,
255            hip,
256            limb_r * 1.15,
257            foot,
258            limb_r,
259            [col.pants[0], col.pants[1], col.pants[2]],
260            limb_r * 0.5,
261        ));
262    }
263
264    // --- Torso pera: cápsula vertical pecho(chico)→panza(grande); funde las piernas.
265    prims.push(geo(
266        SdfKind::Capsula,
267        upper(chest_y, Vec3::ZERO),
268        chest_r,
269        upper(belly_y, Vec3::ZERO),
270        belly_r,
271        col.shirt,
272        limb_r * 0.9,
273    ));
274
275    // --- Brazos: conos redondeados colgando con splay base (no pegados al torso).
276    // El splay crece con la panza: en el cuerpo pera ancho los brazos a plomo quedan
277    // sepultados detrás de la barriga — se abren y adelantan para seguir visibles.
278    let base_out = 0.16 + ((belly_r / chest_r - 1.0) * 0.30).clamp(0.0, 0.24);
279    for (sx, swing, out) in [
280        (-1.0f32, pose.arm_l, pose.arm_l_out),
281        (1.0, pose.arm_r, pose.arm_r_out),
282    ] {
283        let shoulder = upper(shoulder_y, Vec3::new(sx * shoulder_x, 0.0, limb_r * 0.3));
284        // La cápsula ARRANCA DENTRO del torso (no en el pivote del hombro): con el
285        // brazo en alto el hombro queda fuera del elipsoide y el smin no puentea →
286        // brazo flotante. Enterrar era veneno en el campo voxel (anillos); en el SDF
287        // analítico es gratis. El pivote de rotación sigue siendo el hombro.
288        let root = upper(chest_y + chest_r * 0.35, Vec3::new(sx * chest_r * 0.68, 0.0, 0.0));
289        let mut dir = rot_x(Vec3::new(0.0, -arm_len, 0.0), swing);
290        dir = rot_z(dir, sx * (out + base_out));
291        dir = rot_x(dir, pose.lean);
292        let hand = shoulder + dir;
293        // Blend ANGOSTO (0.4, no 0.7): el ancho tendía una membrana smin entre el
294        // brazo levantado y la mejilla (veta gris en la cara). El anclaje al torso ya
295        // lo garantiza la raíz enterrada, no el blend.
296        prims.push(geo(
297            SdfKind::Capsula,
298            root,
299            limb_r * 0.98,
300            hand,
301            limb_r * 0.80,
302            col.shirt,
303            limb_r * 0.4,
304        ));
305        // Mano: esferita de piel, junta suave con la manga.
306        prims.push(geo(
307            SdfKind::Esfera,
308            hand,
309            limb_r * 0.98,
310            hand,
311            0.0,
312            col.skin,
313            limb_r * 0.45,
314        ));
315    }
316
317    // --- Cuello: cápsula corta de piel entre hombros y cabeza — sin ella el smin
318    // no alcanza a cerrar y se ve el fondo por dos huecos a los lados.
319    let head_c = upper(head_cy, Vec3::ZERO);
320    prims.push(geo(
321        SdfKind::Capsula,
322        upper(shoulder_y - neck, Vec3::ZERO),
323        head_r * 0.42,
324        upper(head_cy - head_ry * 0.4, Vec3::ZERO),
325        head_r * 0.40,
326        col.skin,
327        head_r * 0.30,
328    ));
329
330    // --- Cabeza: elipsoide (la mejilla ya ensancha rx); junta smin con el cuello.
331    prims.push(geo(
332        SdfKind::Elipsoide,
333        head_c,
334        0.0,
335        Vec3::new(head_rx, head_ry, head_rz),
336        0.0,
337        col.skin,
338        head_r * 0.35,
339    ));
340
341    // --- Cara: DECALS sobre la superficie de la cabeza (siguen head_pitch + gesto) ---
342    // Punto sobre el elipsoide de la cabeza para un offset (x,y) local: z sale de la
343    // ecuación del elipsoide (+ un pelito hacia afuera para que el decal "moje" la piel).
344    // `face_pt` lleva un punto LOCAL de la cabeza al mundo (aplica cabeceo del clip +
345    // el giro del gesto); `on_face` lo posa sobre la superficie del elipsoide (+push).
346    //
347    // El CRÁNEO (elipsoide) no rota: sus semiejes están alineados a ejes y el shader
348    // sólo des-rota el yaw del ser entero. Rotan los RASGOS (pelo, ojos, boca) — que
349    // es lo que se ve girar. La cabeza es casi de revolución (rz = 0.88·rx), así que
350    // la silueta no delata la trampa.
351    let head_yaw = gesto.cabeza[0];
352    let head_pitch = pose.head_pitch + gesto.cabeza[1];
353    // El giro se hace en **espacio de esfera**: la cabeza es un elipsoide, o sea una
354    // esfera escalada; dividir por los semiejes, rotar, y volver a multiplicar mapea
355    // superficie→superficie EXACTAMENTE. Rotar crudo (que es lo que hacía el cabeceo
356    // del clip, chico) hunde los rasgos dentro del cráneo en cuanto el giro es grande:
357    // el frente está a `rz` del centro y la coronilla a `ry > rz`, así que un ojo
358    // subido 40° se mete un 12 % adentro y se lo traga la cabeza.
359    let hr3 = Vec3::new(head_rx.max(1e-4), head_ry.max(1e-4), head_rz.max(1e-4));
360    let gira = head_pitch.abs() + head_yaw.abs() > 1e-5;
361    let face_pt = |l: Vec3| -> Vec3 {
362        if !gira {
363            return head_c + l;
364        }
365        head_c + rot_y(rot_x(l / hr3, head_pitch), head_yaw) * hr3
366    };
367
368    // --- Pelo: casquete elipsoidal corrido hacia atrás/arriba (frente despejada),
369    // junta casi dura para que lea como cáscara, no como bulto fundido. NO gira con
370    // el gesto: el pelo es del CRÁNEO, y el cráneo acá está quieto. Haciéndolo girar
371    // (probado), al levantar la cara el casquete se iba para atrás y el ser quedaba
372    // pelado. Lo que se desliza sobre la cabeza son los RASGOS, no el peinado.
373    peinado(
374        atuendo,
375        head_c,
376        Vec3::new(head_rx, head_ry, head_rz),
377        head_r,
378        shoulder_y,
379        col.hair,
380        detalle,
381        &mut prims,
382    );
383    let surf_z = |lx: f32, ly: f32| -> f32 {
384        let nx = (lx / head_rx).clamp(-0.95, 0.95);
385        let ny = (ly / head_ry).clamp(-0.95, 0.95);
386        head_rz * (1.0 - nx * nx - ny * ny).max(0.04).sqrt()
387    };
388    let on_face = |lx: f32, ly: f32, push: f32| -> Vec3 {
389        face_pt(Vec3::new(lx, ly, surf_z(lx, ly) + push))
390    };
391    let decal_e = |c: Vec3, r: f32, color: [f32; 3]| SdfPrim {
392        kind: SdfKind::DecalEsfera,
393        a: v3(c),
394        ra: r,
395        b: v3(c),
396        rb: 0.0,
397        color,
398        blend: 0.1,
399    };
400
401    // Radio de ojo CAPADO para el domo geométrico: el 0.32 del baby-schema literal
402    // hace colgar los domos fuera de la cara (globos googly).
403    let er = (head_r * estilo.ojo_tam).min(head_r * 0.27);
404    let ex = head_rx * 0.46;
405    let eoff = estilo.ojo_altura * head_r;
406    // Mirada: desvío de la pupila DENTRO del domo. El domo es una esfera, así que la
407    // pupila viaja por su superficie (dirección normalizada × radio) — no se despega
408    // ni se aplasta al llegar al borde, como pasaría con un offset plano.
409    let gx = gesto.ojos[0].clamp(-1.0, 1.0);
410    let gy = gesto.ojos[1].clamp(-1.0, 1.0);
411    let parp = gesto.parpado.clamp(0.0, 1.0);
412    // Párpado = decal de PIEL que baja sobre el domo. Cerrar un ojo no es achicar
413    // geometría: es taparlo — así el ojo cerrado conserva el bulto del globo bajo el
414    // párpado (que es lo que se ve en una cara real) en vez de hundirse.
415    // El color es la piel EXACTA: el decal no puede recortarse al domo, así que su
416    // borde muerde la mejilla; con un tono apenas distinto eso se veía como un DISCO
417    // pintado en el cachete. Idéntico = invisible; el ojo cerrado lo dibuja la pestaña.
418    let lid = col.skin;
419    let lash = [col.hair[0] * 0.75, col.hair[1] * 0.72, col.hair[2] * 0.72];
420    let lejos = detalle == DetalleSer::Lejos;
421    for s in [-1.0f32, 1.0] {
422        // Esclera = GEOMETRÍA: domo blanco apenas saliente (vinyl-toy, como eligió la
423        // versión voxel) — un decal plano se pierde en el escorzo del 3/4; el domo no.
424        // De lejos el domo se va: sobresale 0.2 del radio de cabeza (sub-píxel a esa
425        // distancia) y a cambio dos puntos oscuros leen como ojos mejor que dos
426        // pelotas blancas.
427        let dome_l = Vec3::new(s * ex, eoff, surf_z(s * ex, eoff) - er * 0.25);
428        if !lejos {
429            prims.push(geo(
430                SdfKind::Esfera,
431                face_pt(dome_l),
432                er,
433                Vec3::ZERO,
434                0.0,
435                [0.94, 0.945, 0.955],
436                er * 0.25,
437            ));
438        }
439        // Pupila/chispa ANCLADAS AL DOMO (no a la mejilla: un decal anclado a la
440        // superficie de la cara también pinta la piel bajo el domo — chorreaba un cono
441        // oscuro). Convergente hacia adentro (mirada amable, no googly).
442        // PEGADAS al frente del domo (0.99·er sobre la dirección): a 0.80er el borde AA
443        // del decal quedaba a exactamente un radio de la mejilla y la pintaba gris.
444        let dir = Vec3::new(-s * 0.20 + gx * 0.55, gy * 0.55, 0.95).normalize();
445        let pup = dome_l + dir * (er * 0.99);
446        prims.push(decal_e(face_pt(pup), er * 0.48, [0.16, 0.13, 0.18]));
447        // Chispa: el punto de luz del ojo. Es lo primero que se cae al alejarse —
448        // mide 0.2 de un ojo que ya es chico, así que de plano medio para atrás no
449        // aporta un píxel y cuesta una primitiva por ojo.
450        if detalle == DetalleSer::Hero {
451            prims.push(decal_e(
452                face_pt(pup + Vec3::new(-s * er * 0.12, er * 0.26, er * 0.10)),
453                er * 0.20,
454                [0.98, 0.985, 0.99],
455            ));
456        }
457        // Párpado: esfera de piel que arranca ARRIBA del domo (invisible con el ojo
458        // abierto) y baja hasta taparlo. Va DESPUÉS de la pupila en la lista → la
459        // pinta encima (el shader aplica los decals en orden).
460        if parp < 0.995 && !lejos {
461            // Radio JUSTO para tapar el domo (que sobresale 0.75·er de la mejilla) sin
462            // invadir media cara: 1.22·er alcanza, 1.42 pintaba hasta la frente.
463            // Adelantado (z = 0.55·er) para que el borde del párpado cruce el FRENTE
464            // del ojo —donde está la pupila— y no sólo su casquete superior.
465            let lid_r = er * 1.22;
466            // Recorrido calibrado sobre el BORDE del párpado en el plano frontal
467            // (`lid_y − 1.13·er`): a párpado=1 el borde queda arriba del domo (ojo
468            // entero libre), a 0.5 lo parte al medio, a 0 pasa por debajo (tapado).
469            let lid_y = er * 2.15 * parp;
470            prims.push(decal_e(
471                face_pt(dome_l + Vec3::new(0.0, lid_y, er * 0.55)),
472                lid_r,
473                lid,
474            ));
475            // Pestaña: LA línea del ojo cerrado (arco de 3 tramos combado hacia abajo,
476            // sobre el frente del domo). Aparece en el último tramo del cierre, cuando
477            // el párpado ya tapó la pupila — antes no hay línea que dibujar.
478            if parp < 0.5 {
479                let w = 1.0 - parp * 2.0;
480                let hw = er * 0.82 * w;
481                // A MEDIA ALTURA del ojo, no en el borde real del párpado: cuando el
482                // ojo está del todo tapado ese borde cae al ras del pómulo y la línea
483                // se confundía con la sombra del cachete. La convención de dibujo pone
484                // la línea donde estaba la pupila.
485                let ly = -er * 0.12;
486                let arco: Vec<Vec3> = (0..4)
487                    .map(|i| {
488                        let t = -1.0 + i as f32 * (2.0 / 3.0);
489                        // Arco ∩ (puntas apenas más bajas que el centro) = ojo contento,
490                        // no dormido; y el frente del domo se aleja en las puntas, así
491                        // que la línea sigue su curvatura para no despegarse.
492                        let z = er * (0.90 - 0.22 * t * t);
493                        dome_l + Vec3::new(t * hw, ly - er * 0.10 * t * t, z)
494                    })
495                    .collect();
496                for seg in arco.windows(2) {
497                    prims.push(SdfPrim {
498                        kind: SdfKind::DecalCapsula,
499                        a: v3(face_pt(seg[0])),
500                        ra: er * 0.17,
501                        b: v3(face_pt(seg[1])),
502                        rb: er * 0.17,
503                        color: lash,
504                        blend: 0.1,
505                    });
506                }
507            }
508        }
509    }
510
511    // Rubor: dos parches cálidos bajo/afuera de los ojos (amabilidad barata).
512    // Suavizado 2026-08-09: a distancia de retrato (la que usa el editor de caras) el
513    // parche fuerte y ancho leía como un lunar rojo más grande que el ojo. Un rubor es
514    // una zona cálida, no una mancha: menos saturación y menos radio.
515    let blush = [
516        (col.skin[0] * 1.02 + 0.04).min(1.0),
517        col.skin[1] * 0.80,
518        col.skin[2] * 0.82,
519    ];
520    // Acotado a la cara: con ojos bajos (bebé) `eoff − er·1.5` se caía del elipsoide
521    // y el decal terminaba pintando los HOMBROS (pinta cualquier superficie cercana).
522    let blush_y = (eoff - er * 1.5).max(-head_ry * 0.58);
523    if detalle == DetalleSer::Hero {
524        for s in [-1.0f32, 1.0] {
525            prims.push(decal_e(on_face(s * head_rx * 0.68, blush_y, 0.0), er * 0.56, blush));
526        }
527    }
528
529    // Sonrisa: arco de decals capsulares (extremos hacia ARRIBA = feliz), en la piel
530    // frontal-baja. Polilínea de 4 segmentos sobre la parábola de `humano::sonrisa`.
531    let mouth = [
532        (col.skin[0] * 0.72 + 0.086).min(1.0),
533        col.skin[1] * 0.55,
534        col.skin[2] * 0.55,
535    ];
536    // DEBAJO del borde inferior de los domos (con ojos grandes y bajos —bebé— una
537    // fracción fija de head_r quedaba sepultada detrás de ellos), acotada al mentón.
538    // Arco corto y chato: puntas largas/altas se meten DEBAJO de los domos de los
539    // ojos y asoman como mancha en la mejilla.
540    let boca_y = (eoff - er - head_r * 0.14).max(-head_ry * 0.62);
541    let half_w = head_rx * 0.30;
542    // La sonrisa del gesto CURVA el arco: positivo = puntas arriba (feliz), 0 = línea
543    // recta, negativo = mueca. La geometría del arco no cambia, sólo su parábola.
544    let curve = head_r * 0.12 * gesto.sonrisa.clamp(-1.5, 1.5);
545    // La boca se traza con menos tramos al alejarse (4 → 2) y desaparece de fondo:
546    // una línea de 0.05 radios de cabeza no sobrevive a un puñado de píxeles.
547    let tramos = match detalle {
548        DetalleSer::Hero => 4,
549        DetalleSer::Medio => 2,
550        DetalleSer::Lejos => 0,
551    };
552    let pts: Vec<Vec3> = (0..=tramos)
553        .map(|i| {
554            let t = -1.0 + i as f32 * (2.0 / tramos.max(1) as f32);
555            on_face(t * half_w, boca_y + curve * t * t, er * 0.1)
556        })
557        .collect();
558    if tramos > 0 {
559        for w in pts.windows(2) {
560            prims.push(SdfPrim {
561                kind: SdfKind::DecalCapsula,
562                a: v3(w[0]),
563                ra: head_r * 0.055,
564                b: v3(w[1]),
565                rb: head_r * 0.055,
566                color: mouth,
567                blend: 0.1,
568            });
569        }
570    }
571    // Boca ABIERTA: hueco oscuro (interior, más oscuro que la línea de labios) pintado
572    // ENCIMA del arco → el arco queda de comisuras y el hueco de apertura. Baja un
573    // poco al abrirse, como una mandíbula.
574    let abre = gesto.boca.clamp(0.0, 1.0);
575    if abre > 0.03 && !lejos {
576        let r = head_r * (0.05 + 0.13 * abre);
577        let hueco = [mouth[0] * 0.45, mouth[1] * 0.40, mouth[2] * 0.42];
578        prims.push(decal_e(
579            on_face(0.0, boca_y - r * 0.45, er * 0.1),
580            r,
581            hueco,
582        ));
583    }
584
585    // --- Lo que se pone encima: falda, barba, anteojos, sombrero ---------------
586    vestir(
587        atuendo,
588        head_c,
589        Vec3::new(head_rx, head_ry, head_rz),
590        head_r,
591        shoulder_y,
592        hip_y,
593        torso_rx,
594        eoff,
595        er,
596        ex,
597        &col,
598        detalle,
599        &mut prims,
600    );
601
602    // --- APLASTAR Y ESTIRAR ---------------------------------------------------
603    // Se aplica al final, como escala no uniforme del ensamble YA armado alrededor
604    // de los pies (`y = 0`), en vez de propagar el factor por las veinte fórmulas de
605    // proporciones: el layout calibrado no se toca, y el elipsoide (que lleva sus
606    // semiejes en `b`) se deforma EXACTO. La cara viaja pegada porque los decals son
607    // puntos del mismo espacio.
608    //
609    // La única aproximación son los radios de esfera/cápsula, que son isótropos: se
610    // les da la media de los tres ejes. A las amplitudes reales del rebote (4-10 %)
611    // eso no se ve; el que manda la lectura es el cuerpo, que sí se deforma bien.
612    if pose.squash.abs() > 1e-4 {
613        let s = pose.squash.clamp(-0.45, 0.45);
614        let ky = 1.0 - s;
615        let kx = 1.0 + 0.55 * s; // ancha al aplastar, afina al estirar (volumen ~)
616        let km = (kx * 2.0 + ky) / 3.0;
617        for p in &mut prims {
618            p.a = [p.a[0] * kx, p.a[1] * ky, p.a[2] * kx];
619            p.ra *= km;
620            p.rb *= km;
621            p.blend *= km;
622            // `b` es el segundo punto de la cápsula O los SEMIEJES del elipsoide: en
623            // los dos casos la escala por eje es la correcta (y en el elipsoide, exacta).
624            p.b = [p.b[0] * kx, p.b[1] * ky, p.b[2] * kx];
625        }
626    }
627
628    SdfSer { prims, pos: [0.0; 3], yaw: 0.0 }
629}
630
631#[cfg(test)]
632mod tests {
633    use super::*;
634    use crate::actor::Age;
635    use crate::Gesto;
636    use crate::Clip;
637
638    fn col() -> Colores {
639        Colores::nuevos([0.85, 0.68, 0.54], [0.2, 0.6, 0.55], [0.18, 0.22, 0.34])
640    }
641
642    fn geometria(s: &SdfSer) -> usize {
643        s.prims
644            .iter()
645            .filter(|p| !matches!(p.kind, SdfKind::DecalEsfera | SdfKind::DecalCapsula))
646            .count()
647    }
648
649    fn decals(s: &SdfSer) -> usize {
650        s.prims.len() - geometria(s)
651    }
652
653    #[test]
654    fn ensamble_con_cuerpo_y_cara() {
655        let s = ser_sdf(&EstiloSer::default(), &Pose::default(), 46.0, col());
656        // 2 piernas + torso + 2 brazos + 2 manos + cuello + cabeza + pelo + 2 escleras
657        // = 12 de geometría.
658        assert_eq!(geometria(&s), 12);
659        // 2 pupilas + 2 chispas + 2 rubor + 4 sonrisa = 10 decals.
660        assert_eq!(decals(&s), 10);
661    }
662
663    /// El atuendo por defecto tiene que dejar al ser EXACTAMENTE como estaba: el
664    /// casquete de pelo de siempre y ni una primitiva de más. Lo de arriba
665    /// (`ensamble_con_cuerpo_y_cara`, 12 geométricas + 10 decals) es el número
666    /// histórico; acá se fija que la puerta nueva no lo movió.
667    #[test]
668    fn el_atuendo_por_defecto_no_cambia_nada() {
669        let a = ser_sdf(&EstiloSer::amable(), &Pose::default(), 40.0, col());
670        let b = ser_sdf_atuendo(
671            &EstiloSer::amable(),
672            &Pose::default(),
673            40.0,
674            col(),
675            &Gesto::neutro(),
676            DetalleSer::Hero,
677            &Atuendo::default(),
678        );
679        assert_eq!(a.prims.len(), b.prims.len());
680        for (x, y) in a.prims.iter().zip(b.prims.iter()) {
681            assert_eq!(x.kind, y.kind);
682            assert_eq!(x.a, y.a);
683            assert_eq!(x.b, y.b);
684            assert_eq!(x.ra, y.ra);
685            assert_eq!(x.color, y.color);
686        }
687    }
688
689    /// Cada pieza del atuendo agrega geometría, y el calvo saca el casquete. Es lo
690    /// que hace que una multitud no sea la misma persona repetida.
691    #[test]
692    fn cada_pieza_del_atuendo_cambia_la_silueta() {
693        let arma = |a: Atuendo| {
694            ser_sdf_atuendo(
695                &EstiloSer::agil(),
696                &Pose::default(),
697                40.0,
698                col(),
699                &Gesto::neutro(),
700                DetalleSer::Hero,
701                &a,
702            )
703        };
704        let base = geometria(&arma(Atuendo::default()));
705        assert_eq!(
706            geometria(&arma(Atuendo::con(Peinado::Calvo))),
707            base - 1,
708            "el calvo pierde el casquete"
709        );
710        assert!(geometria(&arma(Atuendo::con(Peinado::DosColas))) > base);
711        assert!(geometria(&arma(Atuendo::con(Peinado::Larga))) > base);
712        assert!(
713            geometria(&arma(Atuendo { barba: 0.9, ..Atuendo::default() })) > base,
714            "la barba es geometría, no una mancha"
715        );
716        assert!(geometria(&arma(Atuendo { falda: 0.8, ..Atuendo::default() })) > base);
717        assert!(geometria(&arma(Atuendo { sombrero: Sombrero::Ala, ..Atuendo::default() })) > base);
718        assert!(geometria(&arma(Atuendo { sombrero: Sombrero::Corona, ..Atuendo::default() })) > base);
719        assert!(geometria(&arma(Atuendo { manto: 0.8, ..Atuendo::default() })) > base);
720        // Los anteojos son DECALS (un aro de geometría a escala de escena
721        // desaparecería bajo el blend), así que no tocan la silueta.
722        let con_anteojos = arma(Atuendo { anteojos: true, ..Atuendo::default() });
723        assert_eq!(geometria(&con_anteojos), base);
724        assert!(decals(&con_anteojos) > decals(&arma(Atuendo::default())));
725    }
726
727    /// Los diez peinados dan diez siluetas de cabeza distintas — si dos coinciden
728    /// prim a prim, uno de los dos no está haciendo nada.
729    #[test]
730    fn los_peinados_no_se_repiten() {
731        let peinados = [
732            Peinado::Casquete,
733            Peinado::Corto,
734            Peinado::Melena,
735            Peinado::Larga,
736            Peinado::Rodete,
737            Peinado::Coleta,
738            Peinado::DosColas,
739            Peinado::Afro,
740            Peinado::Cresta,
741            Peinado::Calvo,
742        ];
743        let mut firmas = std::collections::HashSet::new();
744        for p in peinados {
745            let s = ser_sdf_atuendo(
746                &EstiloSer::amable(),
747                &Pose::default(),
748                40.0,
749                col(),
750                &Gesto::neutro(),
751                DetalleSer::Hero,
752                &Atuendo::con(p),
753            );
754            // Firma: las primitivas de geometría por encima de los hombros.
755            let firma: Vec<String> = s
756                .prims
757                .iter()
758                .filter(|x| x.kind != SdfKind::DecalEsfera && x.kind != SdfKind::DecalCapsula)
759                .map(|x| format!("{:?}{:?}{:.3}", x.a, x.b, x.ra))
760                .collect();
761            assert!(firmas.insert(firma), "{p:?} dibuja lo mismo que otro peinado");
762        }
763    }
764
765    #[test]
766    fn bound_cubre_del_piso_a_la_cabeza() {
767        let s = ser_sdf(&EstiloSer::default(), &Pose::default(), 46.0, col());
768        let (c, r) = s.bound();
769        assert!(c[1] - r <= 1.0, "llega al piso");
770        assert!(c[1] + r >= 45.0, "cubre la cabeza");
771    }
772
773    #[test]
774    fn el_apoyo_sube_el_pie_de_ese_lado() {
775        // Las dos primeras primitivas son las piernas (izquierda, derecha); su punto
776        // `b` es el pie. Un apoyo del lado derecho lo levanta y deja al izquierdo donde
777        // estaba: es el mismo IK de pies que planta al muñeco de cajas en la ladera.
778        let llano = ser_sdf(&EstiloSer::default(), &Pose::default(), 46.0, col());
779        let ladera = ser_sdf(
780            &EstiloSer::default(),
781            &Pose { pie_r: 0.35, ..Pose::default() },
782            46.0,
783            col(),
784        );
785        let pie_izq = |s: &SdfSer| s.prims[0].b[1];
786        let pie_der = |s: &SdfSer| s.prims[1].b[1];
787        assert!(
788            pie_der(&ladera) > pie_der(&llano) + 1.0,
789            "el pie derecho sube: {} vs {}",
790            pie_der(&ladera),
791            pie_der(&llano)
792        );
793        assert!(
794            (pie_izq(&ladera) - pie_izq(&llano)).abs() < 1e-4,
795            "el izquierdo no se mueve"
796        );
797    }
798
799    #[test]
800    fn la_mano_derecha_es_la_esfera_que_se_dibuja() {
801        for age in [Age::Baby, Age::Child, Age::Adult, Age::Elder] {
802            let estilo = EstiloSer::por_edad(age);
803            for clip in [Clip::Idle, Clip::Walk, Clip::Run, Clip::Wave, Clip::Point, Clip::Cheer] {
804                for fase in [0.0, 0.7, 1.9] {
805                    let pose = clip.pose(fase);
806                    let m = mano_derecha(&estilo, &pose, 1.7);
807                    let ser = ser_sdf(&estilo, &pose, 1.7, col());
808                    let hay = ser.prims.iter().any(|p| {
809                        p.kind == SdfKind::Esfera
810                            && p.color == col().skin
811                            && Vec3::from_array(p.a).distance(m) < 1e-4
812                    });
813                    assert!(hay, "{age:?}/{clip:?}/{fase}: ninguna mano dibujada en {m:?}");
814                }
815            }
816        }
817    }
818
819    #[test]
820    fn saludar_levanta_una_mano() {
821        let idle = ser_sdf(&EstiloSer::default(), &Pose::default(), 46.0, col());
822        let wave = ser_sdf(&EstiloSer::default(), &Clip::Wave.pose(1.2), 46.0, col());
823        // Las manos son las esferas de PIEL (las escleras de los ojos son blancas).
824        let mano_alta = |s: &SdfSer| {
825            s.prims
826                .iter()
827                .filter(|p| p.kind == SdfKind::Esfera && p.color == col().skin)
828                .map(|p| p.a[1])
829                .fold(f32::MIN, f32::max)
830        };
831        assert!(
832            mano_alta(&wave) > mano_alta(&idle) + 5.0,
833            "la mano sube al saludar: {} vs {}",
834            mano_alta(&wave),
835            mano_alta(&idle)
836        );
837    }
838
839    #[test]
840    fn el_bebe_tiene_cabeza_proporcionalmente_mayor() {
841        let head_frac = |age: Age| {
842            let skin = col().skin;
843            let s = ser_sdf(&EstiloSer::por_edad(age), &Pose::default(), 46.0, col());
844            // La cabeza es el ÚNICO elipsoide de piel (torso = remera, pelo = hair).
845            s.prims
846                .iter()
847                .filter(|p| p.kind == SdfKind::Elipsoide && p.color == skin)
848                .map(|p| p.b[0])
849                .fold(f32::MIN, f32::max)
850                / 46.0
851        };
852        assert!(head_frac(Age::Baby) > head_frac(Age::Adult) + 0.05);
853    }
854
855    #[test]
856    fn el_parpado_baja_sobre_el_ojo_y_lo_tapa() {
857        let e = EstiloSer::default();
858        let abierto = ser_sdf_con(&e, &Pose::default(), 46.0, col(), &Gesto::neutro());
859        let cerrado = ser_sdf_con(
860            &e,
861            &Pose::default(),
862            46.0,
863            col(),
864            &Gesto::neutro().con_parpado(0.0),
865        );
866        // Ojo abierto: ni párpado ni pestaña. Cerrado: 2 párpados + 2 arcos de
867        // pestaña (3 tramos cada uno).
868        assert_eq!(decals(&cerrado), decals(&abierto) + 8);
869        // La pupila (decal oscuro) sigue en la lista pero el párpado va DESPUÉS —
870        // el shader pinta en orden, así que la tapa.
871        let pupila = cerrado
872            .prims
873            .iter()
874            .position(|p| p.kind == SdfKind::DecalEsfera && p.color[0] < 0.3)
875            .expect("hay pupila");
876        let parpado = cerrado
877            .prims
878            .iter()
879            .position(|p| p.kind == SdfKind::DecalEsfera && p.color[0] > 0.7 && p.ra > 0.0)
880            .map(|i| {
881                // el primero grande de piel tras la pupila
882                cerrado.prims[i..]
883                    .iter()
884                    .position(|p| p.kind == SdfKind::DecalEsfera && p.ra > cerrado.prims[pupila].ra)
885                    .map(|k| i + k)
886                    .unwrap_or(i)
887            })
888            .expect("hay párpado");
889        assert!(parpado > pupila, "el párpado se pinta después de la pupila");
890    }
891
892    #[test]
893    fn media_pestanada_no_dibuja_la_linea() {
894        let e = EstiloSer::default();
895        let medio = ser_sdf_con(
896            &e,
897            &Pose::default(),
898            46.0,
899            col(),
900            &Gesto::neutro().con_parpado(0.7),
901        );
902        // Párpado sí (2), pestaña no (sólo bajo 0.5).
903        let base = decals(&ser_sdf(&e, &Pose::default(), 46.0, col()));
904        assert_eq!(decals(&medio), base + 2);
905    }
906
907    #[test]
908    fn la_mirada_corre_las_pupilas_dentro_del_ojo() {
909        let e = EstiloSer::default();
910        let pupilas = |g: &Gesto| -> Vec<[f32; 3]> {
911            ser_sdf_con(&e, &Pose::default(), 46.0, col(), g)
912                .prims
913                .iter()
914                .filter(|p| p.kind == SdfKind::DecalEsfera && p.color[0] < 0.3)
915                .map(|p| p.a)
916                .collect()
917        };
918        let centro = pupilas(&Gesto::neutro());
919        let mut lado = Gesto::neutro();
920        lado.ojos = [1.0, 0.0];
921        let corrida = pupilas(&lado);
922        assert_eq!(centro.len(), 2);
923        for (a, b) in centro.iter().zip(corrida.iter()) {
924            assert!(b[0] > a[0] + 0.05, "la pupila se fue a +X: {a:?} → {b:?}");
925        }
926    }
927
928    #[test]
929    fn el_giro_de_cabeza_lleva_la_cara_pero_no_el_craneo() {
930        let e = EstiloSer::default();
931        let recto = ser_sdf(&e, &Pose::default(), 46.0, col());
932        let mut g = Gesto::neutro();
933        g.cabeza = [0.9, 0.0];
934        let girado = ser_sdf_con(&e, &Pose::default(), 46.0, col(), &g);
935        let craneo = |s: &SdfSer| {
936            s.prims
937                .iter()
938                .find(|p| p.kind == SdfKind::Elipsoide && p.color == col().skin)
939                .unwrap()
940                .a
941        };
942        assert_eq!(craneo(&recto), craneo(&girado), "el cráneo no se mueve");
943        // Los domos de los ojos (esferas blancas) SÍ giran: con yaw>0 se corren a +Z/+X.
944        let domos = |s: &SdfSer| -> Vec<[f32; 3]> {
945            s.prims
946                .iter()
947                .filter(|p| p.kind == SdfKind::Esfera && p.color[0] > 0.9 && p.color[2] > 0.9)
948                .map(|p| p.a)
949                .collect()
950        };
951        let (a, b) = (domos(&recto), domos(&girado));
952        assert_eq!(a.len(), 2);
953        assert!(
954            a.iter().zip(b.iter()).any(|(u, v)| (u[2] - v[2]).abs() > 0.1),
955            "los ojos giraron con la cabeza"
956        );
957    }
958
959    #[test]
960    fn la_boca_abierta_agrega_el_hueco_y_la_mueca_invierte_el_arco() {
961        let e = EstiloSer::default();
962        let cerrada = ser_sdf(&e, &Pose::default(), 46.0, col());
963        let abierta = ser_sdf_con(
964            &e,
965            &Pose::default(),
966            46.0,
967            col(),
968            &Gesto::neutro().con_boca(1.0, 1.0),
969        );
970        assert_eq!(decals(&abierta), decals(&cerrada) + 1);
971        // Comisuras: en la sonrisa suben respecto del centro; en la mueca bajan.
972        let comisura = |g: &Gesto| -> f32 {
973            let s = ser_sdf_con(&e, &Pose::default(), 46.0, col(), g);
974            let arco: Vec<&SdfPrim> = s
975                .prims
976                .iter()
977                .filter(|p| p.kind == SdfKind::DecalCapsula)
978                .collect();
979            arco[0].a[1] - arco[1].b[1] // punta izquierda menos el centro del arco
980        };
981        let feliz = comisura(&Gesto::neutro());
982        let triste = comisura(&Gesto::neutro().con_boca(0.0, -1.0));
983        assert!(feliz > 0.0, "sonrisa: puntas arriba ({feliz})");
984        assert!(triste < 0.0, "mueca: puntas abajo ({triste})");
985    }
986
987    #[test]
988    fn altura_ojos_cae_en_la_cabeza() {
989        let e = EstiloSer::default();
990        let y = altura_ojos(&e, 46.0);
991        let s = ser_sdf(&e, &Pose::default(), 46.0, col());
992        // Debe coincidir con la altura real de los domos de los ojos del ensamble.
993        let domo_y = s
994            .prims
995            .iter()
996            .filter(|p| p.kind == SdfKind::Esfera && p.color[0] > 0.9 && p.color[2] > 0.9)
997            .map(|p| p.a[1])
998            .fold(0.0f32, f32::max);
999        assert!((y - domo_y).abs() < 1.5, "ojos a {y} vs domos a {domo_y}");
1000    }
1001
1002    #[test]
1003    fn aplastar_baja_y_ensancha_estirar_hace_lo_contrario() {
1004        let e = EstiloSer::default();
1005        // Alto y ancho del ensamble, medidos sobre las primitivas (no sobre `bound`,
1006        // que es una esfera y mezcla los dos ejes).
1007        let caja = |sq: f32| -> (f32, f32) {
1008            let pose = Pose { squash: sq, ..Pose::default() };
1009            let s = ser_sdf(&e, &pose, 46.0, col());
1010            let mut alto: f32 = 0.0;
1011            let mut ancho: f32 = 0.0;
1012            for p in &s.prims {
1013                let r = p.ra.max(p.rb);
1014                alto = alto.max(p.a[1] + r).max(p.b[1] + r);
1015                ancho = ancho.max((p.a[0].abs() + r).max(p.b[0].abs() + r));
1016            }
1017            (alto, ancho)
1018        };
1019        let (a0, w0) = caja(0.0);
1020        let (a_ap, w_ap) = caja(0.25);
1021        let (a_es, w_es) = caja(-0.25);
1022        assert!(a_ap < a0 * 0.85, "aplastado baja: {a_ap} vs {a0}");
1023        assert!(w_ap > w0 * 1.05, "aplastado ensancha: {w_ap} vs {w0}");
1024        assert!(a_es > a0 * 1.15, "estirado sube: {a_es} vs {a0}");
1025        assert!(w_es < w0 * 0.95, "estirado afina: {w_es} vs {w0}");
1026    }
1027
1028    #[test]
1029    fn el_squash_no_despega_la_cara_de_la_cabeza() {
1030        let e = EstiloSer::default();
1031        // Con el ser aplastado, las pupilas tienen que seguir dentro del domo del ojo:
1032        // si la escala se aplicara sólo a la geometría, la cara quedaría flotando.
1033        let pose = Pose { squash: 0.3, ..Pose::default() };
1034        let s = ser_sdf(&e, &pose, 46.0, col());
1035        let domos: Vec<[f32; 3]> = s
1036            .prims
1037            .iter()
1038            .filter(|p| p.kind == SdfKind::Esfera && p.color[0] > 0.9 && p.color[2] > 0.9)
1039            .map(|p| p.a)
1040            .collect();
1041        let er = s
1042            .prims
1043            .iter()
1044            .find(|p| p.kind == SdfKind::Esfera && p.color[0] > 0.9 && p.color[2] > 0.9)
1045            .unwrap()
1046            .ra;
1047        for pup in s
1048            .prims
1049            .iter()
1050            .filter(|p| p.kind == SdfKind::DecalEsfera && p.color[0] < 0.3)
1051        {
1052            let d = domos
1053                .iter()
1054                .map(|c| {
1055                    let (dx, dy, dz) = (pup.a[0] - c[0], pup.a[1] - c[1], pup.a[2] - c[2]);
1056                    (dx * dx + dy * dy + dz * dz).sqrt()
1057                })
1058                .fold(f32::MAX, f32::min);
1059            assert!(d < er * 1.3, "pupila despegada del domo: {d} > {}", er * 1.3);
1060        }
1061    }
1062
1063    #[test]
1064    fn posado_fija_pos_y_yaw_sin_tocar_primitivas() {
1065        let s = ser_sdf(&EstiloSer::default(), &Pose::default(), 46.0, col());
1066        let t = s.clone().posado([10.0, 0.0, -5.0], 1.2);
1067        assert_eq!(t.pos, [10.0, 0.0, -5.0]);
1068        assert!((t.yaw - 1.2).abs() < 1e-6);
1069        // Las primitivas quedan en espacio LOCAL (el shader aplica pos/yaw).
1070        for (a, b) in s.prims.iter().zip(t.prims.iter()) {
1071            assert_eq!(a.a, b.a);
1072            assert_eq!(a.b, b.b);
1073        }
1074    }
1075}
1076
1077// ============================================================================
1078//  Atuendo: lo que hace que dos personas no sean la misma persona
1079// ============================================================================
1080
1081/// **Peinado.** El ser nació con un único casquete elipsoidal (`Casquete`), que es
1082/// el pelo corto de siempre; el resto se agrega encima de ese casquete o lo
1083/// reemplaza. Ninguno usa textura: son bultos con `smin` y el color del pelo.
1084#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
1085pub enum Peinado {
1086    /// Casquete corto pegado al cráneo (el original, frente despejada).
1087    #[default]
1088    Casquete,
1089    /// Casquete + patillas: pelo corto con volumen a los costados.
1090    Corto,
1091    /// Melena a la altura del mentón, con raya al medio.
1092    Melena,
1093    /// Pelo largo que cae por la espalda hasta los omóplatos.
1094    Larga,
1095    /// Rodete: pelo recogido en un bollo arriba.
1096    Rodete,
1097    /// Coleta: pelo recogido que cae por atrás.
1098    Coleta,
1099    /// Dos colas a los costados.
1100    DosColas,
1101    /// Afro: casquete grande y esférico.
1102    Afro,
1103    /// Cresta: una franja alta al medio, los costados al ras.
1104    Cresta,
1105    /// Sin pelo.
1106    Calvo,
1107}
1108
1109impl Peinado {
1110    /// Todos, en el orden en que un editor los cicla.
1111    pub const TODOS: [Peinado; 10] = [
1112        Peinado::Casquete,
1113        Peinado::Corto,
1114        Peinado::Melena,
1115        Peinado::Larga,
1116        Peinado::Rodete,
1117        Peinado::Coleta,
1118        Peinado::DosColas,
1119        Peinado::Afro,
1120        Peinado::Cresta,
1121        Peinado::Calvo,
1122    ];
1123
1124    /// El siguiente de [`TODOS`](Self::TODOS), dando la vuelta.
1125    pub fn next(self) -> Self {
1126        let i = Self::TODOS.iter().position(|&p| p == self).unwrap_or(0);
1127        Self::TODOS[(i + 1) % Self::TODOS.len()]
1128    }
1129}
1130
1131/// **Lo que lleva en la cabeza.**
1132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
1133pub enum Sombrero {
1134    #[default]
1135    Ninguno,
1136    /// Gorra con visera.
1137    Gorra,
1138    /// Sombrero de ala (sol, campo).
1139    Ala,
1140    /// Gorro de lana ajustado.
1141    Gorro,
1142    /// Pañuelo/velo que cubre el pelo y baja a los hombros.
1143    Panuelo,
1144    /// Casco de obra / bici.
1145    Casco,
1146    /// Corona: aro sobre la cabeza con cinco puntas. Sin `color_sombrero` es de oro,
1147    /// no del color de la remera — una corona del color de la ropa no se lee.
1148    Corona,
1149}
1150
1151impl Sombrero {
1152    /// Todos, en el orden en que un editor los cicla.
1153    pub const TODOS: [Sombrero; 7] = [
1154        Sombrero::Ninguno,
1155        Sombrero::Gorra,
1156        Sombrero::Ala,
1157        Sombrero::Gorro,
1158        Sombrero::Panuelo,
1159        Sombrero::Casco,
1160        Sombrero::Corona,
1161    ];
1162
1163    /// El siguiente de [`TODOS`](Self::TODOS), dando la vuelta.
1164    pub fn next(self) -> Self {
1165        let i = Self::TODOS.iter().position(|&p| p == self).unwrap_or(0);
1166        Self::TODOS[(i + 1) % Self::TODOS.len()]
1167    }
1168}
1169
1170/// **Atuendo**: lo que se le pone encima al cuerpo. Todo es opcional y el default
1171/// reproduce el ser de siempre.
1172///
1173/// Viaja en el documento del estudio (`CharSpec::atuendo`): todo campo tiene
1174/// default, así un `.ron` escrito a mano nombra sólo lo que le pone.
1175#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1176#[serde(default)]
1177pub struct Atuendo {
1178    pub peinado: Peinado,
1179    /// Barba `[0,1]`: `0` = sin, `0.4` = candado, `1` = barba entera.
1180    pub barba: f32,
1181    pub sombrero: Sombrero,
1182    /// Color del sombrero (`None` = el de la remera).
1183    pub color_sombrero: Option<[f32; 3]>,
1184    pub anteojos: bool,
1185    /// Falda/vestido `[0,1]`: `0` = pantalón; `>0` = campana desde la cadera, y
1186    /// cuánto más alto el número más larga.
1187    pub falda: f32,
1188    /// Color de la falda (`None` = el de la remera, o sea vestido de una pieza).
1189    pub color_falda: Option<[f32; 3]>,
1190    /// Manto/capa `[0,1]`: `0` = sin; `>0` = cae de los hombros por la espalda, y
1191    /// cuánto más alto el número más cerca del suelo llega.
1192    pub manto: f32,
1193    /// Color del manto (`None` = la remera oscurecida, para que se despegue del
1194    /// cuerpo).
1195    pub color_manto: Option<[f32; 3]>,
1196}
1197
1198impl Default for Atuendo {
1199    fn default() -> Self {
1200        Self {
1201            peinado: Peinado::Casquete,
1202            barba: 0.0,
1203            sombrero: Sombrero::Ninguno,
1204            color_sombrero: None,
1205            anteojos: false,
1206            falda: 0.0,
1207            color_falda: None,
1208            manto: 0.0,
1209            color_manto: None,
1210        }
1211    }
1212}
1213
1214impl Atuendo {
1215    /// Sólo un peinado (el atajo más usado por la multitud).
1216    pub fn con(peinado: Peinado) -> Self {
1217        Self { peinado, ..Self::default() }
1218    }
1219
1220    /// El color con que se pinta el sombrero: el propio, o el de fábrica (oro para
1221    /// la corona, la remera para lo demás). Lo usan el pintor y el editor, que
1222    /// muestra este valor mientras el ser no tenga uno propio.
1223    pub fn color_sombrero_efectivo(&self, remera: [f32; 3]) -> [f32; 3] {
1224        self.color_sombrero.unwrap_or(match self.sombrero {
1225            Sombrero::Corona => ORO,
1226            _ => remera,
1227        })
1228    }
1229
1230    /// El color de la falda: el propio, o el de la remera (vestido de una pieza).
1231    pub fn color_falda_efectivo(&self, remera: [f32; 3]) -> [f32; 3] {
1232        self.color_falda.unwrap_or(remera)
1233    }
1234
1235    /// El color del manto: el propio, o la remera oscurecida.
1236    pub fn color_manto_efectivo(&self, remera: [f32; 3]) -> [f32; 3] {
1237        self.color_manto.unwrap_or([remera[0] * 0.72, remera[1] * 0.72, remera[2] * 0.72])
1238    }
1239
1240    /// `true` si no le pone nada al ser (lo que el documento no necesita escribir).
1241    pub fn es_default(&self) -> bool {
1242        *self == Self::default()
1243    }
1244
1245    /// **Cuántas primitivas le SUMA este atuendo a un ser** con el detalle dado,
1246    /// contra el ser sin vestir. El techo de [`DetalleSer::costo`] se calibró
1247    /// desnudo; sin esta cuenta, un reparto de seres vestidos se pasa del uniform y
1248    /// el empacador corta las últimas primitivas sin aviso. Se mide armando los dos
1249    /// seres: el atuendo no depende del gesto, así que la diferencia es exacta.
1250    pub fn costo_extra(&self, detalle: DetalleSer) -> usize {
1251        if self.es_default() {
1252            return 0;
1253        }
1254        let arma = |a: &Atuendo| {
1255            ser_sdf_atuendo(
1256                &EstiloSer::default(),
1257                &Pose::default(),
1258                1.8,
1259                Colores::nuevos([0.8, 0.6, 0.5], [0.3, 0.4, 0.7], [0.2, 0.2, 0.3]),
1260                &Gesto::neutro(),
1261                detalle,
1262                a,
1263            )
1264            .prims
1265            .len()
1266        };
1267        arma(self).saturating_sub(arma(&Atuendo::default()))
1268    }
1269}
1270
1271/// El pelo. El **casquete base** es el mismo elipsoide de siempre (corrido hacia
1272/// atrás y arriba, frente despejada, junta casi dura para que lea como cáscara);
1273/// cada peinado agrega bultos encima. No gira con el gesto: el pelo es del cráneo,
1274/// y el cráneo está quieto (los que se deslizan son los rasgos).
1275#[allow(clippy::too_many_arguments)]
1276fn peinado(
1277    a: &Atuendo,
1278    head_c: Vec3,
1279    hr: Vec3,
1280    head_r: f32,
1281    shoulder_y: f32,
1282    hair: [f32; 3],
1283    detalle: DetalleSer,
1284    prims: &mut Vec<SdfPrim>,
1285) {
1286    let (head_rx, head_ry) = (hr.x, hr.y);
1287    let cap = |k: f32, dy: f32, dz: f32| SdfPrim {
1288        kind: SdfKind::Elipsoide,
1289        a: v3(head_c + Vec3::new(0.0, head_ry * dy, head_r * dz)),
1290        ra: 0.0,
1291        b: v3(Vec3::new(head_rx * 1.04 * k, head_ry * 0.76 * k, head_r * 0.94 * k)),
1292        rb: 0.0,
1293        color: hair,
1294        blend: head_r * 0.12,
1295    };
1296    let mecha = |a: Vec3, ra: f32, b: Vec3, rb: f32| SdfPrim {
1297        kind: SdfKind::Capsula,
1298        a: v3(a),
1299        ra,
1300        b: v3(b),
1301        rb,
1302        color: hair,
1303        blend: head_r * 0.14,
1304    };
1305    let lejos = detalle == DetalleSer::Lejos;
1306    match a.peinado {
1307        Peinado::Calvo => {}
1308        Peinado::Casquete => prims.push(cap(1.0, 0.44, -0.20)),
1309        Peinado::Corto => {
1310            prims.push(cap(1.06, 0.40, -0.16));
1311        }
1312        Peinado::Afro => {
1313            prims.push(SdfPrim {
1314                kind: SdfKind::Elipsoide,
1315                a: v3(head_c + Vec3::new(0.0, head_ry * 0.42, -head_r * 0.10)),
1316                ra: 0.0,
1317                b: v3(Vec3::new(head_rx * 1.42, head_ry * 1.20, head_r * 1.34)),
1318                rb: 0.0,
1319                color: hair,
1320                blend: head_r * 0.30,
1321            });
1322        }
1323        Peinado::Cresta => {
1324            prims.push(cap(0.96, 0.42, -0.20));
1325            let alto = head_ry * 0.55;
1326            prims.push(SdfPrim {
1327                kind: SdfKind::Elipsoide,
1328                a: v3(head_c + Vec3::new(0.0, head_ry * 0.92 + alto * 0.35, -head_r * 0.10)),
1329                ra: 0.0,
1330                b: v3(Vec3::new(head_rx * 0.16, alto, head_r * 0.80)),
1331                rb: 0.0,
1332                color: hair,
1333                blend: head_r * 0.10,
1334            });
1335        }
1336        Peinado::Melena => {
1337            prims.push(cap(1.04, 0.40, -0.18));
1338            for s in [-1.0f32, 1.0] {
1339                prims.push(mecha(
1340                    head_c + Vec3::new(s * head_rx * 0.86, head_ry * 0.35, -head_r * 0.10),
1341                    head_r * 0.30,
1342                    head_c + Vec3::new(s * head_rx * 0.92, -head_ry * 0.95, -head_r * 0.12),
1343                    head_r * 0.26,
1344                ));
1345            }
1346        }
1347        Peinado::Larga => {
1348            prims.push(cap(1.05, 0.40, -0.18));
1349            // La cortina de atrás: baja hasta media espalda.
1350            let largo = (head_c.y - shoulder_y) * 0.5 + head_ry * 1.5;
1351            prims.push(mecha(
1352                head_c + Vec3::new(0.0, head_ry * 0.30, -head_r * 0.72),
1353                head_rx * 0.86,
1354                head_c + Vec3::new(0.0, -largo, -head_r * 0.55),
1355                head_rx * 0.70,
1356            ));
1357            if !lejos {
1358                for s in [-1.0f32, 1.0] {
1359                    prims.push(mecha(
1360                        head_c + Vec3::new(s * head_rx * 0.80, head_ry * 0.30, 0.0),
1361                        head_r * 0.26,
1362                        head_c + Vec3::new(s * head_rx * 0.86, -head_ry * 1.1, -head_r * 0.10),
1363                        head_r * 0.20,
1364                    ));
1365                }
1366            }
1367        }
1368        Peinado::Rodete => {
1369            prims.push(cap(1.02, 0.42, -0.18));
1370            prims.push(SdfPrim {
1371                kind: SdfKind::Esfera,
1372                a: v3(head_c + Vec3::new(0.0, head_ry * 1.05, -head_r * 0.42)),
1373                ra: head_r * 0.42,
1374                b: v3(head_c),
1375                rb: 0.0,
1376                color: hair,
1377                blend: head_r * 0.16,
1378            });
1379        }
1380        Peinado::Coleta => {
1381            prims.push(cap(1.02, 0.42, -0.18));
1382            prims.push(mecha(
1383                head_c + Vec3::new(0.0, head_ry * 0.55, -head_r * 0.85),
1384                head_r * 0.30,
1385                head_c + Vec3::new(0.0, -head_ry * 0.75, -head_r * 1.25),
1386                head_r * 0.22,
1387            ));
1388        }
1389        Peinado::DosColas => {
1390            prims.push(cap(1.02, 0.42, -0.18));
1391            for s in [-1.0f32, 1.0] {
1392                prims.push(mecha(
1393                    head_c + Vec3::new(s * head_rx * 0.90, head_ry * 0.45, -head_r * 0.30),
1394                    head_r * 0.26,
1395                    head_c + Vec3::new(s * head_rx * 1.30, -head_ry * 0.55, -head_r * 0.55),
1396                    head_r * 0.20,
1397                ));
1398            }
1399        }
1400    }
1401}
1402
1403/// Manto, falda, barba, anteojos y sombrero — en ese orden, que es el de adentro hacia
1404/// afuera. Todos son geometría salvo los anteojos, que son decals: un aro fino de
1405/// geometría a escala de escena (~2 voxels de alto) desaparecería bajo el `blend`.
1406#[allow(clippy::too_many_arguments)]
1407fn vestir(
1408    a: &Atuendo,
1409    head_c: Vec3,
1410    hr: Vec3,
1411    head_r: f32,
1412    shoulder_y: f32,
1413    hip_y: f32,
1414    torso_rx: f32,
1415    eoff: f32,
1416    er: f32,
1417    ex: f32,
1418    col: &Colores,
1419    detalle: DetalleSer,
1420    prims: &mut Vec<SdfPrim>,
1421) {
1422    let (head_rx, head_ry, head_rz) = (hr.x, hr.y, hr.z);
1423    let lejos = detalle == DetalleSer::Lejos;
1424
1425    // --- Manto: cono redondeado que cuelga de los hombros por detrás. Va primero
1426    // porque es lo de más adentro: la falda y el pelo largo se apoyan encima.
1427    if a.manto > 0.01 {
1428        let color = a.color_manto_efectivo(col.shirt);
1429        let m = a.manto.clamp(0.0, 1.0);
1430        let pie = (shoulder_y * (0.55 - 0.50 * m)).max(head_r * 0.15);
1431        prims.push(SdfPrim {
1432            kind: SdfKind::Capsula,
1433            a: v3(Vec3::new(head_c.x, shoulder_y, head_c.z - torso_rx * 0.55)),
1434            ra: torso_rx * 0.80,
1435            b: v3(Vec3::new(head_c.x * 0.5, pie, -torso_rx * 0.90)),
1436            rb: torso_rx * (0.95 + 0.25 * m),
1437            color,
1438            blend: torso_rx * 0.20,
1439        });
1440    }
1441
1442    // --- Falda / vestido: campana desde la cadera. Las piernas siguen ahí abajo.
1443    if a.falda > 0.01 {
1444        let color = a.color_falda_efectivo(col.shirt);
1445        let largo = hip_y * (0.30 + 0.55 * a.falda.clamp(0.0, 1.0));
1446        prims.push(SdfPrim {
1447            kind: SdfKind::Capsula,
1448            a: v3(Vec3::new(0.0, hip_y + torso_rx * 0.10, 0.0)),
1449            ra: torso_rx * 0.60,
1450            b: v3(Vec3::new(0.0, hip_y - largo, 0.0)),
1451            rb: torso_rx * (0.85 + 0.35 * a.falda),
1452            color,
1453            blend: torso_rx * 0.35,
1454        });
1455    }
1456
1457    // --- Barba: bultos por el mentón y las mejillas bajas.
1458    if a.barba > 0.01 && !lejos {
1459        let b = a.barba.clamp(0.0, 1.0);
1460        let barba_col = [col.hair[0] * 0.94, col.hair[1] * 0.92, col.hair[2] * 0.92];
1461        let y = -head_ry * (0.52 + 0.12 * b);
1462        prims.push(SdfPrim {
1463            kind: SdfKind::Elipsoide,
1464            a: v3(head_c + Vec3::new(0.0, y, head_rz * 0.34)),
1465            ra: 0.0,
1466            b: v3(Vec3::new(
1467                head_rx * (0.58 + 0.24 * b),
1468                head_ry * (0.24 + 0.26 * b),
1469                head_rz * (0.60 + 0.20 * b),
1470            )),
1471            rb: 0.0,
1472            color: barba_col,
1473            blend: head_r * 0.14,
1474        });
1475        if b > 0.55 {
1476            // Bigote: une la barba con la nariz.
1477            prims.push(SdfPrim {
1478                kind: SdfKind::DecalCapsula,
1479                a: v3(head_c + Vec3::new(-head_rx * 0.26, y + head_ry * 0.42, head_rz * 0.86)),
1480                ra: head_r * 0.10,
1481                b: v3(head_c + Vec3::new(head_rx * 0.26, y + head_ry * 0.42, head_rz * 0.86)),
1482                rb: head_r * 0.10,
1483                color: barba_col,
1484                blend: 0.1,
1485            });
1486        }
1487    }
1488
1489    // --- Anteojos: dos aros y el puente, como decals sobre la cara.
1490    if a.anteojos && !lejos {
1491        let marco = [0.20, 0.19, 0.22];
1492        let y = eoff;
1493        for s in [-1.0f32, 1.0] {
1494            let n = 8;
1495            let pts: Vec<Vec3> = (0..=n)
1496                .map(|i| {
1497                    let t = i as f32 / n as f32 * std::f32::consts::TAU;
1498                    let lx = s * ex + t.cos() * er * 1.15;
1499                    let ly = y + t.sin() * er * 1.15;
1500                    let nx = (lx / head_rx).clamp(-0.95, 0.95);
1501                    let ny = (ly / head_ry).clamp(-0.95, 0.95);
1502                    let z = head_rz * (1.0 - nx * nx - ny * ny).max(0.04).sqrt();
1503                    head_c + Vec3::new(lx, ly, z + er * 0.12)
1504                })
1505                .collect();
1506            for w in pts.windows(2) {
1507                prims.push(SdfPrim {
1508                    kind: SdfKind::DecalCapsula,
1509                    a: v3(w[0]),
1510                    ra: er * 0.10,
1511                    b: v3(w[1]),
1512                    rb: er * 0.10,
1513                    color: marco,
1514                    blend: 0.1,
1515                });
1516            }
1517        }
1518        let puente = |lx: f32| {
1519            let nx: f32 = (lx / head_rx).clamp(-0.95, 0.95);
1520            let ny: f32 = (y / head_ry).clamp(-0.95, 0.95);
1521            head_c + Vec3::new(lx, y, head_rz * (1.0 - nx * nx - ny * ny).max(0.04).sqrt() + er * 0.12)
1522        };
1523        prims.push(SdfPrim {
1524            kind: SdfKind::DecalCapsula,
1525            a: v3(puente(-ex * 0.35)),
1526            ra: er * 0.08,
1527            b: v3(puente(ex * 0.35)),
1528            rb: er * 0.08,
1529            color: marco,
1530            blend: 0.1,
1531        });
1532    }
1533
1534    // --- Sombrero.
1535    let color = a.color_sombrero_efectivo(col.shirt);
1536    match a.sombrero {
1537        Sombrero::Ninguno => {}
1538        Sombrero::Gorro => {
1539            prims.push(SdfPrim {
1540                kind: SdfKind::Elipsoide,
1541                a: v3(head_c + Vec3::new(0.0, head_ry * 0.42, -head_r * 0.08)),
1542                ra: 0.0,
1543                b: v3(Vec3::new(head_rx * 1.10, head_ry * 0.86, head_rz * 1.10)),
1544                rb: 0.0,
1545                color,
1546                blend: head_r * 0.10,
1547            });
1548            prims.push(SdfPrim {
1549                kind: SdfKind::Esfera,
1550                a: v3(head_c + Vec3::new(0.0, head_ry * 1.28, -head_r * 0.06)),
1551                ra: head_r * 0.22,
1552                b: v3(head_c),
1553                rb: 0.0,
1554                color: [color[0] * 1.2, color[1] * 1.2, color[2] * 1.2],
1555                blend: head_r * 0.08,
1556            });
1557        }
1558        Sombrero::Gorra => {
1559            prims.push(SdfPrim {
1560                kind: SdfKind::Elipsoide,
1561                a: v3(head_c + Vec3::new(0.0, head_ry * 0.46, -head_r * 0.12)),
1562                ra: 0.0,
1563                b: v3(Vec3::new(head_rx * 1.08, head_ry * 0.72, head_rz * 1.06)),
1564                rb: 0.0,
1565                color,
1566                blend: head_r * 0.10,
1567            });
1568            // Visera: elipsoide chato que sale al frente.
1569            prims.push(SdfPrim {
1570                kind: SdfKind::Elipsoide,
1571                a: v3(head_c + Vec3::new(0.0, head_ry * 0.30, head_rz * 1.10)),
1572                ra: 0.0,
1573                b: v3(Vec3::new(head_rx * 0.90, head_ry * 0.10, head_rz * 0.62)),
1574                rb: 0.0,
1575                color: [color[0] * 0.82, color[1] * 0.82, color[2] * 0.82],
1576                blend: head_r * 0.08,
1577            });
1578        }
1579        Sombrero::Ala => {
1580            prims.push(SdfPrim {
1581                kind: SdfKind::Elipsoide,
1582                a: v3(head_c + Vec3::new(0.0, head_ry * 0.70, -head_r * 0.06)),
1583                ra: 0.0,
1584                b: v3(Vec3::new(head_rx * 0.92, head_ry * 0.62, head_rz * 0.92)),
1585                rb: 0.0,
1586                color,
1587                blend: head_r * 0.10,
1588            });
1589            prims.push(SdfPrim {
1590                kind: SdfKind::Elipsoide,
1591                a: v3(head_c + Vec3::new(0.0, head_ry * 0.62, -head_r * 0.04)),
1592                ra: 0.0,
1593                b: v3(Vec3::new(head_rx * 1.70, head_ry * 0.10, head_rz * 1.70)),
1594                rb: 0.0,
1595                color,
1596                blend: head_r * 0.08,
1597            });
1598        }
1599        Sombrero::Panuelo => {
1600            prims.push(SdfPrim {
1601                kind: SdfKind::Elipsoide,
1602                a: v3(head_c + Vec3::new(0.0, head_ry * 0.30, -head_r * 0.14)),
1603                ra: 0.0,
1604                b: v3(Vec3::new(head_rx * 1.14, head_ry * 1.02, head_rz * 1.12)),
1605                rb: 0.0,
1606                color,
1607                blend: head_r * 0.12,
1608            });
1609            prims.push(SdfPrim {
1610                kind: SdfKind::Capsula,
1611                a: v3(head_c + Vec3::new(0.0, -head_ry * 0.20, -head_r * 0.60)),
1612                ra: head_rx * 0.80,
1613                b: v3(head_c + Vec3::new(0.0, -head_ry * 1.60, -head_r * 0.50)),
1614                rb: head_rx * 0.70,
1615                color,
1616                blend: head_r * 0.16,
1617            });
1618        }
1619        Sombrero::Casco => {
1620            prims.push(SdfPrim {
1621                kind: SdfKind::Elipsoide,
1622                a: v3(head_c + Vec3::new(0.0, head_ry * 0.52, -head_r * 0.06)),
1623                ra: 0.0,
1624                b: v3(Vec3::new(head_rx * 1.14, head_ry * 0.90, head_rz * 1.14)),
1625                rb: 0.0,
1626                color,
1627                blend: head_r * 0.08,
1628            });
1629            prims.push(SdfPrim {
1630                kind: SdfKind::Elipsoide,
1631                a: v3(head_c + Vec3::new(0.0, head_ry * 0.34, head_rz * 0.75)),
1632                ra: 0.0,
1633                b: v3(Vec3::new(head_rx * 1.05, head_ry * 0.12, head_rz * 0.85)),
1634                rb: 0.0,
1635                color: [color[0] * 0.85, color[1] * 0.85, color[2] * 0.85],
1636                blend: head_r * 0.06,
1637            });
1638        }
1639        Sombrero::Corona => {
1640            // El aro: un elipsoide chato apoyado en la coronilla. Junta casi dura
1641            // (blend chico) para que el oro se despegue del pelo en vez de fundirse.
1642            let aro_y = head_ry * 0.80;
1643            let (rx, rz) = (head_rx * 0.74, head_rz * 0.74);
1644            prims.push(SdfPrim {
1645                kind: SdfKind::Elipsoide,
1646                a: v3(head_c + Vec3::new(0.0, aro_y, -head_r * 0.06)),
1647                ra: 0.0,
1648                b: v3(Vec3::new(rx, head_ry * 0.18, rz)),
1649                rb: 0.0,
1650                color,
1651                blend: head_r * 0.03,
1652            });
1653            // Las puntas: cinco conos que salen del aro, uno al frente. A lo lejos
1654            // tres alcanzan para que la silueta diga «corona».
1655            let puntas: &[f32] = if lejos { &[0.0, 2.2, -2.2] } else { &[0.0, 1.25, -1.25, 2.5, -2.5] };
1656            for &ang in puntas {
1657                let base = head_c
1658                    + Vec3::new(ang.sin() * rx * 0.92, aro_y + head_ry * 0.10, ang.cos() * rz * 0.92 - head_r * 0.06);
1659                prims.push(SdfPrim {
1660                    kind: SdfKind::Capsula,
1661                    a: v3(base),
1662                    ra: head_r * 0.13,
1663                    b: v3(base + Vec3::new(0.0, head_ry * 0.42, 0.0)),
1664                    rb: head_r * 0.04,
1665                    color,
1666                    blend: head_r * 0.03,
1667                });
1668            }
1669        }
1670    }
1671}
1672
1673/// El oro de una corona cuando nadie le dio color.
1674const ORO: [f32; 3] = [0.95, 0.74, 0.22];