Skip to main content

primase_core/
skin.rs

1//! A mesh's skin (`core.skin`): how its vertices are bound to joints, on the entity holding the mesh (a prototype or
2//! one of its level-of-detail forms), so every edit of the geometry carries it.
3//!
4//! Two parts, as sources split them:
5//! - **Influences**, per primitive of the mesh: how its vertices name their joints. With [`Influences::Streams`] the
6//!   joints and weights are the primitive's own vertex streams (`Joints(k)` and `Weights(k)` for each set `k`, four
7//!   influences a set), so every vertex edit carries them with the rest of the vertex; [`Influences::SingleIndex`] is
8//!   one joint a vertex (`Joints(0)` alone, weight 1), [`Influences::Whole`] one joint for the whole primitive (no
9//!   per-vertex data). The joint a vertex names is a slot of the primitive's palette, not a joint itself.
10//! - **Bindings**: each gives the bound primitives a palette, slot by slot a joint of a skeleton and its inverse bind
11//!   (mesh space to that joint's space at bind time), and says how the mesh deforms. One geometry may have several
12//!   bindings (a source: renderers sharing one mesh, each binding its bone slots to other bones); an occurrence
13//!   names the binding it draws with. Inverse binds belong to the binding: two meshes on one skeleton may bind
14//!   differently.
15//!
16//! The only deformation core states is linear blend skinning ([`Deformation::Linear`]); a source deforming otherwise
17//! keeps its data in its namespace until a pass lowers it. Weights are as the source stores them ([`WeightRule`]); no
18//! reader renormalises them.
19
20use std::collections::BTreeSet;
21
22use serde::Serialize;
23use serde_json::Value;
24
25use crate::frame::SourceFrame;
26use crate::skeleton::{JointId, Skeleton};
27use crate::{Component, Entity, JsonError, Mesh, Primitive, Semantic, Stream, Transform, World};
28
29/// A binding's identity within its skin (what an occurrence names).
30#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
31#[serde(transparent)]
32pub struct BindingId(pub u32);
33
34/// How a primitive's vertices name the slots of their palette.
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
36pub enum Influences {
37    /// `Joints(k)` (slot indices, `U8x4` or `U16x4`) and `Weights(k)` (`F32x4`, or `U8x4`/`U16x4` normalised) for each
38    /// set `k` below `sets`: four influences a set. Unused influences are zero-weight padding; their slot indices are
39    /// still in range.
40    Streams { sets: u8 },
41    /// `Joints(0)` alone: each vertex bound wholly to the slot in its first index (one source's static meshes).
42    SingleIndex,
43    /// Every vertex bound wholly to palette slot `slot`, with no per-vertex data (a model on a bone).
44    Whole { slot: u32 },
45}
46
47/// What a source says of its stored weights.
48#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
49pub enum WeightRule {
50    /// Each vertex's weights sum to 1 as stored (within quantisation).
51    SumsToOne,
52    /// The engine divides each weight by `max(sum, floor)` when it draws: a vertex whose weights sum to `floor` or
53    /// more is normalised to sum 1, one that sums to less keeps a total below 1 (all four weights zero leave the
54    /// zero matrix). `floor` is the divisor's lower bound, finite and zero or more; zero is the plain sum (a vertex
55    /// with no weight has nothing to divide, and a pass using the rule refuses it). The source's skinning programs
56    /// use a floor of 0.001.
57    EngineNormalizes { floor: f32 },
58    /// The source states nothing of their sum: used as stored.
59    AsStored,
60}
61
62/// How a binding deforms its mesh.
63#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
64pub enum Deformation {
65    /// Linear blend skinning: a vertex goes to `sum_i w_i J_i B_i v`, `J_i` its joint's pose in the skeleton's space
66    /// and `B_i` the slot's inverse bind; normals and tangents by the same blend of linear parts.
67    Linear,
68}
69
70/// One slot of a palette: the joint and its inverse bind (mesh space to the joint's space at bind time).
71#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
72pub struct Slot {
73    pub joint: JointId,
74    pub inverse_bind: Transform,
75}
76
77/// One way the mesh is bound: to which skeleton, each bound primitive's palette, and how it deforms.
78#[derive(Clone, Debug, PartialEq, Serialize)]
79pub struct Binding {
80    pub id: BindingId,
81    /// The entity holding the `core.skeleton`.
82    pub skeleton: Entity,
83    /// Per primitive of the mesh, its palette: `Some` exactly for the primitives the skin's influences bind.
84    pub palettes: Vec<Option<Vec<Slot>>>,
85    pub deformation: Deformation,
86}
87
88/// `core.skin` (see the module documentation).
89#[derive(Clone, Debug, PartialEq, Serialize)]
90pub struct Skin {
91    /// Per primitive of the mesh: how its vertices name their slots; `None` for a primitive not skinned (drawn as
92    /// stored, with no joint or weight streams).
93    pub influences: Vec<Option<Influences>>,
94    pub weights: WeightRule,
95    pub bindings: Vec<Binding>,
96}
97
98impl WeightRule {
99    /// The divisor the engine applies to a vertex whose weights sum to `sum`; `None` for the rules that apply none
100    /// (the weights are used as stored). The floor is the engine's `f32` constant, widened exactly.
101    pub fn divisor(self, sum: f64) -> Option<f64> {
102        match self {
103            WeightRule::EngineNormalizes { floor } => Some(sum.max(f64::from(floor))),
104            WeightRule::SumsToOne | WeightRule::AsStored => None,
105        }
106    }
107}
108
109/// How far a [`WeightRule::SumsToOne`] vertex's weights may sum from 1: one step of 8-bit storage over four
110/// influences, rounded each.
111pub const SUM_TOLERANCE: f32 = 4.0 / 255.0;
112
113/// A weights stream decoded to floats (integers as normalised); `None` for a type weights can't be.
114fn weights(s: &Stream) -> Option<Vec<[f32; 4]>> {
115    Some(match s {
116        Stream::F32x4(v) => v.clone(),
117        Stream::U8x4(v) => v.iter().map(|w| w.map(|x| f32::from(x) / 255.0)).collect(),
118        Stream::U16x4(v) => v.iter().map(|w| w.map(|x| f32::from(x) / 65535.0)).collect(),
119        Stream::F32x2(_) | Stream::F32x3(_) => return None,
120    })
121}
122
123/// A joints stream's slot indices; `None` for a type joints can't be.
124fn joints(s: &Stream) -> Option<Vec<[u32; 4]>> {
125    Some(match s {
126        Stream::U8x4(v) => v.iter().map(|j| j.map(u32::from)).collect(),
127        Stream::U16x4(v) => v.iter().map(|j| j.map(u32::from)).collect(),
128        Stream::F32x2(_) | Stream::F32x3(_) | Stream::F32x4(_) => return None,
129    })
130}
131
132/// Whether a primitive has any joint or weight stream.
133pub fn has_skin_streams(p: &Primitive) -> bool {
134    p.streams
135        .keys()
136        .any(|s| matches!(s, Semantic::Joints(_) | Semantic::Weights(_)))
137}
138
139impl Skin {
140    /// The binding `id`.
141    pub fn binding(&self, id: BindingId) -> Option<&Binding> {
142        self.bindings.iter().find(|b| b.id == id)
143    }
144
145    /// The skin with its inverse binds converted from `frame` into the world's, for a reader whose mesh and skeleton
146    /// are both in that frame: `B' = C B C^-1` ([`SourceFrame::transform`]), exact.
147    pub fn converted(&self, frame: &SourceFrame) -> Skin {
148        let mut out = self.clone();
149        for slot in out
150            .bindings
151            .iter_mut()
152            .flat_map(|b| b.palettes.iter_mut().flatten().flatten())
153        {
154            slot.inverse_bind = frame.transform(&slot.inverse_bind);
155        }
156        out
157    }
158
159    /// Primitive `i`'s palettes, which need no vertex: each binding has one exactly when the primitive has influences,
160    /// naming joints of its skeleton with finite inverse binds, and a whole-primitive slot is within every one. Returns
161    /// the number of slots every palette has (the shortest's): every slot a vertex names must be below it.
162    fn verify_palettes(&self, i: usize, w: &World) -> Result<usize, String> {
163        let influences = self.influences.get(i).copied().flatten();
164        let slots_in_every_palette = self
165            .bindings
166            .iter()
167            .map(|b| b.palettes.get(i).cloned().flatten().map_or(0, |p| p.len()))
168            .min()
169            .unwrap_or(0);
170        for b in &self.bindings {
171            let palette = b.palettes.get(i).cloned().flatten();
172            if palette.is_some() != influences.is_some() {
173                return Err(format!(
174                    "binding {:?}: primitive {i} has a palette {} influences",
175                    b.id,
176                    if palette.is_some() {
177                        "without"
178                    } else {
179                        "missing for its"
180                    }
181                ));
182            }
183            if let Some(slots) = palette {
184                let skeleton = w
185                    .get::<Skeleton>(b.skeleton)
186                    .ok_or_else(|| format!("binding {:?}: {:?} holds no core.skeleton", b.id, b.skeleton))?;
187                for (k, s) in slots.iter().enumerate() {
188                    if skeleton.index_of(s.joint).is_none() {
189                        return Err(format!(
190                            "binding {:?}: primitive {i} slot {k} names joint {:?}, not in its skeleton",
191                            b.id, s.joint
192                        ));
193                    }
194                    if !s.inverse_bind.cols.iter().flatten().all(|x| x.is_finite()) {
195                        return Err(format!(
196                            "binding {:?}: primitive {i} slot {k}: inverse bind not finite",
197                            b.id
198                        ));
199                    }
200                }
201            }
202        }
203        if let Some(Influences::Whole { slot }) = influences
204            && usize::try_from(slot).map_or(true, |s| s >= slots_in_every_palette)
205        {
206            return Err(format!(
207                "primitive {i}: slot {slot} of a palette of {slots_in_every_palette}"
208            ));
209        }
210        Ok(slots_in_every_palette)
211    }
212
213    /// Primitive `i`'s influences against its streams, and its palettes ([`Skin::verify_palettes`]).
214    fn verify_primitive(&self, i: usize, p: &Primitive, w: &World) -> Result<(), String> {
215        let influences = self.influences.get(i).copied().flatten();
216        let slots_in_every_palette = self.verify_palettes(i, w)?;
217        let skin_streams: BTreeSet<Semantic> = p
218            .streams
219            .keys()
220            .copied()
221            .filter(|s| matches!(s, Semantic::Joints(_) | Semantic::Weights(_)))
222            .collect();
223        let expected: BTreeSet<Semantic> = match influences {
224            None | Some(Influences::Whole { .. }) => BTreeSet::new(),
225            Some(Influences::SingleIndex) => [Semantic::Joints(0)].into(),
226            Some(Influences::Streams { sets }) => (0..sets)
227                .flat_map(|k| [Semantic::Joints(k), Semantic::Weights(k)])
228                .collect(),
229        };
230        if skin_streams != expected {
231            return Err(format!(
232                "primitive {i}: joint and weight streams {skin_streams:?} for influences {influences:?}"
233            ));
234        }
235        let in_range = |slot: u32| usize::try_from(slot).is_ok_and(|s| s < slots_in_every_palette);
236        match influences {
237            None => {}
238            // checked with the palettes
239            Some(Influences::Whole { .. }) => {}
240            Some(Influences::SingleIndex) => {
241                let j = joints(&p.streams[&Semantic::Joints(0)])
242                    .ok_or_else(|| format!("primitive {i}: Joints(0) is not an integer stream"))?;
243                if let Some(v) = j.iter().position(|j| !in_range(j[0])) {
244                    return Err(format!(
245                        "primitive {i}: vertex {v} names slot {} of a palette of {slots_in_every_palette}",
246                        j[v][0]
247                    ));
248                }
249            }
250            Some(Influences::Streams { sets }) => {
251                let vertices = p.vertex_count();
252                let mut sums = vec![0.0f32; vertices];
253                for k in 0..sets {
254                    let j = joints(&p.streams[&Semantic::Joints(k)])
255                        .ok_or_else(|| format!("primitive {i}: Joints({k}) is not an integer stream"))?;
256                    let wt = weights(&p.streams[&Semantic::Weights(k)])
257                        .ok_or_else(|| format!("primitive {i}: Weights({k}) is not a four-value stream"))?;
258                    if j.len() != vertices || wt.len() != vertices {
259                        return Err(format!(
260                            "primitive {i}: Joints({k}) and Weights({k}) hold {} and {} values for {vertices} vertices",
261                            j.len(),
262                            wt.len()
263                        ));
264                    }
265                    for (v, (j, wt)) in j.iter().zip(&wt).enumerate() {
266                        if let Some(x) = j.iter().find(|&&x| !in_range(x)) {
267                            return Err(format!(
268                                "primitive {i}: vertex {v} names slot {x} of a palette of {slots_in_every_palette}"
269                            ));
270                        }
271                        if wt.iter().any(|x| !x.is_finite() || *x < 0.0) {
272                            return Err(format!(
273                                "primitive {i}: vertex {v} has a weight not finite or negative"
274                            ));
275                        }
276                        sums[v] += wt.iter().sum::<f32>();
277                    }
278                }
279                if self.weights == WeightRule::SumsToOne
280                    && let Some(v) = sums.iter().position(|s| (s - 1.0).abs() > SUM_TOLERANCE)
281                {
282                    return Err(format!(
283                        "primitive {i}: vertex {v}'s weights sum to {}, not 1",
284                        sums[v]
285                    ));
286                }
287            }
288        }
289        Ok(())
290    }
291}
292
293impl Component for Skin {
294    const NAME: &'static str = "core.skin";
295    /// Per primitive (influences, palettes); its per-vertex data are the mesh's own streams, which carry themselves.
296    const GEOMETRY: crate::Geometry = crate::Geometry::PerPrimitive;
297    fn to_json(&self) -> Result<Option<Value>, JsonError> {
298        crate::json::to_value(self).map(Some)
299    }
300    fn references(&self, out: &mut Vec<Entity>) {
301        out.extend(self.bindings.iter().map(|b| b.skeleton));
302    }
303    fn remap_primitives(&self, from: &[usize]) -> Option<Result<Self, String>> {
304        let remapped = (|| {
305            let mut out = self.clone();
306            out.influences = crate::remap_list(&self.influences, from)?;
307            for b in &mut out.bindings {
308                b.palettes = crate::remap_list(&b.palettes, from)?;
309            }
310            Ok(out)
311        })();
312        Some(remapped)
313    }
314    /// The entity's mesh space mapped by `l` (a pass baking a placement's mirror or shear into its geometry): each
315    /// inverse bind takes the new mesh space back first, `B' = B L^-1`. A singular `l` has no inverse: unsupported, so
316    /// the World refuses the mapping.
317    fn apply_linear(&self, l: &crate::Linear) -> crate::Linearity<Self> {
318        let Some(inv) = l.inverse_columns() else {
319            return crate::Linearity::Unsupported;
320        };
321        let mut out = self.clone();
322        for slot in out
323            .bindings
324            .iter_mut()
325            .flat_map(|b| b.palettes.iter_mut().flatten().flatten())
326        {
327            let b: [[f64; 3]; 4] = slot.inverse_bind.cols.map(|c| c.map(f64::from));
328            let mut cols = [[0.0f32; 3]; 4];
329            for (j, inv_col) in inv.iter().enumerate() {
330                cols[j] = std::array::from_fn(|r| (0..3).map(|k| b[k][r] * inv_col[k]).sum::<f64>() as f32);
331            }
332            cols[3] = slot.inverse_bind.cols[3];
333            slot.inverse_bind = Transform { cols };
334        }
335        crate::Linearity::Applied(out)
336    }
337    /// One entry of influences and of each binding's palettes per primitive of the entity's mesh (when it has one
338    /// loaded); binding ids unique; each binding's skeleton a `core.skeleton` holding every joint its palettes name;
339    /// inverse binds finite; per primitive, joint and weight streams exactly those its influences say, every slot a
340    /// vertex names within every binding's palette (padding included), weights finite and non-negative, and summing to
341    /// 1 under [`WeightRule::SumsToOne`].
342    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
343        if let WeightRule::EngineNormalizes { floor } = self.weights
344            && (!floor.is_finite() || floor < 0.0)
345        {
346            return Err(format!("weight rule floor {floor}: finite, zero or more"));
347        }
348        let mut ids = BTreeSet::new();
349        for b in &self.bindings {
350            if !ids.insert(b.id) {
351                return Err(format!("binding {:?} listed twice", b.id));
352            }
353            if b.palettes.len() != self.influences.len() {
354                return Err(format!(
355                    "binding {:?}: {} palettes for {} primitives",
356                    b.id,
357                    b.palettes.len(),
358                    self.influences.len()
359                ));
360            }
361        }
362        // a mesh not loaded (only its `core.mesh_file`) has no streams to check: its palettes still are
363        let Some(mesh) = w.get::<Mesh>(e) else {
364            for i in 0..self.influences.len() {
365                self.verify_palettes(i, w)?;
366            }
367            return Ok(());
368        };
369        if mesh.primitives().len() != self.influences.len() {
370            return Err(format!(
371                "{} primitives, {} influence entries",
372                mesh.primitives().len(),
373                self.influences.len()
374            ));
375        }
376        for (i, p) in mesh.primitives().iter().enumerate() {
377            self.verify_primitive(i, p, w)?;
378        }
379        Ok(())
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386    use crate::skeleton::{Joint, Rest};
387    use crate::{Linear, Topology};
388
389    fn translate(t: [f32; 3]) -> Transform {
390        let mut m = Transform::IDENTITY;
391        m.cols[3] = t;
392        m
393    }
394
395    fn joint(id: u32, parent: Option<u32>, t: [f32; 3]) -> Joint {
396        Joint {
397            id: JointId(id),
398            name: format!("j{id}"),
399            parent: parent.map(JointId),
400            rest: Rest::Affine(translate(t)),
401        }
402    }
403
404    /// A skeleton of two joints (a root at the origin, a child 1 m up), a mesh of two primitives: a triangle skinned
405    /// with one set of streams (vertex 0 wholly on slot 0, vertex 1 split half and half, vertex 2 wholly on slot 1)
406    /// and three points bound whole to slot 1.
407    fn world() -> (World, Entity, Entity) {
408        let mut w = World::new();
409        let skeleton = w.spawn();
410        w.insert(
411            skeleton,
412            Skeleton {
413                joints: vec![joint(10, None, [0.0; 3]), joint(11, Some(10), [0.0, 1.0, 0.0])],
414            },
415        );
416        let e = w.spawn();
417        let triangle = Primitive {
418            topology: Topology::Triangles,
419            streams: [
420                (
421                    Semantic::Position,
422                    Stream::F32x3(vec![[0.0; 3], [0.0, 1.0, 0.0], [0.0, 2.0, 0.0]]),
423                ),
424                (
425                    Semantic::Joints(0),
426                    Stream::U8x4(vec![[0, 0, 0, 0], [0, 1, 0, 0], [1, 0, 0, 0]]),
427                ),
428                (
429                    Semantic::Weights(0),
430                    Stream::U8x4(vec![[255, 0, 0, 0], [128, 127, 0, 0], [255, 0, 0, 0]]),
431                ),
432            ]
433            .into(),
434            indices: None,
435            material: None,
436        };
437        let points = Primitive {
438            topology: Topology::Points,
439            streams: [(Semantic::Position, Stream::F32x3(vec![[0.0, 1.0, 0.0]; 3]))].into(),
440            indices: None,
441            material: None,
442        };
443        w.insert(e, Mesh::new(vec![triangle, points]));
444        let palette = vec![
445            Slot {
446                joint: JointId(10),
447                inverse_bind: Transform::IDENTITY,
448            },
449            Slot {
450                joint: JointId(11),
451                inverse_bind: translate([0.0, -1.0, 0.0]),
452            },
453        ];
454        w.insert(
455            e,
456            Skin {
457                influences: vec![
458                    Some(Influences::Streams { sets: 1 }),
459                    Some(Influences::Whole { slot: 1 }),
460                ],
461                weights: WeightRule::SumsToOne,
462                bindings: vec![Binding {
463                    id: BindingId(0),
464                    skeleton,
465                    palettes: vec![Some(palette.clone()), Some(palette)],
466                    deformation: Deformation::Linear,
467                }],
468            },
469        );
470        (w, e, skeleton)
471    }
472
473    fn errors(w: &World) -> Vec<String> {
474        w.verify_components().into_iter().map(|(_, _, m)| m).collect()
475    }
476
477    #[test]
478    fn a_skinned_mesh_verifies_and_reaches_its_skeleton() {
479        let (w, e, skeleton) = world();
480        assert_eq!(errors(&w), Vec::<String>::new());
481        assert_eq!(w.closure([e]), [e, skeleton].into());
482    }
483
484    /// Each broken invariant fails verification, naming what broke.
485    #[test]
486    fn malformed_skins_fail_verification() {
487        let fails = |edit: &dyn Fn(&mut World, Entity, Entity), what: &str| {
488            let (mut w, e, s) = world();
489            edit(&mut w, e, s);
490            let errs = errors(&w);
491            assert!(errs.iter().any(|m| m.contains(what)), "{what}: {errs:?}");
492        };
493        // joint streams on a mesh without a skin: the mesh's own check
494        fails(
495            &|w, e, _| {
496                w.remove::<Skin>(e);
497            },
498            "without a core.skin",
499        );
500        // a vertex naming a slot past the palette
501        fails(
502            &|w, e, _| {
503                let mut m = w.get::<Mesh>(e).unwrap().clone();
504                m.primitive_mut(0).unwrap().streams_mut().insert(
505                    Semantic::Joints(0),
506                    Stream::U8x4(vec![[0, 0, 0, 0], [0, 2, 0, 0], [1, 0, 0, 0]]),
507                );
508                w.insert(e, m);
509            },
510            "names slot 2 of a palette of 2",
511        );
512        // weights not summing to one under SumsToOne; fine as stored
513        let low = |w: &mut World, e: Entity| {
514            let mut m = w.get::<Mesh>(e).unwrap().clone();
515            m.primitive_mut(0).unwrap().streams_mut().insert(
516                Semantic::Weights(0),
517                Stream::U8x4(vec![[255, 0, 0, 0], [100, 100, 0, 0], [255, 0, 0, 0]]),
518            );
519            w.insert(e, m);
520        };
521        fails(&|w, e, _| low(w, e), "sum to");
522        let (mut w, e, _) = world();
523        low(&mut w, e);
524        w.get_mut::<Skin>(e).unwrap().weights = WeightRule::AsStored;
525        assert_eq!(errors(&w), Vec::<String>::new());
526        // the divisor floor of the engine-normalised rule: finite and not negative
527        for floor in [f32::NAN, -0.001, f32::INFINITY] {
528            let (mut w, e, _) = world();
529            w.get_mut::<Skin>(e).unwrap().weights = WeightRule::EngineNormalizes { floor };
530            assert!(
531                errors(&w).iter().any(|m| m.contains("floor")),
532                "{floor}: {:?}",
533                errors(&w)
534            );
535        }
536        let (mut w, e, _) = world();
537        w.get_mut::<Skin>(e).unwrap().weights = WeightRule::EngineNormalizes { floor: 0.001 };
538        assert_eq!(errors(&w), Vec::<String>::new());
539        // the divisor is max(sum, floor)
540        let rule = WeightRule::EngineNormalizes { floor: 0.001 };
541        assert_eq!(rule.divisor(0.996), Some(0.996));
542        // the floor is an f32 (the programs' constant 0x3F50624DE0000000 is 0.001f32 written as a double): widened
543        // exactly, not equal to the f64 literal 0.001
544        assert_eq!(rule.divisor(0.0), Some(f64::from(0.001f32)));
545        assert_ne!(rule.divisor(0.0), Some(0.001f64));
546        assert_eq!(WeightRule::AsStored.divisor(0.5), None);
547        // a negative float weight
548        fails(
549            &|w, e, _| {
550                let mut m = w.get::<Mesh>(e).unwrap().clone();
551                m.primitive_mut(0).unwrap().streams_mut().insert(
552                    Semantic::Weights(0),
553                    Stream::F32x4(vec![
554                        [1.0, 0.0, 0.0, 0.0],
555                        [1.5, -0.5, 0.0, 0.0],
556                        [1.0, 0.0, 0.0, 0.0],
557                    ]),
558                );
559                w.insert(e, m);
560            },
561            "not finite or negative",
562        );
563        // joint and weight streams longer than the positions: named, not a panic (the mesh's own check names it too)
564        fails(
565            &|w, e, _| {
566                let mut m = w.get::<Mesh>(e).unwrap().clone();
567                let mut p = m.primitive_mut(0).unwrap();
568                let streams = p.streams_mut();
569                streams.insert(Semantic::Joints(0), Stream::U8x4(vec![[0, 0, 0, 0]; 4]));
570                streams.insert(Semantic::Weights(0), Stream::U8x4(vec![[255, 0, 0, 0]; 4]));
571                w.insert(e, m);
572            },
573            "hold 4 and 4 values for 3 vertices",
574        );
575        // streams that do not match the influences: two sets said, one stored
576        fails(
577            &|w, e, _| w.get_mut::<Skin>(e).unwrap().influences[0] = Some(Influences::Streams { sets: 2 }),
578            "joint and weight streams",
579        );
580        // a palette joint the skeleton lacks; a skeleton entity without a skeleton
581        fails(
582            &|w, e, _| {
583                w.get_mut::<Skin>(e).unwrap().bindings[0].palettes[1]
584                    .as_mut()
585                    .unwrap()[0]
586                    .joint = JointId(99)
587            },
588            "not in its skeleton",
589        );
590        fails(
591            &|w, _, s| {
592                w.remove::<Skeleton>(s);
593            },
594            "holds no core.skeleton",
595        );
596        // a whole-bound slot past the palette; a palette for a primitive the influences do not bind
597        fails(
598            &|w, e, _| w.get_mut::<Skin>(e).unwrap().influences[1] = Some(Influences::Whole { slot: 5 }),
599            "slot 5 of a palette of 2",
600        );
601        fails(
602            &|w, e, _| w.get_mut::<Skin>(e).unwrap().influences[1] = None,
603            "a palette without influences",
604        );
605        // a mesh still only in its file: no streams to check, its palettes still are
606        let unloaded = |w: &mut World, e: Entity| {
607            w.remove::<Mesh>(e);
608            w.insert(
609                e,
610                crate::MeshFile {
611                    path: "skinned.glb".into(),
612                    mesh: 0,
613                    materials: vec![],
614                },
615            );
616        };
617        fails(
618            &|w, e, _| {
619                unloaded(w, e);
620                w.get_mut::<Skin>(e).unwrap().bindings[0].palettes[1]
621                    .as_mut()
622                    .unwrap()[0]
623                    .joint = JointId(99)
624            },
625            "not in its skeleton",
626        );
627        fails(
628            &|w, e, _| {
629                unloaded(w, e);
630                w.get_mut::<Skin>(e).unwrap().influences[1] = Some(Influences::Whole { slot: 5 })
631            },
632            "slot 5 of a palette of 2",
633        );
634        // the entries do not match the mesh's primitives
635        fails(
636            &|w, e, _| {
637                let mut s = w.get::<Skin>(e).unwrap().clone();
638                s.influences.pop();
639                s.bindings[0].palettes.pop();
640                w.insert(e, s);
641            },
642            "2 primitives, 1 influence entries",
643        );
644    }
645
646    /// A rebuild of the primitive list carries the influences and every binding's palettes (the triangle kept twice,
647    /// the points dropped), and the skin still verifies with the rebuilt mesh.
648    #[test]
649    fn the_skin_follows_a_primitive_rebuild() {
650        let (mut w, e, _) = world();
651        w.rebuild_primitives(e, |m| {
652            Ok((
653                vec![m.primitives()[0].clone(), m.primitives()[0].clone()],
654                vec![0, 0],
655            ))
656        })
657        .unwrap();
658        let s = w.get::<Skin>(e).unwrap();
659        assert_eq!(s.influences, [Some(Influences::Streams { sets: 1 }); 2]);
660        assert_eq!(s.bindings[0].palettes.len(), 2);
661        assert_eq!(errors(&w), Vec::<String>::new());
662    }
663
664    /// Hand-worked: a slot binding joint 11 (at y = 1) has inverse bind T(0, -1, 0); the mesh's space scaled by 2 along
665    /// y puts the vertex that was at (0, 1, 0) at (0, 2, 0). B' = B L^-1 takes (0, 2, 0) to the origin of the joint's
666    /// space, as B took (0, 1, 0): its linear part is diag(1, 0.5, 1), its translation unchanged. A singular map is
667    /// refused.
668    #[test]
669    fn inverse_binds_follow_a_linear_map_of_the_mesh() {
670        let (w, e, _) = world();
671        let skin = w.get::<Skin>(e).unwrap();
672        let l = Linear::new([[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 1.0]]);
673        let crate::Linearity::Applied(mapped) = skin.apply_linear(&l) else {
674            panic!("an invertible map applies")
675        };
676        let b = mapped.bindings[0].palettes[0].as_ref().unwrap()[1].inverse_bind;
677        assert_eq!(
678            b.cols,
679            [
680                [1.0, 0.0, 0.0],
681                [0.0, 0.5, 0.0],
682                [0.0, 0.0, 1.0],
683                [0.0, -1.0, 0.0]
684            ]
685        );
686        assert_eq!(b.apply([0.0, 2.0, 0.0]), [0.0, 0.0, 0.0]);
687        let flat = Linear::new([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
688        assert_eq!(skin.apply_linear(&flat), crate::Linearity::Unsupported);
689    }
690
691    /// A reader converting mesh, skeleton and skin from one source frame keeps the bind: the converted joint's rest
692    /// times the converted inverse bind takes the converted vertex where the source's took the source's, converted.
693    #[test]
694    fn frame_conversion_keeps_the_bind() {
695        let (w, e, s) = world();
696        let frame = SourceFrame::ENU;
697        let skin = w.get::<Skin>(e).unwrap().converted(&frame);
698        let skeleton = w.get::<Skeleton>(s).unwrap().converted(&frame);
699        let joint = skeleton.joints[1].rest.matrix();
700        let b = skin.bindings[0].palettes[0].as_ref().unwrap()[1].inverse_bind;
701        let v = [0.0, 2.0, 0.0];
702        // source: rest T(0, 1, 0), inverse bind T(0, -1, 0): v stays where it is
703        assert_eq!(joint.apply(b.apply(frame.point(v))), frame.point(v));
704        assert_eq!(b, frame.transform(&translate([0.0, -1.0, 0.0])));
705    }
706}