tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ main llimphi-voxel/src/humano.rs Rust · 694 lines · 31.9 KB History Who touched what raw

chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos b3f12e136 · 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]
239pub(crate) fn 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 Y por `a` rad (plano X-Z; girar la cabeza). Misma
245/// convención que el `yaw` del ser: `0` mira a `+Z`, positivo gira hacia `+X`.
246#[inline]
247pub(crate) fn rot_y(v: Vec3, a: f32) -> Vec3 {
248    let (s, c) = a.sin_cos();
249    Vec3::new(v.x * c + v.z * s, v.y, -v.x * s + v.z * c)
250}
251
252/// Rota `v` alrededor del eje Z por `a` rad (plano X-Y; abrir brazo al costado).
253#[inline]
254pub(crate) fn rot_z(v: Vec3, a: f32) -> Vec3 {
255    let (s, c) = a.sin_cos();
256    Vec3::new(v.x * c - v.y * s, v.x * s + v.y * c, v.z)
257}
258
259/// **Construye el humanoide voxel** de `altura_vox` voxels de alto (referencia), con el
260/// `estilo` dado, en la `pose` dada (gestos), coloreado por `col`. Pies en `y≈0`, centrado
261/// en X/Z, mirando a `+Z`. Hornea el detalle sub-voxel por voxel (bisel según la redondez).
262/// Puro y determinista.
263pub fn humanoide_voxel(estilo: &EstiloSer, pose: &Pose, altura_vox: u32, col: Colores) -> VoxelGrid {
264    let h = (altura_vox.max(20)) as f32;
265    let cabezas = estilo.cabezas.clamp(1.6, 8.0);
266
267    // --- Proporciones derivadas del canon de cabezas + baby-schema ---
268    let head_r = h / (2.0 * cabezas); // radio de cabeza = ½ (altura / cabezas)
269    // La cabeza tierna es REDONDA/ancha, nunca alargada (lit. 2022): el cráneo casi no estira
270    // en Y (±6 %) — la frente grande la da bajar los ojos, no una cúpula alta.
271    let head_ry = head_r * (1.0 + 0.2 * (estilo.craneo - 1.0)).clamp(0.94, 1.06);
272    // La cabeza corona la figura; su tope roza `h`.
273    let head_cy = h - head_ry * 1.02;
274    let neck = head_r * 0.14;
275    let shoulder_y = (head_cy - head_ry - neck).max(head_r);
276
277    // Torso y piernas se reparten [0, shoulder_y]: chibi = piernas cortas. La fracción de
278    // pierna sube con `cabezas` (más alto = piernas proporcionalmente más largas).
279    let leg_frac = (0.28 + 0.055 * (cabezas - 2.0)).clamp(0.28, 0.52);
280    let hip_y = shoulder_y * leg_frac;
281    let torso_cy = (hip_y + shoulder_y) * 0.5;
282    let torso_ry = (shoulder_y - hip_y) * 0.5;
283    // Ancho del torso: la cabeza debe DOMINAR (head:torso ≈ 1:0,8, cabeza mayor). Base por
284    // altura, modulada por redondez (redondo = más parejo).
285    let torso_rx = h * (0.13 + 0.05 * estilo.redondez);
286    // Profundidad casi igual al ancho (torso REDONDO, no una losa chata): un torso poco
287    // profundo presenta una cara frontal casi plana donde la normal varía lentísimo → el
288    // escalonado residual del gradiente se ve como anillos anchos. Redondearlo aprieta los
289    // pasos y da una panza de peluche.
290    let torso_rz = torso_rx * 0.92;
291
292    let limb_r = h * estilo.extremidad;
293
294    // Shape language triángulo: hombros anchos, cadera angosta con la angulosidad. Los hombros
295    // se anclan JUSTO AFUERA del torso (≥ `torso_rx`) para que los brazos cuelguen al costado con
296    // una junta limpia — hundirlos DENTRO del torso (como antes, 0,72·torso_rx) enterraba la
297    // cápsula del brazo y la normal ancha escalonaba esa junta sepultada → anillos en la panza.
298    let ang = estilo.angulosidad.clamp(0.0, 1.0);
299    let shoulder_x = torso_rx + limb_r * (0.35 + 0.9 * ang);
300    let hip_x = torso_rx * (0.55 - 0.18 * ang);
301    let leg_len = hip_y; // el pie llega a ~0
302    let arm_len = (shoulder_y - hip_y) * 1.02;
303
304    // --- Grid: bbox de la figura + margen (para que el bisel no toque el borde) ---
305    let half_w = (shoulder_x + limb_r * 1.4).max(torso_rx).max(head_r * estilo.mejilla) + 2.0;
306    let half_d = (torso_rz + limb_r).max(head_r) + arm_len * 0.4 + 2.0;
307    let gw = (half_w * 2.0).ceil() as u32 + 1;
308    let gd = (half_d * 2.0).ceil() as u32 + 1;
309    let gh = (h + head_ry * 0.3).ceil() as u32 + 2;
310    let mut g = VoxelGrid::new([gw, gh, gd]);
311    let cx = gw as f32 * 0.5;
312    let cz = gd as f32 * 0.5;
313    let base = Vec3::new(cx, 0.0, cz);
314
315    // Detalle sub-voxel del cuerpo (bisel por redondez) — pincel para todo lo que estampe.
316    let (rug, bisel) = estilo.detalle();
317    g.set_detail_brush(rug, bisel);
318
319    let skin = u8c(col.skin);
320    let shirt = u8c(col.shirt);
321    let pants = u8c(col.pants);
322    let hair = u8c(col.hair);
323
324    // Transform del cuerpo superior (torso/cabeza/brazos): rebote `bob` + inclinación `lean`
325    // alrededor de la cadera (pivote en `hip_y`). Las piernas quedan plantadas.
326    let bob = pose.bob * h * 0.5;
327    let upper = |local_y: f32, off: Vec3| -> Vec3 {
328        // punto (0, local_y, 0)+off, inclinado por lean sobre el pivote de cadera y subido bob
329        let rel = Vec3::new(off.x, local_y - hip_y, off.z);
330        let r = rot_x(rel, pose.lean);
331        base + Vec3::new(r.x, hip_y + r.y + bob, r.z)
332    };
333
334    // --- Piernas: cápsulas rechonchas, articuladas en la cadera (leg swing en X) ---
335    for (sx, swing) in [(-1.0f32, pose.leg_l), (1.0, pose.leg_r)] {
336        let hip = base + Vec3::new(sx * hip_x, hip_y, 0.0);
337        let dir = rot_x(Vec3::new(0.0, -leg_len, 0.0), swing);
338        let foot = hip + dir;
339        capsula(&mut g, hip, foot, limb_r * 1.05, pants);
340    }
341
342    // --- Torso: elipsoide redondo (panza) ---
343    let torso_c = upper(torso_cy, Vec3::ZERO);
344    elipsoide(&mut g, torso_c, Vec3::new(torso_rx, torso_ry * estilo.panza, torso_rz), shirt);
345
346    // --- Brazos: cápsulas al costado, articuladas en el hombro (out en Z, swing en X) ---
347    // Splay base: aun en reposo los brazos abren un poco hacia afuera para NO fundirse con el
348    // torso ni juntar las manos al frente (leía como "manos atadas"). Se suma a la apertura del gesto.
349    let base_out = 0.16;
350    for (sx, swing, out) in [(-1.0f32, pose.arm_l, pose.arm_l_out), (1.0, pose.arm_r, pose.arm_r_out)] {
351        let shoulder = upper(shoulder_y, Vec3::new(sx * shoulder_x, 0.0, 0.0));
352        // dirección local del brazo colgando, rotada: primero swing (X), luego apertura (Z).
353        let mut dir = rot_x(Vec3::new(0.0, -arm_len, 0.0), swing);
354        dir = rot_z(dir, sx * (out + base_out)); // apertura espejada por lado
355        // el brazo también acompaña el lean del cuerpo superior
356        dir = rot_x(dir, pose.lean);
357        let hand = shoulder + dir;
358        capsula(&mut g, shoulder, hand, limb_r, shirt);
359        // Mano: esferita de piel en la punta (apenas mayor que el brazo, no un guante gigante).
360        elipsoide(&mut g, hand, Vec3::splat(limb_r * 0.98), skin);
361    }
362
363    // --- Cabeza + cara (baby-schema) ---
364    let head_c = upper(head_cy, Vec3::ZERO);
365    // Cabeza: elipsoide con cúpula (craneo) arriba y mejilla abajo. La construimos como un
366    // elipsoide con radios asimétricos por hemisferio via `elipsoide_mejilla`.
367    let head_rx = head_r * estilo.mejilla;
368    elipsoide_cabeza(&mut g, head_c, head_r, head_rx, head_ry, skin);
369
370    // **Zona de bigote/barba = coloración PLANA, sin sustancia** (no una boca protuberante,
371    // que parecía un pico de pato): re-tiñe la piel de la mitad inferior FRONTAL de la cara a
372    // un tono apenas más apagado/oscuro (sombra de barba). No agrega geometría; se aplica
373    // ANTES del pelo/ojos para no pisarlos. Sutil a propósito (que no lea como máscara).
374    let eye_y0 = head_c.y + estilo.ojo_altura * head_r;
375    tint_barba(&mut g, head_c, head_rx, head_ry, head_r, eye_y0, skin);
376
377    // Pelo: **casquete alto** sobre la corona, corrido hacia atrás (−Z) para dejar la FRENTE
378    // y las mejillas de piel a la vista (la frente grande es el rasgo tierno). No baja hasta
379    // los ojos: recorte bien por encima → cara abierta, no un antifaz enmarcado.
380    let eye_y = head_c.y + estilo.ojo_altura * head_r;
381    elipsoide_min_y(
382        &mut g,
383        head_c + Vec3::new(0.0, head_ry * 0.40, -head_r * 0.14),
384        Vec3::new(head_rx * 1.02, head_ry * 0.80, head_r * 1.0),
385        hair,
386        eye_y + head_r * 0.58,
387    );
388
389    // Ojos: **esclera clara + pupila oscura + chispa** (NO un óvalo negro suelto, que a esta
390    // resolución + isosuperficie leía como un HUECO/cuenca vacía = cara de monstruo). El blanco
391    // domina y "abre" el ojo; la pupila oscura, más chica y al frente-centro, da la mirada; una
392    // chispa arriba-interior le pone vida. Anchos > altos (ojo infantil), BIEN separados, con piel
393    // en medio y alrededor (no antifaz). A/por debajo de la media (`ojo_altura`). `head_pitch` tilta.
394    let er = head_r * estilo.ojo_tam;
395    let ex = head_rx * 0.5;
396    let zf = head_r * 0.80;
397    let eoff = eye_y - head_c.y;
398    for s in [-1.0f32, 1.0] {
399        let eye = head_c + rot_x(Vec3::new(s * ex, eoff, zf), pose.head_pitch);
400        // Esclera: óvalo claro, ancho, apenas saliente al frente para que no se hunda.
401        elipsoide(&mut g, eye, Vec3::new(er * 1.06, er * 0.86, er * 0.62), [240, 241, 244]);
402        // Pupila/iris: oscura, ~mitad, corrida al FRENTE (+Z) y apenas hacia adentro (mirada
403        // convergente amable) y abajo. Va DESPUÉS de la esclera para pisarla en el centro.
404        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);
405        elipsoide(&mut g, pupil, Vec3::new(er * 0.52, er * 0.6, er * 0.5), [40, 32, 46]);
406        // Chispa (arriba-interior de la pupila) — grande para que lea de lejos.
407        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);
408        elipsoide(&mut g, spark, Vec3::new(er * 0.3, er * 0.3, er * 0.34), [250, 251, 253]);
409    }
410
411    // **Boca = sonrisa como tint PLANO** (sin geometría protuberante: una boca de color en
412    // relieve leía como pico de pato). Re-tiñe una banda corta, curvada hacia ARRIBA (feliz),
413    // de la piel frontal-baja de la cara a un tono cálido apagado. Pequeña y centrada. Va
414    // después de barba/ojos para no pisarse.
415    let boca_y = eye_y - head_r * 0.44;
416    sonrisa(&mut g, head_c, head_rx, head_r, boca_y, skin);
417
418    g.clear_detail_brush();
419    g.reset_dirty();
420    g
421}
422
423/// Rellena un elipsoide de semiejes `r` centrado en `c` con `color`.
424fn elipsoide(g: &mut VoxelGrid, c: Vec3, r: Vec3, color: [u8; 3]) {
425    elipsoide_min_y(g, c, r, color, f32::NEG_INFINITY);
426}
427
428/// Elipsoide recortado por abajo (`y >= y_min`) — para casquetes de pelo.
429fn elipsoide_min_y(g: &mut VoxelGrid, c: Vec3, r: Vec3, color: [u8; 3], y_min: f32) {
430    let [dx, dy, dz] = g.dim();
431    let x0 = (c.x - r.x).floor().max(0.0) as u32;
432    let x1 = (c.x + r.x).ceil().min(dx as f32) as u32;
433    let y0 = (c.y - r.y).floor().max(0.0).max(y_min.max(0.0)) as u32;
434    let y1 = (c.y + r.y).ceil().min(dy as f32) as u32;
435    let z0 = (c.z - r.z).floor().max(0.0) as u32;
436    let z1 = (c.z + r.z).ceil().min(dz as f32) as u32;
437    for x in x0..x1 {
438        for y in y0..y1 {
439            if (y as f32) < y_min {
440                continue;
441            }
442            for z in z0..z1 {
443                let d = Vec3::new(
444                    (x as f32 + 0.5 - c.x) / r.x,
445                    (y as f32 + 0.5 - c.y) / r.y,
446                    (z as f32 + 0.5 - c.z) / r.z,
447                );
448                if d.length_squared() <= 1.0 {
449                    g.set(x, y, z, color);
450                }
451            }
452        }
453    }
454}
455
456/// Cabeza: elipsoide con **cúpula arriba** (semieje Y superior) y **mejillas** (radio X
457/// pleno abajo, afinado arriba) — la forma del baby-schema. `rx` ya lleva el ensanche de
458/// mejilla; arriba de la mitad se afina hacia `head_r` para no quedar cuadrada.
459fn elipsoide_cabeza(g: &mut VoxelGrid, c: Vec3, head_r: f32, rx: f32, ry: f32, color: [u8; 3]) {
460    let [dx, dy, dz] = g.dim();
461    let x0 = (c.x - rx).floor().max(0.0) as u32;
462    let x1 = (c.x + rx).ceil().min(dx as f32) as u32;
463    let y0 = (c.y - ry).floor().max(0.0) as u32;
464    let y1 = (c.y + ry).ceil().min(dy as f32) as u32;
465    let z0 = (c.z - rx).floor().max(0.0) as u32;
466    let z1 = (c.z + rx).ceil().min(dz as f32) as u32;
467    for y in y0..y1 {
468        let ty = (y as f32 + 0.5 - c.y) / ry; // -1..1
469        // Radio horizontal por altura: mejilla plena abajo (ty<0), afina hacia head_r arriba.
470        let rh = if ty >= 0.0 { rx + (head_r - rx) * ty } else { rx };
471        let rz = rh * 0.96;
472        for x in x0..x1 {
473            for z in z0..z1 {
474                let dxx = (x as f32 + 0.5 - c.x) / rh;
475                let dzz = (z as f32 + 0.5 - c.z) / rz;
476                if dxx * dxx + ty * ty + dzz * dzz <= 1.0 {
477                    g.set(x, y, z, color);
478                }
479            }
480        }
481    }
482}
483
484/// **Sombra de barba/bigote como tint PLANO** (sin geometría): re-tiñe la piel de la mitad
485/// inferior FRONTAL de la cara a un tono apenas más oscuro/apagado. Sólo toca voxels que HOY
486/// son piel (`== skin`), así no pisa ojos/pelo (que se estampan después). Sutil a propósito.
487fn tint_barba(g: &mut VoxelGrid, c: Vec3, rx: f32, ry: f32, head_r: f32, eye_y: f32, skin: [u8; 3]) {
488    // Barba = piel un 6% más oscura, apenas más fría (sombra tenue, no una máscara: fuerte
489    // ensuciaba la cara y la volvía adusta/monstruosa).
490    let beard = [
491        (skin[0] as f32 * 0.94) as u8,
492        (skin[1] as f32 * 0.93) as u8,
493        (skin[2] as f32 * 0.94) as u8,
494    ];
495    let [dx, dy, dz] = g.dim();
496    // De la barbilla hasta un poco por debajo de los ojos (zona bigote+barba+mentón).
497    let y0 = (c.y - ry).floor().max(0.0) as u32;
498    let y1 = (eye_y - head_r * 0.30).floor().max(0.0).min(dy as f32) as u32;
499    let x0 = (c.x - rx).floor().max(0.0) as u32;
500    let x1 = (c.x + rx).ceil().min(dx as f32) as u32;
501    // Sólo el frente de la cara (+Z), no la nuca/mandíbula lateral.
502    let z0 = (c.z + head_r * 0.28).floor().max(0.0) as u32;
503    let z1 = (c.z + head_r * 1.1).ceil().min(dz as f32) as u32;
504    for x in x0..x1 {
505        for y in y0..y1 {
506            for z in z0..z1 {
507                if let Some(px) = g.get(x, y, z) {
508                    if px[3] > 0 && [px[0], px[1], px[2]] == skin {
509                        g.set(x, y, z, beard);
510                    }
511                }
512            }
513        }
514    }
515}
516
517/// **Sonrisa como tint PLANO** (sin geometría): re-tiñe una banda corta de piel FRONTAL a la
518/// altura `boca_y`, curvada hacia arriba en los extremos (feliz), a un tono cálido apagado
519/// (labios/sombra de boca, no rojo saturado que a esta resolución grita "pico de pato"). Sólo
520/// pisa voxels que HOY son piel, así no borra ojos/pelo. Centrada en X, en el hemisferio +Z.
521fn sonrisa(g: &mut VoxelGrid, c: Vec3, rx: f32, head_r: f32, boca_y: f32, skin: [u8; 3]) {
522    // Boca = piel bastante más oscura y algo más cálida (rojiza), pero NO roja pura.
523    let mouth = [
524        (skin[0] as f32 * 0.72 + 22.0).min(255.0) as u8,
525        (skin[1] as f32 * 0.55).min(255.0) as u8,
526        (skin[2] as f32 * 0.55).min(255.0) as u8,
527    ];
528    let [dx, dy, dz] = g.dim();
529    let half_w = rx * 0.42; // media boca
530    let thick = head_r * 0.12; // grosor vertical de la línea
531    let curve = head_r * 0.22; // cuánto suben los extremos (sonrisa)
532    let x0 = (c.x - half_w).floor().max(0.0) as u32;
533    let x1 = (c.x + half_w).ceil().min(dx as f32) as u32;
534    // Sólo el frente de la cara (+Z).
535    let z0 = (c.z + head_r * 0.5).floor().max(0.0) as u32;
536    let z1 = (c.z + head_r * 1.1).ceil().min(dz as f32) as u32;
537    for x in x0..x1 {
538        // Altura de la línea de sonrisa en esta X: sube hacia los extremos (parábola).
539        let t = ((x as f32 + 0.5 - c.x) / half_w).clamp(-1.0, 1.0);
540        let ly = boca_y + curve * t * t;
541        let y0 = (ly - thick * 0.5).floor().max(0.0) as u32;
542        let y1 = (ly + thick * 0.5).ceil().min(dy as f32) as u32;
543        for y in y0..y1 {
544            for z in z0..z1 {
545                if let Some(px) = g.get(x, y, z) {
546                    if px[3] > 0 && [px[0], px[1], px[2]] == skin {
547                        g.set(x, y, z, mouth);
548                    }
549                }
550            }
551        }
552    }
553}
554
555/// Cápsula (cilindro con tapas esféricas) de radio `r` entre los puntos `a` y `b`.
556fn capsula(g: &mut VoxelGrid, a: Vec3, b: Vec3, r: f32, color: [u8; 3]) {
557    let [dx, dy, dz] = g.dim();
558    let lo = a.min(b) - Vec3::splat(r + 1.0);
559    let hi = a.max(b) + Vec3::splat(r + 1.0);
560    let x0 = lo.x.floor().max(0.0) as u32;
561    let x1 = hi.x.ceil().min(dx as f32) as u32;
562    let y0 = lo.y.floor().max(0.0) as u32;
563    let y1 = hi.y.ceil().min(dy as f32) as u32;
564    let z0 = lo.z.floor().max(0.0) as u32;
565    let z1 = hi.z.ceil().min(dz as f32) as u32;
566    let ab = b - a;
567    let ab2 = ab.length_squared().max(1e-6);
568    let r2 = r * r;
569    for x in x0..x1 {
570        for y in y0..y1 {
571            for z in z0..z1 {
572                let p = Vec3::new(x as f32 + 0.5, y as f32 + 0.5, z as f32 + 0.5);
573                let t = ((p - a).dot(ab) / ab2).clamp(0.0, 1.0);
574                let closest = a + ab * t;
575                if (p - closest).length_squared() <= r2 {
576                    g.set(x, y, z, color);
577                }
578            }
579        }
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586
587    fn solids(g: &VoxelGrid) -> usize {
588        let [w, h, d] = g.dim();
589        let mut n = 0;
590        for x in 0..w {
591            for y in 0..h {
592                for z in 0..d {
593                    if g.is_solid(x as i32, y as i32, z as i32) {
594                        n += 1;
595                    }
596                }
597            }
598        }
599        n
600    }
601
602    fn col() -> Colores {
603        Colores::nuevos([0.85, 0.68, 0.54], [0.2, 0.6, 0.55], [0.18, 0.22, 0.34])
604    }
605
606    #[test]
607    fn construye_un_ser_con_volumen() {
608        let g = humanoide_voxel(&EstiloSer::default(), &Pose::default(), 46, col());
609        assert!(solids(&g) > 3000, "el ser tiene volumen: {}", solids(&g));
610    }
611
612    #[test]
613    fn pies_en_el_piso() {
614        // El voxel sólido más bajo está cerca de y=0 (pies apoyados).
615        let g = humanoide_voxel(&EstiloSer::default(), &Pose::default(), 46, col());
616        let [w, h, d] = g.dim();
617        let mut ymin = h;
618        for x in 0..w {
619            for y in 0..h {
620                for z in 0..d {
621                    if g.is_solid(x as i32, y as i32, z as i32) {
622                        ymin = ymin.min(y);
623                    }
624                }
625            }
626        }
627        assert!(ymin <= 2, "pies cerca del piso: ymin={ymin}");
628    }
629
630    #[test]
631    fn el_bebe_es_mas_cabezon_que_el_adulto() {
632        // Menos cabezas ⇒ la cabeza ocupa una fracción mayor de la altura.
633        let baby = EstiloSer::por_edad(Age::Baby);
634        let adult = EstiloSer::por_edad(Age::Adult);
635        assert!(baby.cabezas < adult.cabezas, "bebé menos cabezas");
636        // Radio de cabeza relativo a la altura.
637        let hr = |e: &EstiloSer| 1.0 / (2.0 * e.cabezas);
638        assert!(hr(&baby) > hr(&adult) + 0.05, "bebé cabezón");
639    }
640
641    #[test]
642    fn los_gestos_cambian_la_silueta() {
643        // Festejar (brazos arriba) ensancha/eleva la ocupación respecto a idle.
644        let idle = humanoide_voxel(&EstiloSer::default(), &Pose::default(), 46, col());
645        let cheer = crate::Clip::Cheer.pose(1.2);
646        let festeja = humanoide_voxel(&EstiloSer::default(), &cheer, 46, col());
647        // Ocupación en la franja alta (por encima de la cabeza-hombro) crece al levantar brazos.
648        let alto = |g: &VoxelGrid| {
649            let [w, h, d] = g.dim();
650            let y0 = h * 3 / 4;
651            let mut n = 0;
652            for x in 0..w {
653                for y in y0..h {
654                    for z in 0..d {
655                        if g.is_solid(x as i32, y as i32, z as i32) {
656                            n += 1;
657                        }
658                    }
659                }
660            }
661            n
662        };
663        assert!(alto(&festeja) > alto(&idle), "festejar sube volumen alto: {} vs {}", alto(&festeja), alto(&idle));
664    }
665
666    #[test]
667    fn hornea_detalle_por_voxel() {
668        // Con redondez alta el bisel horneado es alto (silueta lisa).
669        let g = humanoide_voxel(&EstiloSer::default(), &Pose::default(), 40, col());
670        let [w, h, d] = g.dim();
671        let mut con_detalle = 0;
672        for x in 0..w {
673            for y in 0..h {
674                for z in 0..d {
675                    if g.is_solid(x as i32, y as i32, z as i32) {
676                        if let Some(det) = g.detail_at(x, y, z) {
677                            if det != [0, 0] {
678                                con_detalle += 1;
679                            }
680                        }
681                    }
682                }
683            }
684        }
685        assert!(con_detalle > 1000, "el cuerpo lleva detalle por voxel: {con_detalle}");
686    }
687
688    #[test]
689    fn presets_de_arquetipo_difieren() {
690        assert!(EstiloSer::amable().redondez > EstiloSer::robusto().redondez);
691        assert!(EstiloSer::agil().angulosidad > EstiloSer::amable().angulosidad);
692        assert!(EstiloSer::agil().cabezas > EstiloSer::amable().cabezas);
693    }
694}