Skip to main content

banished_formats/
skeleton.rs

1//! Skeleton maths for `GraphicsMesh` bones.
2//!
3//! Convention (verified against all eight skinned meshes, combining weighted geometry with
4//! rigid node geometry when comparing stored bounds):
5//! - stored quaternions are conjugated relative to the column-vector convention used here;
6//! - world = parent_world * local;
7//! - skinning matrix = world * inverse_bind, where `inv_rotation`/`inv_translation` is the inverse bind.
8//!
9//! Skinned vertices are stored in a per-mesh frame; the bind-pose palette maps them to model space.
10
11use crate::types::Bone;
12
13/// A bone transform: a rotation and a translation.
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct Transform {
16    /// Unit quaternion (x, y, z, w), column-vector convention.
17    pub rotation: [f32; 4],
18    /// The translation.
19    pub translation: [f32; 3],
20}
21
22fn qmul(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
23    let [ax, ay, az, aw] = a;
24    let [bx, by, bz, bw] = b;
25    [
26        aw * bx + ax * bw + ay * bz - az * by,
27        aw * by - ax * bz + ay * bw + az * bx,
28        aw * bz + ax * by - ay * bx + az * bw,
29        aw * bw - ax * bx - ay * by - az * bz,
30    ]
31}
32
33fn conj(q: [f32; 4]) -> [f32; 4] {
34    [-q[0], -q[1], -q[2], q[3]]
35}
36
37fn rotate(q: [f32; 4], v: [f32; 3]) -> [f32; 3] {
38    let p = qmul(qmul(q, [v[0], v[1], v[2], 0.0]), conj(q));
39    [p[0], p[1], p[2]]
40}
41
42impl Transform {
43    /// The transform that changes nothing.
44    pub const IDENTITY: Transform = Transform {
45        rotation: [0.0, 0.0, 0.0, 1.0],
46        translation: [0.0; 3],
47    };
48
49    /// `self * other` (apply `other` first).
50    pub fn mul(&self, other: &Transform) -> Transform {
51        let t = rotate(self.rotation, other.translation);
52        Transform {
53            rotation: qmul(self.rotation, other.rotation),
54            translation: [
55                t[0] + self.translation[0],
56                t[1] + self.translation[1],
57                t[2] + self.translation[2],
58            ],
59        }
60    }
61
62    /// A point moved by the transform: rotated, then translated.
63    pub fn apply(&self, v: [f32; 3]) -> [f32; 3] {
64        let r = rotate(self.rotation, v);
65        [
66            r[0] + self.translation[0],
67            r[1] + self.translation[1],
68            r[2] + self.translation[2],
69        ]
70    }
71
72    /// Column-major 4x4 matrix.
73    pub fn to_matrix(&self) -> [f32; 16] {
74        let [x, y, z, w] = self.rotation;
75        let [tx, ty, tz] = self.translation;
76        [
77            1.0 - 2.0 * (y * y + z * z),
78            2.0 * (x * y + z * w),
79            2.0 * (x * z - y * w),
80            0.0,
81            2.0 * (x * y - z * w),
82            1.0 - 2.0 * (x * x + z * z),
83            2.0 * (y * z + x * w),
84            0.0,
85            2.0 * (x * z + y * w),
86            2.0 * (y * z - x * w),
87            1.0 - 2.0 * (x * x + y * y),
88            0.0,
89            tx,
90            ty,
91            tz,
92            1.0,
93        ]
94    }
95}
96
97/// Bone-local bind transform.
98pub fn local_bind(bone: &Bone) -> Transform {
99    Transform {
100        rotation: conj(bone.rotation),
101        translation: bone.translation,
102    }
103}
104
105/// The inverse of a bone's bind transform, for skinning.
106pub fn inverse_bind(bone: &Bone) -> Transform {
107    Transform {
108        rotation: conj(bone.inv_rotation),
109        translation: bone.inv_translation,
110    }
111}
112
113/// World transforms for the given local transforms (bones are stored parents-first).
114pub fn world_transforms(bones: &[Bone], locals: &[Transform]) -> Vec<Transform> {
115    let mut world: Vec<Transform> = Vec::with_capacity(bones.len());
116    for (bone, local) in bones.iter().zip(locals) {
117        let w = match usize::try_from(bone.parent).ok().and_then(|p| world.get(p)) {
118            Some(parent) => parent.mul(local),
119            None => *local,
120        };
121        world.push(w);
122    }
123    world
124}
125
126/// Skinning palette (world * inverse bind) for the given local pose.
127pub fn palette(bones: &[Bone], locals: &[Transform]) -> Vec<Transform> {
128    world_transforms(bones, locals)
129        .iter()
130        .zip(bones)
131        .map(|(w, b)| w.mul(&inverse_bind(b)))
132        .collect()
133}
134
135/// Palette for the bind pose.
136pub fn bind_palette(bones: &[Bone]) -> Vec<Transform> {
137    let locals: Vec<Transform> = bones.iter().map(local_bind).collect();
138    palette(bones, &locals)
139}
140
141/// Skin 32-byte `ModelSkinLayout` vertices (i16x4 position /512, i16x4 texcoords, unorm8x4
142/// normal, 4 unused bytes, unorm8x4 weights, u8x4 bone indices) with a palette into the 24-byte
143/// static `ModelLayout` (position, texcoords, normal, zeroed user data). Used to draw characters
144/// in a fixed pose without GPU skinning.
145pub fn skin_to_static(vertices: &[u8], palette: &[Transform]) -> Vec<u8> {
146    use crate::types::{SKINNED_STRIDE, STATIC_STRIDE};
147    let mut out = Vec::with_capacity(vertices.len() / SKINNED_STRIDE * STATIC_STRIDE);
148    for v in vertices.chunks_exact(SKINNED_STRIDE) {
149        let i16_at = |o: usize| i16::from_le_bytes([v[o], v[o + 1]]) as f32;
150        let p = [i16_at(0) / 512.0, i16_at(2) / 512.0, i16_at(4) / 512.0];
151        let n = [
152            v[16] as f32 / 127.5 - 1.0,
153            v[17] as f32 / 127.5 - 1.0,
154            v[18] as f32 / 127.5 - 1.0,
155        ];
156        let (mut sp, mut sn) = ([0.0f32; 3], [0.0f32; 3]);
157        for k in 0..4 {
158            let w = v[24 + k] as f32 / 255.0;
159            if w == 0.0 {
160                continue;
161            }
162            let t = palette
163                .get(v[28 + k] as usize)
164                .copied()
165                .unwrap_or(Transform::IDENTITY);
166            let q = t.apply(p);
167            let r = t.apply([0.0; 3]);
168            let m = t.apply(n);
169            for a in 0..3 {
170                sp[a] += q[a] * w;
171                sn[a] += (m[a] - r[a]) * w;
172            }
173        }
174        let len = (sn[0] * sn[0] + sn[1] * sn[1] + sn[2] * sn[2])
175            .sqrt()
176            .max(1e-6);
177        for value in sp {
178            out.extend_from_slice(&((value * 512.0).round() as i16).to_le_bytes());
179        }
180        out.extend_from_slice(&v[6..16]);
181        for value in sn {
182            out.push(((value / len * 0.5 + 0.5) * 255.0).round() as u8);
183        }
184        out.push(v[19]);
185        out.extend_from_slice(&[0; 4]);
186    }
187    out
188}