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

⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8 llimphi-anim/src/skel.rs Rust · 616 lines · 20.3 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//! Jerarquía de huesos 2D + skinning ponderado de vértices (linear blend
2//! skinning, LBS) — el sustrato de la deformación esqueletal estilo Rive.
3//!
4//! **Puro y sin renderer.** Este módulo sólo calcula geometría: dada una
5//! jerarquía de huesos posada y una malla con pesos por vértice, produce las
6//! **posiciones deformadas** de los vértices (`kurbo::Point`). Quién las pinta
7//! (vello vía paths para malla vectorial, o clip+imagen para malla texturizada)
8//! es trabajo del consumidor; el spike confirmó que ambas rutas funcionan en
9//! vello 0.7.
10//!
11//! ## Bind pose vs pose actual
12//!
13//! El esqueleto se construye en su **bind pose** (la configuración en la que se
14//! ató la malla) y se llama [`Skeleton::bind`] para congelarla: por cada hueso
15//! se guarda su *inverse-bind* (el world transform inverso en el bind). Al
16//! animar, se cambian las poses locales ([`Skeleton::set_pose`]),
17//! [`Skeleton::update`] recompone los world transforms, y la matriz de skinning
18//! de un hueso es `world_actual · inverse_bind`. En el bind pose esa matriz es
19//! la identidad → los vértices quedan en reposo.
20//!
21//! ## LBS
22//!
23//! La posición deformada de un vértice es la media ponderada, sobre los huesos
24//! que lo influyen, de aplicarle a su posición de reposo la matriz de skinning
25//! de cada hueso: `v' = Σ_b w_b · (skin_b · v_rest)`.
26
27use std::collections::HashMap;
28
29use kurbo::{Affine, Point, Vec2};
30
31use crate::RenderFrame;
32
33/// Índice de un hueso dentro del esqueleto.
34pub type BoneId = usize;
35
36/// Pose local de un hueso (TRS) relativa a su padre. Se compone como
37/// `translate · rotate · scale` — el orden estándar.
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub struct Pose {
40    pub translation: Vec2,
41    /// Rotación en radianes (convención de kurbo).
42    pub rotation: f64,
43    pub scale: Vec2,
44}
45
46impl Default for Pose {
47    fn default() -> Self {
48        Self::identity()
49    }
50}
51
52impl Pose {
53    pub fn identity() -> Self {
54        Self {
55            translation: Vec2::ZERO,
56            rotation: 0.0,
57            scale: Vec2::new(1.0, 1.0),
58        }
59    }
60    pub fn translate(t: Vec2) -> Self {
61        Self {
62            translation: t,
63            ..Self::identity()
64        }
65    }
66    pub fn rotate(r: f64) -> Self {
67        Self {
68            rotation: r,
69            ..Self::identity()
70        }
71    }
72    /// Translación + rotación (lo más común al animar un hueso).
73    pub fn new(translation: Vec2, rotation: f64, scale: Vec2) -> Self {
74        Self {
75            translation,
76            rotation,
77            scale,
78        }
79    }
80    pub fn to_affine(&self) -> Affine {
81        Affine::translate(self.translation)
82            * Affine::rotate(self.rotation)
83            * Affine::scale_non_uniform(self.scale.x, self.scale.y)
84    }
85}
86
87#[derive(Debug, Clone)]
88struct Bone {
89    parent: Option<BoneId>,
90    local: Pose,
91}
92
93/// Una jerarquía de huesos posable. Los huesos se agregan **padre antes que
94/// hijo** (el índice del padre debe ser menor), así el cómputo de world
95/// transforms es una sola pasada hacia adelante.
96#[derive(Debug, Clone, Default)]
97pub struct Skeleton {
98    bones: Vec<Bone>,
99    /// World transform inverso de cada hueso, capturado en `bind`.
100    inverse_bind: Vec<Affine>,
101    /// Scratch: world transforms actuales (recomputados por `update`).
102    world: Vec<Affine>,
103}
104
105impl Skeleton {
106    pub fn new() -> Self {
107        Self::default()
108    }
109
110    /// Agrega un hueso con `parent` (o `None` = raíz) y su pose local. Devuelve
111    /// su `BoneId`. **Pánico** si el padre no fue agregado antes (índice ≥ id).
112    pub fn add_bone(&mut self, parent: Option<BoneId>, local: Pose) -> BoneId {
113        if let Some(p) = parent {
114            assert!(
115                p < self.bones.len(),
116                "el padre {p} debe agregarse antes que el hijo"
117            );
118        }
119        let id = self.bones.len();
120        self.bones.push(Bone { parent, local });
121        self.inverse_bind.push(Affine::IDENTITY);
122        self.world.push(Affine::IDENTITY);
123        id
124    }
125
126    fn recompute_world(&mut self) {
127        for i in 0..self.bones.len() {
128            let local = self.bones[i].local.to_affine();
129            self.world[i] = match self.bones[i].parent {
130                Some(p) => self.world[p] * local,
131                None => local,
132            };
133        }
134    }
135
136    /// Congela la pose actual como **bind pose**: recompone los world transforms
137    /// y guarda el inverse-bind de cada hueso. Llamar una vez tras construir el
138    /// esqueleto en reposo (antes de animar).
139    pub fn bind(&mut self) {
140        self.recompute_world();
141        for i in 0..self.bones.len() {
142            self.inverse_bind[i] = self.world[i].inverse();
143        }
144    }
145
146    /// Cambia la pose local de un hueso (animar). Requiere [`update`] después
147    /// para que los world transforms reflejen el cambio.
148    ///
149    /// [`update`]: Skeleton::update
150    pub fn set_pose(&mut self, bone: BoneId, local: Pose) {
151        self.bones[bone].local = local;
152    }
153
154    /// Pose local actual de un hueso.
155    pub fn pose(&self, bone: BoneId) -> Pose {
156        self.bones[bone].local
157    }
158
159    /// Recalcula los world transforms desde las poses actuales. Llamar tras
160    /// posear y antes de deformar/leer `world`/`skin_matrix`.
161    pub fn update(&mut self) {
162        self.recompute_world();
163    }
164
165    /// Matriz de skinning del hueso: `world_actual · inverse_bind`. En el bind
166    /// pose es la identidad.
167    pub fn skin_matrix(&self, bone: BoneId) -> Affine {
168        self.world[bone] * self.inverse_bind[bone]
169    }
170
171    /// World transform actual del hueso (útil para dibujar el hueso mismo o
172    /// adjuntar algo a su punta).
173    pub fn world(&self, bone: BoneId) -> Affine {
174        self.world[bone]
175    }
176
177    /// Padre de un hueso (`None` si es raíz). Lo usan los constraints para
178    /// convertir una rotación deseada en mundo a la pose local.
179    pub fn parent(&self, bone: BoneId) -> Option<BoneId> {
180        self.bones[bone].parent
181    }
182
183    pub fn len(&self) -> usize {
184        self.bones.len()
185    }
186    pub fn is_empty(&self) -> bool {
187        self.bones.is_empty()
188    }
189}
190
191/// Influencia de un hueso sobre un vértice. Idealmente los pesos de un vértice
192/// suman 1; si no, `deform` normaliza.
193#[derive(Debug, Clone, Copy, PartialEq)]
194pub struct Weight {
195    pub bone: BoneId,
196    pub weight: f64,
197}
198
199/// Un vértice de la malla: su posición de reposo (en model/bind space), su UV
200/// `0..1` (para textura, lo usa el render de malla texturizada) y los huesos que
201/// lo influyen.
202#[derive(Debug, Clone)]
203pub struct Vertex {
204    pub rest: Point,
205    pub uv: (f64, f64),
206    pub weights: Vec<Weight>,
207}
208
209impl Vertex {
210    /// Vértice rígido a un solo hueso (peso 1).
211    pub fn rigid(rest: Point, uv: (f64, f64), bone: BoneId) -> Self {
212        Self {
213            rest,
214            uv,
215            weights: vec![Weight { bone, weight: 1.0 }],
216        }
217    }
218}
219
220/// Una malla deformable: vértices con pesos + topología de triángulos (índices
221/// a `vertices`). El render la consume tras `deform`.
222#[derive(Debug, Clone, Default)]
223pub struct Mesh {
224    pub vertices: Vec<Vertex>,
225    pub triangles: Vec<[u32; 3]>,
226}
227
228impl Mesh {
229    pub fn new() -> Self {
230        Self::default()
231    }
232
233    /// Calcula las posiciones deformadas (LBS) en `out`, una por vértice, en el
234    /// mismo orden que `self.vertices`. `skel` debe tener sus world transforms
235    /// actualizados ([`Skeleton::update`]). Reutiliza el buffer `out` (sin
236    /// asignar por frame).
237    pub fn deform_into(&self, skel: &Skeleton, out: &mut Vec<Point>) {
238        out.clear();
239        out.reserve(self.vertices.len());
240        for v in &self.vertices {
241            if v.weights.is_empty() {
242                out.push(v.rest);
243                continue;
244            }
245            let mut acc = Vec2::ZERO;
246            let mut wsum = 0.0;
247            for w in &v.weights {
248                let p = skel.skin_matrix(w.bone) * v.rest;
249                acc += p.to_vec2() * w.weight;
250                wsum += w.weight;
251            }
252            let p = if wsum.abs() > 1e-12 {
253                (acc * (1.0 / wsum)).to_point()
254            } else {
255                v.rest
256            };
257            out.push(p);
258        }
259    }
260
261    /// Variante que asigna y devuelve el `Vec` (conveniencia para tests/uso
262    /// ocasional; en el bucle de render preferí [`deform_into`]).
263    ///
264    /// [`deform_into`]: Mesh::deform_into
265    pub fn deform(&self, skel: &Skeleton) -> Vec<Point> {
266        let mut out = Vec::new();
267        self.deform_into(skel, &mut out);
268        out
269    }
270}
271
272/// Interpola linealmente dos poses (translación/escala lerp, rotación lerp del
273/// ángulo). Es el blend de poses que usa el crossfade esqueletal — mezclar
274/// *poses* da una deformación correcta, a diferencia del crossfade de píxeles.
275pub fn lerp_pose(a: Pose, b: Pose, f: f64) -> Pose {
276    let lerp = |x: f64, y: f64| x + (y - x) * f;
277    Pose {
278        translation: Vec2::new(
279            lerp(a.translation.x, b.translation.x),
280            lerp(a.translation.y, b.translation.y),
281        ),
282        rotation: lerp(a.rotation, b.rotation),
283        scale: Vec2::new(lerp(a.scale.x, b.scale.x), lerp(a.scale.y, b.scale.y)),
284    }
285}
286
287/// Keyframe de pose de un hueso en un instante.
288#[derive(Debug, Clone, Copy, PartialEq)]
289pub struct PoseKey {
290    pub t: f64,
291    pub pose: Pose,
292}
293
294/// Pista de animación de un hueso: keyframes de pose ordenados por tiempo.
295#[derive(Debug, Clone, Default)]
296pub struct BoneTrack {
297    pub bone: BoneId,
298    pub keys: Vec<PoseKey>,
299}
300
301impl BoneTrack {
302    /// Pose en el instante `t` (lineal entre keyframes, clamp en los extremos).
303    pub fn sample(&self, t: f64) -> Pose {
304        match self.keys.as_slice() {
305            [] => Pose::identity(),
306            [only] => only.pose,
307            keys => {
308                if t <= keys[0].t {
309                    return keys[0].pose;
310                }
311                let last = keys[keys.len() - 1];
312                if t >= last.t {
313                    return last.pose;
314                }
315                for w in keys.windows(2) {
316                    let (a, b) = (w[0], w[1]);
317                    if t >= a.t && t <= b.t {
318                        let span = b.t - a.t;
319                        let f = if span > 1e-12 { (t - a.t) / span } else { 0.0 };
320                        return lerp_pose(a.pose, b.pose, f);
321                    }
322                }
323                last.pose
324            }
325        }
326    }
327}
328
329/// Una animación esqueletal: pistas de pose por hueso, con duración y loop. Es
330/// lo que un *estado* de la máquina reproduce (un clip = una `BoneAnimation`).
331/// Los huesos no incluidos en ninguna pista conservan su pose actual.
332#[derive(Debug, Clone, Default)]
333pub struct BoneAnimation {
334    pub duration: f64,
335    pub looping: bool,
336    pub tracks: Vec<BoneTrack>,
337}
338
339impl BoneAnimation {
340    /// Samplea todas las pistas en `t` (envuelto por la duración si es loop) a
341    /// pares `(hueso, pose)`, reusando el buffer `out`.
342    pub fn sample_into(&self, t: f64, out: &mut Vec<(BoneId, Pose)>) {
343        let tt = if self.looping && self.duration > 0.0 {
344            t.rem_euclid(self.duration)
345        } else {
346            t
347        };
348        out.clear();
349        out.extend(self.tracks.iter().map(|tr| (tr.bone, tr.sample(tt))));
350    }
351
352    /// Posa el esqueleto con esta animación en el instante `t` y llama `update`.
353    pub fn apply(&self, skel: &mut Skeleton, t: f64) {
354        let mut buf = Vec::new();
355        self.sample_into(t, &mut buf);
356        for (b, p) in buf {
357            skel.set_pose(b, p);
358        }
359        skel.update();
360    }
361}
362
363/// Posa el esqueleto según el [`RenderFrame`] de una máquina de estados, usando
364/// las `clips` ([`BoneAnimation`]) indexadas por `ClipId`. Durante una
365/// transición blendea **poses** (no píxeles) por la mezcla del frame. Llama
366/// `skel.update()` al final. Es el puente que une máquina-de-estados →
367/// esqueleto: estado/transición deciden la pose, el skinning la convierte en
368/// deformación.
369///
370/// [`RenderFrame`]: crate::RenderFrame
371pub fn pose_from_render_frame(skel: &mut Skeleton, frame: &RenderFrame, clips: &[BoneAnimation]) {
372    let mut poses: HashMap<BoneId, Pose> = HashMap::new();
373    let mut buf = Vec::new();
374    if let Some(a) = clips.get(frame.primary.clip as usize) {
375        a.sample_into(frame.primary.time_secs, &mut buf);
376        for (b, p) in buf.drain(..) {
377            poses.insert(b, p);
378        }
379    }
380    if let Some((inc, mix)) = frame.blend {
381        if let Some(a) = clips.get(inc.clip as usize) {
382            a.sample_into(inc.time_secs, &mut buf);
383            let mix = mix as f64;
384            for (b, p) in buf.drain(..) {
385                poses
386                    .entry(b)
387                    .and_modify(|cur| *cur = lerp_pose(*cur, p, mix))
388                    .or_insert(p);
389            }
390        }
391    }
392    for (b, p) in poses {
393        skel.set_pose(b, p);
394    }
395    skel.update();
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    fn approx(a: Point, x: f64, y: f64) -> bool {
403        (a.x - x).abs() < 1e-9 && (a.y - y).abs() < 1e-9
404    }
405
406    #[test]
407    fn bind_pose_deja_los_vertices_en_reposo() {
408        let mut s = Skeleton::new();
409        let b = s.add_bone(None, Pose::translate(Vec2::new(5.0, 7.0)));
410        s.bind();
411        s.update();
412        let mut m = Mesh::new();
413        m.vertices.push(Vertex::rigid(Point::new(3.0, 4.0), (0.0, 0.0), b));
414        let d = m.deform(&s);
415        // Sin re-posar: la matriz de skinning es identidad → reposo intacto.
416        assert!(approx(d[0], 3.0, 4.0), "fue {:?}", d[0]);
417    }
418
419    #[test]
420    fn rotar_un_hueso_rota_su_vertice() {
421        let mut s = Skeleton::new();
422        let b = s.add_bone(None, Pose::identity());
423        s.bind();
424        // Rotar 90°: (10,0) → (0,10) en convención de kurbo.
425        s.set_pose(b, Pose::rotate(std::f64::consts::FRAC_PI_2));
426        s.update();
427        let mut m = Mesh::new();
428        m.vertices.push(Vertex::rigid(Point::new(10.0, 0.0), (0.0, 0.0), b));
429        let d = m.deform(&s);
430        assert!(approx(d[0], 0.0, 10.0), "fue {:?}", d[0]);
431    }
432
433    #[test]
434    fn cadena_padre_hijo_compone_transforms() {
435        let mut s = Skeleton::new();
436        let root = s.add_bone(None, Pose::identity());
437        let child = s.add_bone(Some(root), Pose::translate(Vec2::new(10.0, 0.0)));
438        s.bind();
439        // Rotar la raíz 90° rota rígidamente al hijo y su vértice.
440        s.set_pose(root, Pose::rotate(std::f64::consts::FRAC_PI_2));
441        s.update();
442        let mut m = Mesh::new();
443        m.vertices
444            .push(Vertex::rigid(Point::new(20.0, 0.0), (0.0, 0.0), child));
445        let d = m.deform(&s);
446        // (20,0) rotado 90° sobre el origen → (0,20).
447        assert!(approx(d[0], 0.0, 20.0), "fue {:?}", d[0]);
448    }
449
450    #[test]
451    fn peso_repartido_mezcla_dos_huesos() {
452        let mut s = Skeleton::new();
453        let a = s.add_bone(None, Pose::identity());
454        let b = s.add_bone(None, Pose::identity());
455        s.bind();
456        // A queda quieto; B se traslada (0,20).
457        s.set_pose(b, Pose::translate(Vec2::new(0.0, 20.0)));
458        s.update();
459        let mut m = Mesh::new();
460        m.vertices.push(Vertex {
461            rest: Point::new(0.0, 0.0),
462            uv: (0.5, 0.5),
463            weights: vec![
464                Weight { bone: a, weight: 0.5 },
465                Weight { bone: b, weight: 0.5 },
466            ],
467        });
468        let d = m.deform(&s);
469        // 0.5·(0,0) + 0.5·(0,20) = (0,10).
470        assert!(approx(d[0], 0.0, 10.0), "fue {:?}", d[0]);
471    }
472
473    #[test]
474    fn pesos_sin_normalizar_se_normalizan() {
475        let mut s = Skeleton::new();
476        let a = s.add_bone(None, Pose::identity());
477        let b = s.add_bone(None, Pose::translate(Vec2::new(0.0, 10.0)));
478        s.bind();
479        s.update();
480        let mut m = Mesh::new();
481        // Pesos 2 y 2 (suman 4) → media ponderada, no suma cruda.
482        m.vertices.push(Vertex {
483            rest: Point::new(0.0, 0.0),
484            uv: (0.0, 0.0),
485            weights: vec![
486                Weight { bone: a, weight: 2.0 },
487                Weight { bone: b, weight: 2.0 },
488            ],
489        });
490        let d = m.deform(&s);
491        // skin_a = id → (0,0); skin_b = id (b en bind pose, no re-posado) → (0,0).
492        // Ambos en reposo: (0,0). (Verifica que no explota por wsum=4.)
493        assert!(approx(d[0], 0.0, 0.0), "fue {:?}", d[0]);
494    }
495
496    #[test]
497    fn vertice_sin_pesos_queda_en_reposo() {
498        let mut s = Skeleton::new();
499        let _b = s.add_bone(None, Pose::rotate(1.0));
500        s.bind();
501        s.update();
502        let mut m = Mesh::new();
503        m.vertices.push(Vertex {
504            rest: Point::new(7.0, 7.0),
505            uv: (0.0, 0.0),
506            weights: vec![],
507        });
508        let d = m.deform(&s);
509        assert!(approx(d[0], 7.0, 7.0), "fue {:?}", d[0]);
510    }
511
512    #[test]
513    fn track_samplea_lineal_entre_keyframes() {
514        let tr = BoneTrack {
515            bone: 0,
516            keys: vec![
517                PoseKey { t: 0.0, pose: Pose::rotate(0.0) },
518                PoseKey { t: 2.0, pose: Pose::rotate(2.0) },
519            ],
520        };
521        assert!((tr.sample(0.0).rotation - 0.0).abs() < 1e-9);
522        assert!((tr.sample(2.0).rotation - 2.0).abs() < 1e-9);
523        assert!((tr.sample(1.0).rotation - 1.0).abs() < 1e-9, "punto medio");
524        // Clamp fuera de rango.
525        assert!((tr.sample(-5.0).rotation - 0.0).abs() < 1e-9);
526        assert!((tr.sample(99.0).rotation - 2.0).abs() < 1e-9);
527    }
528
529    #[test]
530    fn animacion_en_loop_envuelve_el_tiempo() {
531        let anim = BoneAnimation {
532            duration: 2.0,
533            looping: true,
534            tracks: vec![BoneTrack {
535                bone: 0,
536                keys: vec![
537                    PoseKey { t: 0.0, pose: Pose::rotate(0.0) },
538                    PoseKey { t: 2.0, pose: Pose::rotate(2.0) },
539                ],
540            }],
541        };
542        let mut buf = Vec::new();
543        anim.sample_into(3.0, &mut buf); // 3 % 2 = 1 → rotación ~1.0
544        assert!((buf[0].1.rotation - 1.0).abs() < 1e-9, "fue {}", buf[0].1.rotation);
545    }
546
547    #[test]
548    fn render_frame_simple_posa_el_esqueleto() {
549        let mut s = Skeleton::new();
550        let b = s.add_bone(None, Pose::identity());
551        s.bind();
552        let anim = BoneAnimation {
553            duration: 2.0,
554            looping: true,
555            tracks: vec![BoneTrack {
556                bone: b,
557                keys: vec![
558                    PoseKey { t: 0.0, pose: Pose::identity() },
559                    PoseKey {
560                        t: 1.0,
561                        pose: Pose::rotate(std::f64::consts::FRAC_PI_2),
562                    },
563                ],
564            }],
565        };
566        let clips = vec![anim];
567        let frame = crate::RenderFrame {
568            primary: crate::ClipSample { clip: 0, time_secs: 1.0 },
569            blend: None,
570        };
571        pose_from_render_frame(&mut s, &frame, &clips);
572        let mut m = Mesh::new();
573        m.vertices.push(Vertex::rigid(Point::new(10.0, 0.0), (0.0, 0.0), b));
574        let d = m.deform(&s);
575        assert!(approx(d[0], 0.0, 10.0), "fue {:?}", d[0]);
576    }
577
578    #[test]
579    fn render_frame_con_blend_mezcla_poses() {
580        let mut s = Skeleton::new();
581        let b = s.add_bone(None, Pose::identity());
582        s.bind();
583        // clip0: identidad; clip1: 90°. Blend 0.5 → 45°.
584        let rest = BoneAnimation {
585            duration: 1.0,
586            looping: true,
587            tracks: vec![BoneTrack {
588                bone: b,
589                keys: vec![PoseKey { t: 0.0, pose: Pose::identity() }],
590            }],
591        };
592        let turn = BoneAnimation {
593            duration: 1.0,
594            looping: true,
595            tracks: vec![BoneTrack {
596                bone: b,
597                keys: vec![PoseKey {
598                    t: 0.0,
599                    pose: Pose::rotate(std::f64::consts::FRAC_PI_2),
600                }],
601            }],
602        };
603        let clips = vec![rest, turn];
604        let frame = crate::RenderFrame {
605            primary: crate::ClipSample { clip: 0, time_secs: 0.0 },
606            blend: Some((crate::ClipSample { clip: 1, time_secs: 0.0 }, 0.5)),
607        };
608        pose_from_render_frame(&mut s, &frame, &clips);
609        let mut m = Mesh::new();
610        m.vertices.push(Vertex::rigid(Point::new(10.0, 0.0), (0.0, 0.0), b));
611        let d = m.deform(&s);
612        // 10·(cos45, sin45) ≈ (7.071, 7.071).
613        let c = 10.0 * std::f64::consts::FRAC_1_SQRT_2;
614        assert!((d[0].x - c).abs() < 1e-9 && (d[0].y - c).abs() < 1e-9, "fue {:?}", d[0]);
615    }
616}