Skip to main content

primase_core/
skeleton.rs

1//! A skeleton (`core.skeleton`): the joints that skins bind to and clips animate, each with its parent and its local
2//! rest transform as the source authors it. A definition: a skeleton is shared by every mesh bound to it and every
3//! occurrence that poses it, and holds no pose of its own beyond its rest.
4//!
5//! A joint's identity is its [`JointId`], scoped to its skeleton: what bindings, clips and joint maps name. Its name
6//! (and any tag or hash a source keeps beside it) is provenance: names repeat in real data, so nothing finds a joint
7//! by its name. Joints are listed in the source's order; a parent may come after its child.
8
9use std::collections::{BTreeMap, BTreeSet};
10
11use serde::Serialize;
12use serde_json::Value;
13
14use crate::frame::SourceFrame;
15use crate::{Component, Entity, JsonError, Transform, World};
16
17/// A joint's identity within its skeleton (stable while the skeleton is edited; a source's index or id as read).
18#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
19#[serde(transparent)]
20pub struct JointId(pub u32);
21
22/// A joint's rest transform in its parent's space (the skeleton's, for a root): `parent = M * local`. One form is the
23/// authority, as the source states it.
24#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
25pub enum Rest {
26    /// Any affine map (a source storing a matrix).
27    Affine(Transform),
28    /// Translation, rotation, scale, applied scale first: `M = T * R * S` (glTF's node order). `rotation` is a unit
29    /// quaternion `[x, y, z, w]`.
30    Trs {
31        translation: [f32; 3],
32        rotation: [f32; 4],
33        scale: [f32; 3],
34    },
35}
36
37impl Rest {
38    /// The map as an affine transform: a [`Rest::Trs`] composed in f64 and rounded once per entry.
39    pub fn matrix(&self) -> Transform {
40        match *self {
41            Rest::Affine(m) => m,
42            Rest::Trs {
43                translation,
44                rotation,
45                scale,
46            } => {
47                let [x, y, z, w] = rotation.map(f64::from);
48                // A unit quaternion rounded to f32 is not exactly unit (the f32 nearest 1/sqrt(2) twice has norm squared
49                // 1 - 3.4e-8), and the columns below are those of a unit one: made unit first, so the matrix is the
50                // rotation the quaternion stands for and a scale read back from its columns is the scale stored. A zero
51                // or non-finite quaternion is left as it is (a rest the verifier refuses).
52                let norm = (x * x + y * y + z * z + w * w).sqrt();
53                let [x, y, z, w] = if norm > 0.0 && norm.is_finite() {
54                    [x / norm, y / norm, z / norm, w / norm]
55                } else {
56                    [x, y, z, w]
57                };
58                // the rotation's columns (images of x, y, z)
59                let r = [
60                    [
61                        1.0 - 2.0 * (y * y + z * z),
62                        2.0 * (x * y + z * w),
63                        2.0 * (x * z - y * w),
64                    ],
65                    [
66                        2.0 * (x * y - z * w),
67                        1.0 - 2.0 * (x * x + z * z),
68                        2.0 * (y * z + x * w),
69                    ],
70                    [
71                        2.0 * (x * z + y * w),
72                        2.0 * (y * z - x * w),
73                        1.0 - 2.0 * (x * x + y * y),
74                    ],
75                ];
76                let mut cols = [[0.0f32; 3]; 4];
77                for (j, col) in r.iter().enumerate() {
78                    cols[j] = col.map(|v| (v * f64::from(scale[j])) as f32);
79                }
80                cols[3] = translation;
81                Transform { cols }
82            }
83        }
84    }
85
86    /// The rest with both its sides converted from `frame` into the world's (`C M C^-1`, [`SourceFrame::transform`]).
87    /// Exact: a frame is a signed permutation of the axes with a unit scale. A rotation conjugated by a signed
88    /// permutation `C` stays a rotation whose quaternion is `(det(C) C v, w)`; a scale's axes are permuted, the frame's
89    /// signs cancelling (`C diag(s) C^-1`) and the scale's own kept (a mirrored joint stays mirrored); a translation
90    /// converts as a point.
91    pub fn converted(&self, frame: &SourceFrame) -> Rest {
92        match *self {
93            Rest::Affine(m) => Rest::Affine(frame.transform(&m)),
94            Rest::Trs {
95                translation,
96                rotation,
97                scale,
98            } => {
99                let v = frame.direction([rotation[0], rotation[1], rotation[2]]);
100                let v = if frame.determinant() < 0 { v.map(|x| -x) } else { v };
101                // the frame's sign on each axis cancels: direction() applies it once, the axes' own signs once more
102                let signs = frame.direction([1.0; 3]);
103                let d = frame.direction(scale);
104                let s = [d[0] * signs[0], d[1] * signs[1], d[2] * signs[2]];
105                Rest::Trs {
106                    translation: frame.point(translation),
107                    rotation: [v[0], v[1], v[2], rotation[3]],
108                    scale: s,
109                }
110            }
111        }
112    }
113
114    fn values(&self) -> Vec<f32> {
115        match self {
116            Rest::Affine(m) => m.cols.iter().flatten().copied().collect(),
117            Rest::Trs {
118                translation,
119                rotation,
120                scale,
121            } => translation.iter().chain(rotation).chain(scale).copied().collect(),
122        }
123    }
124}
125
126/// One joint of a skeleton.
127#[derive(Clone, Debug, PartialEq, Serialize)]
128pub struct Joint {
129    pub id: JointId,
130    /// The source's name for it: provenance, not identity.
131    pub name: String,
132    /// The joint whose space its rest is in; `None` for a root.
133    pub parent: Option<JointId>,
134    pub rest: Rest,
135}
136
137/// `core.skeleton` (see the module documentation).
138#[derive(Clone, Debug, Default, PartialEq, Serialize)]
139pub struct Skeleton {
140    pub joints: Vec<Joint>,
141}
142
143/// A part skeleton's source-stated hierarchy reference for character assembly. Correspondence is still a
144/// program's choice (names or a joint table); this relation does not equate joints by itself. Corresponding joints
145/// take their parent from the reference, while part-only joints keep their authored hierarchy and bindings.
146/// The reference need not contain every joint of a part and is not itself a drawable part.
147#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
148pub struct AssemblyReference {
149    pub skeleton: Entity,
150}
151
152impl Component for AssemblyReference {
153    const NAME: &'static str = "core.assembly_reference";
154    const GEOMETRY: crate::Geometry = crate::Geometry::None;
155    fn to_json(&self) -> Result<Option<Value>, JsonError> {
156        crate::json::to_value(self).map(Some)
157    }
158    fn references(&self, out: &mut Vec<Entity>) {
159        out.push(self.skeleton);
160    }
161    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
162        if !w.has::<Skeleton>(e) || !w.has::<Skeleton>(self.skeleton) {
163            return Err("assembly reference and its part must both hold core.skeleton".into());
164        }
165        if e == self.skeleton {
166            return Err("a part cannot be its own assembly reference".into());
167        }
168        Ok(())
169    }
170}
171
172/// How far a [`Rest::Trs`] rotation's length may be from 1: sources store unit quaternions rounded to f32 (or
173/// quantised); a length further off is not a rotation.
174const UNIT_TOLERANCE: f32 = 1e-3;
175
176impl Skeleton {
177    /// The position of joint `id` in [`Skeleton::joints`].
178    pub fn index_of(&self, id: JointId) -> Option<usize> {
179        self.joints.iter().position(|j| j.id == id)
180    }
181
182    /// The skeleton with every rest converted from `frame` into the world's ([`Rest::converted`]): for a reader whose
183    /// whole skeleton is in one source frame.
184    pub fn converted(&self, frame: &SourceFrame) -> Skeleton {
185        Skeleton {
186            joints: self
187                .joints
188                .iter()
189                .map(|j| Joint {
190                    rest: j.rest.converted(frame),
191                    ..j.clone()
192                })
193                .collect(),
194        }
195    }
196}
197
198impl Component for Skeleton {
199    const NAME: &'static str = "core.skeleton";
200    /// A definition: no mesh of its entity's is described by it.
201    const GEOMETRY: crate::Geometry = crate::Geometry::None;
202    fn to_json(&self) -> Result<Option<Value>, JsonError> {
203        crate::json::to_value(self).map(Some)
204    }
205    /// Ids unique; each parent a joint of the skeleton other than itself; no cycle through parents; every rest finite;
206    /// a TRS rotation of unit length (within `UNIT_TOLERANCE`).
207    fn verify(&self, _e: Entity, _w: &World) -> Result<(), String> {
208        let mut parents = BTreeMap::new();
209        for j in &self.joints {
210            if parents.insert(j.id, j.parent).is_some() {
211                return Err(format!("joint {:?} listed twice", j.id));
212            }
213            if let Some(i) = j.rest.values().iter().position(|x| !x.is_finite()) {
214                return Err(format!("joint {:?}: rest value {i} is not finite", j.id));
215            }
216            if let Rest::Trs { rotation, .. } = j.rest {
217                let len = rotation.iter().map(|x| x * x).sum::<f32>().sqrt();
218                if (len - 1.0).abs() > UNIT_TOLERANCE {
219                    return Err(format!(
220                        "joint {:?}: rotation of length {len}, not a unit quaternion",
221                        j.id
222                    ));
223                }
224            }
225        }
226        for j in &self.joints {
227            match j.parent {
228                Some(p) if p == j.id => return Err(format!("joint {:?} is its own parent", j.id)),
229                Some(p) if !parents.contains_key(&p) => {
230                    return Err(format!(
231                        "joint {:?}: parent {p:?} is not a joint of the skeleton",
232                        j.id
233                    ));
234                }
235                _ => {}
236            }
237            // up the parents: a cycle comes back to a joint already seen
238            let mut seen = BTreeSet::from([j.id]);
239            let mut at = j.parent;
240            while let Some(p) = at {
241                if !seen.insert(p) {
242                    return Err(format!("joint {:?}: its parents form a cycle", j.id));
243                }
244                at = parents.get(&p).copied().flatten();
245            }
246        }
247        Ok(())
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::frame::Axis;
255
256    const S: f32 = std::f32::consts::FRAC_1_SQRT_2;
257
258    fn joint(id: u32, parent: Option<u32>, rest: Rest) -> Joint {
259        Joint {
260            id: JointId(id),
261            name: format!("j{id}"),
262            parent: parent.map(JointId),
263            rest,
264        }
265    }
266
267    fn trs(translation: [f32; 3], rotation: [f32; 4], scale: [f32; 3]) -> Rest {
268        Rest::Trs {
269            translation,
270            rotation,
271            scale,
272        }
273    }
274
275    fn errors(s: Skeleton) -> Vec<String> {
276        let mut w = World::new();
277        let e = w.spawn();
278        w.insert(e, s);
279        w.verify_components().into_iter().map(|(_, _, m)| m).collect()
280    }
281
282    /// Hand-worked: T(1, 2, 3) R(90 degrees about z) S(2, 1, 1). x scales to (2, 0, 0) then turns to (0, 2, 0); y turns
283    /// to (-1, 0, 0); z stays.
284    #[test]
285    fn trs_composes_translation_rotation_scale() {
286        let m = trs([1.0, 2.0, 3.0], [0.0, 0.0, S, S], [2.0, 1.0, 1.0]).matrix();
287        let near = |a: [f32; 3], b: [f32; 3]| a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6);
288        assert!(near(m.cols[0], [0.0, 2.0, 0.0]), "{m:?}");
289        assert!(near(m.cols[1], [-1.0, 0.0, 0.0]), "{m:?}");
290        assert!(near(m.cols[2], [0.0, 0.0, 1.0]), "{m:?}");
291        assert_eq!(m.cols[3], [1.0, 2.0, 3.0]);
292    }
293
294    /// A unit quaternion rounded to f32 is not exactly unit: (0, 0, S, S) with S the f32 nearest 1/sqrt(2) has norm squared
295    /// 1 - 3.4e-8. The matrix is that of the rotation the quaternion stands for, so its columns are of length 1 (the matrix of
296    /// the quaternion taken as unit had columns 3.4e-8 short), and a scale stored as 1 is read back from them as 1.
297    #[test]
298    fn the_matrix_of_a_quaternion_rounded_to_f32_has_columns_of_the_scale_stored() {
299        let m = trs([0.0; 3], [0.0, 0.0, S, S], [1.0, 1.0, 1.0]).matrix();
300        for c in &m.cols[..3] {
301            let len = c.iter().map(|v| f64::from(*v).powi(2)).sum::<f64>().sqrt();
302            assert!((len - 1.0).abs() < 1e-9, "{c:?} has length {len}");
303        }
304        let scaled = trs([0.0; 3], [0.0, 0.0, S, S], [2.0, 1.0, 3.0]).matrix();
305        for (c, scale) in scaled.cols[..3].iter().zip([2.0, 1.0, 3.0]) {
306            let len = c.iter().map(|v| f64::from(*v).powi(2)).sum::<f64>().sqrt();
307            assert!((len - scale).abs() < 1e-9, "{c:?} has length {len}, not {scale}");
308        }
309    }
310
311    /// Hand-worked frame conversions, both of a proper and of a mirroring frame:
312    /// - ENU (world x = x, y = z, z = -y; det +1): 90 degrees about the source's z (x to y) is 90 degrees about world
313    ///   +y (world x to world -z, which is the source's y): quaternion (0, s, 0, s); (1, 2, 3) is (1, 3, -2).
314    /// - (x, y, -z) (det -1): 90 degrees about x (y to z) becomes y to -z, -90 degrees about x: (-s, 0, 0, s); about
315    ///   z it stays (0, 0, s, s).
316    ///
317    /// Each agrees with converting the rest's matrix.
318    #[test]
319    fn frame_conversion_is_exact_for_proper_and_mirroring_frames() {
320        let enu = SourceFrame::ENU;
321        let r = trs([1.0, 2.0, 3.0], [0.0, 0.0, S, S], [1.0, 2.0, 3.0]).converted(&enu);
322        assert_eq!(r, trs([1.0, 3.0, -2.0], [0.0, S, 0.0, S], [1.0, 3.0, 2.0]));
323        let mirror = SourceFrame::new([Axis::PosX, Axis::PosY, Axis::NegZ], 1.0);
324        assert_eq!(
325            trs([0.0; 3], [S, 0.0, 0.0, S], [1.0; 3]).converted(&mirror),
326            trs([0.0; 3], [-S, 0.0, 0.0, S], [1.0; 3])
327        );
328        assert_eq!(
329            trs([0.0; 3], [0.0, 0.0, S, S], [1.0; 3]).converted(&mirror),
330            trs([0.0; 3], [0.0, 0.0, S, S], [1.0; 3])
331        );
332        // a mirrored joint stays mirrored: its scale's own signs kept, only its axes permuted (the world frame changes
333        // nothing; ENU swaps y and z)
334        let mirrored = trs([0.0; 3], [0.0, 0.0, 0.0, 1.0], [-2.0, 3.0, 4.0]);
335        assert_eq!(mirrored.converted(&SourceFrame::WORLD), mirrored);
336        assert_eq!(
337            mirrored.converted(&enu),
338            trs([0.0; 3], [0.0, 0.0, 0.0, 1.0], [-2.0, 4.0, 3.0])
339        );
340        for frame in [SourceFrame::WORLD, enu, mirror] {
341            for rest in [
342                trs([1.0, 2.0, 3.0], [0.0, 0.0, S, S], [1.0, 2.0, 3.0]),
343                trs([0.5, -1.0, 2.0], [S, 0.0, 0.0, S], [2.0, 1.0, 1.0]),
344                trs([0.5, -1.0, 2.0], [S, 0.0, 0.0, S], [-2.0, 3.0, 4.0]),
345                trs([1.0, 2.0, 3.0], [0.0, 0.0, S, S], [2.0, -1.0, -0.5]),
346            ] {
347                let a = rest.converted(&frame).matrix();
348                let b = frame.transform(&rest.matrix());
349                let near = a
350                    .cols
351                    .iter()
352                    .flatten()
353                    .zip(b.cols.iter().flatten())
354                    .all(|(x, y)| (x - y).abs() < 1e-6);
355                assert!(near, "{frame:?}: {a:?} vs {b:?}");
356            }
357        }
358        // an affine rest converts as a transform, and a skeleton converts every rest, ids and parents unchanged
359        let s = Skeleton {
360            joints: vec![
361                joint(7, None, Rest::Affine(Transform::IDENTITY)),
362                joint(3, Some(7), trs([1.0, 2.0, 3.0], [0.0, 0.0, 0.0, 1.0], [1.0; 3])),
363            ],
364        };
365        let c = s.converted(&enu);
366        assert_eq!(c.joints[0].rest, Rest::Affine(Transform::IDENTITY));
367        assert_eq!(
368            (c.joints[1].id, c.joints[1].parent),
369            (JointId(3), Some(JointId(7)))
370        );
371        assert_eq!(c.index_of(JointId(3)), Some(1));
372    }
373
374    /// A skeleton in the source's order (a parent after its child) verifies; duplicate ids, a missing or own parent,
375    /// a cycle, a non-finite rest and a rotation that is not a unit quaternion each fail, naming the joint.
376    #[test]
377    fn malformed_skeletons_fail_verification() {
378        let id = Rest::Affine(Transform::IDENTITY);
379        let unit = [0.0, 0.0, 0.0, 1.0];
380        assert!(
381            errors(Skeleton {
382                joints: vec![joint(2, Some(1), id), joint(1, None, id)],
383            })
384            .is_empty()
385        );
386        let fails = |joints: Vec<Joint>, what: &str| {
387            let e = errors(Skeleton { joints });
388            assert!(e.iter().any(|m| m.contains(what)), "{what}: {e:?}");
389        };
390        fails(vec![joint(1, None, id), joint(1, None, id)], "listed twice");
391        fails(vec![joint(1, Some(9), id)], "is not a joint");
392        fails(vec![joint(1, Some(1), id)], "its own parent");
393        fails(vec![joint(1, Some(2), id), joint(2, Some(1), id)], "cycle");
394        fails(
395            vec![joint(1, None, trs([f32::NAN, 0.0, 0.0], unit, [1.0; 3]))],
396            "not finite",
397        );
398        fails(
399            vec![joint(1, None, trs([0.0; 3], [0.0, 0.0, 0.0, 2.0], [1.0; 3]))],
400            "not a unit quaternion",
401        );
402    }
403}