tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definitions

⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8 llimphi-voxel/src/humano.rs Rust · 686 lines · 31.6 KB History Who touched what raw

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

1//! **Humanoide voxel REDONDEADO y expresivo** — el ser estilizado que aprovecha la
2//! no-voxelidad del motor (bisel/isosuperficie sub-voxel) para un look vinyl-toy /
3//! Fall-Guys / chibi CON carácter, en vez del muñeco de cajas tipo Minecraft.
4//!
5//! ## Por qué existe (lección 2026-07-18, ver [[detalle-sub-voxel-rugosidad]])
6//!
7//! El [`Actor`](crate::Actor) histórico es una **malla de 11 cajas** (`push_cube`): se
8//! ve cuadrado. Y voxelizar una foto de persona en LITERAL cae en el valle de inquietud
9//! (a tamaño grande da terror). La regla acordada: **la especie tiene su ESTILO PROPIO**
10//! (proporciones cuantizadas, gestos, ojos grandes) y la foto sólo regula RASGOS
11//! (piel/ropa). Este módulo materializa ese estilo como un **volumen voxel** de primitivas
12//! orgánicas (elipsoides + cápsulas) que, renderizado con `bisel` alto + `silueta_por_voxel`,
13//! el ray-marcher redondea a una figura simpática. Comparte la MISMA gramática de campo que
14//! el terreno/fauna (no otra fidelidad → sin valle).
15//!
16//! ## Diseño anclado en literatura (no intuición)
17//!
18//! Los parámetros de [`EstiloSer`] salen de la teoría del diseño de personajes y la
19//! percepción, no de tanteo visual:
20//! - **Kindchenschema / "baby schema"** (Konrad Lorenz, 1943; Glocker et al. 2009): cabeza
21//!   grande respecto al cuerpo, **ojos grandes y BAJOS** (a/por debajo de la mitad de la cara),
22//!   cara **redonda/ancha** (nunca alargada), nariz/boca chicas, mentón blando, mejillas y
23//!   miembros rechonchos → dispara la respuesta de ternura (activa el núcleo accumbens) y
24//!   **escapa del valle**. Dos correcciones de la literatura reciente que este módulo respeta:
25//!   (1) *la frente grande se logra BAJANDO los ojos, no estirando el cráneo* — el lever robusto
26//!   es la posición baja de los rasgos + mandíbula angosta, no la altura de la frente (estudio
27//!   morfométrico 2022); (2) *lo que suma es EVITAR ojos chicos*, no inflarlos sin límite
28//!   (Frontiers 2021: grande-vs-natural no fue significativo). → [`EstiloSer::ojo_tam`]/
29//!   [`ojo_altura`](EstiloSer::ojo_altura)/[`mejilla`](EstiloSer::mejilla)/
30//!   [`extremidad`](EstiloSer::extremidad).
31//! - **Canon de proporciones / "cabezas de alto"**: realista ≈ 7,5–8 cabezas; heroico ≈ 8,5;
32//!   cartoon estilizado ≈ 4–6; niño ≈ 4–5; **chibi/super-deformado ≈ 2–4**. Menos cabezas es
33//!   el **lever más fuerte** de "más tierno / más joven" (la ontogenia real va de cabeza=½ en el
34//!   feto a 1/7 en el adulto → un cabezón se lee juvenil sin importar el resto). → [`EstiloSer::cabezas`]
35//!   (default chibi ~2,6, NO realista).
36//! - **Shape language**: círculo = amable/suave, cuadrado = estable/fiable, triángulo =
37//!   dinámico/agresivo. Un personaje se lee por su **forma dominante** (el "appeal" nace de
38//!   comprometerse con UNA forma). → eje [`redondez`](EstiloSer::redondez) (círculo↔cuadrado,
39//!   controla también el `bisel`) y [`angulosidad`](EstiloSer::angulosidad) (hombros anchos/
40//!   cadera angosta = triángulo).
41//! - **Valle de inquietud** (Mori; Realism Inconsistency Theory, MacDorman): el valle lo
42//!   dispara **mezclar fidelidades** (rasgos en distintos niveles de realismo), no la
43//!   caricatura. Ojos grandes sobre cara FOTORREAL = lo más siniestro. Por eso el realismo se
44//!   mantiene **uniforme y bajo** y la foto SÓLO regula color (piel/ropa), nunca inyecta
45//!   geometría/textura fotográfica.
46//!
47//! ## Gestos
48//!
49//! [`humanoide_voxel`] recibe una [`Pose`](crate::Pose) (los ángulos de articulación del
50//! catálogo de [`Clip`](crate::Clip)) y la aplica a las cápsulas de miembros → el ser
51//! redondeado saluda / señala / camina con la misma expresividad que el [`Actor`], sin
52//! cajas. Es puro y determinista (testeable sin GPU).
53
54use llimphi_3d::glam::Vec3;
55use llimphi_3d::VoxelGrid;
56use serde::{Deserialize, Serialize};
57
58use crate::actor::{Age, Pose};
59
60/// **Estilo de un ser** — el juego de perillas de diseño (ver el módulo para las fuentes).
61/// Todas tienen un default con carga semántica; los presets ([`por_edad`](Self::por_edad),
62/// [`amable`](Self::amable), [`robusto`](Self::robusto), [`agil`](Self::agil)) los combinan.
63#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
64pub struct EstiloSer {
65    /// **Cabezas de alto** (canon de proporciones). Chibi ≈ 2–4, cartoon ≈ 4–6, realista
66    /// ≈ 7,5. Menos = más tierno / más lejos del valle. La cabeza se dimensiona como
67    /// `altura / cabezas`.
68    pub cabezas: f32,
69    /// **Redondez** `[0,1]` (shape language, eje círculo↔cuadrado). `1` = elipsoides casi
70    /// esféricos + bisel máximo (amable/suave); `0` = ejes desiguales y menos bisel (más
71    /// bloque, estable/fiable). Controla también el detalle sub-voxel horneado.
72    pub redondez: f32,
73    /// **Angulosidad** `[0,1]` (shape language, sesgo triángulo): hombros anchos + cadera
74    /// angosta = silueta dinámica/atlética. `0` = cuerpo parejo (más infantil/tierno).
75    pub angulosidad: f32,
76    /// **Tamaño de ojo** como fracción del radio de la cabeza (Kindchenschema: grande =
77    /// tierno). Chibi ≈ 0,26; adulto estilizado ≈ 0,18.
78    pub ojo_tam: f32,
79    /// **Altura del ojo** en la cara, fracción del radio de cabeza desde el centro (positivo =
80    /// arriba). El lever de ternura MÁS robusto (estudio morfométrico 2022): ojos **por debajo**
81    /// de la mitad (**negativo**) agrandan la frente y suben el appeal. Adulto estilizado ≈ 0
82    /// (línea media, canon Loomis); chibi ≈ −0,2..−0,3 (tercio inferior de la cara). Positivo
83    /// (ojos altos) = más adulto/serio.
84    pub ojo_altura: f32,
85    /// **Cráneo**: modulador LEVE de la altura de la cabeza. La literatura reciente advierte
86    /// que la cara tierna es **redonda/ancha, nunca alargada**, y que la frente se logra
87    /// bajando los ojos (no estirando la cúpula) → este campo casi no estira en Y (clamp a
88    /// ~±6 %) y se conserva sólo por compatibilidad/afinado. `1` = cabeza redonda.
89    pub craneo: f32,
90    /// **Mejilla**: ensancha la mitad inferior de la cabeza (cara redonda de bebé). `1` = sin
91    /// ensanche.
92    pub mejilla: f32,
93    /// **Rechonchez de miembros**: radio de brazos/piernas como fracción de la altura
94    /// (chibi ≈ 0,075; esbelto ≈ 0,05). Miembros gorditos = más tierno.
95    pub extremidad: f32,
96    /// **Panza**: redondez del torso (semieje Y/X). `>1` = barriga/pecho abombado (tierno);
97    /// `1` = torso parejo.
98    pub panza: f32,
99}
100
101impl Default for EstiloSer {
102    /// El default es un **chibi amable** (el look probado): ~2,6 cabezas, redondo, ojos
103    /// grandes a la altura media, cráneo abombado. Reproduce el personaje del `demo_elimon`.
104    fn default() -> Self {
105        Self {
106            cabezas: 2.6,
107            redondez: 1.0,
108            angulosidad: 0.0,
109            ojo_tam: 0.26,
110            ojo_altura: -0.20,
111            craneo: 1.0,
112            mejilla: 1.08,
113            extremidad: 0.075,
114            panza: 1.05,
115        }
116    }
117}
118
119impl EstiloSer {
120    /// Estilo por **edad**: el canon de cabezas y el baby-schema varían con el estadio.
121    /// Bebé = 2 cabezas, ojos enormes y bajos, cráneo grande (máxima ternura); adulto
122    /// estilizado = 4,5 cabezas, ojos moderados a la media (NO 7,5 realista: se busca appeal
123    /// y anti-valle). Escala monótona en el medio.
124    pub fn por_edad(age: Age) -> Self {
125        // Escala del "dial de ternura" (mature→cute) coordinada con las cabezas: menos
126        // cabezas ⇒ ojos más grandes y MÁS BAJOS (tercio inferior), cara más ancha, miembros
127        // más gordos. Adulto estilizado = ojos a la línea media (0), NO 7,5 realista (appeal +
128        // anti-valle). (cabezas, ojo_tam, ojo_altura, craneo, mejilla, extremidad, panza)
129        let (cab, ot, oa, cr, mj, ex, pz) = match age {
130            Age::Baby => (2.0, 0.32, -0.32, 1.0, 1.16, 0.092, 1.22),
131            Age::Child => (3.4, 0.28, -0.24, 1.0, 1.10, 0.080, 1.14),
132            Age::Teen => (4.2, 0.22, -0.12, 1.0, 1.04, 0.066, 1.06),
133            Age::Adult => (4.5, 0.19, -0.04, 1.0, 1.00, 0.060, 1.04),
134            Age::Elder => (4.3, 0.19, -0.05, 1.0, 1.02, 0.062, 1.06),
135        };
136        Self {
137            cabezas: cab,
138            redondez: 1.0,
139            angulosidad: 0.0,
140            ojo_tam: ot,
141            ojo_altura: oa,
142            craneo: cr,
143            mejilla: mj,
144            extremidad: ex,
145            panza: pz,
146        }
147    }
148
149    /// **Arquetipo redondo** (shape language: círculo → amable/suave). Chibi cabezón,
150    /// máxima redondez, ojos grandes: el default reforzado.
151    pub fn amable() -> Self {
152        Self::default()
153    }
154
155    /// **Arquetipo robusto** (shape language: cuadrado → estable/fiable). Menos redondez
156    /// (más bloque), cuerpo ancho y parejo, ojos más chicos, algo más de cabezas.
157    pub fn robusto() -> Self {
158        Self {
159            cabezas: 3.6,
160            redondez: 0.45,
161            angulosidad: 0.15,
162            ojo_tam: 0.19,
163            ojo_altura: -0.02,
164            craneo: 1.0,
165            mejilla: 1.12,
166            extremidad: 0.085,
167            panza: 1.02,
168        }
169    }
170
171    /// **Arquetipo ágil** (shape language: triángulo → dinámico/atlético). Más alto, hombros
172    /// anchos y cadera angosta (angulosidad alta), miembros más finos.
173    pub fn agil() -> Self {
174        Self {
175            cabezas: 4.6,
176            redondez: 0.7,
177            angulosidad: 0.7,
178            ojo_tam: 0.19,
179            ojo_altura: 0.02,
180            craneo: 1.0,
181            mejilla: 0.98,
182            extremidad: 0.055,
183            panza: 0.98,
184        }
185    }
186
187    /// Detalle sub-voxel `(rugosidad, bisel)` horneado por voxel: el bisel sube con la
188    /// redondez (silueta lisa), la rugosidad es un grano de piel constante suave. Es la
189    /// gramática de campo del ser vivo — misma que el terreno, con más redondeo.
190    pub fn detalle(&self) -> (f32, f32) {
191        let bisel = (0.35 + 0.65 * self.redondez.clamp(0.0, 1.0)).clamp(0.0, 1.0);
192        // Grano de piel MUY leve: de cerca (el objetivo del plan) una rugosidad fuerte se
193        // ve como escamas/beading. Suficiente para no quedar plástico plano, no más.
194        (0.06, bisel)
195    }
196}
197
198/// Colores de un ser (`[r,g,b]` en `[0,1]`, como [`Actor`](crate::Actor)): los que la foto
199/// regula (piel/remera/pantalón) + el pelo (derivado o fijo).
200#[derive(Debug, Clone, Copy)]
201pub struct Colores {
202    pub skin: [f32; 3],
203    pub shirt: [f32; 3],
204    pub pants: [f32; 3],
205    pub hair: [f32; 3],
206}
207
208impl Colores {
209    /// Colores desde un [`CharSpec`](crate::CharSpec)-like (piel/remera/pantalón), con un
210    /// pelo derivado (marrón oscuro tintado hacia la piel) para que combine. Usa
211    /// [`con_pelo`](Self::con_pelo) si tienes el color de pelo REAL (de la foto).
212    pub fn nuevos(skin: [f32; 3], shirt: [f32; 3], pants: [f32; 3]) -> Self {
213        // Pelo: oscuro cálido, apenas influido por el tono de piel (combina con retratos).
214        let hair = [
215            (0.18 + skin[0] * 0.12).min(1.0),
216            (0.13 + skin[1] * 0.10).min(1.0),
217            (0.12 + skin[2] * 0.08).min(1.0),
218        ];
219        Self { skin, shirt, pants, hair }
220    }
221
222    /// Colores con **pelo explícito** (p.ej. muestreado FIEL de la foto), no derivado.
223    pub fn con_pelo(skin: [f32; 3], shirt: [f32; 3], pants: [f32; 3], hair: [f32; 3]) -> Self {
224        Self { skin, shirt, pants, hair }
225    }
226}
227
228#[inline]
229fn u8c(c: [f32; 3]) -> [u8; 3] {
230    [
231        (c[0].clamp(0.0, 1.0) * 255.0) as u8,
232        (c[1].clamp(0.0, 1.0) * 255.0) as u8,
233        (c[2].clamp(0.0, 1.0) * 255.0) as u8,
234    ]
235}
236
237/// Rota `v` alrededor del eje X por `a` rad (plano Y-Z; adelante = +Z).
238#[inline]
239fn rot_x(v: Vec3, a: f32) -> Vec3 {
240    let (s, c) = a.sin_cos();
241    Vec3::new(v.x, v.y * c - v.z * s, v.y * s + v.z * c)
242}
243
244/// Rota `v` alrededor del eje Z por `a` rad (plano X-Y; abrir brazo al costado).
245#[inline]
246fn rot_z(v: Vec3, a: f32) -> Vec3 {
247    let (s, c) = a.sin_cos();
248    Vec3::new(v.x * c - v.y * s, v.x * s + v.y * c, v.z)
249}
250
251/// **Construye el humanoide voxel** de `altura_vox` voxels de alto (referencia), con el
252/// `estilo` dado, en la `pose` dada (gestos), coloreado por `col`. Pies en `y≈0`, centrado
253/// en X/Z, mirando a `+Z`. Hornea el detalle sub-voxel por voxel (bisel según la redondez).
254/// Puro y determinista.
255pub fn humanoide_voxel(estilo: &EstiloSer, pose: &Pose, altura_vox: u32, col: Colores) -> VoxelGrid {
256    let h = (altura_vox.max(20)) as f32;
257    let cabezas = estilo.cabezas.clamp(1.6, 8.0);
258
259    // --- Proporciones derivadas del canon de cabezas + baby-schema ---
260    let head_r = h / (2.0 * cabezas); // radio de cabeza = ½ (altura / cabezas)
261    // La cabeza tierna es REDONDA/ancha, nunca alargada (lit. 2022): el cráneo casi no estira
262    // en Y (±6 %) — la frente grande la da bajar los ojos, no una cúpula alta.
263    let head_ry = head_r * (1.0 + 0.2 * (estilo.craneo - 1.0)).clamp(0.94, 1.06);
264    // La cabeza corona la figura; su tope roza `h`.
265    let head_cy = h - head_ry * 1.02;
266    let neck = head_r * 0.14;
267    let shoulder_y = (head_cy - head_ry - neck).max(head_r);
268
269    // Torso y piernas se reparten [0, shoulder_y]: chibi = piernas cortas. La fracción de
270    // pierna sube con `cabezas` (más alto = piernas proporcionalmente más largas).
271    let leg_frac = (0.28 + 0.055 * (cabezas - 2.0)).clamp(0.28, 0.52);
272    let hip_y = shoulder_y * leg_frac;
273    let torso_cy = (hip_y + shoulder_y) * 0.5;
274    let torso_ry = (shoulder_y - hip_y) * 0.5;
275    // Ancho del torso: la cabeza debe DOMINAR (head:torso ≈ 1:0,8, cabeza mayor). Base por
276    // altura, modulada por redondez (redondo = más parejo).
277    let torso_rx = h * (0.13 + 0.05 * estilo.redondez);
278    // Profundidad casi igual al ancho (torso REDONDO, no una losa chata): un torso poco
279    // profundo presenta una cara frontal casi plana donde la normal varía lentísimo → el
280    // escalonado residual del gradiente se ve como anillos anchos. Redondearlo aprieta los
281    // pasos y da una panza de peluche.
282    let torso_rz = torso_rx * 0.92;
283
284    let limb_r = h * estilo.extremidad;
285
286    // Shape language triángulo: hombros anchos, cadera angosta con la angulosidad. Los hombros
287    // se anclan JUSTO AFUERA del torso (≥ `torso_rx`) para que los brazos cuelguen al costado con
288    // una junta limpia — hundirlos DENTRO del torso (como antes, 0,72·torso_rx) enterraba la
289    // cápsula del brazo y la normal ancha escalonaba esa junta sepultada → anillos en la panza.
290    let ang = estilo.angulosidad.clamp(0.0, 1.0);
291    let shoulder_x = torso_rx + limb_r * (0.35 + 0.9 * ang);
292    let hip_x = torso_rx * (0.55 - 0.18 * ang);
293    let leg_len = hip_y; // el pie llega a ~0
294    let arm_len = (shoulder_y - hip_y) * 1.02;
295
296    // --- Grid: bbox de la figura + margen (para que el bisel no toque el borde) ---
297    let half_w = (shoulder_x + limb_r * 1.4).max(torso_rx).max(head_r * estilo.mejilla) + 2.0;
298    let half_d = (torso_rz + limb_r).max(head_r) + arm_len * 0.4 + 2.0;
299    let gw = (half_w * 2.0).ceil() as u32 + 1;
300    let gd = (half_d * 2.0).ceil() as u32 + 1;
301    let gh = (h + head_ry * 0.3).ceil() as u32 + 2;
302    let mut g = VoxelGrid::new([gw, gh, gd]);
303    let cx = gw as f32 * 0.5;
304    let cz = gd as f32 * 0.5;
305    let base = Vec3::new(cx, 0.0, cz);
306
307    // Detalle sub-voxel del cuerpo (bisel por redondez) — pincel para todo lo que estampe.
308    let (rug, bisel) = estilo.detalle();
309    g.set_detail_brush(rug, bisel);
310
311    let skin = u8c(col.skin);
312    let shirt = u8c(col.shirt);
313    let pants = u8c(col.pants);
314    let hair = u8c(col.hair);
315
316    // Transform del cuerpo superior (torso/cabeza/brazos): rebote `bob` + inclinación `lean`
317    // alrededor de la cadera (pivote en `hip_y`). Las piernas quedan plantadas.
318    let bob = pose.bob * h * 0.5;
319    let upper = |local_y: f32, off: Vec3| -> Vec3 {
320        // punto (0, local_y, 0)+off, inclinado por lean sobre el pivote de cadera y subido bob
321        let rel = Vec3::new(off.x, local_y - hip_y, off.z);
322        let r = rot_x(rel, pose.lean);
323        base + Vec3::new(r.x, hip_y + r.y + bob, r.z)
324    };
325
326    // --- Piernas: cápsulas rechonchas, articuladas en la cadera (leg swing en X) ---
327    for (sx, swing) in [(-1.0f32, pose.leg_l), (1.0, pose.leg_r)] {
328        let hip = base + Vec3::new(sx * hip_x, hip_y, 0.0);
329        let dir = rot_x(Vec3::new(0.0, -leg_len, 0.0), swing);
330        let foot = hip + dir;
331        capsula(&mut g, hip, foot, limb_r * 1.05, pants);
332    }
333
334    // --- Torso: elipsoide redondo (panza) ---
335    let torso_c = upper(torso_cy, Vec3::ZERO);
336    elipsoide(&mut g, torso_c, Vec3::new(torso_rx, torso_ry * estilo.panza, torso_rz), shirt);
337
338    // --- Brazos: cápsulas al costado, articuladas en el hombro (out en Z, swing en X) ---
339    // Splay base: aun en reposo los brazos abren un poco hacia afuera para NO fundirse con el
340    // torso ni juntar las manos al frente (leía como "manos atadas"). Se suma a la apertura del gesto.
341    let base_out = 0.16;
342    for (sx, swing, out) in [(-1.0f32, pose.arm_l, pose.arm_l_out), (1.0, pose.arm_r, pose.arm_r_out)] {
343        let shoulder = upper(shoulder_y, Vec3::new(sx * shoulder_x, 0.0, 0.0));
344        // dirección local del brazo colgando, rotada: primero swing (X), luego apertura (Z).
345        let mut dir = rot_x(Vec3::new(0.0, -arm_len, 0.0), swing);
346        dir = rot_z(dir, sx * (out + base_out)); // apertura espejada por lado
347        // el brazo también acompaña el lean del cuerpo superior
348        dir = rot_x(dir, pose.lean);
349        let hand = shoulder + dir;
350        capsula(&mut g, shoulder, hand, limb_r, shirt);
351        // Mano: esferita de piel en la punta (apenas mayor que el brazo, no un guante gigante).
352        elipsoide(&mut g, hand, Vec3::splat(limb_r * 0.98), skin);
353    }
354
355    // --- Cabeza + cara (baby-schema) ---
356    let head_c = upper(head_cy, Vec3::ZERO);
357    // Cabeza: elipsoide con cúpula (craneo) arriba y mejilla abajo. La construimos como un
358    // elipsoide con radios asimétricos por hemisferio via `elipsoide_mejilla`.
359    let head_rx = head_r * estilo.mejilla;
360    elipsoide_cabeza(&mut g, head_c, head_r, head_rx, head_ry, skin);
361
362    // **Zona de bigote/barba = coloración PLANA, sin sustancia** (no una boca protuberante,
363    // que parecía un pico de pato): re-tiñe la piel de la mitad inferior FRONTAL de la cara a
364    // un tono apenas más apagado/oscuro (sombra de barba). No agrega geometría; se aplica
365    // ANTES del pelo/ojos para no pisarlos. Sutil a propósito (que no lea como máscara).
366    let eye_y0 = head_c.y + estilo.ojo_altura * head_r;
367    tint_barba(&mut g, head_c, head_rx, head_ry, head_r, eye_y0, skin);
368
369    // Pelo: **casquete alto** sobre la corona, corrido hacia atrás (−Z) para dejar la FRENTE
370    // y las mejillas de piel a la vista (la frente grande es el rasgo tierno). No baja hasta
371    // los ojos: recorte bien por encima → cara abierta, no un antifaz enmarcado.
372    let eye_y = head_c.y + estilo.ojo_altura * head_r;
373    elipsoide_min_y(
374        &mut g,
375        head_c + Vec3::new(0.0, head_ry * 0.40, -head_r * 0.14),
376        Vec3::new(head_rx * 1.02, head_ry * 0.80, head_r * 1.0),
377        hair,
378        eye_y + head_r * 0.58,
379    );
380
381    // Ojos: **esclera clara + pupila oscura + chispa** (NO un óvalo negro suelto, que a esta
382    // resolución + isosuperficie leía como un HUECO/cuenca vacía = cara de monstruo). El blanco
383    // domina y "abre" el ojo; la pupila oscura, más chica y al frente-centro, da la mirada; una
384    // chispa arriba-interior le pone vida. Anchos > altos (ojo infantil), BIEN separados, con piel
385    // en medio y alrededor (no antifaz). A/por debajo de la media (`ojo_altura`). `head_pitch` tilta.
386    let er = head_r * estilo.ojo_tam;
387    let ex = head_rx * 0.5;
388    let zf = head_r * 0.80;
389    let eoff = eye_y - head_c.y;
390    for s in [-1.0f32, 1.0] {
391        let eye = head_c + rot_x(Vec3::new(s * ex, eoff, zf), pose.head_pitch);
392        // Esclera: óvalo claro, ancho, apenas saliente al frente para que no se hunda.
393        elipsoide(&mut g, eye, Vec3::new(er * 1.06, er * 0.86, er * 0.62), [240, 241, 244]);
394        // Pupila/iris: oscura, ~mitad, corrida al FRENTE (+Z) y apenas hacia adentro (mirada
395        // convergente amable) y abajo. Va DESPUÉS de la esclera para pisarla en el centro.
396        let pupil = head_c + rot_x(Vec3::new(s * ex - s * er * 0.12, eoff - er * 0.08, zf + er * 0.28), pose.head_pitch);
397        elipsoide(&mut g, pupil, Vec3::new(er * 0.52, er * 0.6, er * 0.5), [40, 32, 46]);
398        // Chispa (arriba-interior de la pupila) — grande para que lea de lejos.
399        let spark = head_c + rot_x(Vec3::new(s * ex - s * er * 0.28, eoff + er * 0.34, zf + er * 0.5), pose.head_pitch);
400        elipsoide(&mut g, spark, Vec3::new(er * 0.3, er * 0.3, er * 0.34), [250, 251, 253]);
401    }
402
403    // **Boca = sonrisa como tint PLANO** (sin geometría protuberante: una boca de color en
404    // relieve leía como pico de pato). Re-tiñe una banda corta, curvada hacia ARRIBA (feliz),
405    // de la piel frontal-baja de la cara a un tono cálido apagado. Pequeña y centrada. Va
406    // después de barba/ojos para no pisarse.
407    let boca_y = eye_y - head_r * 0.44;
408    sonrisa(&mut g, head_c, head_rx, head_r, boca_y, skin);
409
410    g.clear_detail_brush();
411    g.reset_dirty();
412    g
413}
414
415/// Rellena un elipsoide de semiejes `r` centrado en `c` con `color`.
416fn elipsoide(g: &mut VoxelGrid, c: Vec3, r: Vec3, color: [u8; 3]) {
417    elipsoide_min_y(g, c, r, color, f32::NEG_INFINITY);
418}
419
420/// Elipsoide recortado por abajo (`y >= y_min`) — para casquetes de pelo.
421fn elipsoide_min_y(g: &mut VoxelGrid, c: Vec3, r: Vec3, color: [u8; 3], y_min: f32) {
422    let [dx, dy, dz] = g.dim();
423    let x0 = (c.x - r.x).floor().max(0.0) as u32;
424    let x1 = (c.x + r.x).ceil().min(dx as f32) as u32;
425    let y0 = (c.y - r.y).floor().max(0.0).max(y_min.max(0.0)) as u32;
426    let y1 = (c.y + r.y).ceil().min(dy as f32) as u32;
427    let z0 = (c.z - r.z).floor().max(0.0) as u32;
428    let z1 = (c.z + r.z).ceil().min(dz as f32) as u32;
429    for x in x0..x1 {
430        for y in y0..y1 {
431            if (y as f32) < y_min {
432                continue;
433            }
434            for z in z0..z1 {
435                let d = Vec3::new(
436                    (x as f32 + 0.5 - c.x) / r.x,
437                    (y as f32 + 0.5 - c.y) / r.y,
438                    (z as f32 + 0.5 - c.z) / r.z,
439                );
440                if d.length_squared() <= 1.0 {
441                    g.set(x, y, z, color);
442                }
443            }
444        }
445    }
446}
447
448/// Cabeza: elipsoide con **cúpula arriba** (semieje Y superior) y **mejillas** (radio X
449/// pleno abajo, afinado arriba) — la forma del baby-schema. `rx` ya lleva el ensanche de
450/// mejilla; arriba de la mitad se afina hacia `head_r` para no quedar cuadrada.
451fn elipsoide_cabeza(g: &mut VoxelGrid, c: Vec3, head_r: f32, rx: f32, ry: f32, color: [u8; 3]) {
452    let [dx, dy, dz] = g.dim();
453    let x0 = (c.x - rx).floor().max(0.0) as u32;
454    let x1 = (c.x + rx).ceil().min(dx as f32) as u32;
455    let y0 = (c.y - ry).floor().max(0.0) as u32;
456    let y1 = (c.y + ry).ceil().min(dy as f32) as u32;
457    let z0 = (c.z - rx).floor().max(0.0) as u32;
458    let z1 = (c.z + rx).ceil().min(dz as f32) as u32;
459    for y in y0..y1 {
460        let ty = (y as f32 + 0.5 - c.y) / ry; // -1..1
461        // Radio horizontal por altura: mejilla plena abajo (ty<0), afina hacia head_r arriba.
462        let rh = if ty >= 0.0 { rx + (head_r - rx) * ty } else { rx };
463        let rz = rh * 0.96;
464        for x in x0..x1 {
465            for z in z0..z1 {
466                let dxx = (x as f32 + 0.5 - c.x) / rh;
467                let dzz = (z as f32 + 0.5 - c.z) / rz;
468                if dxx * dxx + ty * ty + dzz * dzz <= 1.0 {
469                    g.set(x, y, z, color);
470                }
471            }
472        }
473    }
474}
475
476/// **Sombra de barba/bigote como tint PLANO** (sin geometría): re-tiñe la piel de la mitad
477/// inferior FRONTAL de la cara a un tono apenas más oscuro/apagado. Sólo toca voxels que HOY
478/// son piel (`== skin`), así no pisa ojos/pelo (que se estampan después). Sutil a propósito.
479fn tint_barba(g: &mut VoxelGrid, c: Vec3, rx: f32, ry: f32, head_r: f32, eye_y: f32, skin: [u8; 3]) {
480    // Barba = piel un 6% más oscura, apenas más fría (sombra tenue, no una máscara: fuerte
481    // ensuciaba la cara y la volvía adusta/monstruosa).
482    let beard = [
483        (skin[0] as f32 * 0.94) as u8,
484        (skin[1] as f32 * 0.93) as u8,
485        (skin[2] as f32 * 0.94) as u8,
486    ];
487    let [dx, dy, dz] = g.dim();
488    // De la barbilla hasta un poco por debajo de los ojos (zona bigote+barba+mentón).
489    let y0 = (c.y - ry).floor().max(0.0) as u32;
490    let y1 = (eye_y - head_r * 0.30).floor().max(0.0).min(dy as f32) as u32;
491    let x0 = (c.x - rx).floor().max(0.0) as u32;
492    let x1 = (c.x + rx).ceil().min(dx as f32) as u32;
493    // Sólo el frente de la cara (+Z), no la nuca/mandíbula lateral.
494    let z0 = (c.z + head_r * 0.28).floor().max(0.0) as u32;
495    let z1 = (c.z + head_r * 1.1).ceil().min(dz as f32) as u32;
496    for x in x0..x1 {
497        for y in y0..y1 {
498            for z in z0..z1 {
499                if let Some(px) = g.get(x, y, z) {
500                    if px[3] > 0 && [px[0], px[1], px[2]] == skin {
501                        g.set(x, y, z, beard);
502                    }
503                }
504            }
505        }
506    }
507}
508
509/// **Sonrisa como tint PLANO** (sin geometría): re-tiñe una banda corta de piel FRONTAL a la
510/// altura `boca_y`, curvada hacia arriba en los extremos (feliz), a un tono cálido apagado
511/// (labios/sombra de boca, no rojo saturado que a esta resolución grita "pico de pato"). Sólo
512/// pisa voxels que HOY son piel, así no borra ojos/pelo. Centrada en X, en el hemisferio +Z.
513fn sonrisa(g: &mut VoxelGrid, c: Vec3, rx: f32, head_r: f32, boca_y: f32, skin: [u8; 3]) {
514    // Boca = piel bastante más oscura y algo más cálida (rojiza), pero NO roja pura.
515    let mouth = [
516        (skin[0] as f32 * 0.72 + 22.0).min(255.0) as u8,
517        (skin[1] as f32 * 0.55).min(255.0) as u8,
518        (skin[2] as f32 * 0.55).min(255.0) as u8,
519    ];
520    let [dx, dy, dz] = g.dim();
521    let half_w = rx * 0.42; // media boca
522    let thick = head_r * 0.12; // grosor vertical de la línea
523    let curve = head_r * 0.22; // cuánto suben los extremos (sonrisa)
524    let x0 = (c.x - half_w).floor().max(0.0) as u32;
525    let x1 = (c.x + half_w).ceil().min(dx as f32) as u32;
526    // Sólo el frente de la cara (+Z).
527    let z0 = (c.z + head_r * 0.5).floor().max(0.0) as u32;
528    let z1 = (c.z + head_r * 1.1).ceil().min(dz as f32) as u32;
529    for x in x0..x1 {
530        // Altura de la línea de sonrisa en esta X: sube hacia los extremos (parábola).
531        let t = ((x as f32 + 0.5 - c.x) / half_w).clamp(-1.0, 1.0);
532        let ly = boca_y + curve * t * t;
533        let y0 = (ly - thick * 0.5).floor().max(0.0) as u32;
534        let y1 = (ly + thick * 0.5).ceil().min(dy as f32) as u32;
535        for y in y0..y1 {
536            for z in z0..z1 {
537                if let Some(px) = g.get(x, y, z) {
538                    if px[3] > 0 && [px[0], px[1], px[2]] == skin {
539                        g.set(x, y, z, mouth);
540                    }
541                }
542            }
543        }
544    }
545}
546
547/// Cápsula (cilindro con tapas esféricas) de radio `r` entre los puntos `a` y `b`.
548fn capsula(g: &mut VoxelGrid, a: Vec3, b: Vec3, r: f32, color: [u8; 3]) {
549    let [dx, dy, dz] = g.dim();
550    let lo = a.min(b) - Vec3::splat(r + 1.0);
551    let hi = a.max(b) + Vec3::splat(r + 1.0);
552    let x0 = lo.x.floor().max(0.0) as u32;
553    let x1 = hi.x.ceil().min(dx as f32) as u32;
554    let y0 = lo.y.floor().max(0.0) as u32;
555    let y1 = hi.y.ceil().min(dy as f32) as u32;
556    let z0 = lo.z.floor().max(0.0) as u32;
557    let z1 = hi.z.ceil().min(dz as f32) as u32;
558    let ab = b - a;
559    let ab2 = ab.length_squared().max(1e-6);
560    let r2 = r * r;
561    for x in x0..x1 {
562        for y in y0..y1 {
563            for z in z0..z1 {
564                let p = Vec3::new(x as f32 + 0.5, y as f32 + 0.5, z as f32 + 0.5);
565                let t = ((p - a).dot(ab) / ab2).clamp(0.0, 1.0);
566                let closest = a + ab * t;
567                if (p - closest).length_squared() <= r2 {
568                    g.set(x, y, z, color);
569                }
570            }
571        }
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578
579    fn solids(g: &VoxelGrid) -> usize {
580        let [w, h, d] = g.dim();
581        let mut n = 0;
582        for x in 0..w {
583            for y in 0..h {
584                for z in 0..d {
585                    if g.is_solid(x as i32, y as i32, z as i32) {
586                        n += 1;
587                    }
588                }
589            }
590        }
591        n
592    }
593
594    fn col() -> Colores {
595        Colores::nuevos([0.85, 0.68, 0.54], [0.2, 0.6, 0.55], [0.18, 0.22, 0.34])
596    }
597
598    #[test]
599    fn construye_un_ser_con_volumen() {
600        let g = humanoide_voxel(&EstiloSer::default(), &Pose::default(), 46, col());
601        assert!(solids(&g) > 3000, "el ser tiene volumen: {}", solids(&g));
602    }
603
604    #[test]
605    fn pies_en_el_piso() {
606        // El voxel sólido más bajo está cerca de y=0 (pies apoyados).
607        let g = humanoide_voxel(&EstiloSer::default(), &Pose::default(), 46, col());
608        let [w, h, d] = g.dim();
609        let mut ymin = h;
610        for x in 0..w {
611            for y in 0..h {
612                for z in 0..d {
613                    if g.is_solid(x as i32, y as i32, z as i32) {
614                        ymin = ymin.min(y);
615                    }
616                }
617            }
618        }
619        assert!(ymin <= 2, "pies cerca del piso: ymin={ymin}");
620    }
621
622    #[test]
623    fn el_bebe_es_mas_cabezon_que_el_adulto() {
624        // Menos cabezas ⇒ la cabeza ocupa una fracción mayor de la altura.
625        let baby = EstiloSer::por_edad(Age::Baby);
626        let adult = EstiloSer::por_edad(Age::Adult);
627        assert!(baby.cabezas < adult.cabezas, "bebé menos cabezas");
628        // Radio de cabeza relativo a la altura.
629        let hr = |e: &EstiloSer| 1.0 / (2.0 * e.cabezas);
630        assert!(hr(&baby) > hr(&adult) + 0.05, "bebé cabezón");
631    }
632
633    #[test]
634    fn los_gestos_cambian_la_silueta() {
635        // Festejar (brazos arriba) ensancha/eleva la ocupación respecto a idle.
636        let idle = humanoide_voxel(&EstiloSer::default(), &Pose::default(), 46, col());
637        let cheer = crate::Clip::Cheer.pose(1.2);
638        let festeja = humanoide_voxel(&EstiloSer::default(), &cheer, 46, col());
639        // Ocupación en la franja alta (por encima de la cabeza-hombro) crece al levantar brazos.
640        let alto = |g: &VoxelGrid| {
641            let [w, h, d] = g.dim();
642            let y0 = h * 3 / 4;
643            let mut n = 0;
644            for x in 0..w {
645                for y in y0..h {
646                    for z in 0..d {
647                        if g.is_solid(x as i32, y as i32, z as i32) {
648                            n += 1;
649                        }
650                    }
651                }
652            }
653            n
654        };
655        assert!(alto(&festeja) > alto(&idle), "festejar sube volumen alto: {} vs {}", alto(&festeja), alto(&idle));
656    }
657
658    #[test]
659    fn hornea_detalle_por_voxel() {
660        // Con redondez alta el bisel horneado es alto (silueta lisa).
661        let g = humanoide_voxel(&EstiloSer::default(), &Pose::default(), 40, col());
662        let [w, h, d] = g.dim();
663        let mut con_detalle = 0;
664        for x in 0..w {
665            for y in 0..h {
666                for z in 0..d {
667                    if g.is_solid(x as i32, y as i32, z as i32) {
668                        if let Some(det) = g.detail_at(x, y, z) {
669                            if det != [0, 0] {
670                                con_detalle += 1;
671                            }
672                        }
673                    }
674                }
675            }
676        }
677        assert!(con_detalle > 1000, "el cuerpo lleva detalle por voxel: {con_detalle}");
678    }
679
680    #[test]
681    fn presets_de_arquetipo_difieren() {
682        assert!(EstiloSer::amable().redondez > EstiloSer::robusto().redondez);
683        assert!(EstiloSer::agil().angulosidad > EstiloSer::amable().angulosidad);
684        assert!(EstiloSer::agil().cabezas > EstiloSer::amable().cabezas);
685    }
686}