Skip to main content

roxlap_core/
kfa_draw.rs

1//! KFA (animated kv6) sprite renderer — voxlap's `kfadraw`
2//! (`voxlap5.c:9759`) plus the bone-transform helpers
3//! `genperp` (voxlap5.c:9546), `mat0` (voxlap5.c:9568), and
4//! `setlimb` (voxlap5.c:9643).
5//!
6//! A KFA sprite is a hierarchy of bones; each bone carries an
7//! optional [`Sprite`] (= one kv6 limb) plus a hinge tying it to
8//! its parent. Per frame, the user updates `kfaval[i]` (a 16-bit
9//! angle) for each animated bone; this module walks the hinge
10//! tree in topological order and computes each limb's world
11//! transform from the parent's, then dispatches the existing
12//! [`crate::sprite::draw_sprite`] per limb to rasterise the kv6
13//! data.
14//!
15//! Voxlap's animation-curve playback (`animsprite` + `seq[]` +
16//! `frmval[]` interpolation) lives in
17//! [`roxlap_formats::kfa::KfaSprite::animsprite`] — call it to
18//! advance `kfaval[]` from a baked curve, or poke `kfaval[]`
19//! directly for procedural animation, then render here. The bone
20//! posing this module computes is also exposed standalone as
21//! [`solve_kfa_limbs`] for non-CPU backends (the GPU sprite pass).
22//!
23//! No oracle pose exercises KFA, so this module's correctness
24//! gate is "looks right + tests verify the bone math". We can
25//! tighten validation when a real `.kfa` asset lands.
26
27#![allow(
28    clippy::cast_possible_truncation,
29    clippy::cast_possible_wrap,
30    clippy::cast_sign_loss,
31    clippy::cast_precision_loss,
32    clippy::similar_names,
33    clippy::too_many_arguments,
34    clippy::doc_markdown,
35    clippy::many_single_char_names,
36    clippy::missing_panics_doc,
37    clippy::float_cmp,
38    clippy::useless_vec
39)]
40
41use roxlap_formats::kfa::{Hinge, KfaSprite, Point3};
42use roxlap_formats::sprite::Sprite;
43
44use crate::camera_math::CameraState;
45use crate::opticast::OpticastSettings;
46use crate::sprite::{draw_sprite, mat2, DrawTarget, SpriteLighting};
47
48/// Voxlap's `genperp` — given a non-zero axis vector `a`, build
49/// two orthonormal vectors `b`, `c` such that `(a, b, c)` form a
50/// right-handed orthonormal basis with `a` along its primary axis.
51/// Mirror of voxlap5.c:9546-9561.
52///
53/// If `a` is zero, returns `([0; 3], [0; 3])` (matches voxlap's
54/// degenerate-input zeroing).
55fn genperp(a: [f32; 3]) -> ([f32; 3], [f32; 3]) {
56    if a == [0.0, 0.0, 0.0] {
57        return ([0.0; 3], [0.0; 3]);
58    }
59    // Pick the smallest-magnitude axis to zero out in `b`, so the
60    // remaining two components dominate and the cross-product
61    // `c = a × b` stays well-conditioned.
62    let ax = a[0].abs();
63    let ay = a[1].abs();
64    let az = a[2].abs();
65    let b = if ax < ay && ax < az {
66        let t = 1.0 / (a[1] * a[1] + a[2] * a[2]).sqrt();
67        [0.0, a[2] * t, -a[1] * t]
68    } else if ay < az {
69        let t = 1.0 / (a[0] * a[0] + a[2] * a[2]).sqrt();
70        [-a[2] * t, 0.0, a[0] * t]
71    } else {
72        let t = 1.0 / (a[0] * a[0] + a[1] * a[1]).sqrt();
73        [a[1] * t, -a[0] * t, 0.0]
74    };
75    let c = [
76        a[1] * b[2] - a[2] * b[1],
77        a[2] * b[0] - a[0] * b[2],
78        a[0] * b[1] - a[1] * b[0],
79    ];
80    (b, c)
81}
82
83/// Voxlap's `mat0` (`voxlap5.c:9568`) — given `B` and `C` such
84/// that `A * B = C`, find `A`. Returns `(a_s, a_h, a_f, a_o)`.
85///
86/// Used by `setlimb` to find the rotation matrix that maps the
87/// parent's hinge frame to the child's hinge frame.
88fn mat0(
89    b_s: [f32; 3],
90    b_h: [f32; 3],
91    b_f: [f32; 3],
92    b_o: [f32; 3],
93    c_s: [f32; 3],
94    c_h: [f32; 3],
95    c_f: [f32; 3],
96    c_o: [f32; 3],
97) -> ([f32; 3], [f32; 3], [f32; 3], [f32; 3]) {
98    // A's columns = C's columns expressed in B's frame.
99    // Voxlap evaluates as `bs.row * c.col + bh.row * c.col + bg.row * c.col`
100    // for each (row, output-col) pair.
101    let ts = [
102        b_s[0] * c_s[0] + b_h[0] * c_h[0] + b_f[0] * c_f[0],
103        b_s[0] * c_s[1] + b_h[0] * c_h[1] + b_f[0] * c_f[1],
104        b_s[0] * c_s[2] + b_h[0] * c_h[2] + b_f[0] * c_f[2],
105    ];
106    let th = [
107        b_s[1] * c_s[0] + b_h[1] * c_h[0] + b_f[1] * c_f[0],
108        b_s[1] * c_s[1] + b_h[1] * c_h[1] + b_f[1] * c_f[1],
109        b_s[1] * c_s[2] + b_h[1] * c_h[2] + b_f[1] * c_f[2],
110    ];
111    let tf = [
112        b_s[2] * c_s[0] + b_h[2] * c_h[0] + b_f[2] * c_f[0],
113        b_s[2] * c_s[1] + b_h[2] * c_h[1] + b_f[2] * c_f[1],
114        b_s[2] * c_s[2] + b_h[2] * c_h[2] + b_f[2] * c_f[2],
115    ];
116    let to = [
117        c_o[0] - b_o[0] * ts[0] - b_o[1] * th[0] - b_o[2] * tf[0],
118        c_o[1] - b_o[0] * ts[1] - b_o[1] * th[1] - b_o[2] * tf[1],
119        c_o[2] - b_o[0] * ts[2] - b_o[1] * th[2] - b_o[2] * tf[2],
120    ];
121    (ts, th, tf, to)
122}
123
124#[inline]
125fn pt(p: Point3) -> [f32; 3] {
126    [p.x, p.y, p.z]
127}
128
129/// Convert voxlap's i16 hinge angle (full circle = 65536) to
130/// `(cos, sin)` floats. Voxlap C uses `ucossin(((int32_t)val)<<16,
131/// ...)`, a precomputed 256-entry table. Our port uses libm
132/// `cos/sin` — no oracle pose exercises this code so the small
133/// precision difference vs voxlap C's table is acceptable.
134fn cossin_q15(val: i16) -> [f32; 2] {
135    let ang = (i32::from(val) as f32) * (std::f32::consts::PI * 2.0 / 65536.0);
136    [ang.cos(), ang.sin()]
137}
138
139/// Voxlap's `setlimb` (`voxlap5.c:9643`) — compute child limb
140/// `i`'s world transform from parent limb `p`'s world transform
141/// via the hinge connecting them.
142///
143/// Math:
144/// 1. Build the child-side velcro frame from the hinge: `qp =
145///    hinge.p[0]`, `(qs, qh, qf) = (hinge.v[0], genperp(hinge.v[0]))`.
146/// 2. Apply the hinge transform — for `htype == 0` (rotate around
147///    `qs` by `val` angle), rotate `(qh, qf)` in their plane.
148/// 3. Build the parent-side velcro frame: `pp = hinge.p[1]`,
149///    `(ps, ph, pf) = (hinge.v[1], genperp(hinge.v[1]))`.
150/// 4. `mat0`: find `R` such that `R * (ps, ph, pf, pp) = (qs, qh,
151///    qf, qp)` — `R` is the limb's hinge rotation in parent
152///    coords.
153/// 5. `mat2`: `child_world = parent_world * R`. The limb's `(s,
154///    h, f, p)` transform updates in place.
155fn setlimb(limbs: &mut [Sprite], hinges: &[Hinge], i: usize, parent: usize, htype: u8, val: i16) {
156    let hinge = &hinges[i];
157
158    // Step 1: child-side velcro frame.
159    let qp = pt(hinge.p[0]);
160    let mut qs = pt(hinge.v[0]);
161    let (mut qh, mut qf) = genperp(qs);
162
163    // Step 2: apply hinge transform.
164    if htype == 0 {
165        // Rotate (qh, qf) around qs by `val` (Q15 angle).
166        let r = cossin_q15(val);
167        let ph = qh;
168        let pf = qf;
169        qh = [
170            ph[0] * r[0] - pf[0] * r[1],
171            ph[1] * r[0] - pf[1] * r[1],
172            ph[2] * r[0] - pf[2] * r[1],
173        ];
174        qf = [
175            ph[0] * r[1] + pf[0] * r[0],
176            ph[1] * r[1] + pf[1] * r[0],
177            ph[2] * r[1] + pf[2] * r[0],
178        ];
179    } else {
180        // Voxlap's only documented htype is 0; non-0 hits a MSVC
181        // `__assume(0)`. Treat as no rotation.
182        // (Suppress the unused `qs` warning if we add types later.)
183        let _ = (&mut qs, &mut qh, &mut qf);
184    }
185
186    // Step 3: parent-side velcro frame.
187    let pp = pt(hinge.p[1]);
188    let ps = pt(hinge.v[1]);
189    let (ph, pf) = genperp(ps);
190
191    // Step 4: mat0 — find R such that R * (ps,ph,pf,pp) = (qs,qh,qf,qp).
192    // R reuses the (qs, qh, qf, qp) variables in voxlap's C code.
193    let (rs, rh, rf, ro) = mat0(ps, ph, pf, pp, qs, qh, qf, qp);
194
195    // Step 5: mat2 — child_world = parent_world * R.
196    let parent_s = limbs[parent].s;
197    let parent_h = limbs[parent].h;
198    let parent_f = limbs[parent].f;
199    let parent_p = limbs[parent].p;
200    let (cs, ch, cf, co) = mat2(parent_s, parent_h, parent_f, parent_p, rs, rh, rf, ro);
201    let child = &mut limbs[i];
202    child.s = cs;
203    child.h = ch;
204    child.f = cf;
205    child.p = co;
206}
207
208/// Render an animated KFA sprite — voxlap's `kfadraw`
209/// (voxlap5.c:9759). Walks the bone tree in topological order
210/// (parents first), computes each limb's world transform from
211/// the parent's via the per-limb `setlimb` walk, then dispatches
212/// [`crate::sprite::draw_sprite`] per limb to rasterise its kv6.
213///
214/// Returns the total number of pixels written across all limbs.
215pub fn draw_kfa_sprite(
216    target: &mut DrawTarget<'_>,
217    cam: &CameraState,
218    settings: &OpticastSettings,
219    lighting: &SpriteLighting<'_>,
220    kfa: &mut KfaSprite,
221) -> u32 {
222    // Pose first, then rasterise. Voxlap interleaves the two in one
223    // descending loop, but a limb's transform depends only on its
224    // (already-posed) parent — never on drawing — so a full solve pass
225    // followed by a full draw pass is identical, and lets non-CPU
226    // backends reuse `solve_kfa_limbs` verbatim.
227    solve_kfa_limbs(kfa);
228    let n = kfa.hinge_sort.len();
229    let mut total: u32 = 0;
230    for k in (0..n).rev() {
231        let j = kfa.hinge_sort[k];
232        total += draw_sprite(target, cam, settings, lighting, &kfa.limbs[j]);
233    }
234    total
235}
236
237/// Pose every limb of a KFA sprite — the bone-transform half of
238/// [`draw_kfa_sprite`], without rasterising. Walks the hinge tree in
239/// topological order (parents first) and writes each limb's world
240/// `(s, h, f, p)` from its parent's via the per-limb `setlimb` math,
241/// reading the current [`KfaSprite::kfaval`] angles. Mirror of the
242/// transform portion of voxlap's `kfadraw` (voxlap5.c:9759).
243///
244/// Split out so non-CPU backends (e.g. the GPU instanced-sprite pass)
245/// can run the exact same posing and then consume `kfa.limbs[*]`
246/// transforms however they need. The host typically calls
247/// [`KfaSprite::animsprite`](roxlap_formats::kfa::KfaSprite::animsprite)
248/// to advance `kfaval[]` first, then this to resolve world transforms.
249pub fn solve_kfa_limbs(kfa: &mut KfaSprite) {
250    // Voxlap iterates `for i = numhin-1; i >= 0; i--`; sort_hinges
251    // puts parents at high indices, so descending iteration walks
252    // parents first.
253    let n = kfa.hinge_sort.len();
254    for k in (0..n).rev() {
255        let j = kfa.hinge_sort[k];
256        let parent = kfa.hinges[j].parent;
257        if parent >= 0 {
258            // Child bone: derive transform from parent.
259            let htype = kfa.hinges[j].htype;
260            let val = kfa.kfaval[j];
261            setlimb(&mut kfa.limbs, &kfa.hinges, j, parent as usize, htype, val);
262        } else {
263            // Root bone: copy world basis from KfaSprite + apply
264            // hinge.p[0] as the velcro offset (voxlap5.c:9772-9782).
265            let s = kfa.s;
266            let h = kfa.h;
267            let f = kfa.f;
268            let p_world = kfa.p;
269            let tp = pt(kfa.hinges[j].p[0]);
270            let limb = &mut kfa.limbs[j];
271            limb.s = s;
272            limb.h = h;
273            limb.f = f;
274            limb.p = [
275                p_world[0] - tp[0] * s[0] - tp[1] * h[0] - tp[2] * f[0],
276                p_world[1] - tp[0] * s[1] - tp[1] * h[1] - tp[2] * f[1],
277                p_world[2] - tp[0] * s[2] - tp[1] * h[2] - tp[2] * f[2],
278            ];
279        }
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    /// genperp produces an orthonormal basis with the input axis
288    /// as the dominant direction.
289    #[test]
290    fn genperp_orthonormal() {
291        let a = [1.0_f32, 0.0, 0.0];
292        let (b, c) = genperp(a);
293        // b · a = 0
294        assert!((a[0] * b[0] + a[1] * b[1] + a[2] * b[2]).abs() < 1e-6);
295        // c · a = 0
296        assert!((a[0] * c[0] + a[1] * c[1] + a[2] * c[2]).abs() < 1e-6);
297        // b · c = 0
298        assert!((b[0] * c[0] + b[1] * c[1] + b[2] * c[2]).abs() < 1e-6);
299        // |b| ≈ 1
300        let lb = b[0] * b[0] + b[1] * b[1] + b[2] * b[2];
301        assert!((lb - 1.0).abs() < 1e-5, "|b|² = {lb}");
302        // |c| ≈ 1
303        let lc = c[0] * c[0] + c[1] * c[1] + c[2] * c[2];
304        assert!((lc - 1.0).abs() < 1e-5, "|c|² = {lc}");
305    }
306
307    /// genperp of zero vector returns zero vectors.
308    #[test]
309    fn genperp_zero() {
310        let (b, c) = genperp([0.0, 0.0, 0.0]);
311        assert_eq!(b, [0.0, 0.0, 0.0]);
312        assert_eq!(c, [0.0, 0.0, 0.0]);
313    }
314}