1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
31#[serde(transparent)]
32pub struct BindingId(pub u32);
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
36pub enum Influences {
37 Streams { sets: u8 },
41 SingleIndex,
43 Whole { slot: u32 },
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
49pub enum WeightRule {
50 SumsToOne,
52 EngineNormalizes { floor: f32 },
58 AsStored,
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
64pub enum Deformation {
65 Linear,
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
72pub struct Slot {
73 pub joint: JointId,
74 pub inverse_bind: Transform,
75}
76
77#[derive(Clone, Debug, PartialEq, Serialize)]
79pub struct Binding {
80 pub id: BindingId,
81 pub skeleton: Entity,
83 pub palettes: Vec<Option<Vec<Slot>>>,
85 pub deformation: Deformation,
86}
87
88#[derive(Clone, Debug, PartialEq, Serialize)]
90pub struct Skin {
91 pub influences: Vec<Option<Influences>>,
94 pub weights: WeightRule,
95 pub bindings: Vec<Binding>,
96}
97
98impl WeightRule {
99 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
109pub const SUM_TOLERANCE: f32 = 4.0 / 255.0;
112
113fn 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
123fn 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
132pub 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 pub fn binding(&self, id: BindingId) -> Option<&Binding> {
142 self.bindings.iter().find(|b| b.id == id)
143 }
144
145 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 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 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 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 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 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 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 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 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 #[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 fails(
495 &|w, e, _| {
496 w.remove::<Skin>(e);
497 },
498 "without a core.skin",
499 );
500 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 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 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 let rule = WeightRule::EngineNormalizes { floor: 0.001 };
541 assert_eq!(rule.divisor(0.996), Some(0.996));
542 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 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 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 fails(
577 &|w, e, _| w.get_mut::<Skin>(e).unwrap().influences[0] = Some(Influences::Streams { sets: 2 }),
578 "joint and weight streams",
579 );
580 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 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 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 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 #[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 #[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 #[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 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}