gioser

tawasuyu/llimphi

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

Rust 238TOML 102Markdown 83Shell 24178 Sutichaykuna

⎇ main llimphi-voxel/src/gesto.rs Rust · 238 siq'ikuna · 9.9 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos b3f12e136 · Sergio ·

1//! **Gesto** — la cara viva del [`ser SDF`](crate::ser_sdf): el estado facial de UN
2//! cuadro (párpados, boca, pupilas, giro de cabeza).
3//!
4//! ## Por qué es dato por cuadro y no un asset
5//!
6//! En el ser SDF la cara son **decals** (primitivas que re-pintan el color de la
7//! superficie sin aportar geometría). Cambiar una expresión no es cambiar una malla ni
8//! un atlas de texturas: es mover, agrandar o agregar un puñado de decals antes de
9//! empacar el uniform. Por eso `Gesto` es un struct chico, `Copy`, puro y determinista
10//! — se calcula por cuadro y se tira.
11//!
12//! ## Las tres capas de la mirada
13//!
14//! 1. **Cabeza** (`cabeza`): giro yaw/pitch de los rasgos, sobre el cabeceo del clip.
15//! 2. **Pupilas** (`ojos`): desvío dentro del domo del ojo — la parte que "sigue" al
16//!    objetivo cuando la cabeza ya llegó a su tope creíble.
17//! 3. **Párpados** (`parpado`): tapan el ojo; el parpadeo es lo que separa a un muñeco
18//!    de algo que respira.
19//!
20//! [`Gesto::vivo`] da las tres gratis a partir de la fase del clip (parpadeo
21//! determinista + boca que reacciona al gesto + micro-sacadas de los ojos), y
22//! [`Gesto::mirar`] resuelve el look-at contra un punto de mundo — el mismo IK de
23//! cabeza que ya tenía el [`Actor`](crate::Actor) de cajas, ahora del lado SDF.
24
25use llimphi_3d::glam::Vec3;
26use serde::{Deserialize, Serialize};
27
28use crate::actor::Clip;
29
30/// Estado facial de un cuadro. `Default` = [`neutro`](Gesto::neutro): ojos abiertos,
31/// boca cerrada sonriendo, cabeza alineada al cuerpo (el ser tal cual se calibró).
32#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
33pub struct Gesto {
34    /// Apertura de párpados: `1` = abierto, `0` = cerrado.
35    pub parpado: f32,
36    /// Curvatura de la boca: `1` = sonrisa plena, `0` = línea recta, negativo = mueca.
37    pub sonrisa: f32,
38    /// Apertura de la boca `0..1` (hablar, festejar, sorpresa).
39    pub boca: f32,
40    /// Desvío de las pupilas dentro del ojo, `[x, y]` en `-1..1` (x+ = a su izquierda
41    /// en pantalla, y+ = arriba). Es lo que "sigue" al objetivo cuando la cabeza topa.
42    pub ojos: [f32; 2],
43    /// Giro extra de la cabeza `[yaw, pitch]` en radianes, SOBRE el `head_pitch` del
44    /// clip. Positivo en pitch = mirar abajo (misma convención que la pose).
45    pub cabeza: [f32; 2],
46}
47
48impl Default for Gesto {
49    fn default() -> Self {
50        Gesto::neutro()
51    }
52}
53
54/// Tope del giro de cabeza (rad): ±57° de yaw, ±23° de pitch. No es sólo anatomía —
55/// es el límite de la trampa: en el ser SDF el CRÁNEO no rota (su elipsoide tiene los
56/// semiejes alineados a ejes), rotan los rasgos sobre él. Mientras el giro es moderado
57/// eso se lee como una cabeza que gira; pasado ese punto los ojos se van trepando a la
58/// coronilla de un huevo quieto. Verificado a ojo (plancha `cara_viva_demo`): a 40° de
59/// cabeceo la cara se desarma. Lo que sobra lo absorben las pupilas, que es además
60/// como funciona una mirada de verdad.
61const YAW_MAX: f32 = 1.0;
62const PITCH_MAX: f32 = 0.40;
63
64impl Gesto {
65    /// Cara neutra: ojos abiertos, sonrisa plena, boca cerrada, cabeza alineada.
66    pub const fn neutro() -> Self {
67        Self {
68            parpado: 1.0,
69            sonrisa: 1.0,
70            boca: 0.0,
71            ojos: [0.0, 0.0],
72            cabeza: [0.0, 0.0],
73        }
74    }
75
76    /// **La cara viva sin que nadie la anime**: parpadeo determinista + boca que
77    /// reacciona al clip + micro-sacadas de la mirada, todo función de la `fase` del
78    /// clip (la misma que ya acumula el reproductor). Puro: la misma fase da la misma
79    /// cara, así que preview y export coinciden cuadro a cuadro.
80    pub fn vivo(fase: f32, clip: Clip) -> Self {
81        let mut g = Gesto::neutro();
82        g.parpado = parpadeo(fase);
83        // La boca reacciona al gesto: el festejo grita, el saludo habla, el resto calla.
84        let (boca, sonrisa) = match clip {
85            Clip::Cheer => (0.45 + 0.25 * (fase * 3.1).sin().abs(), 1.25),
86            Clip::Wave => (0.22 + 0.10 * (fase * 2.4).sin().abs(), 1.1),
87            Clip::Point => (0.14, 0.9),
88            Clip::Run => (0.10, 0.85),
89            Clip::Idle | Clip::Walk => (0.0, 1.0),
90        };
91        g.boca = boca;
92        g.sonrisa = sonrisa;
93        // Micro-sacadas: los ojos saltan a un punto nuevo cada ~5 unidades de fase y
94        // se quedan ahí (así se mueve un ojo de verdad: sacada + fijación, nunca deriva).
95        let k = (fase * 0.21).floor();
96        let h = hash01(k);
97        let h2 = hash01(k + 37.0);
98        g.ojos = [(h * 2.0 - 1.0) * 0.30, (h2 * 2.0 - 1.0) * 0.16];
99        g
100    }
101
102    /// **Look-at**: orienta cabeza y pupilas hacia `objetivo` (coords de mundo) para un
103    /// ser posado en `pos` con rumbo `yaw`, con los ojos a `altura_ojos` sobre sus pies
104    /// (lo da [`altura_ojos`](crate::ser_sdf::altura_ojos)).
105    ///
106    /// La cabeza gira hasta su tope creíble; el resto del ángulo lo absorben las
107    /// pupilas — por eso un ser al que le pasás la cámara *te mira* aunque esté de
108    /// perfil. Encadenable sobre [`vivo`](Self::vivo).
109    pub fn mirar(mut self, objetivo: Vec3, pos: Vec3, yaw: f32, altura_ojos: f32) -> Self {
110        let d = objetivo - (pos + Vec3::new(0.0, altura_ojos, 0.0));
111        if d.length_squared() < 1e-9 {
112            return self;
113        }
114        // A espacio LOCAL del ser (deshacer su yaw) — la misma inversa que hace el
115        // shader en `ser_local`.
116        let (sy, cy) = yaw.sin_cos();
117        let l = Vec3::new(cy * d.x - sy * d.z, d.y, sy * d.x + cy * d.z).normalize();
118        let yaw_q = l.x.atan2(l.z);
119        let pitch_q = (-l.y).asin();
120        let yaw_c = yaw_q.clamp(-YAW_MAX, YAW_MAX);
121        let pitch_c = pitch_q.clamp(-PITCH_MAX, PITCH_MAX);
122        self.cabeza = [yaw_c, pitch_c];
123        // Lo que la cabeza no alcanzó, más un adelanto de los ojos (el ojo llega
124        // ANTES que la cabeza: es lo que hace que la mirada se sienta intencional).
125        let sobra_x = (yaw_q - yaw_c) * 1.6 + yaw_c * 0.22;
126        let sobra_y = -((pitch_q - pitch_c) * 1.6 + pitch_c * 0.22);
127        self.ojos = [
128            (self.ojos[0] * 0.2 + sobra_x).clamp(-1.0, 1.0),
129            (self.ojos[1] * 0.2 + sobra_y).clamp(-1.0, 1.0),
130        ];
131        self
132    }
133
134    /// Fuerza los párpados (`1` abierto, `0` cerrado) — para dormir, guiñar o dirigir
135    /// un parpadeo a mano en vez del automático.
136    pub fn con_parpado(mut self, v: f32) -> Self {
137        self.parpado = v.clamp(0.0, 1.0);
138        self
139    }
140
141    /// Fuerza la boca: `apertura` `0..1` y `sonrisa` (`1` feliz, `0` recta, `-1` mueca).
142    pub fn con_boca(mut self, apertura: f32, sonrisa: f32) -> Self {
143        self.boca = apertura.clamp(0.0, 1.0);
144        self.sonrisa = sonrisa.clamp(-1.5, 1.5);
145        self
146    }
147}
148
149/// Apertura de párpados `0..1` por la fase: cerrado en una ventana corta (~6% del
150/// ciclo lento), triángulo de cierre/apertura. Mismo reloj que el
151/// [`Actor`](crate::Actor) de cajas → los dos caminos parpadean juntos.
152fn parpadeo(fase: f32) -> f32 {
153    let ph = fase * 0.33;
154    let f = ph - ph.floor();
155    if f > 0.94 {
156        let w = (f - 0.94) / 0.06;
157        (1.0 - (1.0 - (2.0 * w - 1.0).abs())).clamp(0.0, 1.0)
158    } else {
159        1.0
160    }
161}
162
163/// Hash determinista `→ [0,1)` (el clásico de shader): sacadas reproducibles sin `rand`.
164fn hash01(x: f32) -> f32 {
165    let v = (x * 12.9898).sin() * 43758.5453;
166    v - v.floor()
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn el_neutro_no_gesticula() {
175        let g = Gesto::default();
176        assert_eq!(g.parpado, 1.0);
177        assert_eq!(g.boca, 0.0);
178        assert_eq!(g.cabeza, [0.0, 0.0]);
179    }
180
181    #[test]
182    fn parpadea_en_algun_momento_del_ciclo() {
183        // Barrer una vuelta larga: tiene que haber al menos un cuadro casi cerrado
184        // y la mayoría abiertos (no es un ojo que tiembla).
185        let mut cerrados = 0;
186        let mut abiertos = 0;
187        for i in 0..600 {
188            let p = Gesto::vivo(i as f32 * 0.05, Clip::Idle).parpado;
189            if p < 0.15 {
190                cerrados += 1;
191            }
192            if p > 0.99 {
193                abiertos += 1;
194            }
195        }
196        assert!(cerrados > 0, "nunca parpadea");
197        assert!(abiertos > 500, "parpadea de más: {abiertos} abiertos de 600");
198    }
199
200    #[test]
201    fn el_festejo_abre_la_boca_y_el_quieto_no() {
202        assert!(Gesto::vivo(1.0, Clip::Cheer).boca > 0.4);
203        assert_eq!(Gesto::vivo(1.0, Clip::Idle).boca, 0.0);
204    }
205
206    #[test]
207    fn mirar_a_un_lado_gira_la_cabeza_hacia_ese_lado() {
208        let pos = Vec3::new(0.0, 0.0, 0.0);
209        // Ser mirando +Z; objetivo a su +X → yaw positivo (convención rot_y del kit).
210        let g = Gesto::neutro().mirar(Vec3::new(10.0, 1.6, 0.0), pos, 0.0, 1.6);
211        assert!(g.cabeza[0] > 0.5, "gira a +X: {:?}", g.cabeza);
212        let izq = Gesto::neutro().mirar(Vec3::new(-10.0, 1.6, 0.0), pos, 0.0, 1.6);
213        assert!(izq.cabeza[0] < -0.5, "gira a -X: {:?}", izq.cabeza);
214    }
215
216    #[test]
217    fn mirar_arriba_levanta_la_cara_y_mirar_atras_lo_absorben_las_pupilas() {
218        let pos = Vec3::ZERO;
219        let arriba = Gesto::neutro().mirar(Vec3::new(0.0, 20.0, 1.0), pos, 0.0, 1.6);
220        assert!(arriba.cabeza[1] < -0.3, "pitch negativo = mirar arriba: {:?}", arriba.cabeza);
221        // Objetivo DETRÁS: la cabeza topa en el límite y las pupilas se van al borde.
222        let atras = Gesto::neutro().mirar(Vec3::new(0.0, 1.6, -10.0), pos, 0.0, 1.6);
223        assert!((atras.cabeza[0].abs() - YAW_MAX).abs() < 1e-3, "cabeza al tope");
224        assert!(atras.ojos[0].abs() > 0.6, "las pupilas absorben el resto: {:?}", atras.ojos);
225    }
226
227    #[test]
228    fn el_yaw_del_ser_se_deshace_al_mirar() {
229        // Ser girado 90° (mirando +X) y objetivo justo enfrente suyo → cabeza recta.
230        let g = Gesto::neutro().mirar(
231            Vec3::new(10.0, 1.6, 0.0),
232            Vec3::ZERO,
233            std::f32::consts::FRAC_PI_2,
234            1.6,
235        );
236        assert!(g.cabeza[0].abs() < 1e-3, "objetivo al frente = sin giro: {:?}", g.cabeza);
237    }
238}