1use glam::{Mat3, Mat4, Quat, Vec3};
9use serde::{Deserialize, Serialize};
10use std::collections::{BTreeMap, BTreeSet};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[non_exhaustive]
19pub enum AffineDomainViolation {
20 NonUniformScale,
22 Sheared,
24 Reflected,
26 Singular,
28 NonFinite,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq)]
39pub(crate) struct PositiveUniformAffineTolerance {
40 pub(crate) equal_axis: f64,
41 pub(crate) relative_orthogonality: f64,
42 pub(crate) singular_determinant_relative: f64,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq)]
52pub(crate) struct AffineGeometryFacts {
53 pub(crate) axis_lengths: [f64; 3],
54 pub(crate) mean_axis_length: f64,
55 pub(crate) determinant: f64,
56 pub(crate) axis_length_product: f64,
57 pub(crate) cross_axis_dots: [f64; 3],
59}
60
61impl AffineGeometryFacts {
62 pub(crate) fn from_linear(linear: Mat3) -> Result<Self, AffineDomainViolation> {
68 if !linear.is_finite() {
69 return Err(AffineDomainViolation::NonFinite);
70 }
71 let columns = [
72 linear.x_axis.as_dvec3(),
73 linear.y_axis.as_dvec3(),
74 linear.z_axis.as_dvec3(),
75 ];
76 let axis_lengths = affine_axis_lengths(linear);
77 let mean_axis_length = average_affine_axis_length(axis_lengths);
78 let determinant = columns[2].dot(columns[0].cross(columns[1]));
79 let axis_length_product = axis_lengths[0] * axis_lengths[1] * axis_lengths[2];
80 let cross_axis_dots = [
81 columns[0].dot(columns[1]),
82 columns[0].dot(columns[2]),
83 columns[1].dot(columns[2]),
84 ];
85 if axis_lengths.iter().any(|value| !value.is_finite())
86 || !mean_axis_length.is_finite()
87 || !determinant.is_finite()
88 || !axis_length_product.is_finite()
89 || cross_axis_dots.iter().any(|value| !value.is_finite())
90 {
91 return Err(AffineDomainViolation::NonFinite);
92 }
93 Ok(Self {
94 axis_lengths,
95 mean_axis_length,
96 determinant,
97 axis_length_product,
98 cross_axis_dots,
99 })
100 }
101
102 pub(crate) fn has_equal_axis_lengths(self, relative_tolerance: f64) -> bool {
108 values_equal_to_mean(
109 &self.axis_lengths,
110 self.mean_axis_length,
111 relative_tolerance,
112 )
113 }
114}
115
116pub(crate) fn values_equal_to_mean(values: &[f64], mean: f64, relative_tolerance: f64) -> bool {
119 values
120 .iter()
121 .all(|&value| (value - mean).abs() <= relative_tolerance * mean.abs().max(value.abs()))
122}
123
124pub(crate) fn classify_positive_uniform_affine(
131 linear: Mat3,
132 tolerance: PositiveUniformAffineTolerance,
133) -> Result<f64, AffineDomainViolation> {
134 let facts = AffineGeometryFacts::from_linear(linear)?;
135 if facts.mean_axis_length <= 0.0 {
136 return Err(AffineDomainViolation::Singular);
137 }
138
139 if facts.determinant.abs()
146 <= tolerance.singular_determinant_relative * facts.axis_length_product
147 {
148 return Err(AffineDomainViolation::Singular);
149 }
150 if !facts.has_equal_axis_lengths(tolerance.equal_axis) {
154 return Err(AffineDomainViolation::NonUniformScale);
155 }
156
157 let orthogonality_tolerance =
160 tolerance.relative_orthogonality * facts.mean_axis_length * facts.mean_axis_length;
161 if facts
162 .cross_axis_dots
163 .iter()
164 .any(|dot| dot.abs() > orthogonality_tolerance)
165 {
166 return Err(AffineDomainViolation::Sheared);
167 }
168 if facts.determinant < 0.0 {
169 return Err(AffineDomainViolation::Reflected);
170 }
171 Ok(facts.mean_axis_length)
172}
173
174pub(crate) fn affine_axis_lengths(linear: Mat3) -> [f64; 3] {
179 [
180 linear.x_axis.as_dvec3().length(),
181 linear.y_axis.as_dvec3().length(),
182 linear.z_axis.as_dvec3().length(),
183 ]
184}
185
186pub(crate) fn average_affine_axis_length(lengths: [f64; 3]) -> f64 {
192 let mut ascending = lengths;
193 ascending.sort_by(f64::total_cmp);
194 (ascending[0] + ascending[1] + ascending[2]) / 3.0
195}
196
197#[cfg(test)]
198pub(crate) mod affine_test_fixtures {
199 use super::{Mat3, Vec3};
200
201 pub(crate) fn tolerance_divergence_basis() -> Mat3 {
205 Mat3::from_diagonal(Vec3::new(1.0, 1.000_05, 1.0))
206 }
207
208 pub(crate) fn orthogonality_tolerance_divergence_basis() -> Mat3 {
211 Mat3::from_cols(Vec3::X, Vec3::new(5.0e-5, 1.0, 0.0), Vec3::Z)
212 }
213
214 pub(crate) fn appendix_d_v6_mean_permutations() -> [Mat3; 6] {
219 let columns = [
220 Vec3::new(
221 f32::from_bits(0x3f0e_8cbb),
222 f32::from_bits(0x3f26_fbbe),
223 f32::from_bits(0x3f21_9bc7),
224 ),
225 Vec3::new(
226 f32::from_bits(0x3d9c_b415),
227 f32::from_bits(0x3e92_d82b),
228 f32::from_bits(0x3f82_e85d),
229 ),
230 Vec3::new(
231 f32::from_bits(0x3f14_5226),
232 f32::from_bits(0x3e9e_e50d),
233 f32::from_bits(0x3f56_817c),
234 ),
235 ];
236 [
237 Mat3::from_cols(columns[0], columns[1], columns[2]),
238 Mat3::from_cols(-columns[0], columns[2], columns[1]),
239 Mat3::from_cols(-columns[1], columns[0], columns[2]),
240 Mat3::from_cols(columns[1], columns[2], columns[0]),
241 Mat3::from_cols(columns[2], columns[0], columns[1]),
242 Mat3::from_cols(-columns[2], columns[1], columns[0]),
243 ]
244 }
245}
246
247pub type BoneId = usize;
249
250#[derive(Debug, Clone, Copy, PartialEq)]
252pub struct Transform {
253 pub translation: Vec3,
255 pub rotation: Quat,
257 pub scale: Vec3,
259}
260
261impl Transform {
262 pub const IDENTITY: Self = Self {
265 translation: Vec3::ZERO,
266 rotation: Quat::IDENTITY,
267 scale: Vec3::ONE,
268 };
269
270 pub fn to_mat4(&self) -> Mat4 {
273 Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation)
274 }
275}
276
277impl Default for Transform {
278 fn default() -> Self {
279 Self::IDENTITY
280 }
281}
282
283#[derive(Debug, Clone)]
285pub struct Bone {
286 pub name: String,
288 pub parent: Option<BoneId>,
290 pub rest: Transform,
293 pub inverse_bind: Option<Mat4>,
295}
296
297#[derive(Debug, Clone, Default)]
300pub struct Skeleton {
301 pub bones: Vec<Bone>,
303}
304
305impl Skeleton {
306 pub fn bone_name(&self, id: BoneId) -> &str {
312 &self.bones[id].name
313 }
314}
315
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324pub(crate) enum WorldMatrixError {
325 NonFiniteTransform {
328 node: BoneId,
330 },
331 InvalidParent {
334 node: BoneId,
336 parent: BoneId,
338 },
339}
340
341pub(crate) fn world_rest_matrices(skeleton: &Skeleton) -> Result<Vec<Mat4>, WorldMatrixError> {
351 let mut worlds = Vec::with_capacity(skeleton.bones.len());
352 for (node, bone) in skeleton.bones.iter().enumerate() {
353 let local = bone.rest.to_mat4();
354 if !mat4_is_finite(local) {
355 return Err(WorldMatrixError::NonFiniteTransform { node });
356 }
357 let world = match bone.parent {
358 Some(parent) if parent < node => worlds[parent] * local,
359 Some(parent) => return Err(WorldMatrixError::InvalidParent { node, parent }),
360 None => local,
361 };
362 if !mat4_is_finite(world) {
363 return Err(WorldMatrixError::NonFiniteTransform { node });
364 }
365 worlds.push(world);
366 }
367 Ok(worlds)
368}
369
370pub(crate) fn tolerant_world_rest_matrices(skeleton: &Skeleton) -> Vec<Option<Mat4>> {
376 let mut worlds = Vec::with_capacity(skeleton.bones.len());
377 for bone in &skeleton.bones {
378 let local = bone.rest.to_mat4();
379 let world = match bone.parent {
380 Some(parent) => worlds
381 .get(parent)
382 .copied()
383 .flatten()
384 .map(|parent_world| parent_world * local),
385 None => Some(local),
386 }
387 .filter(|matrix| mat4_is_finite(*matrix));
388 worlds.push(world);
389 }
390 worlds
391}
392
393pub(crate) fn mat4_is_finite(matrix: Mat4) -> bool {
394 matrix.to_cols_array().into_iter().all(f32::is_finite)
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
399#[serde(rename_all = "snake_case")]
400pub enum Property {
401 Translation,
403 Rotation,
405 Scale,
407}
408
409impl Property {
410 pub fn as_str(self) -> &'static str {
413 match self {
414 Property::Translation => "translation",
415 Property::Rotation => "rotation",
416 Property::Scale => "scale",
417 }
418 }
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq)]
423pub enum Interpolation {
424 Linear,
426 Step,
428 CubicSpline,
432}
433
434#[derive(Debug, Clone)]
436pub enum TrackValues {
437 Vec3s(Vec<Vec3>),
439 Quats(Vec<Quat>),
441}
442
443impl TrackValues {
444 pub fn len(&self) -> usize {
447 match self {
448 TrackValues::Vec3s(v) => v.len(),
449 TrackValues::Quats(v) => v.len(),
450 }
451 }
452
453 pub fn is_empty(&self) -> bool {
455 self.len() == 0
456 }
457}
458
459#[derive(Debug, Clone)]
461pub struct Track {
462 pub bone: BoneId,
464 pub property: Property,
466 pub interpolation: Interpolation,
468 pub times: Vec<f32>,
471 pub values: TrackValues,
473}
474
475impl Track {
476 pub fn key_count(&self) -> usize {
478 self.times.len()
479 }
480
481 pub fn value_index(&self, k: usize) -> usize {
484 match self.interpolation {
485 Interpolation::CubicSpline => 3 * k + 1,
486 _ => k,
487 }
488 }
489
490 pub fn key_vec3(&self, k: usize) -> Option<Vec3> {
492 match &self.values {
493 TrackValues::Vec3s(v) => v.get(self.value_index(k)).copied(),
494 TrackValues::Quats(_) => None,
495 }
496 }
497
498 pub fn key_quat(&self, k: usize) -> Option<Quat> {
500 match &self.values {
501 TrackValues::Quats(v) => v.get(self.value_index(k)).copied(),
502 TrackValues::Vec3s(_) => None,
503 }
504 }
505
506 pub fn start_time(&self) -> f32 {
508 self.times.first().copied().unwrap_or(0.0)
509 }
510
511 pub fn end_time(&self) -> f32 {
513 self.times.last().copied().unwrap_or(0.0)
514 }
515}
516
517#[derive(Debug, Clone)]
519pub struct Clip {
520 pub name: String,
523 pub duration_s: f64,
525 pub tracks: Vec<Track>,
527}
528
529#[derive(Debug, Clone, Default)]
531pub struct SourceInfo {
532 pub path: Option<String>,
534 pub format: Option<String>,
536}
537
538#[derive(Debug, Clone, Default)]
545pub struct Document {
546 pub skeleton: Skeleton,
548 pub clips: Vec<Clip>,
550 pub assets: SceneAssets,
552 pub source: SourceInfo,
554}
555
556#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
562#[non_exhaustive]
563pub enum DocumentShapeError {
564 #[error("node {node} has a non-finite rest transform")]
567 NonFiniteSkeletonRest {
568 node: BoneId,
570 },
571 #[error("node {node} has invalid parent {parent}")]
573 InvalidSkeletonParent {
574 node: BoneId,
576 parent: BoneId,
578 },
579 #[error("source skeleton declares duplicate source node index {source_node_index}")]
581 DuplicateSourceNodeIndex {
582 source_node_index: usize,
584 },
585 #[error("source skeleton declares duplicate source skin index {source_skin_index}")]
587 DuplicateSourceSkinIndex {
588 source_skin_index: usize,
590 },
591 #[error(
593 "source node {source_node_index} contradicts the document skeleton's parent chain ({violation})"
594 )]
595 SourceProjection {
596 source_node_index: usize,
598 violation: SourceProjectionViolation,
600 },
601 #[error("clip {clip_index} declares duplicate {property:?} tracks for node {node}")]
603 DuplicateClipTrack {
604 clip_index: usize,
606 node: BoneId,
608 property: Property,
610 },
611 #[error("clip {clip_index} track for node {node} has an invalid shape ({violation})")]
613 TrackShape {
614 clip_index: usize,
616 node: BoneId,
618 violation: TrackShapeViolation,
620 },
621 #[error("mesh instance {instance_index} is invalid ({violation})")]
623 MeshInstanceShape {
624 instance_index: usize,
626 violation: MeshInstanceShapeViolation,
628 },
629 #[error("node {node} has a non-finite inverse-bind matrix")]
631 NonFiniteBoneInverseBind {
632 node: BoneId,
634 },
635}
636
637#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
639#[non_exhaustive]
640pub enum SourceProjectionViolation {
641 #[error("projected_bone_out_of_range")]
643 ProjectedBoneOutOfRange,
644 #[error("two_source_nodes_project_to_one_bone")]
646 TwoSourceNodesProjectToOneBone,
647 #[error("parent_source_node_is_missing")]
649 ParentSourceNodeMissing,
650 #[error("cyclic_unprojected_source_parent_chain")]
652 CyclicUnprojectedSourceParentChain,
653 #[error("projection_and_skeleton_parents_differ")]
655 NearestProjectedParentMismatch,
656 #[error("projected_bone_has_an_unprojected_skeleton_child")]
658 ProjectedBoneHasUnprojectedSkeletonChild,
659}
660
661#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
663#[non_exhaustive]
664pub enum TrackShapeViolation {
665 #[error("bone_index_out_of_range")]
667 BoneIndexOutOfRange,
668 #[error("empty_times")]
670 EmptyTimes,
671 #[error("non_finite_time")]
673 NonFiniteTime,
674 #[error("times_not_strictly_increasing")]
676 TimesNotStrictlyIncreasing,
677 #[error("value_count_mismatch")]
679 ValueCountMismatch,
680 #[error("value_type_mismatches_property")]
682 ValueTypeMismatchesProperty,
683 #[error("non_finite_value")]
685 NonFiniteValue,
686}
687
688#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
690#[non_exhaustive]
691pub enum MeshInstanceShapeViolation {
692 #[error("node_index_out_of_range")]
694 NodeIndexOutOfRange,
695 #[error("mesh_index_out_of_range")]
697 MeshIndexOutOfRange,
698 #[error("skin_joint_out_of_range")]
700 SkinJointOutOfRange,
701 #[error("skin_ibm_count_mismatch")]
703 SkinInverseBindCountMismatch,
704 #[error("non_finite_inverse_bind")]
706 NonFiniteSkinInverseBind,
707}
708
709#[derive(Debug, Clone, Default)]
725pub struct Primitive {
726 pub material: Option<usize>,
728 pub indices: Vec<u32>,
730 pub positions: Vec<Vec3>,
732 pub normals: Vec<Vec3>,
734 pub uvs: Vec<[f32; 2]>,
736 pub joints: Vec<[u16; 4]>,
738 pub weights: Vec<[f32; 4]>,
740 pub additional_influence_sets: Vec<AdditionalInfluenceSet>,
746}
747
748#[derive(Debug, Clone, Copy, PartialEq, Eq)]
755pub struct AdditionalInfluenceSet {
756 pub set_index: u32,
758 pub joints_present: bool,
760 pub weights_present: bool,
762}
763
764#[derive(Debug, Clone, Default)]
766pub struct MeshAsset {
767 pub name: String,
769 pub source_mesh_index: usize,
774 pub primitives: Vec<Primitive>,
776}
777
778#[derive(Debug, Clone, Default)]
780pub struct MeshInstance {
781 pub source_node_index: usize,
783 pub node: BoneId,
785 pub mesh: usize,
787 pub skin_joints: Vec<BoneId>,
789 pub skin_ibms: Vec<Mat4>,
794}
795
796#[derive(Debug, Clone, Default)]
798pub struct SceneAsset {
799 pub source_scene_index: usize,
801 pub name: Option<String>,
803 pub roots: Vec<BoneId>,
805}
806
807#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
814#[serde(rename_all = "snake_case")]
815pub enum SourceSkeletonCoverage {
816 #[default]
818 Unavailable,
819 Complete,
823}
824
825#[derive(Debug, Clone)]
837pub enum SourceNodeLocalRest {
838 Trs {
840 translation: Vec3,
842 rotation: Quat,
844 scale: Vec3,
846 },
847 Matrix(Mat4),
849}
850
851#[derive(Debug, Clone)]
865#[non_exhaustive]
866pub struct SourceNodeAsset {
867 pub source_node_index: usize,
869 pub name: Option<String>,
871 pub parent_source_node_index: Option<usize>,
873 pub scene_root_indices: Vec<usize>,
875 pub local_rest: SourceNodeLocalRest,
877 pub bone: Option<BoneId>,
897}
898
899impl SourceNodeAsset {
900 pub fn new(source_node_index: usize, local_rest: SourceNodeLocalRest) -> Self {
908 Self {
909 source_node_index,
910 name: None,
911 parent_source_node_index: None,
912 scene_root_indices: Vec::new(),
913 local_rest,
914 bone: None,
915 }
916 }
917}
918
919#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
927#[serde(rename_all = "snake_case")]
928pub enum SourceInverseBindAccessorStatus {
929 #[default]
931 Absent,
932 Available,
934 EmptyAccessor,
936 CountMismatch,
938 Unreadable,
940}
941
942#[derive(Debug, Clone, Default)]
944pub struct SourceInverseBindAccessor {
945 pub status: SourceInverseBindAccessorStatus,
947 pub declared_count: Option<usize>,
949 pub matrices: Vec<Mat4>,
958}
959
960#[derive(Debug, Clone)]
962pub struct SourceSkinAttachment {
963 pub source_node_index: usize,
965 pub source_mesh_index: Option<usize>,
970}
971
972#[derive(Debug, Clone, Default)]
974pub struct SourceSkinAsset {
975 pub source_skin_index: usize,
977 pub name: Option<String>,
979 pub skeleton_root_source_node_index: Option<usize>,
981 pub joint_source_node_indices: Vec<usize>,
983 pub inverse_bind_accessor: SourceInverseBindAccessor,
989 pub attachments: Vec<SourceSkinAttachment>,
991}
992
993#[derive(Debug, Clone, Default)]
995pub struct SourceSkeletonAssets {
996 pub coverage: SourceSkeletonCoverage,
998 pub nodes: Vec<SourceNodeAsset>,
1000 pub skins: Vec<SourceSkinAsset>,
1002}
1003
1004#[derive(Debug, Clone)]
1007pub struct TextureAsset {
1008 pub bytes: Vec<u8>,
1010 pub mime: String,
1012}
1013
1014#[derive(Debug, Clone)]
1019pub struct NormalTextureAsset {
1020 pub texture: TextureAsset,
1022 pub scale: f32,
1024}
1025
1026#[derive(Debug, Clone)]
1032pub struct OcclusionTextureAsset {
1033 pub texture: TextureAsset,
1035 pub strength: f32,
1037}
1038
1039#[derive(Debug, Clone)]
1041pub struct MaterialAsset {
1042 pub name: String,
1044 pub base_color: [f32; 4],
1047 pub metallic: f32,
1049 pub roughness: f32,
1051 pub base_color_texture: Option<TextureAsset>,
1053 pub normal_texture: Option<NormalTextureAsset>,
1055 pub metallic_roughness_texture: Option<TextureAsset>,
1059 pub occlusion_texture: Option<OcclusionTextureAsset>,
1061}
1062
1063#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1069#[serde(rename_all = "snake_case")]
1070pub enum MaterialResourceCoverage {
1071 Complete,
1075 #[default]
1077 Unavailable,
1078}
1079
1080#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1085#[serde(rename_all = "snake_case")]
1086pub enum MaterialTextureSlot {
1087 BaseColor,
1089 Normal,
1091 MetallicRoughness,
1093 Occlusion,
1095 Emissive,
1097}
1098
1099#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1101pub struct SourceMaterialTextureBinding {
1102 pub slot: MaterialTextureSlot,
1104 pub texture_index: usize,
1106}
1107
1108#[derive(Debug, Clone, Default)]
1110pub struct SourceMaterialAsset {
1111 pub material_index: usize,
1113 pub name: Option<String>,
1115 pub texture_bindings: Vec<SourceMaterialTextureBinding>,
1117}
1118
1119#[derive(Debug, Clone, Default)]
1121pub struct SourceTextureAsset {
1122 pub texture_index: usize,
1124 pub name: Option<String>,
1126 pub image_index: usize,
1128}
1129
1130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1132#[serde(rename_all = "snake_case")]
1133pub enum ImageSourceKind {
1134 Embedded,
1136 DataUri,
1138 External,
1140}
1141
1142#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1144#[serde(rename_all = "snake_case")]
1145pub enum ImageContainerFormat {
1146 Png,
1148 Jpeg,
1150}
1151
1152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1154#[serde(rename_all = "snake_case")]
1155pub enum DecodedImageColorType {
1156 L8,
1158 La8,
1160 Rgb8,
1162 Rgba8,
1164 L16,
1166 La16,
1168 Rgb16,
1170 Rgba16,
1172}
1173
1174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1176#[serde(rename_all = "snake_case")]
1177pub enum ImageUnavailableReason {
1178 SourceUnavailable,
1180 InvalidDataUri,
1182 UnsupportedContainer,
1184 DecodeFailed,
1186 ResourceLimit,
1188}
1189
1190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1192pub enum SourceImageInspection {
1193 Available {
1195 width: u32,
1197 height: u32,
1199 channel_count: u8,
1201 color_type: DecodedImageColorType,
1203 },
1204 Unavailable {
1206 reason: ImageUnavailableReason,
1208 },
1209}
1210
1211#[derive(Debug, Clone)]
1213pub struct SourceImageAsset {
1214 pub image_index: usize,
1216 pub name: Option<String>,
1218 pub source_kind: ImageSourceKind,
1220 pub declared_mime_type: Option<String>,
1222 pub detected_container: Option<ImageContainerFormat>,
1224 pub inspection: SourceImageInspection,
1226}
1227
1228#[derive(Debug, Clone, Default)]
1230pub struct MaterialResourceAssets {
1231 pub coverage: MaterialResourceCoverage,
1233 pub materials: Vec<SourceMaterialAsset>,
1235 pub textures: Vec<SourceTextureAsset>,
1237 pub images: Vec<SourceImageAsset>,
1239}
1240
1241impl Primitive {
1242 pub fn weld(&mut self) {
1246 if !self.indices.is_empty() || self.positions.is_empty() {
1247 return;
1248 }
1249 let corner_key = |i: usize| -> Vec<u8> {
1250 let mut key = Vec::with_capacity(64);
1251 let mut push_f32s = |vals: &[f32]| {
1252 for v in vals {
1253 key.extend_from_slice(&v.to_le_bytes());
1254 }
1255 };
1256 push_f32s(&self.positions[i].to_array());
1257 if let Some(n) = self.normals.get(i) {
1258 push_f32s(&n.to_array());
1259 }
1260 if let Some(uv) = self.uvs.get(i) {
1261 push_f32s(uv);
1262 }
1263 if let Some(w) = self.weights.get(i) {
1264 push_f32s(w);
1265 }
1266 if let Some(j) = self.joints.get(i) {
1267 for v in j {
1268 key.extend_from_slice(&v.to_le_bytes());
1269 }
1270 }
1271 key
1272 };
1273 let mut seen: std::collections::HashMap<Vec<u8>, u32> = std::collections::HashMap::new();
1274 let mut indices = Vec::with_capacity(self.positions.len());
1275 let mut positions = Vec::new();
1276 let mut normals = Vec::new();
1277 let mut uvs = Vec::new();
1278 let mut joints = Vec::new();
1279 let mut weights = Vec::new();
1280 for i in 0..self.positions.len() {
1281 let index = *seen.entry(corner_key(i)).or_insert_with(|| {
1282 positions.push(self.positions[i]);
1283 if let Some(n) = self.normals.get(i) {
1284 normals.push(*n);
1285 }
1286 if let Some(uv) = self.uvs.get(i) {
1287 uvs.push(*uv);
1288 }
1289 if let Some(j) = self.joints.get(i) {
1290 joints.push(*j);
1291 }
1292 if let Some(w) = self.weights.get(i) {
1293 weights.push(*w);
1294 }
1295 (positions.len() - 1) as u32
1296 });
1297 indices.push(index);
1298 }
1299 self.indices = indices;
1300 self.positions = positions;
1301 self.normals = normals;
1302 self.uvs = uvs;
1303 self.joints = joints;
1304 self.weights = weights;
1305 }
1306}
1307
1308#[derive(Debug, Clone, Default)]
1311pub struct SceneAssets {
1312 pub meshes: Vec<MeshAsset>,
1315 pub instances: Vec<MeshInstance>,
1317 pub materials: Vec<MaterialAsset>,
1319 pub material_resources: MaterialResourceAssets,
1322 pub scenes: Vec<SceneAsset>,
1324 pub default_scene: Option<usize>,
1326 pub source_skeleton: SourceSkeletonAssets,
1333}
1334
1335pub fn validate_document_shape(document: &Document) -> Result<(), DocumentShapeError> {
1353 validate_skeleton_rest(&document.skeleton)?;
1354 validate_source_skeleton_identity(&document.assets.source_skeleton)?;
1355 validate_source_projection(document)?;
1356 validate_clip_tracks(document)?;
1357 validate_mesh_instances(document)?;
1358 validate_bone_inverse_binds(&document.skeleton)
1359}
1360
1361fn validate_skeleton_rest(skeleton: &Skeleton) -> Result<(), DocumentShapeError> {
1362 world_rest_matrices(skeleton)
1363 .map(|_| ())
1364 .map_err(|error| match error {
1365 WorldMatrixError::NonFiniteTransform { node } => {
1366 DocumentShapeError::NonFiniteSkeletonRest { node }
1367 }
1368 WorldMatrixError::InvalidParent { node, parent } => {
1369 DocumentShapeError::InvalidSkeletonParent { node, parent }
1370 }
1371 })
1372}
1373
1374fn validate_source_skeleton_identity(
1375 source_skeleton: &SourceSkeletonAssets,
1376) -> Result<(), DocumentShapeError> {
1377 let mut seen_nodes = BTreeSet::new();
1378 for node in &source_skeleton.nodes {
1379 if !seen_nodes.insert(node.source_node_index) {
1380 return Err(DocumentShapeError::DuplicateSourceNodeIndex {
1381 source_node_index: node.source_node_index,
1382 });
1383 }
1384 }
1385 let mut seen_skins = BTreeSet::new();
1386 for skin in &source_skeleton.skins {
1387 if !seen_skins.insert(skin.source_skin_index) {
1388 return Err(DocumentShapeError::DuplicateSourceSkinIndex {
1389 source_skin_index: skin.source_skin_index,
1390 });
1391 }
1392 }
1393 Ok(())
1394}
1395
1396fn validate_source_projection(document: &Document) -> Result<(), DocumentShapeError> {
1410 let source_skeleton = &document.assets.source_skeleton;
1411 if source_skeleton.coverage != SourceSkeletonCoverage::Complete {
1412 return Ok(());
1413 }
1414
1415 let bones = &document.skeleton.bones;
1416 let mut bone_of_source = BTreeMap::new();
1417 let mut source_of_bone = BTreeMap::new();
1418 let mut skeleton_parents = Vec::with_capacity(source_skeleton.nodes.len());
1419 for node in &source_skeleton.nodes {
1420 let Some(bone) = node.bone else {
1421 continue;
1422 };
1423 let skeleton_parent = bones
1424 .get(bone)
1425 .ok_or(DocumentShapeError::SourceProjection {
1426 source_node_index: node.source_node_index,
1427 violation: SourceProjectionViolation::ProjectedBoneOutOfRange,
1428 })?
1429 .parent;
1430 if source_of_bone
1431 .insert(bone, node.source_node_index)
1432 .is_some()
1433 {
1434 return Err(DocumentShapeError::SourceProjection {
1435 source_node_index: node.source_node_index,
1436 violation: SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
1437 });
1438 }
1439 bone_of_source.insert(node.source_node_index, bone);
1440 skeleton_parents.push((node, skeleton_parent));
1441 }
1442
1443 let by_source_index: BTreeMap<_, _> = source_skeleton
1444 .nodes
1445 .iter()
1446 .map(|node| (node.source_node_index, node))
1447 .collect();
1448 let unprojected_rows = source_skeleton.nodes.len() - bone_of_source.len();
1449 let mut resolved_unprojected = BTreeMap::<usize, Option<BoneId>>::new();
1453 for (node, skeleton_parent) in skeleton_parents {
1454 let mut cursor = node.parent_source_node_index;
1455 let mut unresolved_suffix = Vec::new();
1456 let projected_parent = loop {
1457 let Some(parent_source_node_index) = cursor else {
1458 break None;
1459 };
1460 if let Some(&bone) = bone_of_source.get(&parent_source_node_index) {
1461 break Some(bone);
1462 }
1463 if let Some(&projected_parent) = resolved_unprojected.get(&parent_source_node_index) {
1464 break projected_parent;
1465 }
1466 let parent = by_source_index.get(&parent_source_node_index).ok_or(
1467 DocumentShapeError::SourceProjection {
1468 source_node_index: node.source_node_index,
1469 violation: SourceProjectionViolation::ParentSourceNodeMissing,
1470 },
1471 )?;
1472 unresolved_suffix.push(parent_source_node_index);
1473 if unresolved_suffix.len() > unprojected_rows {
1474 return Err(DocumentShapeError::SourceProjection {
1475 source_node_index: node.source_node_index,
1476 violation: SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
1477 });
1478 }
1479 cursor = parent.parent_source_node_index;
1480 };
1481 for source_node_index in unresolved_suffix {
1482 resolved_unprojected.insert(source_node_index, projected_parent);
1483 }
1484 if projected_parent != skeleton_parent {
1485 return Err(DocumentShapeError::SourceProjection {
1486 source_node_index: node.source_node_index,
1487 violation: SourceProjectionViolation::NearestProjectedParentMismatch,
1488 });
1489 }
1490 }
1491
1492 for (bone, child) in bones.iter().enumerate() {
1493 if source_of_bone.contains_key(&bone) {
1494 continue;
1495 }
1496 if let Some(parent) = child.parent
1497 && let Some(&source_node_index) = source_of_bone.get(&parent)
1498 {
1499 return Err(DocumentShapeError::SourceProjection {
1500 source_node_index,
1501 violation: SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
1502 });
1503 }
1504 }
1505 Ok(())
1506}
1507
1508fn validate_clip_tracks(document: &Document) -> Result<(), DocumentShapeError> {
1509 let bone_count = document.skeleton.bones.len();
1510 for (clip_index, clip) in document.clips.iter().enumerate() {
1511 let mut seen = Vec::with_capacity(clip.tracks.len());
1512 for track in &clip.tracks {
1513 if track.bone >= bone_count {
1514 return Err(DocumentShapeError::TrackShape {
1515 clip_index,
1516 node: track.bone,
1517 violation: TrackShapeViolation::BoneIndexOutOfRange,
1518 });
1519 }
1520 if seen.contains(&(track.bone, track.property)) {
1521 return Err(DocumentShapeError::DuplicateClipTrack {
1522 clip_index,
1523 node: track.bone,
1524 property: track.property,
1525 });
1526 }
1527 seen.push((track.bone, track.property));
1528 validate_track_shape(clip_index, track)?;
1529 }
1530 }
1531 Ok(())
1532}
1533
1534pub(crate) fn validate_track_shape(
1535 clip_index: usize,
1536 track: &Track,
1537) -> Result<(), DocumentShapeError> {
1538 let violation = if track.times.is_empty() {
1539 Some(TrackShapeViolation::EmptyTimes)
1540 } else if track.times.iter().any(|time| !time.is_finite()) {
1541 Some(TrackShapeViolation::NonFiniteTime)
1542 } else if track.times.windows(2).any(|times| times[0] >= times[1]) {
1543 Some(TrackShapeViolation::TimesNotStrictlyIncreasing)
1544 } else {
1545 let expected_values = match track.interpolation {
1546 Interpolation::CubicSpline => track.times.len().checked_mul(3),
1547 Interpolation::Linear | Interpolation::Step => Some(track.times.len()),
1548 };
1549 if expected_values != Some(track.values.len()) {
1550 Some(TrackShapeViolation::ValueCountMismatch)
1551 } else if !matches!(
1552 (&track.values, track.property),
1553 (
1554 TrackValues::Vec3s(_),
1555 Property::Translation | Property::Scale
1556 ) | (TrackValues::Quats(_), Property::Rotation)
1557 ) {
1558 Some(TrackShapeViolation::ValueTypeMismatchesProperty)
1559 } else if match &track.values {
1560 TrackValues::Vec3s(values) => values.iter().any(|value| !value.is_finite()),
1561 TrackValues::Quats(values) => values.iter().any(|value| !value.is_finite()),
1562 } {
1563 Some(TrackShapeViolation::NonFiniteValue)
1564 } else {
1565 None
1566 }
1567 };
1568 violation.map_or(Ok(()), |violation| {
1569 Err(DocumentShapeError::TrackShape {
1570 clip_index,
1571 node: track.bone,
1572 violation,
1573 })
1574 })
1575}
1576
1577fn validate_mesh_instances(document: &Document) -> Result<(), DocumentShapeError> {
1578 let bone_count = document.skeleton.bones.len();
1579 let mesh_count = document.assets.meshes.len();
1580 for (instance_index, instance) in document.assets.instances.iter().enumerate() {
1581 let violation = if instance.node >= bone_count {
1582 Some(MeshInstanceShapeViolation::NodeIndexOutOfRange)
1583 } else if instance.mesh >= mesh_count {
1584 Some(MeshInstanceShapeViolation::MeshIndexOutOfRange)
1585 } else if instance
1586 .skin_joints
1587 .iter()
1588 .any(|&joint| joint >= bone_count)
1589 {
1590 Some(MeshInstanceShapeViolation::SkinJointOutOfRange)
1591 } else if !instance.skin_ibms.is_empty()
1592 && instance.skin_ibms.len() != instance.skin_joints.len()
1593 {
1594 Some(MeshInstanceShapeViolation::SkinInverseBindCountMismatch)
1595 } else if instance.skin_ibms.iter().any(|ibm| !mat4_is_finite(*ibm)) {
1596 Some(MeshInstanceShapeViolation::NonFiniteSkinInverseBind)
1597 } else {
1598 None
1599 };
1600 if let Some(violation) = violation {
1601 return Err(DocumentShapeError::MeshInstanceShape {
1602 instance_index,
1603 violation,
1604 });
1605 }
1606 }
1607 Ok(())
1608}
1609
1610fn validate_bone_inverse_binds(skeleton: &Skeleton) -> Result<(), DocumentShapeError> {
1611 for (node, bone) in skeleton.bones.iter().enumerate() {
1612 if let Some(inverse_bind) = bone.inverse_bind
1613 && !mat4_is_finite(inverse_bind)
1614 {
1615 return Err(DocumentShapeError::NonFiniteBoneInverseBind { node });
1616 }
1617 }
1618 Ok(())
1619}
1620
1621#[cfg(test)]
1622mod tests {
1623 use super::*;
1624
1625 #[test]
1626 fn property_serde_uses_the_stable_trs_vocabulary() {
1627 assert_eq!(
1628 serde_json::to_value([Property::Translation, Property::Rotation, Property::Scale,])
1629 .expect("properties serialize"),
1630 serde_json::json!(["translation", "rotation", "scale"])
1631 );
1632 assert_eq!(
1633 serde_json::from_value::<Vec<Property>>(serde_json::json!([
1634 "translation",
1635 "rotation",
1636 "scale"
1637 ]))
1638 .expect("properties deserialize"),
1639 [Property::Translation, Property::Rotation, Property::Scale,]
1640 );
1641 }
1642
1643 fn bone(parent: Option<BoneId>) -> Bone {
1644 Bone {
1645 name: "bone".into(),
1646 parent,
1647 rest: Transform::IDENTITY,
1648 inverse_bind: None,
1649 }
1650 }
1651
1652 fn one_bone_document() -> Document {
1653 Document {
1654 skeleton: Skeleton {
1655 bones: vec![bone(None)],
1656 },
1657 ..Document::default()
1658 }
1659 }
1660
1661 fn source_node(
1662 source_node_index: usize,
1663 parent_source_node_index: Option<usize>,
1664 bone: Option<BoneId>,
1665 ) -> SourceNodeAsset {
1666 SourceNodeAsset {
1667 source_node_index,
1668 name: None,
1669 parent_source_node_index,
1670 scene_root_indices: Vec::new(),
1671 local_rest: SourceNodeLocalRest::Trs {
1672 translation: Vec3::ZERO,
1673 rotation: Quat::IDENTITY,
1674 scale: Vec3::ONE,
1675 },
1676 bone,
1677 }
1678 }
1679
1680 fn valid_track() -> Track {
1681 Track {
1682 bone: 0,
1683 property: Property::Translation,
1684 interpolation: Interpolation::Linear,
1685 times: vec![0.0],
1686 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
1687 }
1688 }
1689
1690 fn track_document(track: Track) -> Document {
1691 let mut document = one_bone_document();
1692 document.clips.push(Clip {
1693 name: "clip".into(),
1694 duration_s: 0.0,
1695 tracks: vec![track],
1696 });
1697 document
1698 }
1699
1700 fn instance_document() -> Document {
1701 let mut document = one_bone_document();
1702 document.assets.meshes.push(MeshAsset::default());
1703 document.assets.instances.push(MeshInstance {
1704 node: 0,
1705 mesh: 0,
1706 ..MeshInstance::default()
1707 });
1708 document
1709 }
1710
1711 #[test]
1712 fn document_shape_validation_accepts_a_complete_projection_with_an_unprojected_intermediate() {
1713 let mut document = Document {
1714 skeleton: Skeleton {
1715 bones: vec![bone(None), bone(Some(0))],
1716 },
1717 assets: SceneAssets {
1718 source_skeleton: SourceSkeletonAssets {
1719 coverage: SourceSkeletonCoverage::Complete,
1720 nodes: vec![
1721 source_node(10, None, Some(0)),
1722 source_node(11, Some(10), None),
1723 source_node(12, Some(11), Some(1)),
1724 ],
1725 ..SourceSkeletonAssets::default()
1726 },
1727 meshes: vec![MeshAsset::default()],
1728 instances: vec![MeshInstance {
1729 node: 1,
1730 mesh: 0,
1731 skin_joints: vec![0, 1],
1732 skin_ibms: vec![Mat4::IDENTITY, Mat4::IDENTITY],
1733 ..MeshInstance::default()
1734 }],
1735 ..SceneAssets::default()
1736 },
1737 ..Document::default()
1738 };
1739 document.clips.push(Clip {
1740 name: "clip".into(),
1741 duration_s: 0.0,
1742 tracks: vec![valid_track()],
1743 });
1744
1745 assert_eq!(validate_document_shape(&document), Ok(()));
1746 }
1747
1748 #[test]
1749 fn shared_unprojected_parent_suffix_preserves_each_projected_parent() {
1750 const CONNECTORS: usize = 64;
1751 const PROJECTED_CHILDREN: usize = 64;
1752
1753 let mut nodes = Vec::with_capacity(1 + CONNECTORS + PROJECTED_CHILDREN);
1754 nodes.push(source_node(0, None, Some(0)));
1755 for source_node_index in 1..=CONNECTORS {
1756 nodes.push(source_node(
1757 source_node_index,
1758 Some(source_node_index - 1),
1759 None,
1760 ));
1761 }
1762 for child in 0..PROJECTED_CHILDREN {
1763 nodes.push(source_node(
1764 1 + CONNECTORS + child,
1765 Some(CONNECTORS),
1766 Some(1 + child),
1767 ));
1768 }
1769 let document = Document {
1770 skeleton: Skeleton {
1771 bones: std::iter::once(bone(None))
1772 .chain((0..PROJECTED_CHILDREN).map(|_| bone(Some(0))))
1773 .collect(),
1774 },
1775 assets: SceneAssets {
1776 source_skeleton: SourceSkeletonAssets {
1777 coverage: SourceSkeletonCoverage::Complete,
1778 nodes,
1779 ..SourceSkeletonAssets::default()
1780 },
1781 ..SceneAssets::default()
1782 },
1783 ..Document::default()
1784 };
1785
1786 assert_eq!(validate_document_shape(&document), Ok(()));
1787 let mut mismatched = document.clone();
1788 mismatched.skeleton.bones[PROJECTED_CHILDREN].parent = None;
1789 assert_eq!(
1790 validate_document_shape(&mismatched),
1791 Err(DocumentShapeError::SourceProjection {
1792 source_node_index: CONNECTORS + PROJECTED_CHILDREN,
1793 violation: SourceProjectionViolation::NearestProjectedParentMismatch,
1794 })
1795 );
1796 }
1797
1798 #[test]
1799 fn document_shape_validation_has_an_analytic_error_for_every_variant() {
1800 let projection_error =
1801 |source_node_index, violation| DocumentShapeError::SourceProjection {
1802 source_node_index,
1803 violation,
1804 };
1805 let track_error = |node, violation| DocumentShapeError::TrackShape {
1806 clip_index: 0,
1807 node,
1808 violation,
1809 };
1810 let instance_error = |violation| DocumentShapeError::MeshInstanceShape {
1811 instance_index: 0,
1812 violation,
1813 };
1814
1815 let mut non_finite_rest = one_bone_document();
1816 non_finite_rest.skeleton.bones[0].rest.translation.x = f32::NAN;
1817 let overflowed_rest_world = Document {
1818 skeleton: Skeleton {
1819 bones: vec![
1820 Bone {
1821 rest: Transform {
1822 scale: Vec3::splat(f32::MAX),
1823 ..Transform::IDENTITY
1824 },
1825 ..bone(None)
1826 },
1827 Bone {
1828 rest: Transform {
1829 translation: Vec3::splat(2.0),
1830 ..Transform::IDENTITY
1831 },
1832 ..bone(Some(0))
1833 },
1834 ],
1835 },
1836 ..Document::default()
1837 };
1838 let self_parent = Document {
1839 skeleton: Skeleton {
1840 bones: vec![bone(Some(0))],
1841 },
1842 ..Document::default()
1843 };
1844 let forward_parent = Document {
1845 skeleton: Skeleton {
1846 bones: vec![bone(Some(1)), bone(None)],
1847 },
1848 ..Document::default()
1849 };
1850 let far_parent = Document {
1851 skeleton: Skeleton {
1852 bones: vec![bone(Some(99))],
1853 },
1854 ..Document::default()
1855 };
1856 let duplicate_node = Document {
1857 assets: SceneAssets {
1858 source_skeleton: SourceSkeletonAssets {
1859 nodes: vec![
1860 source_node(9, None, None),
1861 source_node(10, None, None),
1862 source_node(9, None, None),
1863 ],
1864 ..SourceSkeletonAssets::default()
1865 },
1866 ..SceneAssets::default()
1867 },
1868 ..Document::default()
1869 };
1870 let duplicate_skin = Document {
1871 assets: SceneAssets {
1872 source_skeleton: SourceSkeletonAssets {
1873 skins: vec![
1874 SourceSkinAsset {
1875 source_skin_index: 4,
1876 ..SourceSkinAsset::default()
1877 },
1878 SourceSkinAsset {
1879 source_skin_index: 5,
1880 ..SourceSkinAsset::default()
1881 },
1882 SourceSkinAsset {
1883 source_skin_index: 4,
1884 ..SourceSkinAsset::default()
1885 },
1886 ],
1887 ..SourceSkeletonAssets::default()
1888 },
1889 ..SceneAssets::default()
1890 },
1891 ..Document::default()
1892 };
1893 let complete_projection = |nodes| SceneAssets {
1894 source_skeleton: SourceSkeletonAssets {
1895 coverage: SourceSkeletonCoverage::Complete,
1896 nodes,
1897 ..SourceSkeletonAssets::default()
1898 },
1899 ..SceneAssets::default()
1900 };
1901 let out_of_range_projection = Document {
1902 skeleton: Skeleton {
1903 bones: vec![bone(None)],
1904 },
1905 assets: complete_projection(vec![source_node(10, None, Some(1))]),
1906 ..Document::default()
1907 };
1908 let non_injective_projection = Document {
1909 skeleton: Skeleton {
1910 bones: vec![bone(None)],
1911 },
1912 assets: complete_projection(vec![
1913 source_node(10, None, Some(0)),
1914 source_node(11, None, Some(0)),
1915 ]),
1916 ..Document::default()
1917 };
1918 let missing_projection_parent = Document {
1919 skeleton: Skeleton {
1920 bones: vec![bone(None), bone(Some(0))],
1921 },
1922 assets: complete_projection(vec![source_node(11, Some(99), Some(1))]),
1923 ..Document::default()
1924 };
1925 let missing_projection_parent_at_cycle_bound = Document {
1930 skeleton: Skeleton {
1931 bones: vec![bone(None), bone(Some(0))],
1932 },
1933 assets: complete_projection(vec![
1934 source_node(10, None, Some(0)),
1935 source_node(11, Some(12), Some(1)),
1936 source_node(12, Some(99), None),
1937 ]),
1938 ..Document::default()
1939 };
1940 let cyclic_unprojected_parent = Document {
1941 skeleton: Skeleton {
1942 bones: vec![bone(None), bone(Some(0))],
1943 },
1944 assets: complete_projection(vec![
1945 source_node(11, Some(12), Some(1)),
1946 source_node(12, Some(12), None),
1947 ]),
1948 ..Document::default()
1949 };
1950 let cyclic_unprojected_parent_pair = Document {
1951 skeleton: Skeleton {
1952 bones: vec![bone(None), bone(Some(0))],
1953 },
1954 assets: complete_projection(vec![
1955 source_node(11, Some(12), Some(1)),
1956 source_node(12, Some(13), None),
1957 source_node(13, Some(12), None),
1958 ]),
1959 ..Document::default()
1960 };
1961 let mismatched_nearest_parent = Document {
1962 skeleton: Skeleton {
1963 bones: vec![bone(None), bone(Some(0))],
1964 },
1965 assets: complete_projection(vec![
1966 source_node(10, None, Some(0)),
1967 source_node(11, None, Some(1)),
1968 ]),
1969 ..Document::default()
1970 };
1971 let unprojected_child = Document {
1972 skeleton: Skeleton {
1973 bones: vec![bone(None), bone(Some(0))],
1974 },
1975 assets: complete_projection(vec![source_node(10, None, Some(0))]),
1976 ..Document::default()
1977 };
1978
1979 let duplicate_track = {
1980 let track = valid_track();
1981 let mut document = track_document(track.clone());
1982 document.clips[0].tracks.push(Track {
1983 property: Property::Scale,
1984 ..valid_track()
1985 });
1986 document.clips[0].tracks.push(track);
1987 document
1988 };
1989 let mut boundary_out_of_range_track = valid_track();
1990 boundary_out_of_range_track.bone = 1;
1991 let mut far_out_of_range_track = valid_track();
1992 far_out_of_range_track.bone = 99;
1993 let empty_track = Track {
1994 times: Vec::new(),
1995 values: TrackValues::Vec3s(Vec::new()),
1996 ..valid_track()
1997 };
1998 let non_finite_later_time = Track {
1999 times: vec![0.0, f32::NAN],
2000 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
2001 ..valid_track()
2002 };
2003 let unordered_times = Track {
2004 times: vec![1.0, 0.0],
2005 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
2006 ..valid_track()
2007 };
2008 let equal_times = Track {
2009 times: vec![0.0, 0.0],
2010 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
2011 ..valid_track()
2012 };
2013 let wrong_linear_value_count = Track {
2014 values: TrackValues::Vec3s(Vec::new()),
2015 ..valid_track()
2016 };
2017 let excess_linear_value_count = Track {
2018 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
2019 ..valid_track()
2020 };
2021 let wrong_step_value_count = Track {
2022 interpolation: Interpolation::Step,
2023 times: vec![0.0, 1.0],
2024 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
2025 ..valid_track()
2026 };
2027 let excess_step_value_count = Track {
2028 interpolation: Interpolation::Step,
2029 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
2030 ..valid_track()
2031 };
2032 let wrong_cubic_value_count = Track {
2033 interpolation: Interpolation::CubicSpline,
2034 times: vec![0.0, 1.0],
2035 values: TrackValues::Vec3s(vec![Vec3::ZERO; 4]),
2036 ..valid_track()
2037 };
2038 let excess_cubic_value_count = Track {
2039 interpolation: Interpolation::CubicSpline,
2040 values: TrackValues::Vec3s(vec![Vec3::ZERO; 4]),
2041 ..valid_track()
2042 };
2043 let wrong_translation_value_type = Track {
2044 values: TrackValues::Quats(vec![Quat::IDENTITY]),
2045 ..valid_track()
2046 };
2047 let wrong_scale_value_type = Track {
2048 property: Property::Scale,
2049 values: TrackValues::Quats(vec![Quat::IDENTITY]),
2050 ..valid_track()
2051 };
2052 let wrong_rotation_value_type = Track {
2053 property: Property::Rotation,
2054 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
2055 ..valid_track()
2056 };
2057 let non_finite_value = Track {
2058 values: TrackValues::Vec3s(vec![Vec3::splat(f32::NAN)]),
2059 ..valid_track()
2060 };
2061
2062 let mut bad_instance_node = instance_document();
2063 bad_instance_node.assets.instances[0].node = 1;
2064 let mut far_instance_node = instance_document();
2065 far_instance_node.assets.instances[0].node = 99;
2066 let mut bad_instance_mesh = instance_document();
2067 bad_instance_mesh.assets.instances[0].mesh = 1;
2068 let mut far_instance_mesh = instance_document();
2069 far_instance_mesh.assets.instances[0].mesh = 99;
2070 let mut bad_instance_joint = instance_document();
2071 bad_instance_joint.assets.instances[0].skin_joints = vec![1];
2072 let mut far_instance_joint = instance_document();
2073 far_instance_joint.assets.instances[0].skin_joints = vec![99];
2074 let mut bad_instance_count = instance_document();
2075 bad_instance_count.assets.instances[0].skin_joints = vec![0];
2076 bad_instance_count.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY, Mat4::IDENTITY];
2077 let mut short_instance_count = instance_document();
2078 short_instance_count.skeleton.bones.push(bone(Some(0)));
2079 short_instance_count.assets.instances[0].skin_joints = vec![0, 1];
2080 short_instance_count.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY];
2081 let mut bad_instance_ibm = instance_document();
2082 bad_instance_ibm.assets.instances[0].skin_joints = vec![0];
2083 bad_instance_ibm.assets.instances[0].skin_ibms =
2084 vec![Mat4::from_cols_array(&[f32::NAN; 16])];
2085 let mut bad_bone_ibm = one_bone_document();
2086 bad_bone_ibm.skeleton.bones[0].inverse_bind = Some(Mat4::from_cols_array(&[f32::NAN; 16]));
2087
2088 let cases = vec![
2089 (
2090 "non-finite rest",
2091 non_finite_rest,
2092 DocumentShapeError::NonFiniteSkeletonRest { node: 0 },
2093 ),
2094 (
2095 "non-finite composed rest world",
2096 overflowed_rest_world,
2097 DocumentShapeError::NonFiniteSkeletonRest { node: 1 },
2098 ),
2099 (
2100 "self parent",
2101 self_parent,
2102 DocumentShapeError::InvalidSkeletonParent { node: 0, parent: 0 },
2103 ),
2104 (
2105 "forward parent",
2106 forward_parent,
2107 DocumentShapeError::InvalidSkeletonParent { node: 0, parent: 1 },
2108 ),
2109 (
2110 "far parent",
2111 far_parent,
2112 DocumentShapeError::InvalidSkeletonParent {
2113 node: 0,
2114 parent: 99,
2115 },
2116 ),
2117 (
2118 "duplicate source node",
2119 duplicate_node,
2120 DocumentShapeError::DuplicateSourceNodeIndex {
2121 source_node_index: 9,
2122 },
2123 ),
2124 (
2125 "duplicate source skin",
2126 duplicate_skin,
2127 DocumentShapeError::DuplicateSourceSkinIndex {
2128 source_skin_index: 4,
2129 },
2130 ),
2131 (
2132 "projected bone range",
2133 out_of_range_projection,
2134 projection_error(10, SourceProjectionViolation::ProjectedBoneOutOfRange),
2135 ),
2136 (
2137 "projection injectivity",
2138 non_injective_projection,
2139 projection_error(
2140 11,
2141 SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
2142 ),
2143 ),
2144 (
2145 "missing projection parent",
2146 missing_projection_parent,
2147 projection_error(11, SourceProjectionViolation::ParentSourceNodeMissing),
2148 ),
2149 (
2150 "missing projection parent at cycle bound",
2151 missing_projection_parent_at_cycle_bound,
2152 projection_error(11, SourceProjectionViolation::ParentSourceNodeMissing),
2153 ),
2154 (
2155 "cyclic projection parent",
2156 cyclic_unprojected_parent,
2157 projection_error(
2158 11,
2159 SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
2160 ),
2161 ),
2162 (
2163 "cyclic projection parent pair",
2164 cyclic_unprojected_parent_pair,
2165 projection_error(
2166 11,
2167 SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
2168 ),
2169 ),
2170 (
2171 "nearest projection parent",
2172 mismatched_nearest_parent,
2173 projection_error(
2174 11,
2175 SourceProjectionViolation::NearestProjectedParentMismatch,
2176 ),
2177 ),
2178 (
2179 "projection downward closure",
2180 unprojected_child,
2181 projection_error(
2182 10,
2183 SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
2184 ),
2185 ),
2186 (
2187 "duplicate track",
2188 duplicate_track,
2189 DocumentShapeError::DuplicateClipTrack {
2190 clip_index: 0,
2191 node: 0,
2192 property: Property::Translation,
2193 },
2194 ),
2195 (
2196 "track bone range boundary",
2197 track_document(boundary_out_of_range_track),
2198 track_error(1, TrackShapeViolation::BoneIndexOutOfRange),
2199 ),
2200 (
2201 "track bone range far",
2202 track_document(far_out_of_range_track),
2203 track_error(99, TrackShapeViolation::BoneIndexOutOfRange),
2204 ),
2205 (
2206 "empty track",
2207 track_document(empty_track),
2208 track_error(0, TrackShapeViolation::EmptyTimes),
2209 ),
2210 (
2211 "non-finite time",
2212 track_document(non_finite_later_time),
2213 track_error(0, TrackShapeViolation::NonFiniteTime),
2214 ),
2215 (
2216 "unordered times",
2217 track_document(unordered_times),
2218 track_error(0, TrackShapeViolation::TimesNotStrictlyIncreasing),
2219 ),
2220 (
2221 "equal times",
2222 track_document(equal_times),
2223 track_error(0, TrackShapeViolation::TimesNotStrictlyIncreasing),
2224 ),
2225 (
2226 "linear value count",
2227 track_document(wrong_linear_value_count),
2228 track_error(0, TrackShapeViolation::ValueCountMismatch),
2229 ),
2230 (
2231 "linear excess value count",
2232 track_document(excess_linear_value_count),
2233 track_error(0, TrackShapeViolation::ValueCountMismatch),
2234 ),
2235 (
2236 "step value count",
2237 track_document(wrong_step_value_count),
2238 track_error(0, TrackShapeViolation::ValueCountMismatch),
2239 ),
2240 (
2241 "step excess value count",
2242 track_document(excess_step_value_count),
2243 track_error(0, TrackShapeViolation::ValueCountMismatch),
2244 ),
2245 (
2246 "cubic value count",
2247 track_document(wrong_cubic_value_count),
2248 track_error(0, TrackShapeViolation::ValueCountMismatch),
2249 ),
2250 (
2251 "cubic excess value count",
2252 track_document(excess_cubic_value_count),
2253 track_error(0, TrackShapeViolation::ValueCountMismatch),
2254 ),
2255 (
2256 "translation value type",
2257 track_document(wrong_translation_value_type),
2258 track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
2259 ),
2260 (
2261 "scale value type",
2262 track_document(wrong_scale_value_type),
2263 track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
2264 ),
2265 (
2266 "rotation value type",
2267 track_document(wrong_rotation_value_type),
2268 track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
2269 ),
2270 (
2271 "non-finite value",
2272 track_document(non_finite_value),
2273 track_error(0, TrackShapeViolation::NonFiniteValue),
2274 ),
2275 (
2276 "instance node boundary",
2277 bad_instance_node,
2278 instance_error(MeshInstanceShapeViolation::NodeIndexOutOfRange),
2279 ),
2280 (
2281 "instance node far",
2282 far_instance_node,
2283 instance_error(MeshInstanceShapeViolation::NodeIndexOutOfRange),
2284 ),
2285 (
2286 "instance mesh boundary",
2287 bad_instance_mesh,
2288 instance_error(MeshInstanceShapeViolation::MeshIndexOutOfRange),
2289 ),
2290 (
2291 "instance mesh far",
2292 far_instance_mesh,
2293 instance_error(MeshInstanceShapeViolation::MeshIndexOutOfRange),
2294 ),
2295 (
2296 "instance joint boundary",
2297 bad_instance_joint,
2298 instance_error(MeshInstanceShapeViolation::SkinJointOutOfRange),
2299 ),
2300 (
2301 "instance joint far",
2302 far_instance_joint,
2303 instance_error(MeshInstanceShapeViolation::SkinJointOutOfRange),
2304 ),
2305 (
2306 "instance ibm count excess",
2307 bad_instance_count,
2308 instance_error(MeshInstanceShapeViolation::SkinInverseBindCountMismatch),
2309 ),
2310 (
2311 "instance ibm count short",
2312 short_instance_count,
2313 instance_error(MeshInstanceShapeViolation::SkinInverseBindCountMismatch),
2314 ),
2315 (
2316 "instance ibm finite",
2317 bad_instance_ibm,
2318 instance_error(MeshInstanceShapeViolation::NonFiniteSkinInverseBind),
2319 ),
2320 (
2321 "bone ibm finite",
2322 bad_bone_ibm,
2323 DocumentShapeError::NonFiniteBoneInverseBind { node: 0 },
2324 ),
2325 ];
2326 for (name, document, expected) in cases {
2327 assert_eq!(validate_document_shape(&document), Err(expected), "{name}");
2328 }
2329 }
2330
2331 #[test]
2332 fn document_shape_finiteness_checks_every_stored_component() {
2333 for component in 0..3 {
2334 let mut translation = Vec3::ZERO.to_array();
2335 translation[component] = f32::NAN;
2336 let mut document = one_bone_document();
2337 document.skeleton.bones[0].rest.translation = Vec3::from_array(translation);
2338 assert_eq!(
2339 validate_document_shape(&document),
2340 Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
2341 "rest translation component {component}"
2342 );
2343
2344 let mut scale = Vec3::ONE.to_array();
2345 scale[component] = f32::NAN;
2346 let mut document = one_bone_document();
2347 document.skeleton.bones[0].rest.scale = Vec3::from_array(scale);
2348 assert_eq!(
2349 validate_document_shape(&document),
2350 Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
2351 "rest scale component {component}"
2352 );
2353
2354 let mut value = Vec3::ZERO.to_array();
2355 value[component] = f32::NAN;
2356 let document = track_document(Track {
2357 values: TrackValues::Vec3s(vec![Vec3::from_array(value)]),
2358 ..valid_track()
2359 });
2360 assert_eq!(
2361 validate_document_shape(&document),
2362 Err(DocumentShapeError::TrackShape {
2363 clip_index: 0,
2364 node: 0,
2365 violation: TrackShapeViolation::NonFiniteValue,
2366 }),
2367 "track Vec3 component {component}"
2368 );
2369 }
2370
2371 for component in 0..4 {
2372 let mut rotation = Quat::IDENTITY.to_array();
2373 rotation[component] = f32::NAN;
2374 let mut document = one_bone_document();
2375 document.skeleton.bones[0].rest.rotation = Quat::from_array(rotation);
2376 assert_eq!(
2377 validate_document_shape(&document),
2378 Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
2379 "rest rotation component {component}"
2380 );
2381
2382 let document = track_document(Track {
2383 property: Property::Rotation,
2384 values: TrackValues::Quats(vec![Quat::from_array(rotation)]),
2385 ..valid_track()
2386 });
2387 assert_eq!(
2388 validate_document_shape(&document),
2389 Err(DocumentShapeError::TrackShape {
2390 clip_index: 0,
2391 node: 0,
2392 violation: TrackShapeViolation::NonFiniteValue,
2393 }),
2394 "track quaternion component {component}"
2395 );
2396 }
2397
2398 for key in 0..3 {
2399 let mut times = vec![0.0, 1.0, 2.0];
2400 times[key] = f32::NAN;
2401 let document = track_document(Track {
2402 times,
2403 values: TrackValues::Vec3s(vec![Vec3::ZERO; 3]),
2404 ..valid_track()
2405 });
2406 assert_eq!(
2407 validate_document_shape(&document),
2408 Err(DocumentShapeError::TrackShape {
2409 clip_index: 0,
2410 node: 0,
2411 violation: TrackShapeViolation::NonFiniteTime,
2412 }),
2413 "track time {key}"
2414 );
2415 }
2416
2417 for component in 0..16 {
2418 let mut columns = Mat4::IDENTITY.to_cols_array();
2419 columns[component] = f32::NAN;
2420 let inverse_bind = Mat4::from_cols_array(&columns);
2421
2422 let mut instance_document = instance_document();
2423 instance_document.assets.instances[0].skin_joints = vec![0];
2424 instance_document.assets.instances[0].skin_ibms = vec![inverse_bind];
2425 assert_eq!(
2426 validate_document_shape(&instance_document),
2427 Err(DocumentShapeError::MeshInstanceShape {
2428 instance_index: 0,
2429 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2430 }),
2431 "instance inverse-bind component {component}"
2432 );
2433
2434 let mut bone_document = one_bone_document();
2435 bone_document.skeleton.bones[0].inverse_bind = Some(inverse_bind);
2436 assert_eq!(
2437 validate_document_shape(&bone_document),
2438 Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 0 }),
2439 "bone inverse-bind component {component}"
2440 );
2441 }
2442 }
2443
2444 #[test]
2445 fn document_shape_rejects_duplicate_tracks_for_every_property() {
2446 let tracks = [
2447 (Property::Translation, TrackValues::Vec3s(vec![Vec3::ZERO])),
2448 (Property::Scale, TrackValues::Vec3s(vec![Vec3::ONE])),
2449 (Property::Rotation, TrackValues::Quats(vec![Quat::IDENTITY])),
2450 ];
2451
2452 for (property, values) in tracks {
2453 let track = Track {
2454 property,
2455 values,
2456 ..valid_track()
2457 };
2458 let mut document = track_document(track.clone());
2459 document.clips[0].tracks.push(track);
2460
2461 assert_eq!(
2462 validate_document_shape(&document),
2463 Err(DocumentShapeError::DuplicateClipTrack {
2464 clip_index: 0,
2465 node: 0,
2466 property,
2467 }),
2468 "duplicate {property:?} track"
2469 );
2470 }
2471 }
2472
2473 #[test]
2474 fn document_shape_rejects_infinite_times_quaternions_and_inverse_binds() {
2475 for non_finite in [f32::INFINITY, f32::NEG_INFINITY] {
2476 let document = track_document(Track {
2477 times: vec![non_finite],
2478 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
2479 ..valid_track()
2480 });
2481 assert_eq!(
2482 validate_document_shape(&document),
2483 Err(DocumentShapeError::TrackShape {
2484 clip_index: 0,
2485 node: 0,
2486 violation: TrackShapeViolation::NonFiniteTime,
2487 }),
2488 "track time {non_finite}"
2489 );
2490
2491 let document = track_document(Track {
2492 property: Property::Rotation,
2493 values: TrackValues::Quats(vec![Quat::from_xyzw(non_finite, 0.0, 0.0, 1.0)]),
2494 ..valid_track()
2495 });
2496 assert_eq!(
2497 validate_document_shape(&document),
2498 Err(DocumentShapeError::TrackShape {
2499 clip_index: 0,
2500 node: 0,
2501 violation: TrackShapeViolation::NonFiniteValue,
2502 }),
2503 "track quaternion {non_finite}"
2504 );
2505
2506 let mut columns = Mat4::IDENTITY.to_cols_array();
2507 columns[0] = non_finite;
2508 let inverse_bind = Mat4::from_cols_array(&columns);
2509 let mut document = instance_document();
2510 document.assets.instances[0].skin_joints = vec![0];
2511 document.assets.instances[0].skin_ibms = vec![inverse_bind];
2512 assert_eq!(
2513 validate_document_shape(&document),
2514 Err(DocumentShapeError::MeshInstanceShape {
2515 instance_index: 0,
2516 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2517 }),
2518 "instance inverse bind {non_finite}"
2519 );
2520
2521 let mut document = one_bone_document();
2522 document.skeleton.bones[0].inverse_bind = Some(inverse_bind);
2523 assert_eq!(
2524 validate_document_shape(&document),
2525 Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 0 }),
2526 "bone inverse bind {non_finite}"
2527 );
2528 }
2529 }
2530
2531 #[test]
2532 fn document_shape_checks_mesh_and_joint_references_on_later_instances() {
2533 let later_instance = MeshInstance {
2534 node: 0,
2535 mesh: 0,
2536 ..MeshInstance::default()
2537 };
2538
2539 let mut document = instance_document();
2540 document.assets.instances.push(later_instance.clone());
2541 document.assets.instances[1].mesh = 1;
2542 assert_eq!(
2543 validate_document_shape(&document),
2544 Err(DocumentShapeError::MeshInstanceShape {
2545 instance_index: 1,
2546 violation: MeshInstanceShapeViolation::MeshIndexOutOfRange,
2547 })
2548 );
2549
2550 let mut document = instance_document();
2551 document.assets.instances.push(later_instance);
2552 document.assets.instances[1].skin_joints = vec![1];
2553 assert_eq!(
2554 validate_document_shape(&document),
2555 Err(DocumentShapeError::MeshInstanceShape {
2556 instance_index: 1,
2557 violation: MeshInstanceShapeViolation::SkinJointOutOfRange,
2558 })
2559 );
2560 }
2561
2562 #[test]
2563 fn document_shape_finds_duplicates_that_do_not_involve_the_first_item() {
2564 let mut document = Document::default();
2565 document.assets.source_skeleton.skins = [4, 5, 5]
2566 .into_iter()
2567 .map(|source_skin_index| SourceSkinAsset {
2568 source_skin_index,
2569 ..SourceSkinAsset::default()
2570 })
2571 .collect();
2572 assert_eq!(
2573 validate_document_shape(&document),
2574 Err(DocumentShapeError::DuplicateSourceSkinIndex {
2575 source_skin_index: 5,
2576 })
2577 );
2578
2579 let scale_track = Track {
2580 property: Property::Scale,
2581 values: TrackValues::Vec3s(vec![Vec3::ONE]),
2582 ..valid_track()
2583 };
2584 let mut document = track_document(valid_track());
2585 document.clips[0].tracks.push(scale_track.clone());
2586 document.clips[0].tracks.push(scale_track);
2587 assert_eq!(
2588 validate_document_shape(&document),
2589 Err(DocumentShapeError::DuplicateClipTrack {
2590 clip_index: 0,
2591 node: 0,
2592 property: Property::Scale,
2593 })
2594 );
2595 }
2596
2597 #[test]
2598 fn document_shape_checks_later_tracks_and_inverse_binds() {
2599 let mut document = track_document(valid_track());
2600 document.clips[0].tracks.push(Track {
2601 property: Property::Scale,
2602 times: Vec::new(),
2603 values: TrackValues::Vec3s(Vec::new()),
2604 ..valid_track()
2605 });
2606 assert_eq!(
2607 validate_document_shape(&document),
2608 Err(DocumentShapeError::TrackShape {
2609 clip_index: 0,
2610 node: 0,
2611 violation: TrackShapeViolation::EmptyTimes,
2612 })
2613 );
2614
2615 let scale_track = Track {
2616 property: Property::Scale,
2617 values: TrackValues::Vec3s(vec![Vec3::ONE]),
2618 ..valid_track()
2619 };
2620 let mut document = track_document(valid_track());
2621 document.clips.push(Clip {
2622 name: "later".into(),
2623 duration_s: 0.0,
2624 tracks: vec![scale_track.clone(), scale_track],
2625 });
2626 assert_eq!(
2627 validate_document_shape(&document),
2628 Err(DocumentShapeError::DuplicateClipTrack {
2629 clip_index: 1,
2630 node: 0,
2631 property: Property::Scale,
2632 })
2633 );
2634
2635 let mut document = track_document(valid_track());
2636 document.clips.push(Clip {
2637 name: "later".into(),
2638 duration_s: 0.0,
2639 tracks: vec![Track {
2640 property: Property::Scale,
2641 times: Vec::new(),
2642 values: TrackValues::Vec3s(Vec::new()),
2643 ..valid_track()
2644 }],
2645 });
2646 assert_eq!(
2647 validate_document_shape(&document),
2648 Err(DocumentShapeError::TrackShape {
2649 clip_index: 1,
2650 node: 0,
2651 violation: TrackShapeViolation::EmptyTimes,
2652 })
2653 );
2654
2655 let mut columns = Mat4::IDENTITY.to_cols_array();
2656 columns[15] = f32::NAN;
2657 let non_finite_inverse_bind = Mat4::from_cols_array(&columns);
2658
2659 let mut document = instance_document();
2660 document.skeleton.bones.push(bone(Some(0)));
2661 document.assets.instances[0].skin_joints = vec![0, 1];
2662 document.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY, non_finite_inverse_bind];
2663 assert_eq!(
2664 validate_document_shape(&document),
2665 Err(DocumentShapeError::MeshInstanceShape {
2666 instance_index: 0,
2667 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2668 })
2669 );
2670
2671 let mut document = instance_document();
2672 document.assets.instances.push(MeshInstance {
2673 node: 0,
2674 mesh: 0,
2675 skin_joints: vec![0],
2676 skin_ibms: vec![non_finite_inverse_bind],
2677 ..MeshInstance::default()
2678 });
2679 assert_eq!(
2680 validate_document_shape(&document),
2681 Err(DocumentShapeError::MeshInstanceShape {
2682 instance_index: 1,
2683 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2684 })
2685 );
2686
2687 let mut document = instance_document();
2688 document.assets.instances.push(MeshInstance {
2689 node: 0,
2690 mesh: 0,
2691 skin_joints: vec![0],
2692 skin_ibms: vec![Mat4::IDENTITY, Mat4::IDENTITY],
2693 ..MeshInstance::default()
2694 });
2695 assert_eq!(
2696 validate_document_shape(&document),
2697 Err(DocumentShapeError::MeshInstanceShape {
2698 instance_index: 1,
2699 violation: MeshInstanceShapeViolation::SkinInverseBindCountMismatch,
2700 })
2701 );
2702
2703 let mut document = one_bone_document();
2704 document.skeleton.bones.push(Bone {
2705 inverse_bind: Some(non_finite_inverse_bind),
2706 ..bone(Some(0))
2707 });
2708 assert_eq!(
2709 validate_document_shape(&document),
2710 Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 1 })
2711 );
2712 }
2713
2714 #[test]
2715 fn document_shape_violation_names_remain_machine_stable() {
2716 let source_projection = [
2717 (
2718 SourceProjectionViolation::ProjectedBoneOutOfRange,
2719 "projected_bone_out_of_range",
2720 ),
2721 (
2722 SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
2723 "two_source_nodes_project_to_one_bone",
2724 ),
2725 (
2726 SourceProjectionViolation::ParentSourceNodeMissing,
2727 "parent_source_node_is_missing",
2728 ),
2729 (
2730 SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
2731 "cyclic_unprojected_source_parent_chain",
2732 ),
2733 (
2734 SourceProjectionViolation::NearestProjectedParentMismatch,
2735 "projection_and_skeleton_parents_differ",
2736 ),
2737 (
2738 SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
2739 "projected_bone_has_an_unprojected_skeleton_child",
2740 ),
2741 ];
2742 for (violation, expected) in source_projection {
2743 assert_eq!(violation.to_string(), expected);
2744 }
2745
2746 let track = [
2747 (
2748 TrackShapeViolation::BoneIndexOutOfRange,
2749 "bone_index_out_of_range",
2750 ),
2751 (TrackShapeViolation::EmptyTimes, "empty_times"),
2752 (TrackShapeViolation::NonFiniteTime, "non_finite_time"),
2753 (
2754 TrackShapeViolation::TimesNotStrictlyIncreasing,
2755 "times_not_strictly_increasing",
2756 ),
2757 (
2758 TrackShapeViolation::ValueCountMismatch,
2759 "value_count_mismatch",
2760 ),
2761 (
2762 TrackShapeViolation::ValueTypeMismatchesProperty,
2763 "value_type_mismatches_property",
2764 ),
2765 (TrackShapeViolation::NonFiniteValue, "non_finite_value"),
2766 ];
2767 for (violation, expected) in track {
2768 assert_eq!(violation.to_string(), expected);
2769 }
2770
2771 let instance = [
2772 (
2773 MeshInstanceShapeViolation::NodeIndexOutOfRange,
2774 "node_index_out_of_range",
2775 ),
2776 (
2777 MeshInstanceShapeViolation::MeshIndexOutOfRange,
2778 "mesh_index_out_of_range",
2779 ),
2780 (
2781 MeshInstanceShapeViolation::SkinJointOutOfRange,
2782 "skin_joint_out_of_range",
2783 ),
2784 (
2785 MeshInstanceShapeViolation::SkinInverseBindCountMismatch,
2786 "skin_ibm_count_mismatch",
2787 ),
2788 (
2789 MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2790 "non_finite_inverse_bind",
2791 ),
2792 ];
2793 for (violation, expected) in instance {
2794 assert_eq!(violation.to_string(), expected);
2795 }
2796 }
2797
2798 #[test]
2799 fn tolerant_world_rests_keep_unrelated_partial_evidence() {
2800 let skeleton = Skeleton {
2801 bones: vec![
2802 bone(None),
2803 bone(Some(99)),
2804 Bone {
2805 rest: Transform {
2806 translation: Vec3::X,
2807 ..Transform::IDENTITY
2808 },
2809 ..bone(None)
2810 },
2811 Bone {
2812 rest: Transform {
2813 translation: Vec3::Y,
2814 ..Transform::IDENTITY
2815 },
2816 ..bone(Some(2))
2817 },
2818 bone(Some(1)),
2819 ],
2820 };
2821
2822 let worlds = tolerant_world_rest_matrices(&skeleton);
2823 assert_eq!(worlds.len(), 5);
2824 assert_eq!(worlds[0], Some(Mat4::IDENTITY));
2825 assert_eq!(worlds[1], None, "the malformed parent is unavailable");
2826 assert_eq!(worlds[2], Some(Mat4::from_translation(Vec3::X)));
2827 assert_eq!(
2828 worlds[3],
2829 Some(Mat4::from_translation(Vec3::new(1.0, 1.0, 0.0))),
2830 "a finite independent chain remains measurable"
2831 );
2832 assert_eq!(
2833 worlds[4], None,
2834 "a child of unavailable evidence is unavailable"
2835 );
2836 }
2837
2838 #[test]
2839 fn shared_affine_classifier_respects_distinct_caller_tolerances() {
2840 let equal_axis_basis = affine_test_fixtures::tolerance_divergence_basis();
2841 let strict = PositiveUniformAffineTolerance {
2842 equal_axis: 1.0e-5,
2843 relative_orthogonality: 1.0e-5,
2844 singular_determinant_relative: 1.0e-6,
2845 };
2846 let loose = PositiveUniformAffineTolerance {
2847 equal_axis: 1.0e-4,
2848 relative_orthogonality: 1.0e-4,
2849 singular_determinant_relative: 0.0,
2850 };
2851
2852 assert_eq!(
2853 classify_positive_uniform_affine(equal_axis_basis, strict),
2854 Err(AffineDomainViolation::NonUniformScale),
2855 "the stricter caller rejects this equal-axis difference"
2856 );
2857 assert!(
2858 classify_positive_uniform_affine(equal_axis_basis, loose).is_ok(),
2859 "the looser caller accepts this equal-axis difference"
2860 );
2861
2862 let orthogonality_basis = affine_test_fixtures::orthogonality_tolerance_divergence_basis();
2863 assert_eq!(
2864 classify_positive_uniform_affine(orthogonality_basis, strict),
2865 Err(AffineDomainViolation::Sheared),
2866 "the stricter caller rejects this cross-axis dot product"
2867 );
2868 assert!(
2869 classify_positive_uniform_affine(orthogonality_basis, loose).is_ok(),
2870 "the looser caller accepts this cross-axis dot product"
2871 );
2872 }
2873
2874 #[test]
2875 fn shared_affine_classifier_pins_its_symmetric_f64_formula() {
2876 let policy = PositiveUniformAffineTolerance {
2877 equal_axis: 1.0e-5,
2878 relative_orthogonality: 1.0e-5,
2879 singular_determinant_relative: 1.0e-6,
2880 };
2881
2882 let on_long_edge = Mat3::from_diagonal(Vec3::new(99_998.5, 99_998.5, 100_000.0));
2887 assert_eq!(
2888 classify_positive_uniform_affine(on_long_edge, policy),
2889 Ok(99_999.0)
2890 );
2891 let short = 99_998.5;
2892 let long = 100_000.0 + 0.007_812_5;
2893 for diagonal in [
2894 Vec3::new(long, short, short),
2895 Vec3::new(short, long, short),
2896 Vec3::new(short, short, long),
2897 ] {
2898 assert_eq!(
2899 classify_positive_uniform_affine(Mat3::from_diagonal(diagonal), policy),
2900 Err(AffineDomainViolation::NonUniformScale)
2901 );
2902 }
2903
2904 let short = 1.0 - 2.0_f32.powi(-16);
2908 for diagonal in [
2909 Vec3::new(short, 1.0, 1.0),
2910 Vec3::new(1.0, short, 1.0),
2911 Vec3::new(1.0, 1.0, short),
2912 ] {
2913 assert_eq!(
2914 classify_positive_uniform_affine(Mat3::from_diagonal(diagonal), policy),
2915 Err(AffineDomainViolation::NonUniformScale)
2916 );
2917 }
2918
2919 let c0 = Vec3::new(0.12792248, -0.99066633, -0.047073245);
2922 let c1 = Vec3::new(-0.34637994, -0.00016034879, -0.93809813);
2923 let c2 = Vec3::new(0.92933476, 0.13630849, -0.3431568);
2924 assert!((c1.dot(c2) as f64).abs() < 1.0e-5);
2925 assert!(c1.as_dvec3().dot(c2.as_dvec3()).abs() > 1.0e-5);
2926 assert_eq!(
2927 classify_positive_uniform_affine(Mat3::from_cols(c0, c1, c2), policy),
2928 Err(AffineDomainViolation::Sheared)
2929 );
2930
2931 for shear in [2.0_f32.powi(-15), -2.0_f32.powi(-15)] {
2934 let basis = Mat3::from_cols(Vec3::X, Vec3::new(shear, 1.0, 0.0), Vec3::Z);
2935 assert_eq!(
2936 classify_positive_uniform_affine(basis, policy),
2937 Err(AffineDomainViolation::Sheared)
2938 );
2939 }
2940 }
2941
2942 #[test]
2943 fn affine_axis_mean_is_ascending_and_column_order_invariant() {
2944 let lengths = [
2949 f64::from_bits(0x3ff1_09e7_e000_022c),
2950 f64::from_bits(0x3ff1_09ec_6000_0eb5),
2951 f64::from_bits(0x3ff1_09fa_e000_3cde),
2952 ];
2953 let expected = f64::from_bits(0x3ff1_09ef_b555_6f3f);
2954 let ascending = (lengths[0] + lengths[1] + lengths[2]) / 3.0;
2955 let descending = (lengths[2] + lengths[1] + lengths[0]) / 3.0;
2956 assert_eq!(expected.to_bits(), 0x3ff1_09ef_b555_6f3f);
2957 assert_eq!(ascending.to_bits(), expected.to_bits());
2958 assert_eq!(descending.to_bits(), 0x3ff1_09ef_b555_6f40);
2959 for order in [
2960 [0, 1, 2],
2961 [0, 2, 1],
2962 [1, 0, 2],
2963 [1, 2, 0],
2964 [2, 0, 1],
2965 [2, 1, 0],
2966 ] {
2967 assert_eq!(
2968 average_affine_axis_length(order.map(|index| lengths[index])),
2969 expected,
2970 "axis order {order:?}"
2971 );
2972 }
2973
2974 let dyadic = [2.0_f64.powi(53), 1.0, 1.0];
2979 let ascending = (dyadic[1] + dyadic[2] + dyadic[0]) / 3.0;
2980 let descending = (dyadic[0] + dyadic[1] + dyadic[2]) / 3.0;
2981 assert_ne!(ascending, descending);
2982 assert_eq!(average_affine_axis_length(dyadic), ascending);
2983
2984 let permutations = affine_test_fixtures::appendix_d_v6_mean_permutations();
2990 let expected_length_bits = lengths.map(f64::to_bits);
2991 assert_eq!(
2992 affine_axis_lengths(permutations[0]).map(f64::to_bits),
2993 expected_length_bits
2994 );
2995 let tolerance = PositiveUniformAffineTolerance {
2996 equal_axis: 1.0e-5,
2997 relative_orthogonality: 1.0e-5,
2998 singular_determinant_relative: 1.0e-6,
2999 };
3000 for (permutation, linear) in permutations.into_iter().enumerate() {
3001 assert!(
3002 linear
3003 .x_axis
3004 .as_dvec3()
3005 .cross(linear.y_axis.as_dvec3())
3006 .dot(linear.z_axis.as_dvec3())
3007 > 0.0,
3008 "orientation for permutation {permutation}"
3009 );
3010 assert_eq!(
3011 average_affine_axis_length(affine_axis_lengths(linear)).to_bits(),
3012 expected.to_bits(),
3013 "mean for permutation {permutation}"
3014 );
3015 assert_eq!(
3016 classify_positive_uniform_affine(linear, tolerance),
3017 Err(AffineDomainViolation::NonUniformScale),
3018 "classification for permutation {permutation}"
3019 );
3020 }
3021 }
3022
3023 #[test]
3024 fn shared_affine_classifier_pins_f64_determinant_arithmetic() {
3025 let linear = Mat3::from_cols(
3030 Vec3::new(
3031 f32::from_bits(0x3ff3_5574),
3032 f32::from_bits(0x3f0e_fa3c),
3033 0.0,
3034 ),
3035 Vec3::new(
3036 f32::from_bits(0x3ff5_5e17),
3037 f32::from_bits(0x3f10_2c31),
3038 0.0,
3039 ),
3040 Vec3::Z,
3041 );
3042 let columns = [
3043 linear.x_axis.as_dvec3(),
3044 linear.y_axis.as_dvec3(),
3045 linear.z_axis.as_dvec3(),
3046 ];
3047 let determinant_f64 = columns[2].dot(columns[0].cross(columns[1]));
3048 let determinant_f32 = f64::from(linear.determinant());
3049 let lengths = affine_axis_lengths(linear);
3050 let threshold = (determinant_f64 + determinant_f32) / 2.0;
3051 assert!(determinant_f64 < threshold);
3052 assert!(determinant_f32 > threshold);
3053
3054 assert_eq!(
3055 classify_positive_uniform_affine(
3056 linear,
3057 PositiveUniformAffineTolerance {
3058 equal_axis: 10.0,
3059 relative_orthogonality: 10.0,
3060 singular_determinant_relative: threshold
3061 / (lengths[0] * lengths[1] * lengths[2]),
3062 },
3063 ),
3064 Err(AffineDomainViolation::Singular)
3065 );
3066
3067 let large_uniform = 2.0e19_f32;
3072 assert_eq!(
3073 classify_positive_uniform_affine(
3074 Mat3::from_diagonal(Vec3::splat(large_uniform)),
3075 PositiveUniformAffineTolerance {
3076 equal_axis: 1.0e-5,
3077 relative_orthogonality: 1.0e-5,
3078 singular_determinant_relative: 1.0e-6,
3079 },
3080 ),
3081 Ok(f64::from(large_uniform))
3082 );
3083 }
3084
3085 #[test]
3086 fn affine_geometry_facts_pin_every_widened_field_and_slot() {
3087 let linear = Mat3::from_cols(
3088 Vec3::new(1.0, 2.0, 3.0),
3089 Vec3::new(4.0, 5.0, 6.0),
3090 Vec3::new(7.0, 8.0, 10.0),
3091 );
3092
3093 let facts = AffineGeometryFacts::from_linear(linear).expect("finite widened facts");
3094 assert_eq!(
3095 facts.axis_lengths.map(f64::to_bits),
3096 [
3097 0x400d_eeea_1168_3f49,
3098 0x4021_8cc8_21d6_d3e3,
3099 0x402d_3064_dcc8_ae67,
3100 ]
3101 );
3102 assert_eq!(facts.mean_axis_length.to_bits(), 0x4022_12f7_d653_30b4);
3103 assert_eq!(facts.determinant.to_bits(), 0xc008_0000_0000_0000);
3104 assert_eq!(facts.axis_length_product.to_bits(), 0x407d_f2e3_88f2_1b01);
3105 assert_eq!(
3106 facts.cross_axis_dots.map(f64::to_bits),
3107 [
3108 0x4040_0000_0000_0000,
3109 0x404a_8000_0000_0000,
3110 0x4060_0000_0000_0000,
3111 ],
3112 "cross-axis slots are XY, XZ, YZ"
3113 );
3114 }
3115
3116 #[test]
3117 fn affine_geometry_facts_widen_every_dot_product_before_multiplying() {
3118 let x = Vec3::new(
3119 f32::from_bits(0x3ff3_5574),
3120 f32::from_bits(0x3f0e_fa3c),
3121 0.0,
3122 );
3123 let y = Vec3::new(
3124 f32::from_bits(0x3ff5_5e17),
3125 f32::from_bits(0x3f10_2c31),
3126 0.0,
3127 );
3128 let widened_dot = x.as_dvec3().dot(y.as_dvec3());
3129 let f32_then_widened = f64::from(x.dot(y));
3130
3131 for (slot, linear) in [
3132 (0, Mat3::from_cols(x, y, Vec3::Z)),
3133 (1, Mat3::from_cols(x, Vec3::Z, y)),
3134 (2, Mat3::from_cols(Vec3::Z, x, y)),
3135 ] {
3136 let facts = AffineGeometryFacts::from_linear(linear).expect("finite widened facts");
3137 assert_eq!(facts.cross_axis_dots[slot], widened_dot);
3138 assert_ne!(
3139 facts.cross_axis_dots[slot], f32_then_widened,
3140 "dot slot {slot} must multiply and add in f64, not widen an f32 result"
3141 );
3142 }
3143 }
3144
3145 #[test]
3146 fn weld_preserves_uv_seams_at_shared_positions() {
3147 let mut primitive = Primitive {
3148 positions: vec![Vec3::ZERO, Vec3::ZERO, Vec3::ZERO],
3149 uvs: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 0.0]],
3150 ..Primitive::default()
3151 };
3152
3153 primitive.weld();
3154
3155 assert_eq!(primitive.positions.len(), 2);
3156 let reconstructed_corners = primitive
3157 .indices
3158 .iter()
3159 .map(|&index| {
3160 let index = index as usize;
3161 (primitive.positions[index], primitive.uvs[index])
3162 })
3163 .collect::<Vec<_>>();
3164 assert_eq!(
3165 reconstructed_corners,
3166 vec![
3167 (Vec3::ZERO, [0.0, 0.0]),
3168 (Vec3::ZERO, [1.0, 0.0]),
3169 (Vec3::ZERO, [0.0, 0.0]),
3170 ]
3171 );
3172 }
3173}