tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
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];