//! **Gesto** — la cara viva del [`ser SDF`](crate::ser_sdf): el estado facial de UN //! cuadro (párpados, boca, pupilas, giro de cabeza). //! //! ## Por qué es dato por cuadro y no un asset //! //! En el ser SDF la cara son **decals** (primitivas que re-pintan el color de la //! superficie sin aportar geometría). Cambiar una expresión no es cambiar una malla ni //! un atlas de texturas: es mover, agrandar o agregar un puñado de decals antes de //! empacar el uniform. Por eso `Gesto` es un struct chico, `Copy`, puro y determinista //! — se calcula por cuadro y se tira. //! //! ## Las tres capas de la mirada //! //! 1. **Cabeza** (`cabeza`): giro yaw/pitch de los rasgos, sobre el cabeceo del clip. //! 2. **Pupilas** (`ojos`): desvío dentro del domo del ojo — la parte que "sigue" al //! objetivo cuando la cabeza ya llegó a su tope creíble. //! 3. **Párpados** (`parpado`): tapan el ojo; el parpadeo es lo que separa a un muñeco //! de algo que respira. //! //! [`Gesto::vivo`] da las tres gratis a partir de la fase del clip (parpadeo //! determinista + boca que reacciona al gesto + micro-sacadas de los ojos), y //! [`Gesto::mirar`] resuelve el look-at contra un punto de mundo — el mismo IK de //! cabeza que ya tenía el [`Actor`](crate::Actor) de cajas, ahora del lado SDF. use llimphi_3d::glam::Vec3; use serde::{Deserialize, Serialize}; use crate::actor::Clip; /// Estado facial de un cuadro. `Default` = [`neutro`](Gesto::neutro): ojos abiertos, /// boca cerrada sonriendo, cabeza alineada al cuerpo (el ser tal cual se calibró). #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Gesto { /// Apertura de párpados: `1` = abierto, `0` = cerrado. pub parpado: f32, /// Curvatura de la boca: `1` = sonrisa plena, `0` = línea recta, negativo = mueca. pub sonrisa: f32, /// Apertura de la boca `0..1` (hablar, festejar, sorpresa). pub boca: f32, /// Desvío de las pupilas dentro del ojo, `[x, y]` en `-1..1` (x+ = a su izquierda /// en pantalla, y+ = arriba). Es lo que "sigue" al objetivo cuando la cabeza topa. pub ojos: [f32; 2], /// Giro extra de la cabeza `[yaw, pitch]` en radianes, SOBRE el `head_pitch` del /// clip. Positivo en pitch = mirar abajo (misma convención que la pose). pub cabeza: [f32; 2], } impl Default for Gesto { fn default() -> Self { Gesto::neutro() } } /// Tope del giro de cabeza (rad): ±57° de yaw, ±23° de pitch. No es sólo anatomía — /// es el límite de la trampa: en el ser SDF el CRÁNEO no rota (su elipsoide tiene los /// semiejes alineados a ejes), rotan los rasgos sobre él. Mientras el giro es moderado /// eso se lee como una cabeza que gira; pasado ese punto los ojos se van trepando a la /// coronilla de un huevo quieto. Verificado a ojo (plancha `cara_viva_demo`): a 40° de /// cabeceo la cara se desarma. Lo que sobra lo absorben las pupilas, que es además /// como funciona una mirada de verdad. const YAW_MAX: f32 = 1.0; const PITCH_MAX: f32 = 0.40; impl Gesto { /// Cara neutra: ojos abiertos, sonrisa plena, boca cerrada, cabeza alineada. pub const fn neutro() -> Self { Self { parpado: 1.0, sonrisa: 1.0, boca: 0.0, ojos: [0.0, 0.0], cabeza: [0.0, 0.0], } } /// **La cara viva sin que nadie la anime**: parpadeo determinista + boca que /// reacciona al clip + micro-sacadas de la mirada, todo función de la `fase` del /// clip (la misma que ya acumula el reproductor). Puro: la misma fase da la misma /// cara, así que preview y export coinciden cuadro a cuadro. pub fn vivo(fase: f32, clip: Clip) -> Self { let mut g = Gesto::neutro(); g.parpado = parpadeo(fase); // La boca reacciona al gesto: el festejo grita, el saludo habla, el resto calla. let (boca, sonrisa) = match clip { Clip::Cheer => (0.45 + 0.25 * (fase * 3.1).sin().abs(), 1.25), Clip::Wave => (0.22 + 0.10 * (fase * 2.4).sin().abs(), 1.1), Clip::Point => (0.14, 0.9), Clip::Run => (0.10, 0.85), Clip::Idle | Clip::Walk => (0.0, 1.0), }; g.boca = boca; g.sonrisa = sonrisa; // Micro-sacadas: los ojos saltan a un punto nuevo cada ~5 unidades de fase y // se quedan ahí (así se mueve un ojo de verdad: sacada + fijación, nunca deriva). let k = (fase * 0.21).floor(); let h = hash01(k); let h2 = hash01(k + 37.0); g.ojos = [(h * 2.0 - 1.0) * 0.30, (h2 * 2.0 - 1.0) * 0.16]; g } /// **Look-at**: orienta cabeza y pupilas hacia `objetivo` (coords de mundo) para un /// ser posado en `pos` con rumbo `yaw`, con los ojos a `altura_ojos` sobre sus pies /// (lo da [`altura_ojos`](crate::ser_sdf::altura_ojos)). /// /// La cabeza gira hasta su tope creíble; el resto del ángulo lo absorben las /// pupilas — por eso un ser al que le pasás la cámara *te mira* aunque esté de /// perfil. Encadenable sobre [`vivo`](Self::vivo). pub fn mirar(mut self, objetivo: Vec3, pos: Vec3, yaw: f32, altura_ojos: f32) -> Self { let d = objetivo - (pos + Vec3::new(0.0, altura_ojos, 0.0)); if d.length_squared() < 1e-9 { return self; } // A espacio LOCAL del ser (deshacer su yaw) — la misma inversa que hace el // shader en `ser_local`. let (sy, cy) = yaw.sin_cos(); let l = Vec3::new(cy * d.x - sy * d.z, d.y, sy * d.x + cy * d.z).normalize(); let yaw_q = l.x.atan2(l.z); let pitch_q = (-l.y).asin(); let yaw_c = yaw_q.clamp(-YAW_MAX, YAW_MAX); let pitch_c = pitch_q.clamp(-PITCH_MAX, PITCH_MAX); self.cabeza = [yaw_c, pitch_c]; // Lo que la cabeza no alcanzó, más un adelanto de los ojos (el ojo llega // ANTES que la cabeza: es lo que hace que la mirada se sienta intencional). let sobra_x = (yaw_q - yaw_c) * 1.6 + yaw_c * 0.22; let sobra_y = -((pitch_q - pitch_c) * 1.6 + pitch_c * 0.22); self.ojos = [ (self.ojos[0] * 0.2 + sobra_x).clamp(-1.0, 1.0), (self.ojos[1] * 0.2 + sobra_y).clamp(-1.0, 1.0), ]; self } /// Fuerza los párpados (`1` abierto, `0` cerrado) — para dormir, guiñar o dirigir /// un parpadeo a mano en vez del automático. pub fn con_parpado(mut self, v: f32) -> Self { self.parpado = v.clamp(0.0, 1.0); self } /// Fuerza la boca: `apertura` `0..1` y `sonrisa` (`1` feliz, `0` recta, `-1` mueca). pub fn con_boca(mut self, apertura: f32, sonrisa: f32) -> Self { self.boca = apertura.clamp(0.0, 1.0); self.sonrisa = sonrisa.clamp(-1.5, 1.5); self } } /// Apertura de párpados `0..1` por la fase: cerrado en una ventana corta (~6% del /// ciclo lento), triángulo de cierre/apertura. Mismo reloj que el /// [`Actor`](crate::Actor) de cajas → los dos caminos parpadean juntos. fn parpadeo(fase: f32) -> f32 { let ph = fase * 0.33; let f = ph - ph.floor(); if f > 0.94 { let w = (f - 0.94) / 0.06; (1.0 - (1.0 - (2.0 * w - 1.0).abs())).clamp(0.0, 1.0) } else { 1.0 } } /// Hash determinista `→ [0,1)` (el clásico de shader): sacadas reproducibles sin `rand`. fn hash01(x: f32) -> f32 { let v = (x * 12.9898).sin() * 43758.5453; v - v.floor() } #[cfg(test)] mod tests { use super::*; #[test] fn el_neutro_no_gesticula() { let g = Gesto::default(); assert_eq!(g.parpado, 1.0); assert_eq!(g.boca, 0.0); assert_eq!(g.cabeza, [0.0, 0.0]); } #[test] fn parpadea_en_algun_momento_del_ciclo() { // Barrer una vuelta larga: tiene que haber al menos un cuadro casi cerrado // y la mayoría abiertos (no es un ojo que tiembla). let mut cerrados = 0; let mut abiertos = 0; for i in 0..600 { let p = Gesto::vivo(i as f32 * 0.05, Clip::Idle).parpado; if p < 0.15 { cerrados += 1; } if p > 0.99 { abiertos += 1; } } assert!(cerrados > 0, "nunca parpadea"); assert!(abiertos > 500, "parpadea de más: {abiertos} abiertos de 600"); } #[test] fn el_festejo_abre_la_boca_y_el_quieto_no() { assert!(Gesto::vivo(1.0, Clip::Cheer).boca > 0.4); assert_eq!(Gesto::vivo(1.0, Clip::Idle).boca, 0.0); } #[test] fn mirar_a_un_lado_gira_la_cabeza_hacia_ese_lado() { let pos = Vec3::new(0.0, 0.0, 0.0); // Ser mirando +Z; objetivo a su +X → yaw positivo (convención rot_y del kit). let g = Gesto::neutro().mirar(Vec3::new(10.0, 1.6, 0.0), pos, 0.0, 1.6); assert!(g.cabeza[0] > 0.5, "gira a +X: {:?}", g.cabeza); let izq = Gesto::neutro().mirar(Vec3::new(-10.0, 1.6, 0.0), pos, 0.0, 1.6); assert!(izq.cabeza[0] < -0.5, "gira a -X: {:?}", izq.cabeza); } #[test] fn mirar_arriba_levanta_la_cara_y_mirar_atras_lo_absorben_las_pupilas() { let pos = Vec3::ZERO; let arriba = Gesto::neutro().mirar(Vec3::new(0.0, 20.0, 1.0), pos, 0.0, 1.6); assert!(arriba.cabeza[1] < -0.3, "pitch negativo = mirar arriba: {:?}", arriba.cabeza); // Objetivo DETRÁS: la cabeza topa en el límite y las pupilas se van al borde. let atras = Gesto::neutro().mirar(Vec3::new(0.0, 1.6, -10.0), pos, 0.0, 1.6); assert!((atras.cabeza[0].abs() - YAW_MAX).abs() < 1e-3, "cabeza al tope"); assert!(atras.ojos[0].abs() > 0.6, "las pupilas absorben el resto: {:?}", atras.ojos); } #[test] fn el_yaw_del_ser_se_deshace_al_mirar() { // Ser girado 90° (mirando +X) y objetivo justo enfrente suyo → cabeza recta. let g = Gesto::neutro().mirar( Vec3::new(10.0, 1.6, 0.0), Vec3::ZERO, std::f32::consts::FRAC_PI_2, 1.6, ); assert!(g.cabeza[0].abs() < 1e-3, "objetivo al frente = sin giro: {:?}", g.cabeza); } }