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("scene {scene_index} root {root_index} references missing node {bone}")]
581 SceneRootOutOfBounds {
582 scene_index: usize,
584 root_index: usize,
586 bone: BoneId,
588 },
589 #[error("source skeleton declares duplicate source node index {source_node_index}")]
591 DuplicateSourceNodeIndex {
592 source_node_index: usize,
594 },
595 #[error("source skeleton declares duplicate source skin index {source_skin_index}")]
597 DuplicateSourceSkinIndex {
598 source_skin_index: usize,
600 },
601 #[error(
603 "source node {source_node_index} contradicts the document skeleton's parent chain ({violation})"
604 )]
605 SourceProjection {
606 source_node_index: usize,
608 violation: SourceProjectionViolation,
610 },
611 #[error("clip {clip_index} declares duplicate {property:?} tracks for node {node}")]
613 DuplicateClipTrack {
614 clip_index: usize,
616 node: BoneId,
618 property: Property,
620 },
621 #[error("clip {clip_index} track for node {node} has an invalid shape ({violation})")]
623 TrackShape {
624 clip_index: usize,
626 node: BoneId,
628 violation: TrackShapeViolation,
630 },
631 #[error("mesh instance {instance_index} is invalid ({violation})")]
633 MeshInstanceShape {
634 instance_index: usize,
636 violation: MeshInstanceShapeViolation,
638 },
639 #[error("node {node} has a non-finite inverse-bind matrix")]
641 NonFiniteBoneInverseBind {
642 node: BoneId,
644 },
645}
646
647#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
649#[non_exhaustive]
650pub enum SourceProjectionViolation {
651 #[error("projected_bone_out_of_range")]
653 ProjectedBoneOutOfRange,
654 #[error("two_source_nodes_project_to_one_bone")]
656 TwoSourceNodesProjectToOneBone,
657 #[error("parent_source_node_is_missing")]
659 ParentSourceNodeMissing,
660 #[error("cyclic_unprojected_source_parent_chain")]
662 CyclicUnprojectedSourceParentChain,
663 #[error("projection_and_skeleton_parents_differ")]
665 NearestProjectedParentMismatch,
666 #[error("projected_bone_has_an_unprojected_skeleton_child")]
668 ProjectedBoneHasUnprojectedSkeletonChild,
669}
670
671#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
673#[non_exhaustive]
674pub enum TrackShapeViolation {
675 #[error("bone_index_out_of_range")]
677 BoneIndexOutOfRange,
678 #[error("empty_times")]
680 EmptyTimes,
681 #[error("non_finite_time")]
683 NonFiniteTime,
684 #[error("times_not_strictly_increasing")]
686 TimesNotStrictlyIncreasing,
687 #[error("value_count_mismatch")]
689 ValueCountMismatch,
690 #[error("value_type_mismatches_property")]
692 ValueTypeMismatchesProperty,
693 #[error("non_finite_value")]
695 NonFiniteValue,
696}
697
698#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
700#[non_exhaustive]
701pub enum MeshInstanceShapeViolation {
702 #[error("node_index_out_of_range")]
704 NodeIndexOutOfRange,
705 #[error("mesh_index_out_of_range")]
707 MeshIndexOutOfRange,
708 #[error("skin_joint_out_of_range")]
710 SkinJointOutOfRange,
711 #[error("skin_ibm_count_mismatch")]
713 SkinInverseBindCountMismatch,
714 #[error("non_finite_inverse_bind")]
716 NonFiniteSkinInverseBind,
717}
718
719#[derive(Debug, Clone, Default)]
735pub struct Primitive {
736 pub source_primitive_index: Option<usize>,
741 pub material: Option<usize>,
743 pub indices: Vec<u32>,
745 pub positions: Vec<Vec3>,
747 pub normals: Vec<Vec3>,
749 pub uvs: Vec<[f32; 2]>,
751 pub joints: Vec<[u16; 4]>,
753 pub weights: Vec<[f32; 4]>,
755 pub additional_influence_sets: Vec<AdditionalInfluenceSet>,
761}
762
763#[derive(Debug, Clone, Copy, PartialEq, Eq)]
770pub struct AdditionalInfluenceSet {
771 pub set_index: u32,
773 pub joints_present: bool,
775 pub weights_present: bool,
777}
778
779#[derive(Debug, Clone, Default)]
781pub struct MeshAsset {
782 pub name: String,
784 pub source_mesh_index: usize,
789 pub primitives: Vec<Primitive>,
791}
792
793#[derive(Debug, Clone, Default)]
795pub struct MeshInstance {
796 pub source_node_index: usize,
798 pub node: BoneId,
800 pub mesh: usize,
802 pub skin_joints: Vec<BoneId>,
804 pub skin_ibms: Vec<Mat4>,
809}
810
811#[derive(Debug, Clone, Default)]
813pub struct SceneAsset {
814 pub source_scene_index: usize,
816 pub name: Option<String>,
818 pub roots: Vec<BoneId>,
820}
821
822#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
829#[serde(rename_all = "snake_case")]
830pub enum SourceSkeletonCoverage {
831 #[default]
833 Unavailable,
834 Complete,
838}
839
840#[derive(Debug, Clone)]
852pub enum SourceNodeLocalRest {
853 Trs {
855 translation: Vec3,
857 rotation: Quat,
859 scale: Vec3,
861 },
862 Matrix(Mat4),
864}
865
866#[derive(Debug, Clone)]
880#[non_exhaustive]
881pub struct SourceNodeAsset {
882 pub source_node_index: usize,
884 pub name: Option<String>,
886 pub parent_source_node_index: Option<usize>,
888 pub scene_root_indices: Vec<usize>,
890 pub local_rest: SourceNodeLocalRest,
892 pub bone: Option<BoneId>,
912}
913
914impl SourceNodeAsset {
915 pub fn new(source_node_index: usize, local_rest: SourceNodeLocalRest) -> Self {
923 Self {
924 source_node_index,
925 name: None,
926 parent_source_node_index: None,
927 scene_root_indices: Vec::new(),
928 local_rest,
929 bone: None,
930 }
931 }
932}
933
934#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
942#[serde(rename_all = "snake_case")]
943pub enum SourceInverseBindAccessorStatus {
944 #[default]
946 Absent,
947 Available,
949 EmptyAccessor,
951 CountMismatch,
953 Unreadable,
955}
956
957#[derive(Debug, Clone, Default)]
959pub struct SourceInverseBindAccessor {
960 pub status: SourceInverseBindAccessorStatus,
962 pub declared_count: Option<usize>,
964 pub matrices: Vec<Mat4>,
973}
974
975#[derive(Debug, Clone)]
977pub struct SourceSkinAttachment {
978 pub source_node_index: usize,
980 pub source_mesh_index: Option<usize>,
985}
986
987#[derive(Debug, Clone, Default)]
989pub struct SourceSkinAsset {
990 pub source_skin_index: usize,
992 pub name: Option<String>,
994 pub skeleton_root_source_node_index: Option<usize>,
996 pub joint_source_node_indices: Vec<usize>,
998 pub inverse_bind_accessor: SourceInverseBindAccessor,
1004 pub attachments: Vec<SourceSkinAttachment>,
1006}
1007
1008#[derive(Debug, Clone, Default)]
1010pub struct SourceSkeletonAssets {
1011 pub coverage: SourceSkeletonCoverage,
1013 pub nodes: Vec<SourceNodeAsset>,
1015 pub skins: Vec<SourceSkinAsset>,
1017}
1018
1019#[derive(Debug, Clone)]
1022pub struct TextureAsset {
1023 pub bytes: Vec<u8>,
1025 pub mime: String,
1027}
1028
1029#[derive(Debug, Clone)]
1034pub struct NormalTextureAsset {
1035 pub texture: TextureAsset,
1037 pub scale: f32,
1039}
1040
1041#[derive(Debug, Clone)]
1047pub struct OcclusionTextureAsset {
1048 pub texture: TextureAsset,
1050 pub strength: f32,
1052}
1053
1054#[derive(Debug, Clone)]
1056pub struct MaterialAsset {
1057 pub name: String,
1059 pub base_color: [f32; 4],
1062 pub metallic: f32,
1064 pub roughness: f32,
1066 pub base_color_texture: Option<TextureAsset>,
1068 pub normal_texture: Option<NormalTextureAsset>,
1070 pub metallic_roughness_texture: Option<TextureAsset>,
1074 pub occlusion_texture: Option<OcclusionTextureAsset>,
1076}
1077
1078#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1084#[serde(rename_all = "snake_case")]
1085pub enum MaterialResourceCoverage {
1086 Complete,
1090 #[default]
1092 Unavailable,
1093}
1094
1095#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1100#[serde(rename_all = "snake_case")]
1101pub enum MaterialTextureSlot {
1102 BaseColor,
1104 Normal,
1106 MetallicRoughness,
1108 Occlusion,
1110 Emissive,
1112}
1113
1114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1116pub struct SourceMaterialTextureBinding {
1117 pub slot: MaterialTextureSlot,
1119 pub texture_index: usize,
1121}
1122
1123#[derive(Debug, Clone, Default)]
1125pub struct SourceMaterialAsset {
1126 pub material_index: usize,
1128 pub name: Option<String>,
1130 pub texture_bindings: Vec<SourceMaterialTextureBinding>,
1132}
1133
1134#[derive(Debug, Clone, Default)]
1136pub struct SourceTextureAsset {
1137 pub texture_index: usize,
1139 pub name: Option<String>,
1141 pub image_index: usize,
1143}
1144
1145#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1147#[serde(rename_all = "snake_case")]
1148pub enum ImageSourceKind {
1149 Embedded,
1151 DataUri,
1153 External,
1155}
1156
1157#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1159#[serde(rename_all = "snake_case")]
1160pub enum ImageContainerFormat {
1161 Png,
1163 Jpeg,
1165}
1166
1167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1169#[serde(rename_all = "snake_case")]
1170pub enum DecodedImageColorType {
1171 L8,
1173 La8,
1175 Rgb8,
1177 Rgba8,
1179 L16,
1181 La16,
1183 Rgb16,
1185 Rgba16,
1187}
1188
1189#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1191#[serde(rename_all = "snake_case")]
1192pub enum ImageUnavailableReason {
1193 SourceUnavailable,
1195 InvalidDataUri,
1197 UnsupportedContainer,
1199 DecodeFailed,
1201 ResourceLimit,
1203}
1204
1205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1207pub enum SourceImageInspection {
1208 Available {
1210 width: u32,
1212 height: u32,
1214 channel_count: u8,
1216 color_type: DecodedImageColorType,
1218 },
1219 Unavailable {
1221 reason: ImageUnavailableReason,
1223 },
1224}
1225
1226#[derive(Debug, Clone)]
1228pub struct SourceImageAsset {
1229 pub image_index: usize,
1231 pub name: Option<String>,
1233 pub source_kind: ImageSourceKind,
1235 pub declared_mime_type: Option<String>,
1237 pub detected_container: Option<ImageContainerFormat>,
1239 pub leading_magic_hex: Option<String>,
1242 pub inspection: SourceImageInspection,
1244}
1245
1246#[derive(Debug, Clone, Default)]
1248pub struct MaterialResourceAssets {
1249 pub coverage: MaterialResourceCoverage,
1251 pub materials: Vec<SourceMaterialAsset>,
1253 pub textures: Vec<SourceTextureAsset>,
1255 pub images: Vec<SourceImageAsset>,
1257}
1258
1259impl Primitive {
1260 pub fn weld(&mut self) {
1264 if !self.indices.is_empty() || self.positions.is_empty() {
1265 return;
1266 }
1267 let corner_key = |i: usize| -> Vec<u8> {
1268 let mut key = Vec::with_capacity(64);
1269 let mut push_f32s = |vals: &[f32]| {
1270 for v in vals {
1271 key.extend_from_slice(&v.to_le_bytes());
1272 }
1273 };
1274 push_f32s(&self.positions[i].to_array());
1275 if let Some(n) = self.normals.get(i) {
1276 push_f32s(&n.to_array());
1277 }
1278 if let Some(uv) = self.uvs.get(i) {
1279 push_f32s(uv);
1280 }
1281 if let Some(w) = self.weights.get(i) {
1282 push_f32s(w);
1283 }
1284 if let Some(j) = self.joints.get(i) {
1285 for v in j {
1286 key.extend_from_slice(&v.to_le_bytes());
1287 }
1288 }
1289 key
1290 };
1291 let mut seen: std::collections::HashMap<Vec<u8>, u32> = std::collections::HashMap::new();
1292 let mut indices = Vec::with_capacity(self.positions.len());
1293 let mut positions = Vec::new();
1294 let mut normals = Vec::new();
1295 let mut uvs = Vec::new();
1296 let mut joints = Vec::new();
1297 let mut weights = Vec::new();
1298 for i in 0..self.positions.len() {
1299 let index = *seen.entry(corner_key(i)).or_insert_with(|| {
1300 positions.push(self.positions[i]);
1301 if let Some(n) = self.normals.get(i) {
1302 normals.push(*n);
1303 }
1304 if let Some(uv) = self.uvs.get(i) {
1305 uvs.push(*uv);
1306 }
1307 if let Some(j) = self.joints.get(i) {
1308 joints.push(*j);
1309 }
1310 if let Some(w) = self.weights.get(i) {
1311 weights.push(*w);
1312 }
1313 (positions.len() - 1) as u32
1314 });
1315 indices.push(index);
1316 }
1317 self.indices = indices;
1318 self.positions = positions;
1319 self.normals = normals;
1320 self.uvs = uvs;
1321 self.joints = joints;
1322 self.weights = weights;
1323 }
1324}
1325
1326#[derive(Debug, Clone, Default)]
1329pub struct SceneAssets {
1330 pub meshes: Vec<MeshAsset>,
1333 pub instances: Vec<MeshInstance>,
1335 pub materials: Vec<MaterialAsset>,
1337 pub material_resources: MaterialResourceAssets,
1340 pub scenes: Vec<SceneAsset>,
1342 pub default_scene: Option<usize>,
1344 pub source_skeleton: SourceSkeletonAssets,
1351}
1352
1353pub fn validate_document_shape(document: &Document) -> Result<(), DocumentShapeError> {
1371 validate_document_structure_prefix(document)?;
1372 validate_clip_tracks(document)?;
1373 validate_mesh_instances(document)?;
1374 validate_bone_inverse_binds(&document.skeleton)
1375}
1376
1377fn validate_document_structure_prefix(document: &Document) -> Result<(), DocumentShapeError> {
1378 validate_skeleton_rest(&document.skeleton)?;
1379 validate_scene_roots(document)?;
1380 validate_source_skeleton_identity(&document.assets.source_skeleton)?;
1381 validate_source_projection(document)
1382}
1383
1384fn validate_scene_roots(document: &Document) -> Result<(), DocumentShapeError> {
1385 let bone_count = document.skeleton.bones.len();
1386 for (scene_index, scene) in document.assets.scenes.iter().enumerate() {
1387 for (root_index, &bone) in scene.roots.iter().enumerate() {
1388 if bone >= bone_count {
1389 return Err(DocumentShapeError::SceneRootOutOfBounds {
1390 scene_index,
1391 root_index,
1392 bone,
1393 });
1394 }
1395 }
1396 }
1397 Ok(())
1398}
1399
1400fn validate_skeleton_rest(skeleton: &Skeleton) -> Result<(), DocumentShapeError> {
1401 world_rest_matrices(skeleton)
1402 .map(|_| ())
1403 .map_err(|error| match error {
1404 WorldMatrixError::NonFiniteTransform { node } => {
1405 DocumentShapeError::NonFiniteSkeletonRest { node }
1406 }
1407 WorldMatrixError::InvalidParent { node, parent } => {
1408 DocumentShapeError::InvalidSkeletonParent { node, parent }
1409 }
1410 })
1411}
1412
1413fn validate_source_skeleton_identity(
1414 source_skeleton: &SourceSkeletonAssets,
1415) -> Result<(), DocumentShapeError> {
1416 let mut seen_nodes = BTreeSet::new();
1417 for node in &source_skeleton.nodes {
1418 if !seen_nodes.insert(node.source_node_index) {
1419 return Err(DocumentShapeError::DuplicateSourceNodeIndex {
1420 source_node_index: node.source_node_index,
1421 });
1422 }
1423 }
1424 let mut seen_skins = BTreeSet::new();
1425 for skin in &source_skeleton.skins {
1426 if !seen_skins.insert(skin.source_skin_index) {
1427 return Err(DocumentShapeError::DuplicateSourceSkinIndex {
1428 source_skin_index: skin.source_skin_index,
1429 });
1430 }
1431 }
1432 Ok(())
1433}
1434
1435fn validate_source_projection(document: &Document) -> Result<(), DocumentShapeError> {
1449 let source_skeleton = &document.assets.source_skeleton;
1450 if source_skeleton.coverage != SourceSkeletonCoverage::Complete {
1451 return Ok(());
1452 }
1453
1454 let bones = &document.skeleton.bones;
1455 let mut bone_of_source = BTreeMap::new();
1456 let mut source_of_bone = BTreeMap::new();
1457 let mut skeleton_parents = Vec::with_capacity(source_skeleton.nodes.len());
1458 for node in &source_skeleton.nodes {
1459 let Some(bone) = node.bone else {
1460 continue;
1461 };
1462 let skeleton_parent = bones
1463 .get(bone)
1464 .ok_or(DocumentShapeError::SourceProjection {
1465 source_node_index: node.source_node_index,
1466 violation: SourceProjectionViolation::ProjectedBoneOutOfRange,
1467 })?
1468 .parent;
1469 if source_of_bone
1470 .insert(bone, node.source_node_index)
1471 .is_some()
1472 {
1473 return Err(DocumentShapeError::SourceProjection {
1474 source_node_index: node.source_node_index,
1475 violation: SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
1476 });
1477 }
1478 bone_of_source.insert(node.source_node_index, bone);
1479 skeleton_parents.push((node, skeleton_parent));
1480 }
1481
1482 let by_source_index: BTreeMap<_, _> = source_skeleton
1483 .nodes
1484 .iter()
1485 .map(|node| (node.source_node_index, node))
1486 .collect();
1487 let unprojected_rows = source_skeleton.nodes.len() - bone_of_source.len();
1488 let mut resolved_unprojected = BTreeMap::<usize, Option<BoneId>>::new();
1492 for (node, skeleton_parent) in skeleton_parents {
1493 let mut cursor = node.parent_source_node_index;
1494 let mut unresolved_suffix = Vec::new();
1495 let projected_parent = loop {
1496 let Some(parent_source_node_index) = cursor else {
1497 break None;
1498 };
1499 if let Some(&bone) = bone_of_source.get(&parent_source_node_index) {
1500 break Some(bone);
1501 }
1502 if let Some(&projected_parent) = resolved_unprojected.get(&parent_source_node_index) {
1503 break projected_parent;
1504 }
1505 let parent = by_source_index.get(&parent_source_node_index).ok_or(
1506 DocumentShapeError::SourceProjection {
1507 source_node_index: node.source_node_index,
1508 violation: SourceProjectionViolation::ParentSourceNodeMissing,
1509 },
1510 )?;
1511 unresolved_suffix.push(parent_source_node_index);
1512 if unresolved_suffix.len() > unprojected_rows {
1513 return Err(DocumentShapeError::SourceProjection {
1514 source_node_index: node.source_node_index,
1515 violation: SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
1516 });
1517 }
1518 cursor = parent.parent_source_node_index;
1519 };
1520 for source_node_index in unresolved_suffix {
1521 resolved_unprojected.insert(source_node_index, projected_parent);
1522 }
1523 if projected_parent != skeleton_parent {
1524 return Err(DocumentShapeError::SourceProjection {
1525 source_node_index: node.source_node_index,
1526 violation: SourceProjectionViolation::NearestProjectedParentMismatch,
1527 });
1528 }
1529 }
1530
1531 for (bone, child) in bones.iter().enumerate() {
1532 if source_of_bone.contains_key(&bone) {
1533 continue;
1534 }
1535 if let Some(parent) = child.parent
1536 && let Some(&source_node_index) = source_of_bone.get(&parent)
1537 {
1538 return Err(DocumentShapeError::SourceProjection {
1539 source_node_index,
1540 violation: SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
1541 });
1542 }
1543 }
1544 Ok(())
1545}
1546
1547fn validate_clip_tracks(document: &Document) -> Result<(), DocumentShapeError> {
1548 let bone_count = document.skeleton.bones.len();
1549 for (clip_index, clip) in document.clips.iter().enumerate() {
1550 let mut seen = Vec::with_capacity(clip.tracks.len());
1551 for track in &clip.tracks {
1552 if track.bone >= bone_count {
1553 return Err(DocumentShapeError::TrackShape {
1554 clip_index,
1555 node: track.bone,
1556 violation: TrackShapeViolation::BoneIndexOutOfRange,
1557 });
1558 }
1559 if seen.contains(&(track.bone, track.property)) {
1560 return Err(DocumentShapeError::DuplicateClipTrack {
1561 clip_index,
1562 node: track.bone,
1563 property: track.property,
1564 });
1565 }
1566 seen.push((track.bone, track.property));
1567 validate_track_shape(clip_index, track)?;
1568 }
1569 }
1570 Ok(())
1571}
1572
1573pub(crate) fn validate_track_shape(
1574 clip_index: usize,
1575 track: &Track,
1576) -> Result<(), DocumentShapeError> {
1577 let violation = if track.times.is_empty() {
1578 Some(TrackShapeViolation::EmptyTimes)
1579 } else if track.times.iter().any(|time| !time.is_finite()) {
1580 Some(TrackShapeViolation::NonFiniteTime)
1581 } else if track.times.windows(2).any(|times| times[0] >= times[1]) {
1582 Some(TrackShapeViolation::TimesNotStrictlyIncreasing)
1583 } else {
1584 let expected_values = match track.interpolation {
1585 Interpolation::CubicSpline => track.times.len().checked_mul(3),
1586 Interpolation::Linear | Interpolation::Step => Some(track.times.len()),
1587 };
1588 if expected_values != Some(track.values.len()) {
1589 Some(TrackShapeViolation::ValueCountMismatch)
1590 } else if !matches!(
1591 (&track.values, track.property),
1592 (
1593 TrackValues::Vec3s(_),
1594 Property::Translation | Property::Scale
1595 ) | (TrackValues::Quats(_), Property::Rotation)
1596 ) {
1597 Some(TrackShapeViolation::ValueTypeMismatchesProperty)
1598 } else if match &track.values {
1599 TrackValues::Vec3s(values) => values.iter().any(|value| !value.is_finite()),
1600 TrackValues::Quats(values) => values.iter().any(|value| !value.is_finite()),
1601 } {
1602 Some(TrackShapeViolation::NonFiniteValue)
1603 } else {
1604 None
1605 }
1606 };
1607 violation.map_or(Ok(()), |violation| {
1608 Err(DocumentShapeError::TrackShape {
1609 clip_index,
1610 node: track.bone,
1611 violation,
1612 })
1613 })
1614}
1615
1616fn validate_mesh_instances(document: &Document) -> Result<(), DocumentShapeError> {
1617 let bone_count = document.skeleton.bones.len();
1618 let mesh_count = document.assets.meshes.len();
1619 for (instance_index, instance) in document.assets.instances.iter().enumerate() {
1620 let violation = if instance.node >= bone_count {
1621 Some(MeshInstanceShapeViolation::NodeIndexOutOfRange)
1622 } else if instance.mesh >= mesh_count {
1623 Some(MeshInstanceShapeViolation::MeshIndexOutOfRange)
1624 } else if instance
1625 .skin_joints
1626 .iter()
1627 .any(|&joint| joint >= bone_count)
1628 {
1629 Some(MeshInstanceShapeViolation::SkinJointOutOfRange)
1630 } else if !instance.skin_ibms.is_empty()
1631 && instance.skin_ibms.len() != instance.skin_joints.len()
1632 {
1633 Some(MeshInstanceShapeViolation::SkinInverseBindCountMismatch)
1634 } else if instance.skin_ibms.iter().any(|ibm| !mat4_is_finite(*ibm)) {
1635 Some(MeshInstanceShapeViolation::NonFiniteSkinInverseBind)
1636 } else {
1637 None
1638 };
1639 if let Some(violation) = violation {
1640 return Err(DocumentShapeError::MeshInstanceShape {
1641 instance_index,
1642 violation,
1643 });
1644 }
1645 }
1646 Ok(())
1647}
1648
1649fn validate_bone_inverse_binds(skeleton: &Skeleton) -> Result<(), DocumentShapeError> {
1650 for (node, bone) in skeleton.bones.iter().enumerate() {
1651 if let Some(inverse_bind) = bone.inverse_bind
1652 && !mat4_is_finite(inverse_bind)
1653 {
1654 return Err(DocumentShapeError::NonFiniteBoneInverseBind { node });
1655 }
1656 }
1657 Ok(())
1658}
1659
1660#[cfg(test)]
1661mod tests {
1662 use super::*;
1663
1664 #[test]
1665 fn property_serde_uses_the_stable_trs_vocabulary() {
1666 assert_eq!(
1667 serde_json::to_value([Property::Translation, Property::Rotation, Property::Scale,])
1668 .expect("properties serialize"),
1669 serde_json::json!(["translation", "rotation", "scale"])
1670 );
1671 assert_eq!(
1672 serde_json::from_value::<Vec<Property>>(serde_json::json!([
1673 "translation",
1674 "rotation",
1675 "scale"
1676 ]))
1677 .expect("properties deserialize"),
1678 [Property::Translation, Property::Rotation, Property::Scale,]
1679 );
1680 }
1681
1682 fn bone(parent: Option<BoneId>) -> Bone {
1683 Bone {
1684 name: "bone".into(),
1685 parent,
1686 rest: Transform::IDENTITY,
1687 inverse_bind: None,
1688 }
1689 }
1690
1691 fn one_bone_document() -> Document {
1692 Document {
1693 skeleton: Skeleton {
1694 bones: vec![bone(None)],
1695 },
1696 ..Document::default()
1697 }
1698 }
1699
1700 #[test]
1701 fn document_shape_validation_checks_all_scene_roots_in_stable_order() {
1702 let mut document = one_bone_document();
1703 document.assets.scenes = vec![
1704 SceneAsset {
1705 roots: vec![0, 17],
1706 ..SceneAsset::default()
1707 },
1708 SceneAsset {
1709 roots: vec![23],
1710 ..SceneAsset::default()
1711 },
1712 ];
1713 assert_eq!(
1714 validate_document_shape(&document),
1715 Err(DocumentShapeError::SceneRootOutOfBounds {
1716 scene_index: 0,
1717 root_index: 1,
1718 bone: 17,
1719 })
1720 );
1721 }
1722
1723 #[test]
1724 fn document_shape_scene_roots_precede_source_identity_errors() {
1725 let mut document = one_bone_document();
1726 document.assets.scenes.push(SceneAsset {
1727 roots: vec![99],
1728 ..SceneAsset::default()
1729 });
1730 document.assets.source_skeleton.nodes =
1731 vec![source_node(0, None, None), source_node(0, None, None)];
1732 assert_eq!(
1733 validate_document_shape(&document),
1734 Err(DocumentShapeError::SceneRootOutOfBounds {
1735 scene_index: 0,
1736 root_index: 0,
1737 bone: 99,
1738 })
1739 );
1740 }
1741
1742 fn source_node(
1743 source_node_index: usize,
1744 parent_source_node_index: Option<usize>,
1745 bone: Option<BoneId>,
1746 ) -> SourceNodeAsset {
1747 SourceNodeAsset {
1748 source_node_index,
1749 name: None,
1750 parent_source_node_index,
1751 scene_root_indices: Vec::new(),
1752 local_rest: SourceNodeLocalRest::Trs {
1753 translation: Vec3::ZERO,
1754 rotation: Quat::IDENTITY,
1755 scale: Vec3::ONE,
1756 },
1757 bone,
1758 }
1759 }
1760
1761 fn valid_track() -> Track {
1762 Track {
1763 bone: 0,
1764 property: Property::Translation,
1765 interpolation: Interpolation::Linear,
1766 times: vec![0.0],
1767 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
1768 }
1769 }
1770
1771 fn track_document(track: Track) -> Document {
1772 let mut document = one_bone_document();
1773 document.clips.push(Clip {
1774 name: "clip".into(),
1775 duration_s: 0.0,
1776 tracks: vec![track],
1777 });
1778 document
1779 }
1780
1781 fn instance_document() -> Document {
1782 let mut document = one_bone_document();
1783 document.assets.meshes.push(MeshAsset::default());
1784 document.assets.instances.push(MeshInstance {
1785 node: 0,
1786 mesh: 0,
1787 ..MeshInstance::default()
1788 });
1789 document
1790 }
1791
1792 #[test]
1793 fn document_shape_validation_accepts_a_complete_projection_with_an_unprojected_intermediate() {
1794 let mut document = Document {
1795 skeleton: Skeleton {
1796 bones: vec![bone(None), bone(Some(0))],
1797 },
1798 assets: SceneAssets {
1799 source_skeleton: SourceSkeletonAssets {
1800 coverage: SourceSkeletonCoverage::Complete,
1801 nodes: vec![
1802 source_node(10, None, Some(0)),
1803 source_node(11, Some(10), None),
1804 source_node(12, Some(11), Some(1)),
1805 ],
1806 ..SourceSkeletonAssets::default()
1807 },
1808 meshes: vec![MeshAsset::default()],
1809 instances: vec![MeshInstance {
1810 node: 1,
1811 mesh: 0,
1812 skin_joints: vec![0, 1],
1813 skin_ibms: vec![Mat4::IDENTITY, Mat4::IDENTITY],
1814 ..MeshInstance::default()
1815 }],
1816 ..SceneAssets::default()
1817 },
1818 ..Document::default()
1819 };
1820 document.clips.push(Clip {
1821 name: "clip".into(),
1822 duration_s: 0.0,
1823 tracks: vec![valid_track()],
1824 });
1825
1826 assert_eq!(validate_document_shape(&document), Ok(()));
1827 }
1828
1829 #[test]
1830 fn shared_unprojected_parent_suffix_preserves_each_projected_parent() {
1831 const CONNECTORS: usize = 64;
1832 const PROJECTED_CHILDREN: usize = 64;
1833
1834 let mut nodes = Vec::with_capacity(1 + CONNECTORS + PROJECTED_CHILDREN);
1835 nodes.push(source_node(0, None, Some(0)));
1836 for source_node_index in 1..=CONNECTORS {
1837 nodes.push(source_node(
1838 source_node_index,
1839 Some(source_node_index - 1),
1840 None,
1841 ));
1842 }
1843 for child in 0..PROJECTED_CHILDREN {
1844 nodes.push(source_node(
1845 1 + CONNECTORS + child,
1846 Some(CONNECTORS),
1847 Some(1 + child),
1848 ));
1849 }
1850 let document = Document {
1851 skeleton: Skeleton {
1852 bones: std::iter::once(bone(None))
1853 .chain((0..PROJECTED_CHILDREN).map(|_| bone(Some(0))))
1854 .collect(),
1855 },
1856 assets: SceneAssets {
1857 source_skeleton: SourceSkeletonAssets {
1858 coverage: SourceSkeletonCoverage::Complete,
1859 nodes,
1860 ..SourceSkeletonAssets::default()
1861 },
1862 ..SceneAssets::default()
1863 },
1864 ..Document::default()
1865 };
1866
1867 assert_eq!(validate_document_shape(&document), Ok(()));
1868 let mut mismatched = document.clone();
1869 mismatched.skeleton.bones[PROJECTED_CHILDREN].parent = None;
1870 assert_eq!(
1871 validate_document_shape(&mismatched),
1872 Err(DocumentShapeError::SourceProjection {
1873 source_node_index: CONNECTORS + PROJECTED_CHILDREN,
1874 violation: SourceProjectionViolation::NearestProjectedParentMismatch,
1875 })
1876 );
1877 }
1878
1879 #[test]
1880 fn document_shape_validation_has_an_analytic_error_for_every_variant() {
1881 let projection_error =
1882 |source_node_index, violation| DocumentShapeError::SourceProjection {
1883 source_node_index,
1884 violation,
1885 };
1886 let track_error = |node, violation| DocumentShapeError::TrackShape {
1887 clip_index: 0,
1888 node,
1889 violation,
1890 };
1891 let instance_error = |violation| DocumentShapeError::MeshInstanceShape {
1892 instance_index: 0,
1893 violation,
1894 };
1895 let mut bad_scene_root = one_bone_document();
1896 bad_scene_root.assets.scenes = vec![
1897 SceneAsset {
1898 roots: vec![0],
1899 ..SceneAsset::default()
1900 },
1901 SceneAsset {
1902 roots: vec![99],
1903 ..SceneAsset::default()
1904 },
1905 ];
1906
1907 let mut non_finite_rest = one_bone_document();
1908 non_finite_rest.skeleton.bones[0].rest.translation.x = f32::NAN;
1909 let overflowed_rest_world = Document {
1910 skeleton: Skeleton {
1911 bones: vec![
1912 Bone {
1913 rest: Transform {
1914 scale: Vec3::splat(f32::MAX),
1915 ..Transform::IDENTITY
1916 },
1917 ..bone(None)
1918 },
1919 Bone {
1920 rest: Transform {
1921 translation: Vec3::splat(2.0),
1922 ..Transform::IDENTITY
1923 },
1924 ..bone(Some(0))
1925 },
1926 ],
1927 },
1928 ..Document::default()
1929 };
1930 let self_parent = Document {
1931 skeleton: Skeleton {
1932 bones: vec![bone(Some(0))],
1933 },
1934 ..Document::default()
1935 };
1936 let forward_parent = Document {
1937 skeleton: Skeleton {
1938 bones: vec![bone(Some(1)), bone(None)],
1939 },
1940 ..Document::default()
1941 };
1942 let far_parent = Document {
1943 skeleton: Skeleton {
1944 bones: vec![bone(Some(99))],
1945 },
1946 ..Document::default()
1947 };
1948 let duplicate_node = Document {
1949 assets: SceneAssets {
1950 source_skeleton: SourceSkeletonAssets {
1951 nodes: vec![
1952 source_node(9, None, None),
1953 source_node(10, None, None),
1954 source_node(9, None, None),
1955 ],
1956 ..SourceSkeletonAssets::default()
1957 },
1958 ..SceneAssets::default()
1959 },
1960 ..Document::default()
1961 };
1962 let duplicate_skin = Document {
1963 assets: SceneAssets {
1964 source_skeleton: SourceSkeletonAssets {
1965 skins: vec![
1966 SourceSkinAsset {
1967 source_skin_index: 4,
1968 ..SourceSkinAsset::default()
1969 },
1970 SourceSkinAsset {
1971 source_skin_index: 5,
1972 ..SourceSkinAsset::default()
1973 },
1974 SourceSkinAsset {
1975 source_skin_index: 4,
1976 ..SourceSkinAsset::default()
1977 },
1978 ],
1979 ..SourceSkeletonAssets::default()
1980 },
1981 ..SceneAssets::default()
1982 },
1983 ..Document::default()
1984 };
1985 let complete_projection = |nodes| SceneAssets {
1986 source_skeleton: SourceSkeletonAssets {
1987 coverage: SourceSkeletonCoverage::Complete,
1988 nodes,
1989 ..SourceSkeletonAssets::default()
1990 },
1991 ..SceneAssets::default()
1992 };
1993 let out_of_range_projection = Document {
1994 skeleton: Skeleton {
1995 bones: vec![bone(None)],
1996 },
1997 assets: complete_projection(vec![source_node(10, None, Some(1))]),
1998 ..Document::default()
1999 };
2000 let non_injective_projection = Document {
2001 skeleton: Skeleton {
2002 bones: vec![bone(None)],
2003 },
2004 assets: complete_projection(vec![
2005 source_node(10, None, Some(0)),
2006 source_node(11, None, Some(0)),
2007 ]),
2008 ..Document::default()
2009 };
2010 let missing_projection_parent = Document {
2011 skeleton: Skeleton {
2012 bones: vec![bone(None), bone(Some(0))],
2013 },
2014 assets: complete_projection(vec![source_node(11, Some(99), Some(1))]),
2015 ..Document::default()
2016 };
2017 let missing_projection_parent_at_cycle_bound = Document {
2022 skeleton: Skeleton {
2023 bones: vec![bone(None), bone(Some(0))],
2024 },
2025 assets: complete_projection(vec![
2026 source_node(10, None, Some(0)),
2027 source_node(11, Some(12), Some(1)),
2028 source_node(12, Some(99), None),
2029 ]),
2030 ..Document::default()
2031 };
2032 let cyclic_unprojected_parent = Document {
2033 skeleton: Skeleton {
2034 bones: vec![bone(None), bone(Some(0))],
2035 },
2036 assets: complete_projection(vec![
2037 source_node(11, Some(12), Some(1)),
2038 source_node(12, Some(12), None),
2039 ]),
2040 ..Document::default()
2041 };
2042 let cyclic_unprojected_parent_pair = Document {
2043 skeleton: Skeleton {
2044 bones: vec![bone(None), bone(Some(0))],
2045 },
2046 assets: complete_projection(vec![
2047 source_node(11, Some(12), Some(1)),
2048 source_node(12, Some(13), None),
2049 source_node(13, Some(12), None),
2050 ]),
2051 ..Document::default()
2052 };
2053 let mismatched_nearest_parent = Document {
2054 skeleton: Skeleton {
2055 bones: vec![bone(None), bone(Some(0))],
2056 },
2057 assets: complete_projection(vec![
2058 source_node(10, None, Some(0)),
2059 source_node(11, None, Some(1)),
2060 ]),
2061 ..Document::default()
2062 };
2063 let unprojected_child = Document {
2064 skeleton: Skeleton {
2065 bones: vec![bone(None), bone(Some(0))],
2066 },
2067 assets: complete_projection(vec![source_node(10, None, Some(0))]),
2068 ..Document::default()
2069 };
2070
2071 let duplicate_track = {
2072 let track = valid_track();
2073 let mut document = track_document(track.clone());
2074 document.clips[0].tracks.push(Track {
2075 property: Property::Scale,
2076 ..valid_track()
2077 });
2078 document.clips[0].tracks.push(track);
2079 document
2080 };
2081 let mut boundary_out_of_range_track = valid_track();
2082 boundary_out_of_range_track.bone = 1;
2083 let mut far_out_of_range_track = valid_track();
2084 far_out_of_range_track.bone = 99;
2085 let empty_track = Track {
2086 times: Vec::new(),
2087 values: TrackValues::Vec3s(Vec::new()),
2088 ..valid_track()
2089 };
2090 let non_finite_later_time = Track {
2091 times: vec![0.0, f32::NAN],
2092 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
2093 ..valid_track()
2094 };
2095 let unordered_times = Track {
2096 times: vec![1.0, 0.0],
2097 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
2098 ..valid_track()
2099 };
2100 let equal_times = Track {
2101 times: vec![0.0, 0.0],
2102 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
2103 ..valid_track()
2104 };
2105 let wrong_linear_value_count = Track {
2106 values: TrackValues::Vec3s(Vec::new()),
2107 ..valid_track()
2108 };
2109 let excess_linear_value_count = Track {
2110 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
2111 ..valid_track()
2112 };
2113 let wrong_step_value_count = Track {
2114 interpolation: Interpolation::Step,
2115 times: vec![0.0, 1.0],
2116 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
2117 ..valid_track()
2118 };
2119 let excess_step_value_count = Track {
2120 interpolation: Interpolation::Step,
2121 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
2122 ..valid_track()
2123 };
2124 let wrong_cubic_value_count = Track {
2125 interpolation: Interpolation::CubicSpline,
2126 times: vec![0.0, 1.0],
2127 values: TrackValues::Vec3s(vec![Vec3::ZERO; 4]),
2128 ..valid_track()
2129 };
2130 let excess_cubic_value_count = Track {
2131 interpolation: Interpolation::CubicSpline,
2132 values: TrackValues::Vec3s(vec![Vec3::ZERO; 4]),
2133 ..valid_track()
2134 };
2135 let wrong_translation_value_type = Track {
2136 values: TrackValues::Quats(vec![Quat::IDENTITY]),
2137 ..valid_track()
2138 };
2139 let wrong_scale_value_type = Track {
2140 property: Property::Scale,
2141 values: TrackValues::Quats(vec![Quat::IDENTITY]),
2142 ..valid_track()
2143 };
2144 let wrong_rotation_value_type = Track {
2145 property: Property::Rotation,
2146 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
2147 ..valid_track()
2148 };
2149 let non_finite_value = Track {
2150 values: TrackValues::Vec3s(vec![Vec3::splat(f32::NAN)]),
2151 ..valid_track()
2152 };
2153
2154 let mut bad_instance_node = instance_document();
2155 bad_instance_node.assets.instances[0].node = 1;
2156 let mut far_instance_node = instance_document();
2157 far_instance_node.assets.instances[0].node = 99;
2158 let mut bad_instance_mesh = instance_document();
2159 bad_instance_mesh.assets.instances[0].mesh = 1;
2160 let mut far_instance_mesh = instance_document();
2161 far_instance_mesh.assets.instances[0].mesh = 99;
2162 let mut bad_instance_joint = instance_document();
2163 bad_instance_joint.assets.instances[0].skin_joints = vec![1];
2164 let mut far_instance_joint = instance_document();
2165 far_instance_joint.assets.instances[0].skin_joints = vec![99];
2166 let mut bad_instance_count = instance_document();
2167 bad_instance_count.assets.instances[0].skin_joints = vec![0];
2168 bad_instance_count.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY, Mat4::IDENTITY];
2169 let mut short_instance_count = instance_document();
2170 short_instance_count.skeleton.bones.push(bone(Some(0)));
2171 short_instance_count.assets.instances[0].skin_joints = vec![0, 1];
2172 short_instance_count.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY];
2173 let mut bad_instance_ibm = instance_document();
2174 bad_instance_ibm.assets.instances[0].skin_joints = vec![0];
2175 bad_instance_ibm.assets.instances[0].skin_ibms =
2176 vec![Mat4::from_cols_array(&[f32::NAN; 16])];
2177 let mut bad_bone_ibm = one_bone_document();
2178 bad_bone_ibm.skeleton.bones[0].inverse_bind = Some(Mat4::from_cols_array(&[f32::NAN; 16]));
2179
2180 let cases = vec![
2181 (
2182 "scene root boundary",
2183 bad_scene_root,
2184 DocumentShapeError::SceneRootOutOfBounds {
2185 scene_index: 1,
2186 root_index: 0,
2187 bone: 99,
2188 },
2189 ),
2190 (
2191 "non-finite rest",
2192 non_finite_rest,
2193 DocumentShapeError::NonFiniteSkeletonRest { node: 0 },
2194 ),
2195 (
2196 "non-finite composed rest world",
2197 overflowed_rest_world,
2198 DocumentShapeError::NonFiniteSkeletonRest { node: 1 },
2199 ),
2200 (
2201 "self parent",
2202 self_parent,
2203 DocumentShapeError::InvalidSkeletonParent { node: 0, parent: 0 },
2204 ),
2205 (
2206 "forward parent",
2207 forward_parent,
2208 DocumentShapeError::InvalidSkeletonParent { node: 0, parent: 1 },
2209 ),
2210 (
2211 "far parent",
2212 far_parent,
2213 DocumentShapeError::InvalidSkeletonParent {
2214 node: 0,
2215 parent: 99,
2216 },
2217 ),
2218 (
2219 "duplicate source node",
2220 duplicate_node,
2221 DocumentShapeError::DuplicateSourceNodeIndex {
2222 source_node_index: 9,
2223 },
2224 ),
2225 (
2226 "duplicate source skin",
2227 duplicate_skin,
2228 DocumentShapeError::DuplicateSourceSkinIndex {
2229 source_skin_index: 4,
2230 },
2231 ),
2232 (
2233 "projected bone range",
2234 out_of_range_projection,
2235 projection_error(10, SourceProjectionViolation::ProjectedBoneOutOfRange),
2236 ),
2237 (
2238 "projection injectivity",
2239 non_injective_projection,
2240 projection_error(
2241 11,
2242 SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
2243 ),
2244 ),
2245 (
2246 "missing projection parent",
2247 missing_projection_parent,
2248 projection_error(11, SourceProjectionViolation::ParentSourceNodeMissing),
2249 ),
2250 (
2251 "missing projection parent at cycle bound",
2252 missing_projection_parent_at_cycle_bound,
2253 projection_error(11, SourceProjectionViolation::ParentSourceNodeMissing),
2254 ),
2255 (
2256 "cyclic projection parent",
2257 cyclic_unprojected_parent,
2258 projection_error(
2259 11,
2260 SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
2261 ),
2262 ),
2263 (
2264 "cyclic projection parent pair",
2265 cyclic_unprojected_parent_pair,
2266 projection_error(
2267 11,
2268 SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
2269 ),
2270 ),
2271 (
2272 "nearest projection parent",
2273 mismatched_nearest_parent,
2274 projection_error(
2275 11,
2276 SourceProjectionViolation::NearestProjectedParentMismatch,
2277 ),
2278 ),
2279 (
2280 "projection downward closure",
2281 unprojected_child,
2282 projection_error(
2283 10,
2284 SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
2285 ),
2286 ),
2287 (
2288 "duplicate track",
2289 duplicate_track,
2290 DocumentShapeError::DuplicateClipTrack {
2291 clip_index: 0,
2292 node: 0,
2293 property: Property::Translation,
2294 },
2295 ),
2296 (
2297 "track bone range boundary",
2298 track_document(boundary_out_of_range_track),
2299 track_error(1, TrackShapeViolation::BoneIndexOutOfRange),
2300 ),
2301 (
2302 "track bone range far",
2303 track_document(far_out_of_range_track),
2304 track_error(99, TrackShapeViolation::BoneIndexOutOfRange),
2305 ),
2306 (
2307 "empty track",
2308 track_document(empty_track),
2309 track_error(0, TrackShapeViolation::EmptyTimes),
2310 ),
2311 (
2312 "non-finite time",
2313 track_document(non_finite_later_time),
2314 track_error(0, TrackShapeViolation::NonFiniteTime),
2315 ),
2316 (
2317 "unordered times",
2318 track_document(unordered_times),
2319 track_error(0, TrackShapeViolation::TimesNotStrictlyIncreasing),
2320 ),
2321 (
2322 "equal times",
2323 track_document(equal_times),
2324 track_error(0, TrackShapeViolation::TimesNotStrictlyIncreasing),
2325 ),
2326 (
2327 "linear value count",
2328 track_document(wrong_linear_value_count),
2329 track_error(0, TrackShapeViolation::ValueCountMismatch),
2330 ),
2331 (
2332 "linear excess value count",
2333 track_document(excess_linear_value_count),
2334 track_error(0, TrackShapeViolation::ValueCountMismatch),
2335 ),
2336 (
2337 "step value count",
2338 track_document(wrong_step_value_count),
2339 track_error(0, TrackShapeViolation::ValueCountMismatch),
2340 ),
2341 (
2342 "step excess value count",
2343 track_document(excess_step_value_count),
2344 track_error(0, TrackShapeViolation::ValueCountMismatch),
2345 ),
2346 (
2347 "cubic value count",
2348 track_document(wrong_cubic_value_count),
2349 track_error(0, TrackShapeViolation::ValueCountMismatch),
2350 ),
2351 (
2352 "cubic excess value count",
2353 track_document(excess_cubic_value_count),
2354 track_error(0, TrackShapeViolation::ValueCountMismatch),
2355 ),
2356 (
2357 "translation value type",
2358 track_document(wrong_translation_value_type),
2359 track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
2360 ),
2361 (
2362 "scale value type",
2363 track_document(wrong_scale_value_type),
2364 track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
2365 ),
2366 (
2367 "rotation value type",
2368 track_document(wrong_rotation_value_type),
2369 track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
2370 ),
2371 (
2372 "non-finite value",
2373 track_document(non_finite_value),
2374 track_error(0, TrackShapeViolation::NonFiniteValue),
2375 ),
2376 (
2377 "instance node boundary",
2378 bad_instance_node,
2379 instance_error(MeshInstanceShapeViolation::NodeIndexOutOfRange),
2380 ),
2381 (
2382 "instance node far",
2383 far_instance_node,
2384 instance_error(MeshInstanceShapeViolation::NodeIndexOutOfRange),
2385 ),
2386 (
2387 "instance mesh boundary",
2388 bad_instance_mesh,
2389 instance_error(MeshInstanceShapeViolation::MeshIndexOutOfRange),
2390 ),
2391 (
2392 "instance mesh far",
2393 far_instance_mesh,
2394 instance_error(MeshInstanceShapeViolation::MeshIndexOutOfRange),
2395 ),
2396 (
2397 "instance joint boundary",
2398 bad_instance_joint,
2399 instance_error(MeshInstanceShapeViolation::SkinJointOutOfRange),
2400 ),
2401 (
2402 "instance joint far",
2403 far_instance_joint,
2404 instance_error(MeshInstanceShapeViolation::SkinJointOutOfRange),
2405 ),
2406 (
2407 "instance ibm count excess",
2408 bad_instance_count,
2409 instance_error(MeshInstanceShapeViolation::SkinInverseBindCountMismatch),
2410 ),
2411 (
2412 "instance ibm count short",
2413 short_instance_count,
2414 instance_error(MeshInstanceShapeViolation::SkinInverseBindCountMismatch),
2415 ),
2416 (
2417 "instance ibm finite",
2418 bad_instance_ibm,
2419 instance_error(MeshInstanceShapeViolation::NonFiniteSkinInverseBind),
2420 ),
2421 (
2422 "bone ibm finite",
2423 bad_bone_ibm,
2424 DocumentShapeError::NonFiniteBoneInverseBind { node: 0 },
2425 ),
2426 ];
2427 for (name, document, expected) in cases {
2428 assert_eq!(validate_document_shape(&document), Err(expected), "{name}");
2429 }
2430 }
2431
2432 #[test]
2433 fn document_shape_finiteness_checks_every_stored_component() {
2434 for component in 0..3 {
2435 let mut translation = Vec3::ZERO.to_array();
2436 translation[component] = f32::NAN;
2437 let mut document = one_bone_document();
2438 document.skeleton.bones[0].rest.translation = Vec3::from_array(translation);
2439 assert_eq!(
2440 validate_document_shape(&document),
2441 Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
2442 "rest translation component {component}"
2443 );
2444
2445 let mut scale = Vec3::ONE.to_array();
2446 scale[component] = f32::NAN;
2447 let mut document = one_bone_document();
2448 document.skeleton.bones[0].rest.scale = Vec3::from_array(scale);
2449 assert_eq!(
2450 validate_document_shape(&document),
2451 Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
2452 "rest scale component {component}"
2453 );
2454
2455 let mut value = Vec3::ZERO.to_array();
2456 value[component] = f32::NAN;
2457 let document = track_document(Track {
2458 values: TrackValues::Vec3s(vec![Vec3::from_array(value)]),
2459 ..valid_track()
2460 });
2461 assert_eq!(
2462 validate_document_shape(&document),
2463 Err(DocumentShapeError::TrackShape {
2464 clip_index: 0,
2465 node: 0,
2466 violation: TrackShapeViolation::NonFiniteValue,
2467 }),
2468 "track Vec3 component {component}"
2469 );
2470 }
2471
2472 for component in 0..4 {
2473 let mut rotation = Quat::IDENTITY.to_array();
2474 rotation[component] = f32::NAN;
2475 let mut document = one_bone_document();
2476 document.skeleton.bones[0].rest.rotation = Quat::from_array(rotation);
2477 assert_eq!(
2478 validate_document_shape(&document),
2479 Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
2480 "rest rotation component {component}"
2481 );
2482
2483 let document = track_document(Track {
2484 property: Property::Rotation,
2485 values: TrackValues::Quats(vec![Quat::from_array(rotation)]),
2486 ..valid_track()
2487 });
2488 assert_eq!(
2489 validate_document_shape(&document),
2490 Err(DocumentShapeError::TrackShape {
2491 clip_index: 0,
2492 node: 0,
2493 violation: TrackShapeViolation::NonFiniteValue,
2494 }),
2495 "track quaternion component {component}"
2496 );
2497 }
2498
2499 for key in 0..3 {
2500 let mut times = vec![0.0, 1.0, 2.0];
2501 times[key] = f32::NAN;
2502 let document = track_document(Track {
2503 times,
2504 values: TrackValues::Vec3s(vec![Vec3::ZERO; 3]),
2505 ..valid_track()
2506 });
2507 assert_eq!(
2508 validate_document_shape(&document),
2509 Err(DocumentShapeError::TrackShape {
2510 clip_index: 0,
2511 node: 0,
2512 violation: TrackShapeViolation::NonFiniteTime,
2513 }),
2514 "track time {key}"
2515 );
2516 }
2517
2518 for component in 0..16 {
2519 let mut columns = Mat4::IDENTITY.to_cols_array();
2520 columns[component] = f32::NAN;
2521 let inverse_bind = Mat4::from_cols_array(&columns);
2522
2523 let mut instance_document = instance_document();
2524 instance_document.assets.instances[0].skin_joints = vec![0];
2525 instance_document.assets.instances[0].skin_ibms = vec![inverse_bind];
2526 assert_eq!(
2527 validate_document_shape(&instance_document),
2528 Err(DocumentShapeError::MeshInstanceShape {
2529 instance_index: 0,
2530 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2531 }),
2532 "instance inverse-bind component {component}"
2533 );
2534
2535 let mut bone_document = one_bone_document();
2536 bone_document.skeleton.bones[0].inverse_bind = Some(inverse_bind);
2537 assert_eq!(
2538 validate_document_shape(&bone_document),
2539 Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 0 }),
2540 "bone inverse-bind component {component}"
2541 );
2542 }
2543 }
2544
2545 #[test]
2546 fn document_shape_rejects_duplicate_tracks_for_every_property() {
2547 let tracks = [
2548 (Property::Translation, TrackValues::Vec3s(vec![Vec3::ZERO])),
2549 (Property::Scale, TrackValues::Vec3s(vec![Vec3::ONE])),
2550 (Property::Rotation, TrackValues::Quats(vec![Quat::IDENTITY])),
2551 ];
2552
2553 for (property, values) in tracks {
2554 let track = Track {
2555 property,
2556 values,
2557 ..valid_track()
2558 };
2559 let mut document = track_document(track.clone());
2560 document.clips[0].tracks.push(track);
2561
2562 assert_eq!(
2563 validate_document_shape(&document),
2564 Err(DocumentShapeError::DuplicateClipTrack {
2565 clip_index: 0,
2566 node: 0,
2567 property,
2568 }),
2569 "duplicate {property:?} track"
2570 );
2571 }
2572 }
2573
2574 #[test]
2575 fn document_shape_rejects_infinite_times_quaternions_and_inverse_binds() {
2576 for non_finite in [f32::INFINITY, f32::NEG_INFINITY] {
2577 let document = track_document(Track {
2578 times: vec![non_finite],
2579 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
2580 ..valid_track()
2581 });
2582 assert_eq!(
2583 validate_document_shape(&document),
2584 Err(DocumentShapeError::TrackShape {
2585 clip_index: 0,
2586 node: 0,
2587 violation: TrackShapeViolation::NonFiniteTime,
2588 }),
2589 "track time {non_finite}"
2590 );
2591
2592 let document = track_document(Track {
2593 property: Property::Rotation,
2594 values: TrackValues::Quats(vec![Quat::from_xyzw(non_finite, 0.0, 0.0, 1.0)]),
2595 ..valid_track()
2596 });
2597 assert_eq!(
2598 validate_document_shape(&document),
2599 Err(DocumentShapeError::TrackShape {
2600 clip_index: 0,
2601 node: 0,
2602 violation: TrackShapeViolation::NonFiniteValue,
2603 }),
2604 "track quaternion {non_finite}"
2605 );
2606
2607 let mut columns = Mat4::IDENTITY.to_cols_array();
2608 columns[0] = non_finite;
2609 let inverse_bind = Mat4::from_cols_array(&columns);
2610 let mut document = instance_document();
2611 document.assets.instances[0].skin_joints = vec![0];
2612 document.assets.instances[0].skin_ibms = vec![inverse_bind];
2613 assert_eq!(
2614 validate_document_shape(&document),
2615 Err(DocumentShapeError::MeshInstanceShape {
2616 instance_index: 0,
2617 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2618 }),
2619 "instance inverse bind {non_finite}"
2620 );
2621
2622 let mut document = one_bone_document();
2623 document.skeleton.bones[0].inverse_bind = Some(inverse_bind);
2624 assert_eq!(
2625 validate_document_shape(&document),
2626 Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 0 }),
2627 "bone inverse bind {non_finite}"
2628 );
2629 }
2630 }
2631
2632 #[test]
2633 fn document_shape_checks_mesh_and_joint_references_on_later_instances() {
2634 let later_instance = MeshInstance {
2635 node: 0,
2636 mesh: 0,
2637 ..MeshInstance::default()
2638 };
2639
2640 let mut document = instance_document();
2641 document.assets.instances.push(later_instance.clone());
2642 document.assets.instances[1].mesh = 1;
2643 assert_eq!(
2644 validate_document_shape(&document),
2645 Err(DocumentShapeError::MeshInstanceShape {
2646 instance_index: 1,
2647 violation: MeshInstanceShapeViolation::MeshIndexOutOfRange,
2648 })
2649 );
2650
2651 let mut document = instance_document();
2652 document.assets.instances.push(later_instance);
2653 document.assets.instances[1].skin_joints = vec![1];
2654 assert_eq!(
2655 validate_document_shape(&document),
2656 Err(DocumentShapeError::MeshInstanceShape {
2657 instance_index: 1,
2658 violation: MeshInstanceShapeViolation::SkinJointOutOfRange,
2659 })
2660 );
2661 }
2662
2663 #[test]
2664 fn document_shape_finds_duplicates_that_do_not_involve_the_first_item() {
2665 let mut document = Document::default();
2666 document.assets.source_skeleton.skins = [4, 5, 5]
2667 .into_iter()
2668 .map(|source_skin_index| SourceSkinAsset {
2669 source_skin_index,
2670 ..SourceSkinAsset::default()
2671 })
2672 .collect();
2673 assert_eq!(
2674 validate_document_shape(&document),
2675 Err(DocumentShapeError::DuplicateSourceSkinIndex {
2676 source_skin_index: 5,
2677 })
2678 );
2679
2680 let scale_track = Track {
2681 property: Property::Scale,
2682 values: TrackValues::Vec3s(vec![Vec3::ONE]),
2683 ..valid_track()
2684 };
2685 let mut document = track_document(valid_track());
2686 document.clips[0].tracks.push(scale_track.clone());
2687 document.clips[0].tracks.push(scale_track);
2688 assert_eq!(
2689 validate_document_shape(&document),
2690 Err(DocumentShapeError::DuplicateClipTrack {
2691 clip_index: 0,
2692 node: 0,
2693 property: Property::Scale,
2694 })
2695 );
2696 }
2697
2698 #[test]
2699 fn document_shape_checks_later_tracks_and_inverse_binds() {
2700 let mut document = track_document(valid_track());
2701 document.clips[0].tracks.push(Track {
2702 property: Property::Scale,
2703 times: Vec::new(),
2704 values: TrackValues::Vec3s(Vec::new()),
2705 ..valid_track()
2706 });
2707 assert_eq!(
2708 validate_document_shape(&document),
2709 Err(DocumentShapeError::TrackShape {
2710 clip_index: 0,
2711 node: 0,
2712 violation: TrackShapeViolation::EmptyTimes,
2713 })
2714 );
2715
2716 let scale_track = Track {
2717 property: Property::Scale,
2718 values: TrackValues::Vec3s(vec![Vec3::ONE]),
2719 ..valid_track()
2720 };
2721 let mut document = track_document(valid_track());
2722 document.clips.push(Clip {
2723 name: "later".into(),
2724 duration_s: 0.0,
2725 tracks: vec![scale_track.clone(), scale_track],
2726 });
2727 assert_eq!(
2728 validate_document_shape(&document),
2729 Err(DocumentShapeError::DuplicateClipTrack {
2730 clip_index: 1,
2731 node: 0,
2732 property: Property::Scale,
2733 })
2734 );
2735
2736 let mut document = track_document(valid_track());
2737 document.clips.push(Clip {
2738 name: "later".into(),
2739 duration_s: 0.0,
2740 tracks: vec![Track {
2741 property: Property::Scale,
2742 times: Vec::new(),
2743 values: TrackValues::Vec3s(Vec::new()),
2744 ..valid_track()
2745 }],
2746 });
2747 assert_eq!(
2748 validate_document_shape(&document),
2749 Err(DocumentShapeError::TrackShape {
2750 clip_index: 1,
2751 node: 0,
2752 violation: TrackShapeViolation::EmptyTimes,
2753 })
2754 );
2755
2756 let mut columns = Mat4::IDENTITY.to_cols_array();
2757 columns[15] = f32::NAN;
2758 let non_finite_inverse_bind = Mat4::from_cols_array(&columns);
2759
2760 let mut document = instance_document();
2761 document.skeleton.bones.push(bone(Some(0)));
2762 document.assets.instances[0].skin_joints = vec![0, 1];
2763 document.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY, non_finite_inverse_bind];
2764 assert_eq!(
2765 validate_document_shape(&document),
2766 Err(DocumentShapeError::MeshInstanceShape {
2767 instance_index: 0,
2768 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2769 })
2770 );
2771
2772 let mut document = instance_document();
2773 document.assets.instances.push(MeshInstance {
2774 node: 0,
2775 mesh: 0,
2776 skin_joints: vec![0],
2777 skin_ibms: vec![non_finite_inverse_bind],
2778 ..MeshInstance::default()
2779 });
2780 assert_eq!(
2781 validate_document_shape(&document),
2782 Err(DocumentShapeError::MeshInstanceShape {
2783 instance_index: 1,
2784 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2785 })
2786 );
2787
2788 let mut document = instance_document();
2789 document.assets.instances.push(MeshInstance {
2790 node: 0,
2791 mesh: 0,
2792 skin_joints: vec![0],
2793 skin_ibms: vec![Mat4::IDENTITY, Mat4::IDENTITY],
2794 ..MeshInstance::default()
2795 });
2796 assert_eq!(
2797 validate_document_shape(&document),
2798 Err(DocumentShapeError::MeshInstanceShape {
2799 instance_index: 1,
2800 violation: MeshInstanceShapeViolation::SkinInverseBindCountMismatch,
2801 })
2802 );
2803
2804 let mut document = one_bone_document();
2805 document.skeleton.bones.push(Bone {
2806 inverse_bind: Some(non_finite_inverse_bind),
2807 ..bone(Some(0))
2808 });
2809 assert_eq!(
2810 validate_document_shape(&document),
2811 Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 1 })
2812 );
2813 }
2814
2815 #[test]
2816 fn document_shape_violation_names_remain_machine_stable() {
2817 let source_projection = [
2818 (
2819 SourceProjectionViolation::ProjectedBoneOutOfRange,
2820 "projected_bone_out_of_range",
2821 ),
2822 (
2823 SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
2824 "two_source_nodes_project_to_one_bone",
2825 ),
2826 (
2827 SourceProjectionViolation::ParentSourceNodeMissing,
2828 "parent_source_node_is_missing",
2829 ),
2830 (
2831 SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
2832 "cyclic_unprojected_source_parent_chain",
2833 ),
2834 (
2835 SourceProjectionViolation::NearestProjectedParentMismatch,
2836 "projection_and_skeleton_parents_differ",
2837 ),
2838 (
2839 SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
2840 "projected_bone_has_an_unprojected_skeleton_child",
2841 ),
2842 ];
2843 for (violation, expected) in source_projection {
2844 assert_eq!(violation.to_string(), expected);
2845 }
2846
2847 let track = [
2848 (
2849 TrackShapeViolation::BoneIndexOutOfRange,
2850 "bone_index_out_of_range",
2851 ),
2852 (TrackShapeViolation::EmptyTimes, "empty_times"),
2853 (TrackShapeViolation::NonFiniteTime, "non_finite_time"),
2854 (
2855 TrackShapeViolation::TimesNotStrictlyIncreasing,
2856 "times_not_strictly_increasing",
2857 ),
2858 (
2859 TrackShapeViolation::ValueCountMismatch,
2860 "value_count_mismatch",
2861 ),
2862 (
2863 TrackShapeViolation::ValueTypeMismatchesProperty,
2864 "value_type_mismatches_property",
2865 ),
2866 (TrackShapeViolation::NonFiniteValue, "non_finite_value"),
2867 ];
2868 for (violation, expected) in track {
2869 assert_eq!(violation.to_string(), expected);
2870 }
2871
2872 let instance = [
2873 (
2874 MeshInstanceShapeViolation::NodeIndexOutOfRange,
2875 "node_index_out_of_range",
2876 ),
2877 (
2878 MeshInstanceShapeViolation::MeshIndexOutOfRange,
2879 "mesh_index_out_of_range",
2880 ),
2881 (
2882 MeshInstanceShapeViolation::SkinJointOutOfRange,
2883 "skin_joint_out_of_range",
2884 ),
2885 (
2886 MeshInstanceShapeViolation::SkinInverseBindCountMismatch,
2887 "skin_ibm_count_mismatch",
2888 ),
2889 (
2890 MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2891 "non_finite_inverse_bind",
2892 ),
2893 ];
2894 for (violation, expected) in instance {
2895 assert_eq!(violation.to_string(), expected);
2896 }
2897 }
2898
2899 #[test]
2900 fn tolerant_world_rests_keep_unrelated_partial_evidence() {
2901 let skeleton = Skeleton {
2902 bones: vec![
2903 bone(None),
2904 bone(Some(99)),
2905 Bone {
2906 rest: Transform {
2907 translation: Vec3::X,
2908 ..Transform::IDENTITY
2909 },
2910 ..bone(None)
2911 },
2912 Bone {
2913 rest: Transform {
2914 translation: Vec3::Y,
2915 ..Transform::IDENTITY
2916 },
2917 ..bone(Some(2))
2918 },
2919 bone(Some(1)),
2920 ],
2921 };
2922
2923 let worlds = tolerant_world_rest_matrices(&skeleton);
2924 assert_eq!(worlds.len(), 5);
2925 assert_eq!(worlds[0], Some(Mat4::IDENTITY));
2926 assert_eq!(worlds[1], None, "the malformed parent is unavailable");
2927 assert_eq!(worlds[2], Some(Mat4::from_translation(Vec3::X)));
2928 assert_eq!(
2929 worlds[3],
2930 Some(Mat4::from_translation(Vec3::new(1.0, 1.0, 0.0))),
2931 "a finite independent chain remains measurable"
2932 );
2933 assert_eq!(
2934 worlds[4], None,
2935 "a child of unavailable evidence is unavailable"
2936 );
2937 }
2938
2939 #[test]
2940 fn shared_affine_classifier_respects_distinct_caller_tolerances() {
2941 let equal_axis_basis = affine_test_fixtures::tolerance_divergence_basis();
2942 let strict = PositiveUniformAffineTolerance {
2943 equal_axis: 1.0e-5,
2944 relative_orthogonality: 1.0e-5,
2945 singular_determinant_relative: 1.0e-6,
2946 };
2947 let loose = PositiveUniformAffineTolerance {
2948 equal_axis: 1.0e-4,
2949 relative_orthogonality: 1.0e-4,
2950 singular_determinant_relative: 0.0,
2951 };
2952
2953 assert_eq!(
2954 classify_positive_uniform_affine(equal_axis_basis, strict),
2955 Err(AffineDomainViolation::NonUniformScale),
2956 "the stricter caller rejects this equal-axis difference"
2957 );
2958 assert!(
2959 classify_positive_uniform_affine(equal_axis_basis, loose).is_ok(),
2960 "the looser caller accepts this equal-axis difference"
2961 );
2962
2963 let orthogonality_basis = affine_test_fixtures::orthogonality_tolerance_divergence_basis();
2964 assert_eq!(
2965 classify_positive_uniform_affine(orthogonality_basis, strict),
2966 Err(AffineDomainViolation::Sheared),
2967 "the stricter caller rejects this cross-axis dot product"
2968 );
2969 assert!(
2970 classify_positive_uniform_affine(orthogonality_basis, loose).is_ok(),
2971 "the looser caller accepts this cross-axis dot product"
2972 );
2973 }
2974
2975 #[test]
2976 fn shared_affine_classifier_pins_its_symmetric_f64_formula() {
2977 let policy = PositiveUniformAffineTolerance {
2978 equal_axis: 1.0e-5,
2979 relative_orthogonality: 1.0e-5,
2980 singular_determinant_relative: 1.0e-6,
2981 };
2982
2983 let on_long_edge = Mat3::from_diagonal(Vec3::new(99_998.5, 99_998.5, 100_000.0));
2988 assert_eq!(
2989 classify_positive_uniform_affine(on_long_edge, policy),
2990 Ok(99_999.0)
2991 );
2992 let short = 99_998.5;
2993 let long = 100_000.0 + 0.007_812_5;
2994 for diagonal in [
2995 Vec3::new(long, short, short),
2996 Vec3::new(short, long, short),
2997 Vec3::new(short, short, long),
2998 ] {
2999 assert_eq!(
3000 classify_positive_uniform_affine(Mat3::from_diagonal(diagonal), policy),
3001 Err(AffineDomainViolation::NonUniformScale)
3002 );
3003 }
3004
3005 let short = 1.0 - 2.0_f32.powi(-16);
3009 for diagonal in [
3010 Vec3::new(short, 1.0, 1.0),
3011 Vec3::new(1.0, short, 1.0),
3012 Vec3::new(1.0, 1.0, short),
3013 ] {
3014 assert_eq!(
3015 classify_positive_uniform_affine(Mat3::from_diagonal(diagonal), policy),
3016 Err(AffineDomainViolation::NonUniformScale)
3017 );
3018 }
3019
3020 let c0 = Vec3::new(0.12792248, -0.99066633, -0.047073245);
3023 let c1 = Vec3::new(-0.34637994, -0.00016034879, -0.93809813);
3024 let c2 = Vec3::new(0.92933476, 0.13630849, -0.3431568);
3025 assert!((c1.dot(c2) as f64).abs() < 1.0e-5);
3026 assert!(c1.as_dvec3().dot(c2.as_dvec3()).abs() > 1.0e-5);
3027 assert_eq!(
3028 classify_positive_uniform_affine(Mat3::from_cols(c0, c1, c2), policy),
3029 Err(AffineDomainViolation::Sheared)
3030 );
3031
3032 for shear in [2.0_f32.powi(-15), -2.0_f32.powi(-15)] {
3035 let basis = Mat3::from_cols(Vec3::X, Vec3::new(shear, 1.0, 0.0), Vec3::Z);
3036 assert_eq!(
3037 classify_positive_uniform_affine(basis, policy),
3038 Err(AffineDomainViolation::Sheared)
3039 );
3040 }
3041 }
3042
3043 #[test]
3044 fn affine_axis_mean_is_ascending_and_column_order_invariant() {
3045 let lengths = [
3050 f64::from_bits(0x3ff1_09e7_e000_022c),
3051 f64::from_bits(0x3ff1_09ec_6000_0eb5),
3052 f64::from_bits(0x3ff1_09fa_e000_3cde),
3053 ];
3054 let expected = f64::from_bits(0x3ff1_09ef_b555_6f3f);
3055 let ascending = (lengths[0] + lengths[1] + lengths[2]) / 3.0;
3056 let descending = (lengths[2] + lengths[1] + lengths[0]) / 3.0;
3057 assert_eq!(expected.to_bits(), 0x3ff1_09ef_b555_6f3f);
3058 assert_eq!(ascending.to_bits(), expected.to_bits());
3059 assert_eq!(descending.to_bits(), 0x3ff1_09ef_b555_6f40);
3060 for order in [
3061 [0, 1, 2],
3062 [0, 2, 1],
3063 [1, 0, 2],
3064 [1, 2, 0],
3065 [2, 0, 1],
3066 [2, 1, 0],
3067 ] {
3068 assert_eq!(
3069 average_affine_axis_length(order.map(|index| lengths[index])),
3070 expected,
3071 "axis order {order:?}"
3072 );
3073 }
3074
3075 let dyadic = [2.0_f64.powi(53), 1.0, 1.0];
3080 let ascending = (dyadic[1] + dyadic[2] + dyadic[0]) / 3.0;
3081 let descending = (dyadic[0] + dyadic[1] + dyadic[2]) / 3.0;
3082 assert_ne!(ascending, descending);
3083 assert_eq!(average_affine_axis_length(dyadic), ascending);
3084
3085 let permutations = affine_test_fixtures::appendix_d_v6_mean_permutations();
3091 let expected_length_bits = lengths.map(f64::to_bits);
3092 assert_eq!(
3093 affine_axis_lengths(permutations[0]).map(f64::to_bits),
3094 expected_length_bits
3095 );
3096 let tolerance = PositiveUniformAffineTolerance {
3097 equal_axis: 1.0e-5,
3098 relative_orthogonality: 1.0e-5,
3099 singular_determinant_relative: 1.0e-6,
3100 };
3101 for (permutation, linear) in permutations.into_iter().enumerate() {
3102 assert!(
3103 linear
3104 .x_axis
3105 .as_dvec3()
3106 .cross(linear.y_axis.as_dvec3())
3107 .dot(linear.z_axis.as_dvec3())
3108 > 0.0,
3109 "orientation for permutation {permutation}"
3110 );
3111 assert_eq!(
3112 average_affine_axis_length(affine_axis_lengths(linear)).to_bits(),
3113 expected.to_bits(),
3114 "mean for permutation {permutation}"
3115 );
3116 assert_eq!(
3117 classify_positive_uniform_affine(linear, tolerance),
3118 Err(AffineDomainViolation::NonUniformScale),
3119 "classification for permutation {permutation}"
3120 );
3121 }
3122 }
3123
3124 #[test]
3125 fn shared_affine_classifier_pins_f64_determinant_arithmetic() {
3126 let linear = Mat3::from_cols(
3131 Vec3::new(
3132 f32::from_bits(0x3ff3_5574),
3133 f32::from_bits(0x3f0e_fa3c),
3134 0.0,
3135 ),
3136 Vec3::new(
3137 f32::from_bits(0x3ff5_5e17),
3138 f32::from_bits(0x3f10_2c31),
3139 0.0,
3140 ),
3141 Vec3::Z,
3142 );
3143 let columns = [
3144 linear.x_axis.as_dvec3(),
3145 linear.y_axis.as_dvec3(),
3146 linear.z_axis.as_dvec3(),
3147 ];
3148 let determinant_f64 = columns[2].dot(columns[0].cross(columns[1]));
3149 let determinant_f32 = f64::from(linear.determinant());
3150 let lengths = affine_axis_lengths(linear);
3151 let threshold = (determinant_f64 + determinant_f32) / 2.0;
3152 assert!(determinant_f64 < threshold);
3153 assert!(determinant_f32 > threshold);
3154
3155 assert_eq!(
3156 classify_positive_uniform_affine(
3157 linear,
3158 PositiveUniformAffineTolerance {
3159 equal_axis: 10.0,
3160 relative_orthogonality: 10.0,
3161 singular_determinant_relative: threshold
3162 / (lengths[0] * lengths[1] * lengths[2]),
3163 },
3164 ),
3165 Err(AffineDomainViolation::Singular)
3166 );
3167
3168 let large_uniform = 2.0e19_f32;
3173 assert_eq!(
3174 classify_positive_uniform_affine(
3175 Mat3::from_diagonal(Vec3::splat(large_uniform)),
3176 PositiveUniformAffineTolerance {
3177 equal_axis: 1.0e-5,
3178 relative_orthogonality: 1.0e-5,
3179 singular_determinant_relative: 1.0e-6,
3180 },
3181 ),
3182 Ok(f64::from(large_uniform))
3183 );
3184 }
3185
3186 #[test]
3187 fn affine_geometry_facts_pin_every_widened_field_and_slot() {
3188 let linear = Mat3::from_cols(
3189 Vec3::new(1.0, 2.0, 3.0),
3190 Vec3::new(4.0, 5.0, 6.0),
3191 Vec3::new(7.0, 8.0, 10.0),
3192 );
3193
3194 let facts = AffineGeometryFacts::from_linear(linear).expect("finite widened facts");
3195 assert_eq!(
3196 facts.axis_lengths.map(f64::to_bits),
3197 [
3198 0x400d_eeea_1168_3f49,
3199 0x4021_8cc8_21d6_d3e3,
3200 0x402d_3064_dcc8_ae67,
3201 ]
3202 );
3203 assert_eq!(facts.mean_axis_length.to_bits(), 0x4022_12f7_d653_30b4);
3204 assert_eq!(facts.determinant.to_bits(), 0xc008_0000_0000_0000);
3205 assert_eq!(facts.axis_length_product.to_bits(), 0x407d_f2e3_88f2_1b01);
3206 assert_eq!(
3207 facts.cross_axis_dots.map(f64::to_bits),
3208 [
3209 0x4040_0000_0000_0000,
3210 0x404a_8000_0000_0000,
3211 0x4060_0000_0000_0000,
3212 ],
3213 "cross-axis slots are XY, XZ, YZ"
3214 );
3215 }
3216
3217 #[test]
3218 fn affine_geometry_facts_widen_every_dot_product_before_multiplying() {
3219 let x = Vec3::new(
3220 f32::from_bits(0x3ff3_5574),
3221 f32::from_bits(0x3f0e_fa3c),
3222 0.0,
3223 );
3224 let y = Vec3::new(
3225 f32::from_bits(0x3ff5_5e17),
3226 f32::from_bits(0x3f10_2c31),
3227 0.0,
3228 );
3229 let widened_dot = x.as_dvec3().dot(y.as_dvec3());
3230 let f32_then_widened = f64::from(x.dot(y));
3231
3232 for (slot, linear) in [
3233 (0, Mat3::from_cols(x, y, Vec3::Z)),
3234 (1, Mat3::from_cols(x, Vec3::Z, y)),
3235 (2, Mat3::from_cols(Vec3::Z, x, y)),
3236 ] {
3237 let facts = AffineGeometryFacts::from_linear(linear).expect("finite widened facts");
3238 assert_eq!(facts.cross_axis_dots[slot], widened_dot);
3239 assert_ne!(
3240 facts.cross_axis_dots[slot], f32_then_widened,
3241 "dot slot {slot} must multiply and add in f64, not widen an f32 result"
3242 );
3243 }
3244 }
3245
3246 #[test]
3247 fn weld_preserves_uv_seams_at_shared_positions() {
3248 let mut primitive = Primitive {
3249 positions: vec![Vec3::ZERO, Vec3::ZERO, Vec3::ZERO],
3250 uvs: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 0.0]],
3251 ..Primitive::default()
3252 };
3253
3254 primitive.weld();
3255
3256 assert_eq!(primitive.positions.len(), 2);
3257 let reconstructed_corners = primitive
3258 .indices
3259 .iter()
3260 .map(|&index| {
3261 let index = index as usize;
3262 (primitive.positions[index], primitive.uvs[index])
3263 })
3264 .collect::<Vec<_>>();
3265 assert_eq!(
3266 reconstructed_corners,
3267 vec![
3268 (Vec3::ZERO, [0.0, 0.0]),
3269 (Vec3::ZERO, [1.0, 0.0]),
3270 (Vec3::ZERO, [0.0, 0.0]),
3271 ]
3272 );
3273 }
3274}