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)]
399pub enum Property {
400 Translation,
402 Rotation,
404 Scale,
406}
407
408impl Property {
409 pub fn as_str(self) -> &'static str {
412 match self {
413 Property::Translation => "translation",
414 Property::Rotation => "rotation",
415 Property::Scale => "scale",
416 }
417 }
418}
419
420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub enum Interpolation {
423 Linear,
425 Step,
427 CubicSpline,
431}
432
433#[derive(Debug, Clone)]
435pub enum TrackValues {
436 Vec3s(Vec<Vec3>),
438 Quats(Vec<Quat>),
440}
441
442impl TrackValues {
443 pub fn len(&self) -> usize {
446 match self {
447 TrackValues::Vec3s(v) => v.len(),
448 TrackValues::Quats(v) => v.len(),
449 }
450 }
451
452 pub fn is_empty(&self) -> bool {
454 self.len() == 0
455 }
456}
457
458#[derive(Debug, Clone)]
460pub struct Track {
461 pub bone: BoneId,
463 pub property: Property,
465 pub interpolation: Interpolation,
467 pub times: Vec<f32>,
470 pub values: TrackValues,
472}
473
474impl Track {
475 pub fn key_count(&self) -> usize {
477 self.times.len()
478 }
479
480 pub fn value_index(&self, k: usize) -> usize {
483 match self.interpolation {
484 Interpolation::CubicSpline => 3 * k + 1,
485 _ => k,
486 }
487 }
488
489 pub fn key_vec3(&self, k: usize) -> Option<Vec3> {
491 match &self.values {
492 TrackValues::Vec3s(v) => v.get(self.value_index(k)).copied(),
493 TrackValues::Quats(_) => None,
494 }
495 }
496
497 pub fn key_quat(&self, k: usize) -> Option<Quat> {
499 match &self.values {
500 TrackValues::Quats(v) => v.get(self.value_index(k)).copied(),
501 TrackValues::Vec3s(_) => None,
502 }
503 }
504
505 pub fn start_time(&self) -> f32 {
507 self.times.first().copied().unwrap_or(0.0)
508 }
509
510 pub fn end_time(&self) -> f32 {
512 self.times.last().copied().unwrap_or(0.0)
513 }
514}
515
516#[derive(Debug, Clone)]
518pub struct Clip {
519 pub name: String,
522 pub duration_s: f64,
524 pub tracks: Vec<Track>,
526}
527
528#[derive(Debug, Clone, Default)]
530pub struct SourceInfo {
531 pub path: Option<String>,
533 pub format: Option<String>,
535}
536
537#[derive(Debug, Clone, Default)]
544pub struct Document {
545 pub skeleton: Skeleton,
547 pub clips: Vec<Clip>,
549 pub assets: SceneAssets,
551 pub source: SourceInfo,
553}
554
555#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
561#[non_exhaustive]
562pub enum DocumentShapeError {
563 #[error("node {node} has a non-finite rest transform")]
566 NonFiniteSkeletonRest {
567 node: BoneId,
569 },
570 #[error("node {node} has invalid parent {parent}")]
572 InvalidSkeletonParent {
573 node: BoneId,
575 parent: BoneId,
577 },
578 #[error("source skeleton declares duplicate source node index {source_node_index}")]
580 DuplicateSourceNodeIndex {
581 source_node_index: usize,
583 },
584 #[error("source skeleton declares duplicate source skin index {source_skin_index}")]
586 DuplicateSourceSkinIndex {
587 source_skin_index: usize,
589 },
590 #[error(
592 "source node {source_node_index} contradicts the document skeleton's parent chain ({violation})"
593 )]
594 SourceProjection {
595 source_node_index: usize,
597 violation: SourceProjectionViolation,
599 },
600 #[error("clip {clip_index} declares duplicate {property:?} tracks for node {node}")]
602 DuplicateClipTrack {
603 clip_index: usize,
605 node: BoneId,
607 property: Property,
609 },
610 #[error("clip {clip_index} track for node {node} has an invalid shape ({violation})")]
612 TrackShape {
613 clip_index: usize,
615 node: BoneId,
617 violation: TrackShapeViolation,
619 },
620 #[error("mesh instance {instance_index} is invalid ({violation})")]
622 MeshInstanceShape {
623 instance_index: usize,
625 violation: MeshInstanceShapeViolation,
627 },
628 #[error("node {node} has a non-finite inverse-bind matrix")]
630 NonFiniteBoneInverseBind {
631 node: BoneId,
633 },
634}
635
636#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
638#[non_exhaustive]
639pub enum SourceProjectionViolation {
640 #[error("projected_bone_out_of_range")]
642 ProjectedBoneOutOfRange,
643 #[error("two_source_nodes_project_to_one_bone")]
645 TwoSourceNodesProjectToOneBone,
646 #[error("parent_source_node_is_missing")]
648 ParentSourceNodeMissing,
649 #[error("cyclic_unprojected_source_parent_chain")]
651 CyclicUnprojectedSourceParentChain,
652 #[error("projection_and_skeleton_parents_differ")]
654 NearestProjectedParentMismatch,
655 #[error("projected_bone_has_an_unprojected_skeleton_child")]
657 ProjectedBoneHasUnprojectedSkeletonChild,
658}
659
660#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
662#[non_exhaustive]
663pub enum TrackShapeViolation {
664 #[error("bone_index_out_of_range")]
666 BoneIndexOutOfRange,
667 #[error("empty_times")]
669 EmptyTimes,
670 #[error("non_finite_time")]
672 NonFiniteTime,
673 #[error("times_not_strictly_increasing")]
675 TimesNotStrictlyIncreasing,
676 #[error("value_count_mismatch")]
678 ValueCountMismatch,
679 #[error("value_type_mismatches_property")]
681 ValueTypeMismatchesProperty,
682 #[error("non_finite_value")]
684 NonFiniteValue,
685}
686
687#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
689#[non_exhaustive]
690pub enum MeshInstanceShapeViolation {
691 #[error("node_index_out_of_range")]
693 NodeIndexOutOfRange,
694 #[error("mesh_index_out_of_range")]
696 MeshIndexOutOfRange,
697 #[error("skin_joint_out_of_range")]
699 SkinJointOutOfRange,
700 #[error("skin_ibm_count_mismatch")]
702 SkinInverseBindCountMismatch,
703 #[error("non_finite_inverse_bind")]
705 NonFiniteSkinInverseBind,
706}
707
708#[derive(Debug, Clone, Default)]
724pub struct Primitive {
725 pub material: Option<usize>,
727 pub indices: Vec<u32>,
729 pub positions: Vec<Vec3>,
731 pub normals: Vec<Vec3>,
733 pub uvs: Vec<[f32; 2]>,
735 pub joints: Vec<[u16; 4]>,
737 pub weights: Vec<[f32; 4]>,
739 pub additional_influence_sets: Vec<AdditionalInfluenceSet>,
745}
746
747#[derive(Debug, Clone, Copy, PartialEq, Eq)]
754pub struct AdditionalInfluenceSet {
755 pub set_index: u32,
757 pub joints_present: bool,
759 pub weights_present: bool,
761}
762
763#[derive(Debug, Clone, Default)]
765pub struct MeshAsset {
766 pub name: String,
768 pub source_mesh_index: usize,
773 pub primitives: Vec<Primitive>,
775}
776
777#[derive(Debug, Clone, Default)]
779pub struct MeshInstance {
780 pub source_node_index: usize,
782 pub node: BoneId,
784 pub mesh: usize,
786 pub skin_joints: Vec<BoneId>,
788 pub skin_ibms: Vec<Mat4>,
793}
794
795#[derive(Debug, Clone, Default)]
797pub struct SceneAsset {
798 pub source_scene_index: usize,
800 pub name: Option<String>,
802 pub roots: Vec<BoneId>,
804}
805
806#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
813#[serde(rename_all = "snake_case")]
814pub enum SourceSkeletonCoverage {
815 #[default]
817 Unavailable,
818 Complete,
822}
823
824#[derive(Debug, Clone)]
830pub enum SourceNodeLocalRest {
831 Trs {
833 translation: Vec3,
835 rotation: Quat,
837 scale: Vec3,
839 },
840 Matrix(Mat4),
842}
843
844#[derive(Debug, Clone)]
854#[non_exhaustive]
855pub struct SourceNodeAsset {
856 pub source_node_index: usize,
858 pub name: Option<String>,
860 pub parent_source_node_index: Option<usize>,
862 pub scene_root_indices: Vec<usize>,
865 pub local_rest: SourceNodeLocalRest,
867 pub bone: Option<BoneId>,
887}
888
889impl SourceNodeAsset {
890 pub fn new(source_node_index: usize, local_rest: SourceNodeLocalRest) -> Self {
898 Self {
899 source_node_index,
900 name: None,
901 parent_source_node_index: None,
902 scene_root_indices: Vec::new(),
903 local_rest,
904 bone: None,
905 }
906 }
907}
908
909#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
911#[serde(rename_all = "snake_case")]
912pub enum SourceInverseBindAccessorStatus {
913 #[default]
915 Absent,
916 Available,
918 EmptyAccessor,
920 CountMismatch,
922 Unreadable,
924}
925
926#[derive(Debug, Clone, Default)]
928pub struct SourceInverseBindAccessor {
929 pub status: SourceInverseBindAccessorStatus,
931 pub declared_count: Option<usize>,
933 pub matrices: Vec<Mat4>,
939}
940
941#[derive(Debug, Clone)]
943pub struct SourceSkinAttachment {
944 pub source_node_index: usize,
946 pub source_mesh_index: Option<usize>,
951}
952
953#[derive(Debug, Clone, Default)]
955pub struct SourceSkinAsset {
956 pub source_skin_index: usize,
958 pub name: Option<String>,
960 pub skeleton_root_source_node_index: Option<usize>,
962 pub joint_source_node_indices: Vec<usize>,
964 pub inverse_bind_accessor: SourceInverseBindAccessor,
966 pub attachments: Vec<SourceSkinAttachment>,
968}
969
970#[derive(Debug, Clone, Default)]
972pub struct SourceSkeletonAssets {
973 pub coverage: SourceSkeletonCoverage,
975 pub nodes: Vec<SourceNodeAsset>,
977 pub skins: Vec<SourceSkinAsset>,
979}
980
981#[derive(Debug, Clone)]
984pub struct TextureAsset {
985 pub bytes: Vec<u8>,
987 pub mime: String,
989}
990
991#[derive(Debug, Clone)]
996pub struct NormalTextureAsset {
997 pub texture: TextureAsset,
999 pub scale: f32,
1001}
1002
1003#[derive(Debug, Clone)]
1009pub struct OcclusionTextureAsset {
1010 pub texture: TextureAsset,
1012 pub strength: f32,
1014}
1015
1016#[derive(Debug, Clone)]
1018pub struct MaterialAsset {
1019 pub name: String,
1021 pub base_color: [f32; 4],
1024 pub metallic: f32,
1026 pub roughness: f32,
1028 pub base_color_texture: Option<TextureAsset>,
1030 pub normal_texture: Option<NormalTextureAsset>,
1032 pub metallic_roughness_texture: Option<TextureAsset>,
1036 pub occlusion_texture: Option<OcclusionTextureAsset>,
1038}
1039
1040#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1046#[serde(rename_all = "snake_case")]
1047pub enum MaterialResourceCoverage {
1048 Complete,
1052 #[default]
1054 Unavailable,
1055}
1056
1057#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1062#[serde(rename_all = "snake_case")]
1063pub enum MaterialTextureSlot {
1064 BaseColor,
1066 Normal,
1068 MetallicRoughness,
1070 Occlusion,
1072 Emissive,
1074}
1075
1076#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1078pub struct SourceMaterialTextureBinding {
1079 pub slot: MaterialTextureSlot,
1081 pub texture_index: usize,
1083}
1084
1085#[derive(Debug, Clone, Default)]
1087pub struct SourceMaterialAsset {
1088 pub material_index: usize,
1090 pub name: Option<String>,
1092 pub texture_bindings: Vec<SourceMaterialTextureBinding>,
1094}
1095
1096#[derive(Debug, Clone, Default)]
1098pub struct SourceTextureAsset {
1099 pub texture_index: usize,
1101 pub name: Option<String>,
1103 pub image_index: usize,
1105}
1106
1107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1109#[serde(rename_all = "snake_case")]
1110pub enum ImageSourceKind {
1111 Embedded,
1113 DataUri,
1115 External,
1117}
1118
1119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1121#[serde(rename_all = "snake_case")]
1122pub enum ImageContainerFormat {
1123 Png,
1125 Jpeg,
1127}
1128
1129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1131#[serde(rename_all = "snake_case")]
1132pub enum DecodedImageColorType {
1133 L8,
1135 La8,
1137 Rgb8,
1139 Rgba8,
1141 L16,
1143 La16,
1145 Rgb16,
1147 Rgba16,
1149}
1150
1151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1153#[serde(rename_all = "snake_case")]
1154pub enum ImageUnavailableReason {
1155 SourceUnavailable,
1157 InvalidDataUri,
1159 UnsupportedContainer,
1161 DecodeFailed,
1163 ResourceLimit,
1165}
1166
1167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1169pub enum SourceImageInspection {
1170 Available {
1172 width: u32,
1174 height: u32,
1176 channel_count: u8,
1178 color_type: DecodedImageColorType,
1180 },
1181 Unavailable {
1183 reason: ImageUnavailableReason,
1185 },
1186}
1187
1188#[derive(Debug, Clone)]
1190pub struct SourceImageAsset {
1191 pub image_index: usize,
1193 pub name: Option<String>,
1195 pub source_kind: ImageSourceKind,
1197 pub declared_mime_type: Option<String>,
1199 pub detected_container: Option<ImageContainerFormat>,
1201 pub inspection: SourceImageInspection,
1203}
1204
1205#[derive(Debug, Clone, Default)]
1207pub struct MaterialResourceAssets {
1208 pub coverage: MaterialResourceCoverage,
1210 pub materials: Vec<SourceMaterialAsset>,
1212 pub textures: Vec<SourceTextureAsset>,
1214 pub images: Vec<SourceImageAsset>,
1216}
1217
1218impl Primitive {
1219 pub fn weld(&mut self) {
1223 if !self.indices.is_empty() || self.positions.is_empty() {
1224 return;
1225 }
1226 let corner_key = |i: usize| -> Vec<u8> {
1227 let mut key = Vec::with_capacity(64);
1228 let mut push_f32s = |vals: &[f32]| {
1229 for v in vals {
1230 key.extend_from_slice(&v.to_le_bytes());
1231 }
1232 };
1233 push_f32s(&self.positions[i].to_array());
1234 if let Some(n) = self.normals.get(i) {
1235 push_f32s(&n.to_array());
1236 }
1237 if let Some(uv) = self.uvs.get(i) {
1238 push_f32s(uv);
1239 }
1240 if let Some(w) = self.weights.get(i) {
1241 push_f32s(w);
1242 }
1243 if let Some(j) = self.joints.get(i) {
1244 for v in j {
1245 key.extend_from_slice(&v.to_le_bytes());
1246 }
1247 }
1248 key
1249 };
1250 let mut seen: std::collections::HashMap<Vec<u8>, u32> = std::collections::HashMap::new();
1251 let mut indices = Vec::with_capacity(self.positions.len());
1252 let mut positions = Vec::new();
1253 let mut normals = Vec::new();
1254 let mut uvs = Vec::new();
1255 let mut joints = Vec::new();
1256 let mut weights = Vec::new();
1257 for i in 0..self.positions.len() {
1258 let index = *seen.entry(corner_key(i)).or_insert_with(|| {
1259 positions.push(self.positions[i]);
1260 if let Some(n) = self.normals.get(i) {
1261 normals.push(*n);
1262 }
1263 if let Some(uv) = self.uvs.get(i) {
1264 uvs.push(*uv);
1265 }
1266 if let Some(j) = self.joints.get(i) {
1267 joints.push(*j);
1268 }
1269 if let Some(w) = self.weights.get(i) {
1270 weights.push(*w);
1271 }
1272 (positions.len() - 1) as u32
1273 });
1274 indices.push(index);
1275 }
1276 self.indices = indices;
1277 self.positions = positions;
1278 self.normals = normals;
1279 self.uvs = uvs;
1280 self.joints = joints;
1281 self.weights = weights;
1282 }
1283}
1284
1285#[derive(Debug, Clone, Default)]
1288pub struct SceneAssets {
1289 pub meshes: Vec<MeshAsset>,
1292 pub instances: Vec<MeshInstance>,
1294 pub materials: Vec<MaterialAsset>,
1296 pub material_resources: MaterialResourceAssets,
1299 pub scenes: Vec<SceneAsset>,
1301 pub default_scene: Option<usize>,
1303 pub source_skeleton: SourceSkeletonAssets,
1310}
1311
1312pub fn validate_document_shape(document: &Document) -> Result<(), DocumentShapeError> {
1330 validate_skeleton_rest(&document.skeleton)?;
1331 validate_source_skeleton_identity(&document.assets.source_skeleton)?;
1332 validate_source_projection(document)?;
1333 validate_clip_tracks(document)?;
1334 validate_mesh_instances(document)?;
1335 validate_bone_inverse_binds(&document.skeleton)
1336}
1337
1338fn validate_skeleton_rest(skeleton: &Skeleton) -> Result<(), DocumentShapeError> {
1339 world_rest_matrices(skeleton)
1340 .map(|_| ())
1341 .map_err(|error| match error {
1342 WorldMatrixError::NonFiniteTransform { node } => {
1343 DocumentShapeError::NonFiniteSkeletonRest { node }
1344 }
1345 WorldMatrixError::InvalidParent { node, parent } => {
1346 DocumentShapeError::InvalidSkeletonParent { node, parent }
1347 }
1348 })
1349}
1350
1351fn validate_source_skeleton_identity(
1352 source_skeleton: &SourceSkeletonAssets,
1353) -> Result<(), DocumentShapeError> {
1354 let mut seen_nodes = BTreeSet::new();
1355 for node in &source_skeleton.nodes {
1356 if !seen_nodes.insert(node.source_node_index) {
1357 return Err(DocumentShapeError::DuplicateSourceNodeIndex {
1358 source_node_index: node.source_node_index,
1359 });
1360 }
1361 }
1362 let mut seen_skins = BTreeSet::new();
1363 for skin in &source_skeleton.skins {
1364 if !seen_skins.insert(skin.source_skin_index) {
1365 return Err(DocumentShapeError::DuplicateSourceSkinIndex {
1366 source_skin_index: skin.source_skin_index,
1367 });
1368 }
1369 }
1370 Ok(())
1371}
1372
1373fn validate_source_projection(document: &Document) -> Result<(), DocumentShapeError> {
1387 let source_skeleton = &document.assets.source_skeleton;
1388 if source_skeleton.coverage != SourceSkeletonCoverage::Complete {
1389 return Ok(());
1390 }
1391
1392 let bones = &document.skeleton.bones;
1393 let mut bone_of_source = BTreeMap::new();
1394 let mut source_of_bone = BTreeMap::new();
1395 let mut skeleton_parents = Vec::with_capacity(source_skeleton.nodes.len());
1396 for node in &source_skeleton.nodes {
1397 let Some(bone) = node.bone else {
1398 continue;
1399 };
1400 let skeleton_parent = bones
1401 .get(bone)
1402 .ok_or(DocumentShapeError::SourceProjection {
1403 source_node_index: node.source_node_index,
1404 violation: SourceProjectionViolation::ProjectedBoneOutOfRange,
1405 })?
1406 .parent;
1407 if source_of_bone
1408 .insert(bone, node.source_node_index)
1409 .is_some()
1410 {
1411 return Err(DocumentShapeError::SourceProjection {
1412 source_node_index: node.source_node_index,
1413 violation: SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
1414 });
1415 }
1416 bone_of_source.insert(node.source_node_index, bone);
1417 skeleton_parents.push((node, skeleton_parent));
1418 }
1419
1420 let by_source_index: BTreeMap<_, _> = source_skeleton
1421 .nodes
1422 .iter()
1423 .map(|node| (node.source_node_index, node))
1424 .collect();
1425 let unprojected_rows = source_skeleton.nodes.len() - bone_of_source.len();
1426 let mut resolved_unprojected = BTreeMap::<usize, Option<BoneId>>::new();
1430 for (node, skeleton_parent) in skeleton_parents {
1431 let mut cursor = node.parent_source_node_index;
1432 let mut unresolved_suffix = Vec::new();
1433 let projected_parent = loop {
1434 let Some(parent_source_node_index) = cursor else {
1435 break None;
1436 };
1437 if let Some(&bone) = bone_of_source.get(&parent_source_node_index) {
1438 break Some(bone);
1439 }
1440 if let Some(&projected_parent) = resolved_unprojected.get(&parent_source_node_index) {
1441 break projected_parent;
1442 }
1443 let parent = by_source_index.get(&parent_source_node_index).ok_or(
1444 DocumentShapeError::SourceProjection {
1445 source_node_index: node.source_node_index,
1446 violation: SourceProjectionViolation::ParentSourceNodeMissing,
1447 },
1448 )?;
1449 unresolved_suffix.push(parent_source_node_index);
1450 if unresolved_suffix.len() > unprojected_rows {
1451 return Err(DocumentShapeError::SourceProjection {
1452 source_node_index: node.source_node_index,
1453 violation: SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
1454 });
1455 }
1456 cursor = parent.parent_source_node_index;
1457 };
1458 for source_node_index in unresolved_suffix {
1459 resolved_unprojected.insert(source_node_index, projected_parent);
1460 }
1461 if projected_parent != skeleton_parent {
1462 return Err(DocumentShapeError::SourceProjection {
1463 source_node_index: node.source_node_index,
1464 violation: SourceProjectionViolation::NearestProjectedParentMismatch,
1465 });
1466 }
1467 }
1468
1469 for (bone, child) in bones.iter().enumerate() {
1470 if source_of_bone.contains_key(&bone) {
1471 continue;
1472 }
1473 if let Some(parent) = child.parent
1474 && let Some(&source_node_index) = source_of_bone.get(&parent)
1475 {
1476 return Err(DocumentShapeError::SourceProjection {
1477 source_node_index,
1478 violation: SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
1479 });
1480 }
1481 }
1482 Ok(())
1483}
1484
1485fn validate_clip_tracks(document: &Document) -> Result<(), DocumentShapeError> {
1486 let bone_count = document.skeleton.bones.len();
1487 for (clip_index, clip) in document.clips.iter().enumerate() {
1488 let mut seen = Vec::with_capacity(clip.tracks.len());
1489 for track in &clip.tracks {
1490 if track.bone >= bone_count {
1491 return Err(DocumentShapeError::TrackShape {
1492 clip_index,
1493 node: track.bone,
1494 violation: TrackShapeViolation::BoneIndexOutOfRange,
1495 });
1496 }
1497 if seen.contains(&(track.bone, track.property)) {
1498 return Err(DocumentShapeError::DuplicateClipTrack {
1499 clip_index,
1500 node: track.bone,
1501 property: track.property,
1502 });
1503 }
1504 seen.push((track.bone, track.property));
1505 validate_track_shape(clip_index, track)?;
1506 }
1507 }
1508 Ok(())
1509}
1510
1511fn validate_track_shape(clip_index: usize, track: &Track) -> Result<(), DocumentShapeError> {
1512 let violation = if track.times.is_empty() {
1513 Some(TrackShapeViolation::EmptyTimes)
1514 } else if track.times.iter().any(|time| !time.is_finite()) {
1515 Some(TrackShapeViolation::NonFiniteTime)
1516 } else if track.times.windows(2).any(|times| times[0] >= times[1]) {
1517 Some(TrackShapeViolation::TimesNotStrictlyIncreasing)
1518 } else {
1519 let expected_values = match track.interpolation {
1520 Interpolation::CubicSpline => track.times.len().checked_mul(3),
1521 Interpolation::Linear | Interpolation::Step => Some(track.times.len()),
1522 };
1523 if expected_values != Some(track.values.len()) {
1524 Some(TrackShapeViolation::ValueCountMismatch)
1525 } else if !matches!(
1526 (&track.values, track.property),
1527 (
1528 TrackValues::Vec3s(_),
1529 Property::Translation | Property::Scale
1530 ) | (TrackValues::Quats(_), Property::Rotation)
1531 ) {
1532 Some(TrackShapeViolation::ValueTypeMismatchesProperty)
1533 } else if match &track.values {
1534 TrackValues::Vec3s(values) => values.iter().any(|value| !value.is_finite()),
1535 TrackValues::Quats(values) => values.iter().any(|value| !value.is_finite()),
1536 } {
1537 Some(TrackShapeViolation::NonFiniteValue)
1538 } else {
1539 None
1540 }
1541 };
1542 violation.map_or(Ok(()), |violation| {
1543 Err(DocumentShapeError::TrackShape {
1544 clip_index,
1545 node: track.bone,
1546 violation,
1547 })
1548 })
1549}
1550
1551fn validate_mesh_instances(document: &Document) -> Result<(), DocumentShapeError> {
1552 let bone_count = document.skeleton.bones.len();
1553 let mesh_count = document.assets.meshes.len();
1554 for (instance_index, instance) in document.assets.instances.iter().enumerate() {
1555 let violation = if instance.node >= bone_count {
1556 Some(MeshInstanceShapeViolation::NodeIndexOutOfRange)
1557 } else if instance.mesh >= mesh_count {
1558 Some(MeshInstanceShapeViolation::MeshIndexOutOfRange)
1559 } else if instance
1560 .skin_joints
1561 .iter()
1562 .any(|&joint| joint >= bone_count)
1563 {
1564 Some(MeshInstanceShapeViolation::SkinJointOutOfRange)
1565 } else if !instance.skin_ibms.is_empty()
1566 && instance.skin_ibms.len() != instance.skin_joints.len()
1567 {
1568 Some(MeshInstanceShapeViolation::SkinInverseBindCountMismatch)
1569 } else if instance.skin_ibms.iter().any(|ibm| !mat4_is_finite(*ibm)) {
1570 Some(MeshInstanceShapeViolation::NonFiniteSkinInverseBind)
1571 } else {
1572 None
1573 };
1574 if let Some(violation) = violation {
1575 return Err(DocumentShapeError::MeshInstanceShape {
1576 instance_index,
1577 violation,
1578 });
1579 }
1580 }
1581 Ok(())
1582}
1583
1584fn validate_bone_inverse_binds(skeleton: &Skeleton) -> Result<(), DocumentShapeError> {
1585 for (node, bone) in skeleton.bones.iter().enumerate() {
1586 if let Some(inverse_bind) = bone.inverse_bind
1587 && !mat4_is_finite(inverse_bind)
1588 {
1589 return Err(DocumentShapeError::NonFiniteBoneInverseBind { node });
1590 }
1591 }
1592 Ok(())
1593}
1594
1595#[cfg(test)]
1596mod tests {
1597 use super::*;
1598
1599 fn bone(parent: Option<BoneId>) -> Bone {
1600 Bone {
1601 name: "bone".into(),
1602 parent,
1603 rest: Transform::IDENTITY,
1604 inverse_bind: None,
1605 }
1606 }
1607
1608 fn one_bone_document() -> Document {
1609 Document {
1610 skeleton: Skeleton {
1611 bones: vec![bone(None)],
1612 },
1613 ..Document::default()
1614 }
1615 }
1616
1617 fn source_node(
1618 source_node_index: usize,
1619 parent_source_node_index: Option<usize>,
1620 bone: Option<BoneId>,
1621 ) -> SourceNodeAsset {
1622 SourceNodeAsset {
1623 source_node_index,
1624 name: None,
1625 parent_source_node_index,
1626 scene_root_indices: Vec::new(),
1627 local_rest: SourceNodeLocalRest::Trs {
1628 translation: Vec3::ZERO,
1629 rotation: Quat::IDENTITY,
1630 scale: Vec3::ONE,
1631 },
1632 bone,
1633 }
1634 }
1635
1636 fn valid_track() -> Track {
1637 Track {
1638 bone: 0,
1639 property: Property::Translation,
1640 interpolation: Interpolation::Linear,
1641 times: vec![0.0],
1642 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
1643 }
1644 }
1645
1646 fn track_document(track: Track) -> Document {
1647 let mut document = one_bone_document();
1648 document.clips.push(Clip {
1649 name: "clip".into(),
1650 duration_s: 0.0,
1651 tracks: vec![track],
1652 });
1653 document
1654 }
1655
1656 fn instance_document() -> Document {
1657 let mut document = one_bone_document();
1658 document.assets.meshes.push(MeshAsset::default());
1659 document.assets.instances.push(MeshInstance {
1660 node: 0,
1661 mesh: 0,
1662 ..MeshInstance::default()
1663 });
1664 document
1665 }
1666
1667 #[test]
1668 fn document_shape_validation_accepts_a_complete_projection_with_an_unprojected_intermediate() {
1669 let mut document = Document {
1670 skeleton: Skeleton {
1671 bones: vec![bone(None), bone(Some(0))],
1672 },
1673 assets: SceneAssets {
1674 source_skeleton: SourceSkeletonAssets {
1675 coverage: SourceSkeletonCoverage::Complete,
1676 nodes: vec![
1677 source_node(10, None, Some(0)),
1678 source_node(11, Some(10), None),
1679 source_node(12, Some(11), Some(1)),
1680 ],
1681 ..SourceSkeletonAssets::default()
1682 },
1683 meshes: vec![MeshAsset::default()],
1684 instances: vec![MeshInstance {
1685 node: 1,
1686 mesh: 0,
1687 skin_joints: vec![0, 1],
1688 skin_ibms: vec![Mat4::IDENTITY, Mat4::IDENTITY],
1689 ..MeshInstance::default()
1690 }],
1691 ..SceneAssets::default()
1692 },
1693 ..Document::default()
1694 };
1695 document.clips.push(Clip {
1696 name: "clip".into(),
1697 duration_s: 0.0,
1698 tracks: vec![valid_track()],
1699 });
1700
1701 assert_eq!(validate_document_shape(&document), Ok(()));
1702 }
1703
1704 #[test]
1705 fn shared_unprojected_parent_suffix_preserves_each_projected_parent() {
1706 const CONNECTORS: usize = 64;
1707 const PROJECTED_CHILDREN: usize = 64;
1708
1709 let mut nodes = Vec::with_capacity(1 + CONNECTORS + PROJECTED_CHILDREN);
1710 nodes.push(source_node(0, None, Some(0)));
1711 for source_node_index in 1..=CONNECTORS {
1712 nodes.push(source_node(
1713 source_node_index,
1714 Some(source_node_index - 1),
1715 None,
1716 ));
1717 }
1718 for child in 0..PROJECTED_CHILDREN {
1719 nodes.push(source_node(
1720 1 + CONNECTORS + child,
1721 Some(CONNECTORS),
1722 Some(1 + child),
1723 ));
1724 }
1725 let document = Document {
1726 skeleton: Skeleton {
1727 bones: std::iter::once(bone(None))
1728 .chain((0..PROJECTED_CHILDREN).map(|_| bone(Some(0))))
1729 .collect(),
1730 },
1731 assets: SceneAssets {
1732 source_skeleton: SourceSkeletonAssets {
1733 coverage: SourceSkeletonCoverage::Complete,
1734 nodes,
1735 ..SourceSkeletonAssets::default()
1736 },
1737 ..SceneAssets::default()
1738 },
1739 ..Document::default()
1740 };
1741
1742 assert_eq!(validate_document_shape(&document), Ok(()));
1743 let mut mismatched = document.clone();
1744 mismatched.skeleton.bones[PROJECTED_CHILDREN].parent = None;
1745 assert_eq!(
1746 validate_document_shape(&mismatched),
1747 Err(DocumentShapeError::SourceProjection {
1748 source_node_index: CONNECTORS + PROJECTED_CHILDREN,
1749 violation: SourceProjectionViolation::NearestProjectedParentMismatch,
1750 })
1751 );
1752 }
1753
1754 #[test]
1755 fn document_shape_validation_has_an_analytic_error_for_every_variant() {
1756 let projection_error =
1757 |source_node_index, violation| DocumentShapeError::SourceProjection {
1758 source_node_index,
1759 violation,
1760 };
1761 let track_error = |node, violation| DocumentShapeError::TrackShape {
1762 clip_index: 0,
1763 node,
1764 violation,
1765 };
1766 let instance_error = |violation| DocumentShapeError::MeshInstanceShape {
1767 instance_index: 0,
1768 violation,
1769 };
1770
1771 let mut non_finite_rest = one_bone_document();
1772 non_finite_rest.skeleton.bones[0].rest.translation.x = f32::NAN;
1773 let overflowed_rest_world = Document {
1774 skeleton: Skeleton {
1775 bones: vec![
1776 Bone {
1777 rest: Transform {
1778 scale: Vec3::splat(f32::MAX),
1779 ..Transform::IDENTITY
1780 },
1781 ..bone(None)
1782 },
1783 Bone {
1784 rest: Transform {
1785 translation: Vec3::splat(2.0),
1786 ..Transform::IDENTITY
1787 },
1788 ..bone(Some(0))
1789 },
1790 ],
1791 },
1792 ..Document::default()
1793 };
1794 let self_parent = Document {
1795 skeleton: Skeleton {
1796 bones: vec![bone(Some(0))],
1797 },
1798 ..Document::default()
1799 };
1800 let forward_parent = Document {
1801 skeleton: Skeleton {
1802 bones: vec![bone(Some(1)), bone(None)],
1803 },
1804 ..Document::default()
1805 };
1806 let far_parent = Document {
1807 skeleton: Skeleton {
1808 bones: vec![bone(Some(99))],
1809 },
1810 ..Document::default()
1811 };
1812 let duplicate_node = Document {
1813 assets: SceneAssets {
1814 source_skeleton: SourceSkeletonAssets {
1815 nodes: vec![
1816 source_node(9, None, None),
1817 source_node(10, None, None),
1818 source_node(9, None, None),
1819 ],
1820 ..SourceSkeletonAssets::default()
1821 },
1822 ..SceneAssets::default()
1823 },
1824 ..Document::default()
1825 };
1826 let duplicate_skin = Document {
1827 assets: SceneAssets {
1828 source_skeleton: SourceSkeletonAssets {
1829 skins: vec![
1830 SourceSkinAsset {
1831 source_skin_index: 4,
1832 ..SourceSkinAsset::default()
1833 },
1834 SourceSkinAsset {
1835 source_skin_index: 5,
1836 ..SourceSkinAsset::default()
1837 },
1838 SourceSkinAsset {
1839 source_skin_index: 4,
1840 ..SourceSkinAsset::default()
1841 },
1842 ],
1843 ..SourceSkeletonAssets::default()
1844 },
1845 ..SceneAssets::default()
1846 },
1847 ..Document::default()
1848 };
1849 let complete_projection = |nodes| SceneAssets {
1850 source_skeleton: SourceSkeletonAssets {
1851 coverage: SourceSkeletonCoverage::Complete,
1852 nodes,
1853 ..SourceSkeletonAssets::default()
1854 },
1855 ..SceneAssets::default()
1856 };
1857 let out_of_range_projection = Document {
1858 skeleton: Skeleton {
1859 bones: vec![bone(None)],
1860 },
1861 assets: complete_projection(vec![source_node(10, None, Some(1))]),
1862 ..Document::default()
1863 };
1864 let non_injective_projection = Document {
1865 skeleton: Skeleton {
1866 bones: vec![bone(None)],
1867 },
1868 assets: complete_projection(vec![
1869 source_node(10, None, Some(0)),
1870 source_node(11, None, Some(0)),
1871 ]),
1872 ..Document::default()
1873 };
1874 let missing_projection_parent = Document {
1875 skeleton: Skeleton {
1876 bones: vec![bone(None), bone(Some(0))],
1877 },
1878 assets: complete_projection(vec![source_node(11, Some(99), Some(1))]),
1879 ..Document::default()
1880 };
1881 let missing_projection_parent_at_cycle_bound = Document {
1886 skeleton: Skeleton {
1887 bones: vec![bone(None), bone(Some(0))],
1888 },
1889 assets: complete_projection(vec![
1890 source_node(10, None, Some(0)),
1891 source_node(11, Some(12), Some(1)),
1892 source_node(12, Some(99), None),
1893 ]),
1894 ..Document::default()
1895 };
1896 let cyclic_unprojected_parent = Document {
1897 skeleton: Skeleton {
1898 bones: vec![bone(None), bone(Some(0))],
1899 },
1900 assets: complete_projection(vec![
1901 source_node(11, Some(12), Some(1)),
1902 source_node(12, Some(12), None),
1903 ]),
1904 ..Document::default()
1905 };
1906 let cyclic_unprojected_parent_pair = Document {
1907 skeleton: Skeleton {
1908 bones: vec![bone(None), bone(Some(0))],
1909 },
1910 assets: complete_projection(vec![
1911 source_node(11, Some(12), Some(1)),
1912 source_node(12, Some(13), None),
1913 source_node(13, Some(12), None),
1914 ]),
1915 ..Document::default()
1916 };
1917 let mismatched_nearest_parent = Document {
1918 skeleton: Skeleton {
1919 bones: vec![bone(None), bone(Some(0))],
1920 },
1921 assets: complete_projection(vec![
1922 source_node(10, None, Some(0)),
1923 source_node(11, None, Some(1)),
1924 ]),
1925 ..Document::default()
1926 };
1927 let unprojected_child = Document {
1928 skeleton: Skeleton {
1929 bones: vec![bone(None), bone(Some(0))],
1930 },
1931 assets: complete_projection(vec![source_node(10, None, Some(0))]),
1932 ..Document::default()
1933 };
1934
1935 let duplicate_track = {
1936 let track = valid_track();
1937 let mut document = track_document(track.clone());
1938 document.clips[0].tracks.push(Track {
1939 property: Property::Scale,
1940 ..valid_track()
1941 });
1942 document.clips[0].tracks.push(track);
1943 document
1944 };
1945 let mut boundary_out_of_range_track = valid_track();
1946 boundary_out_of_range_track.bone = 1;
1947 let mut far_out_of_range_track = valid_track();
1948 far_out_of_range_track.bone = 99;
1949 let empty_track = Track {
1950 times: Vec::new(),
1951 values: TrackValues::Vec3s(Vec::new()),
1952 ..valid_track()
1953 };
1954 let non_finite_later_time = Track {
1955 times: vec![0.0, f32::NAN],
1956 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
1957 ..valid_track()
1958 };
1959 let unordered_times = Track {
1960 times: vec![1.0, 0.0],
1961 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
1962 ..valid_track()
1963 };
1964 let equal_times = Track {
1965 times: vec![0.0, 0.0],
1966 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
1967 ..valid_track()
1968 };
1969 let wrong_linear_value_count = Track {
1970 values: TrackValues::Vec3s(Vec::new()),
1971 ..valid_track()
1972 };
1973 let excess_linear_value_count = Track {
1974 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
1975 ..valid_track()
1976 };
1977 let wrong_step_value_count = Track {
1978 interpolation: Interpolation::Step,
1979 times: vec![0.0, 1.0],
1980 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
1981 ..valid_track()
1982 };
1983 let excess_step_value_count = Track {
1984 interpolation: Interpolation::Step,
1985 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
1986 ..valid_track()
1987 };
1988 let wrong_cubic_value_count = Track {
1989 interpolation: Interpolation::CubicSpline,
1990 times: vec![0.0, 1.0],
1991 values: TrackValues::Vec3s(vec![Vec3::ZERO; 4]),
1992 ..valid_track()
1993 };
1994 let excess_cubic_value_count = Track {
1995 interpolation: Interpolation::CubicSpline,
1996 values: TrackValues::Vec3s(vec![Vec3::ZERO; 4]),
1997 ..valid_track()
1998 };
1999 let wrong_translation_value_type = Track {
2000 values: TrackValues::Quats(vec![Quat::IDENTITY]),
2001 ..valid_track()
2002 };
2003 let wrong_scale_value_type = Track {
2004 property: Property::Scale,
2005 values: TrackValues::Quats(vec![Quat::IDENTITY]),
2006 ..valid_track()
2007 };
2008 let wrong_rotation_value_type = Track {
2009 property: Property::Rotation,
2010 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
2011 ..valid_track()
2012 };
2013 let non_finite_value = Track {
2014 values: TrackValues::Vec3s(vec![Vec3::splat(f32::NAN)]),
2015 ..valid_track()
2016 };
2017
2018 let mut bad_instance_node = instance_document();
2019 bad_instance_node.assets.instances[0].node = 1;
2020 let mut far_instance_node = instance_document();
2021 far_instance_node.assets.instances[0].node = 99;
2022 let mut bad_instance_mesh = instance_document();
2023 bad_instance_mesh.assets.instances[0].mesh = 1;
2024 let mut far_instance_mesh = instance_document();
2025 far_instance_mesh.assets.instances[0].mesh = 99;
2026 let mut bad_instance_joint = instance_document();
2027 bad_instance_joint.assets.instances[0].skin_joints = vec![1];
2028 let mut far_instance_joint = instance_document();
2029 far_instance_joint.assets.instances[0].skin_joints = vec![99];
2030 let mut bad_instance_count = instance_document();
2031 bad_instance_count.assets.instances[0].skin_joints = vec![0];
2032 bad_instance_count.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY, Mat4::IDENTITY];
2033 let mut short_instance_count = instance_document();
2034 short_instance_count.skeleton.bones.push(bone(Some(0)));
2035 short_instance_count.assets.instances[0].skin_joints = vec![0, 1];
2036 short_instance_count.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY];
2037 let mut bad_instance_ibm = instance_document();
2038 bad_instance_ibm.assets.instances[0].skin_joints = vec![0];
2039 bad_instance_ibm.assets.instances[0].skin_ibms =
2040 vec![Mat4::from_cols_array(&[f32::NAN; 16])];
2041 let mut bad_bone_ibm = one_bone_document();
2042 bad_bone_ibm.skeleton.bones[0].inverse_bind = Some(Mat4::from_cols_array(&[f32::NAN; 16]));
2043
2044 let cases = vec![
2045 (
2046 "non-finite rest",
2047 non_finite_rest,
2048 DocumentShapeError::NonFiniteSkeletonRest { node: 0 },
2049 ),
2050 (
2051 "non-finite composed rest world",
2052 overflowed_rest_world,
2053 DocumentShapeError::NonFiniteSkeletonRest { node: 1 },
2054 ),
2055 (
2056 "self parent",
2057 self_parent,
2058 DocumentShapeError::InvalidSkeletonParent { node: 0, parent: 0 },
2059 ),
2060 (
2061 "forward parent",
2062 forward_parent,
2063 DocumentShapeError::InvalidSkeletonParent { node: 0, parent: 1 },
2064 ),
2065 (
2066 "far parent",
2067 far_parent,
2068 DocumentShapeError::InvalidSkeletonParent {
2069 node: 0,
2070 parent: 99,
2071 },
2072 ),
2073 (
2074 "duplicate source node",
2075 duplicate_node,
2076 DocumentShapeError::DuplicateSourceNodeIndex {
2077 source_node_index: 9,
2078 },
2079 ),
2080 (
2081 "duplicate source skin",
2082 duplicate_skin,
2083 DocumentShapeError::DuplicateSourceSkinIndex {
2084 source_skin_index: 4,
2085 },
2086 ),
2087 (
2088 "projected bone range",
2089 out_of_range_projection,
2090 projection_error(10, SourceProjectionViolation::ProjectedBoneOutOfRange),
2091 ),
2092 (
2093 "projection injectivity",
2094 non_injective_projection,
2095 projection_error(
2096 11,
2097 SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
2098 ),
2099 ),
2100 (
2101 "missing projection parent",
2102 missing_projection_parent,
2103 projection_error(11, SourceProjectionViolation::ParentSourceNodeMissing),
2104 ),
2105 (
2106 "missing projection parent at cycle bound",
2107 missing_projection_parent_at_cycle_bound,
2108 projection_error(11, SourceProjectionViolation::ParentSourceNodeMissing),
2109 ),
2110 (
2111 "cyclic projection parent",
2112 cyclic_unprojected_parent,
2113 projection_error(
2114 11,
2115 SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
2116 ),
2117 ),
2118 (
2119 "cyclic projection parent pair",
2120 cyclic_unprojected_parent_pair,
2121 projection_error(
2122 11,
2123 SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
2124 ),
2125 ),
2126 (
2127 "nearest projection parent",
2128 mismatched_nearest_parent,
2129 projection_error(
2130 11,
2131 SourceProjectionViolation::NearestProjectedParentMismatch,
2132 ),
2133 ),
2134 (
2135 "projection downward closure",
2136 unprojected_child,
2137 projection_error(
2138 10,
2139 SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
2140 ),
2141 ),
2142 (
2143 "duplicate track",
2144 duplicate_track,
2145 DocumentShapeError::DuplicateClipTrack {
2146 clip_index: 0,
2147 node: 0,
2148 property: Property::Translation,
2149 },
2150 ),
2151 (
2152 "track bone range boundary",
2153 track_document(boundary_out_of_range_track),
2154 track_error(1, TrackShapeViolation::BoneIndexOutOfRange),
2155 ),
2156 (
2157 "track bone range far",
2158 track_document(far_out_of_range_track),
2159 track_error(99, TrackShapeViolation::BoneIndexOutOfRange),
2160 ),
2161 (
2162 "empty track",
2163 track_document(empty_track),
2164 track_error(0, TrackShapeViolation::EmptyTimes),
2165 ),
2166 (
2167 "non-finite time",
2168 track_document(non_finite_later_time),
2169 track_error(0, TrackShapeViolation::NonFiniteTime),
2170 ),
2171 (
2172 "unordered times",
2173 track_document(unordered_times),
2174 track_error(0, TrackShapeViolation::TimesNotStrictlyIncreasing),
2175 ),
2176 (
2177 "equal times",
2178 track_document(equal_times),
2179 track_error(0, TrackShapeViolation::TimesNotStrictlyIncreasing),
2180 ),
2181 (
2182 "linear value count",
2183 track_document(wrong_linear_value_count),
2184 track_error(0, TrackShapeViolation::ValueCountMismatch),
2185 ),
2186 (
2187 "linear excess value count",
2188 track_document(excess_linear_value_count),
2189 track_error(0, TrackShapeViolation::ValueCountMismatch),
2190 ),
2191 (
2192 "step value count",
2193 track_document(wrong_step_value_count),
2194 track_error(0, TrackShapeViolation::ValueCountMismatch),
2195 ),
2196 (
2197 "step excess value count",
2198 track_document(excess_step_value_count),
2199 track_error(0, TrackShapeViolation::ValueCountMismatch),
2200 ),
2201 (
2202 "cubic value count",
2203 track_document(wrong_cubic_value_count),
2204 track_error(0, TrackShapeViolation::ValueCountMismatch),
2205 ),
2206 (
2207 "cubic excess value count",
2208 track_document(excess_cubic_value_count),
2209 track_error(0, TrackShapeViolation::ValueCountMismatch),
2210 ),
2211 (
2212 "translation value type",
2213 track_document(wrong_translation_value_type),
2214 track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
2215 ),
2216 (
2217 "scale value type",
2218 track_document(wrong_scale_value_type),
2219 track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
2220 ),
2221 (
2222 "rotation value type",
2223 track_document(wrong_rotation_value_type),
2224 track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
2225 ),
2226 (
2227 "non-finite value",
2228 track_document(non_finite_value),
2229 track_error(0, TrackShapeViolation::NonFiniteValue),
2230 ),
2231 (
2232 "instance node boundary",
2233 bad_instance_node,
2234 instance_error(MeshInstanceShapeViolation::NodeIndexOutOfRange),
2235 ),
2236 (
2237 "instance node far",
2238 far_instance_node,
2239 instance_error(MeshInstanceShapeViolation::NodeIndexOutOfRange),
2240 ),
2241 (
2242 "instance mesh boundary",
2243 bad_instance_mesh,
2244 instance_error(MeshInstanceShapeViolation::MeshIndexOutOfRange),
2245 ),
2246 (
2247 "instance mesh far",
2248 far_instance_mesh,
2249 instance_error(MeshInstanceShapeViolation::MeshIndexOutOfRange),
2250 ),
2251 (
2252 "instance joint boundary",
2253 bad_instance_joint,
2254 instance_error(MeshInstanceShapeViolation::SkinJointOutOfRange),
2255 ),
2256 (
2257 "instance joint far",
2258 far_instance_joint,
2259 instance_error(MeshInstanceShapeViolation::SkinJointOutOfRange),
2260 ),
2261 (
2262 "instance ibm count excess",
2263 bad_instance_count,
2264 instance_error(MeshInstanceShapeViolation::SkinInverseBindCountMismatch),
2265 ),
2266 (
2267 "instance ibm count short",
2268 short_instance_count,
2269 instance_error(MeshInstanceShapeViolation::SkinInverseBindCountMismatch),
2270 ),
2271 (
2272 "instance ibm finite",
2273 bad_instance_ibm,
2274 instance_error(MeshInstanceShapeViolation::NonFiniteSkinInverseBind),
2275 ),
2276 (
2277 "bone ibm finite",
2278 bad_bone_ibm,
2279 DocumentShapeError::NonFiniteBoneInverseBind { node: 0 },
2280 ),
2281 ];
2282 for (name, document, expected) in cases {
2283 assert_eq!(validate_document_shape(&document), Err(expected), "{name}");
2284 }
2285 }
2286
2287 #[test]
2288 fn document_shape_finiteness_checks_every_stored_component() {
2289 for component in 0..3 {
2290 let mut translation = Vec3::ZERO.to_array();
2291 translation[component] = f32::NAN;
2292 let mut document = one_bone_document();
2293 document.skeleton.bones[0].rest.translation = Vec3::from_array(translation);
2294 assert_eq!(
2295 validate_document_shape(&document),
2296 Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
2297 "rest translation component {component}"
2298 );
2299
2300 let mut scale = Vec3::ONE.to_array();
2301 scale[component] = f32::NAN;
2302 let mut document = one_bone_document();
2303 document.skeleton.bones[0].rest.scale = Vec3::from_array(scale);
2304 assert_eq!(
2305 validate_document_shape(&document),
2306 Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
2307 "rest scale component {component}"
2308 );
2309
2310 let mut value = Vec3::ZERO.to_array();
2311 value[component] = f32::NAN;
2312 let document = track_document(Track {
2313 values: TrackValues::Vec3s(vec![Vec3::from_array(value)]),
2314 ..valid_track()
2315 });
2316 assert_eq!(
2317 validate_document_shape(&document),
2318 Err(DocumentShapeError::TrackShape {
2319 clip_index: 0,
2320 node: 0,
2321 violation: TrackShapeViolation::NonFiniteValue,
2322 }),
2323 "track Vec3 component {component}"
2324 );
2325 }
2326
2327 for component in 0..4 {
2328 let mut rotation = Quat::IDENTITY.to_array();
2329 rotation[component] = f32::NAN;
2330 let mut document = one_bone_document();
2331 document.skeleton.bones[0].rest.rotation = Quat::from_array(rotation);
2332 assert_eq!(
2333 validate_document_shape(&document),
2334 Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
2335 "rest rotation component {component}"
2336 );
2337
2338 let document = track_document(Track {
2339 property: Property::Rotation,
2340 values: TrackValues::Quats(vec![Quat::from_array(rotation)]),
2341 ..valid_track()
2342 });
2343 assert_eq!(
2344 validate_document_shape(&document),
2345 Err(DocumentShapeError::TrackShape {
2346 clip_index: 0,
2347 node: 0,
2348 violation: TrackShapeViolation::NonFiniteValue,
2349 }),
2350 "track quaternion component {component}"
2351 );
2352 }
2353
2354 for key in 0..3 {
2355 let mut times = vec![0.0, 1.0, 2.0];
2356 times[key] = f32::NAN;
2357 let document = track_document(Track {
2358 times,
2359 values: TrackValues::Vec3s(vec![Vec3::ZERO; 3]),
2360 ..valid_track()
2361 });
2362 assert_eq!(
2363 validate_document_shape(&document),
2364 Err(DocumentShapeError::TrackShape {
2365 clip_index: 0,
2366 node: 0,
2367 violation: TrackShapeViolation::NonFiniteTime,
2368 }),
2369 "track time {key}"
2370 );
2371 }
2372
2373 for component in 0..16 {
2374 let mut columns = Mat4::IDENTITY.to_cols_array();
2375 columns[component] = f32::NAN;
2376 let inverse_bind = Mat4::from_cols_array(&columns);
2377
2378 let mut instance_document = instance_document();
2379 instance_document.assets.instances[0].skin_joints = vec![0];
2380 instance_document.assets.instances[0].skin_ibms = vec![inverse_bind];
2381 assert_eq!(
2382 validate_document_shape(&instance_document),
2383 Err(DocumentShapeError::MeshInstanceShape {
2384 instance_index: 0,
2385 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2386 }),
2387 "instance inverse-bind component {component}"
2388 );
2389
2390 let mut bone_document = one_bone_document();
2391 bone_document.skeleton.bones[0].inverse_bind = Some(inverse_bind);
2392 assert_eq!(
2393 validate_document_shape(&bone_document),
2394 Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 0 }),
2395 "bone inverse-bind component {component}"
2396 );
2397 }
2398 }
2399
2400 #[test]
2401 fn document_shape_rejects_duplicate_tracks_for_every_property() {
2402 let tracks = [
2403 (Property::Translation, TrackValues::Vec3s(vec![Vec3::ZERO])),
2404 (Property::Scale, TrackValues::Vec3s(vec![Vec3::ONE])),
2405 (Property::Rotation, TrackValues::Quats(vec![Quat::IDENTITY])),
2406 ];
2407
2408 for (property, values) in tracks {
2409 let track = Track {
2410 property,
2411 values,
2412 ..valid_track()
2413 };
2414 let mut document = track_document(track.clone());
2415 document.clips[0].tracks.push(track);
2416
2417 assert_eq!(
2418 validate_document_shape(&document),
2419 Err(DocumentShapeError::DuplicateClipTrack {
2420 clip_index: 0,
2421 node: 0,
2422 property,
2423 }),
2424 "duplicate {property:?} track"
2425 );
2426 }
2427 }
2428
2429 #[test]
2430 fn document_shape_rejects_infinite_times_quaternions_and_inverse_binds() {
2431 for non_finite in [f32::INFINITY, f32::NEG_INFINITY] {
2432 let document = track_document(Track {
2433 times: vec![non_finite],
2434 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
2435 ..valid_track()
2436 });
2437 assert_eq!(
2438 validate_document_shape(&document),
2439 Err(DocumentShapeError::TrackShape {
2440 clip_index: 0,
2441 node: 0,
2442 violation: TrackShapeViolation::NonFiniteTime,
2443 }),
2444 "track time {non_finite}"
2445 );
2446
2447 let document = track_document(Track {
2448 property: Property::Rotation,
2449 values: TrackValues::Quats(vec![Quat::from_xyzw(non_finite, 0.0, 0.0, 1.0)]),
2450 ..valid_track()
2451 });
2452 assert_eq!(
2453 validate_document_shape(&document),
2454 Err(DocumentShapeError::TrackShape {
2455 clip_index: 0,
2456 node: 0,
2457 violation: TrackShapeViolation::NonFiniteValue,
2458 }),
2459 "track quaternion {non_finite}"
2460 );
2461
2462 let mut columns = Mat4::IDENTITY.to_cols_array();
2463 columns[0] = non_finite;
2464 let inverse_bind = Mat4::from_cols_array(&columns);
2465 let mut document = instance_document();
2466 document.assets.instances[0].skin_joints = vec![0];
2467 document.assets.instances[0].skin_ibms = vec![inverse_bind];
2468 assert_eq!(
2469 validate_document_shape(&document),
2470 Err(DocumentShapeError::MeshInstanceShape {
2471 instance_index: 0,
2472 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2473 }),
2474 "instance inverse bind {non_finite}"
2475 );
2476
2477 let mut document = one_bone_document();
2478 document.skeleton.bones[0].inverse_bind = Some(inverse_bind);
2479 assert_eq!(
2480 validate_document_shape(&document),
2481 Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 0 }),
2482 "bone inverse bind {non_finite}"
2483 );
2484 }
2485 }
2486
2487 #[test]
2488 fn document_shape_checks_mesh_and_joint_references_on_later_instances() {
2489 let later_instance = MeshInstance {
2490 node: 0,
2491 mesh: 0,
2492 ..MeshInstance::default()
2493 };
2494
2495 let mut document = instance_document();
2496 document.assets.instances.push(later_instance.clone());
2497 document.assets.instances[1].mesh = 1;
2498 assert_eq!(
2499 validate_document_shape(&document),
2500 Err(DocumentShapeError::MeshInstanceShape {
2501 instance_index: 1,
2502 violation: MeshInstanceShapeViolation::MeshIndexOutOfRange,
2503 })
2504 );
2505
2506 let mut document = instance_document();
2507 document.assets.instances.push(later_instance);
2508 document.assets.instances[1].skin_joints = vec![1];
2509 assert_eq!(
2510 validate_document_shape(&document),
2511 Err(DocumentShapeError::MeshInstanceShape {
2512 instance_index: 1,
2513 violation: MeshInstanceShapeViolation::SkinJointOutOfRange,
2514 })
2515 );
2516 }
2517
2518 #[test]
2519 fn document_shape_finds_duplicates_that_do_not_involve_the_first_item() {
2520 let mut document = Document::default();
2521 document.assets.source_skeleton.skins = [4, 5, 5]
2522 .into_iter()
2523 .map(|source_skin_index| SourceSkinAsset {
2524 source_skin_index,
2525 ..SourceSkinAsset::default()
2526 })
2527 .collect();
2528 assert_eq!(
2529 validate_document_shape(&document),
2530 Err(DocumentShapeError::DuplicateSourceSkinIndex {
2531 source_skin_index: 5,
2532 })
2533 );
2534
2535 let scale_track = Track {
2536 property: Property::Scale,
2537 values: TrackValues::Vec3s(vec![Vec3::ONE]),
2538 ..valid_track()
2539 };
2540 let mut document = track_document(valid_track());
2541 document.clips[0].tracks.push(scale_track.clone());
2542 document.clips[0].tracks.push(scale_track);
2543 assert_eq!(
2544 validate_document_shape(&document),
2545 Err(DocumentShapeError::DuplicateClipTrack {
2546 clip_index: 0,
2547 node: 0,
2548 property: Property::Scale,
2549 })
2550 );
2551 }
2552
2553 #[test]
2554 fn document_shape_checks_later_tracks_and_inverse_binds() {
2555 let mut document = track_document(valid_track());
2556 document.clips[0].tracks.push(Track {
2557 property: Property::Scale,
2558 times: Vec::new(),
2559 values: TrackValues::Vec3s(Vec::new()),
2560 ..valid_track()
2561 });
2562 assert_eq!(
2563 validate_document_shape(&document),
2564 Err(DocumentShapeError::TrackShape {
2565 clip_index: 0,
2566 node: 0,
2567 violation: TrackShapeViolation::EmptyTimes,
2568 })
2569 );
2570
2571 let scale_track = Track {
2572 property: Property::Scale,
2573 values: TrackValues::Vec3s(vec![Vec3::ONE]),
2574 ..valid_track()
2575 };
2576 let mut document = track_document(valid_track());
2577 document.clips.push(Clip {
2578 name: "later".into(),
2579 duration_s: 0.0,
2580 tracks: vec![scale_track.clone(), scale_track],
2581 });
2582 assert_eq!(
2583 validate_document_shape(&document),
2584 Err(DocumentShapeError::DuplicateClipTrack {
2585 clip_index: 1,
2586 node: 0,
2587 property: Property::Scale,
2588 })
2589 );
2590
2591 let mut document = track_document(valid_track());
2592 document.clips.push(Clip {
2593 name: "later".into(),
2594 duration_s: 0.0,
2595 tracks: vec![Track {
2596 property: Property::Scale,
2597 times: Vec::new(),
2598 values: TrackValues::Vec3s(Vec::new()),
2599 ..valid_track()
2600 }],
2601 });
2602 assert_eq!(
2603 validate_document_shape(&document),
2604 Err(DocumentShapeError::TrackShape {
2605 clip_index: 1,
2606 node: 0,
2607 violation: TrackShapeViolation::EmptyTimes,
2608 })
2609 );
2610
2611 let mut columns = Mat4::IDENTITY.to_cols_array();
2612 columns[15] = f32::NAN;
2613 let non_finite_inverse_bind = Mat4::from_cols_array(&columns);
2614
2615 let mut document = instance_document();
2616 document.skeleton.bones.push(bone(Some(0)));
2617 document.assets.instances[0].skin_joints = vec![0, 1];
2618 document.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY, non_finite_inverse_bind];
2619 assert_eq!(
2620 validate_document_shape(&document),
2621 Err(DocumentShapeError::MeshInstanceShape {
2622 instance_index: 0,
2623 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2624 })
2625 );
2626
2627 let mut document = instance_document();
2628 document.assets.instances.push(MeshInstance {
2629 node: 0,
2630 mesh: 0,
2631 skin_joints: vec![0],
2632 skin_ibms: vec![non_finite_inverse_bind],
2633 ..MeshInstance::default()
2634 });
2635 assert_eq!(
2636 validate_document_shape(&document),
2637 Err(DocumentShapeError::MeshInstanceShape {
2638 instance_index: 1,
2639 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2640 })
2641 );
2642
2643 let mut document = instance_document();
2644 document.assets.instances.push(MeshInstance {
2645 node: 0,
2646 mesh: 0,
2647 skin_joints: vec![0],
2648 skin_ibms: vec![Mat4::IDENTITY, Mat4::IDENTITY],
2649 ..MeshInstance::default()
2650 });
2651 assert_eq!(
2652 validate_document_shape(&document),
2653 Err(DocumentShapeError::MeshInstanceShape {
2654 instance_index: 1,
2655 violation: MeshInstanceShapeViolation::SkinInverseBindCountMismatch,
2656 })
2657 );
2658
2659 let mut document = one_bone_document();
2660 document.skeleton.bones.push(Bone {
2661 inverse_bind: Some(non_finite_inverse_bind),
2662 ..bone(Some(0))
2663 });
2664 assert_eq!(
2665 validate_document_shape(&document),
2666 Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 1 })
2667 );
2668 }
2669
2670 #[test]
2671 fn document_shape_violation_names_remain_machine_stable() {
2672 let source_projection = [
2673 (
2674 SourceProjectionViolation::ProjectedBoneOutOfRange,
2675 "projected_bone_out_of_range",
2676 ),
2677 (
2678 SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
2679 "two_source_nodes_project_to_one_bone",
2680 ),
2681 (
2682 SourceProjectionViolation::ParentSourceNodeMissing,
2683 "parent_source_node_is_missing",
2684 ),
2685 (
2686 SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
2687 "cyclic_unprojected_source_parent_chain",
2688 ),
2689 (
2690 SourceProjectionViolation::NearestProjectedParentMismatch,
2691 "projection_and_skeleton_parents_differ",
2692 ),
2693 (
2694 SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
2695 "projected_bone_has_an_unprojected_skeleton_child",
2696 ),
2697 ];
2698 for (violation, expected) in source_projection {
2699 assert_eq!(violation.to_string(), expected);
2700 }
2701
2702 let track = [
2703 (
2704 TrackShapeViolation::BoneIndexOutOfRange,
2705 "bone_index_out_of_range",
2706 ),
2707 (TrackShapeViolation::EmptyTimes, "empty_times"),
2708 (TrackShapeViolation::NonFiniteTime, "non_finite_time"),
2709 (
2710 TrackShapeViolation::TimesNotStrictlyIncreasing,
2711 "times_not_strictly_increasing",
2712 ),
2713 (
2714 TrackShapeViolation::ValueCountMismatch,
2715 "value_count_mismatch",
2716 ),
2717 (
2718 TrackShapeViolation::ValueTypeMismatchesProperty,
2719 "value_type_mismatches_property",
2720 ),
2721 (TrackShapeViolation::NonFiniteValue, "non_finite_value"),
2722 ];
2723 for (violation, expected) in track {
2724 assert_eq!(violation.to_string(), expected);
2725 }
2726
2727 let instance = [
2728 (
2729 MeshInstanceShapeViolation::NodeIndexOutOfRange,
2730 "node_index_out_of_range",
2731 ),
2732 (
2733 MeshInstanceShapeViolation::MeshIndexOutOfRange,
2734 "mesh_index_out_of_range",
2735 ),
2736 (
2737 MeshInstanceShapeViolation::SkinJointOutOfRange,
2738 "skin_joint_out_of_range",
2739 ),
2740 (
2741 MeshInstanceShapeViolation::SkinInverseBindCountMismatch,
2742 "skin_ibm_count_mismatch",
2743 ),
2744 (
2745 MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2746 "non_finite_inverse_bind",
2747 ),
2748 ];
2749 for (violation, expected) in instance {
2750 assert_eq!(violation.to_string(), expected);
2751 }
2752 }
2753
2754 #[test]
2755 fn tolerant_world_rests_keep_unrelated_partial_evidence() {
2756 let skeleton = Skeleton {
2757 bones: vec![
2758 bone(None),
2759 bone(Some(99)),
2760 Bone {
2761 rest: Transform {
2762 translation: Vec3::X,
2763 ..Transform::IDENTITY
2764 },
2765 ..bone(None)
2766 },
2767 Bone {
2768 rest: Transform {
2769 translation: Vec3::Y,
2770 ..Transform::IDENTITY
2771 },
2772 ..bone(Some(2))
2773 },
2774 bone(Some(1)),
2775 ],
2776 };
2777
2778 let worlds = tolerant_world_rest_matrices(&skeleton);
2779 assert_eq!(worlds.len(), 5);
2780 assert_eq!(worlds[0], Some(Mat4::IDENTITY));
2781 assert_eq!(worlds[1], None, "the malformed parent is unavailable");
2782 assert_eq!(worlds[2], Some(Mat4::from_translation(Vec3::X)));
2783 assert_eq!(
2784 worlds[3],
2785 Some(Mat4::from_translation(Vec3::new(1.0, 1.0, 0.0))),
2786 "a finite independent chain remains measurable"
2787 );
2788 assert_eq!(
2789 worlds[4], None,
2790 "a child of unavailable evidence is unavailable"
2791 );
2792 }
2793
2794 #[test]
2795 fn shared_affine_classifier_respects_distinct_caller_tolerances() {
2796 let equal_axis_basis = affine_test_fixtures::tolerance_divergence_basis();
2797 let strict = PositiveUniformAffineTolerance {
2798 equal_axis: 1.0e-5,
2799 relative_orthogonality: 1.0e-5,
2800 singular_determinant_relative: 1.0e-6,
2801 };
2802 let loose = PositiveUniformAffineTolerance {
2803 equal_axis: 1.0e-4,
2804 relative_orthogonality: 1.0e-4,
2805 singular_determinant_relative: 0.0,
2806 };
2807
2808 assert_eq!(
2809 classify_positive_uniform_affine(equal_axis_basis, strict),
2810 Err(AffineDomainViolation::NonUniformScale),
2811 "the stricter caller rejects this equal-axis difference"
2812 );
2813 assert!(
2814 classify_positive_uniform_affine(equal_axis_basis, loose).is_ok(),
2815 "the looser caller accepts this equal-axis difference"
2816 );
2817
2818 let orthogonality_basis = affine_test_fixtures::orthogonality_tolerance_divergence_basis();
2819 assert_eq!(
2820 classify_positive_uniform_affine(orthogonality_basis, strict),
2821 Err(AffineDomainViolation::Sheared),
2822 "the stricter caller rejects this cross-axis dot product"
2823 );
2824 assert!(
2825 classify_positive_uniform_affine(orthogonality_basis, loose).is_ok(),
2826 "the looser caller accepts this cross-axis dot product"
2827 );
2828 }
2829
2830 #[test]
2831 fn shared_affine_classifier_pins_its_symmetric_f64_formula() {
2832 let policy = PositiveUniformAffineTolerance {
2833 equal_axis: 1.0e-5,
2834 relative_orthogonality: 1.0e-5,
2835 singular_determinant_relative: 1.0e-6,
2836 };
2837
2838 let on_long_edge = Mat3::from_diagonal(Vec3::new(99_998.5, 99_998.5, 100_000.0));
2843 assert_eq!(
2844 classify_positive_uniform_affine(on_long_edge, policy),
2845 Ok(99_999.0)
2846 );
2847 let short = 99_998.5;
2848 let long = 100_000.0 + 0.007_812_5;
2849 for diagonal in [
2850 Vec3::new(long, short, short),
2851 Vec3::new(short, long, short),
2852 Vec3::new(short, short, long),
2853 ] {
2854 assert_eq!(
2855 classify_positive_uniform_affine(Mat3::from_diagonal(diagonal), policy),
2856 Err(AffineDomainViolation::NonUniformScale)
2857 );
2858 }
2859
2860 let short = 1.0 - 2.0_f32.powi(-16);
2864 for diagonal in [
2865 Vec3::new(short, 1.0, 1.0),
2866 Vec3::new(1.0, short, 1.0),
2867 Vec3::new(1.0, 1.0, short),
2868 ] {
2869 assert_eq!(
2870 classify_positive_uniform_affine(Mat3::from_diagonal(diagonal), policy),
2871 Err(AffineDomainViolation::NonUniformScale)
2872 );
2873 }
2874
2875 let c0 = Vec3::new(0.12792248, -0.99066633, -0.047073245);
2878 let c1 = Vec3::new(-0.34637994, -0.00016034879, -0.93809813);
2879 let c2 = Vec3::new(0.92933476, 0.13630849, -0.3431568);
2880 assert!((c1.dot(c2) as f64).abs() < 1.0e-5);
2881 assert!(c1.as_dvec3().dot(c2.as_dvec3()).abs() > 1.0e-5);
2882 assert_eq!(
2883 classify_positive_uniform_affine(Mat3::from_cols(c0, c1, c2), policy),
2884 Err(AffineDomainViolation::Sheared)
2885 );
2886
2887 for shear in [2.0_f32.powi(-15), -2.0_f32.powi(-15)] {
2890 let basis = Mat3::from_cols(Vec3::X, Vec3::new(shear, 1.0, 0.0), Vec3::Z);
2891 assert_eq!(
2892 classify_positive_uniform_affine(basis, policy),
2893 Err(AffineDomainViolation::Sheared)
2894 );
2895 }
2896 }
2897
2898 #[test]
2899 fn affine_axis_mean_is_ascending_and_column_order_invariant() {
2900 let lengths = [
2905 f64::from_bits(0x3ff1_09e7_e000_022c),
2906 f64::from_bits(0x3ff1_09ec_6000_0eb5),
2907 f64::from_bits(0x3ff1_09fa_e000_3cde),
2908 ];
2909 let expected = f64::from_bits(0x3ff1_09ef_b555_6f3f);
2910 let ascending = (lengths[0] + lengths[1] + lengths[2]) / 3.0;
2911 let descending = (lengths[2] + lengths[1] + lengths[0]) / 3.0;
2912 assert_eq!(expected.to_bits(), 0x3ff1_09ef_b555_6f3f);
2913 assert_eq!(ascending.to_bits(), expected.to_bits());
2914 assert_eq!(descending.to_bits(), 0x3ff1_09ef_b555_6f40);
2915 for order in [
2916 [0, 1, 2],
2917 [0, 2, 1],
2918 [1, 0, 2],
2919 [1, 2, 0],
2920 [2, 0, 1],
2921 [2, 1, 0],
2922 ] {
2923 assert_eq!(
2924 average_affine_axis_length(order.map(|index| lengths[index])),
2925 expected,
2926 "axis order {order:?}"
2927 );
2928 }
2929
2930 let dyadic = [2.0_f64.powi(53), 1.0, 1.0];
2935 let ascending = (dyadic[1] + dyadic[2] + dyadic[0]) / 3.0;
2936 let descending = (dyadic[0] + dyadic[1] + dyadic[2]) / 3.0;
2937 assert_ne!(ascending, descending);
2938 assert_eq!(average_affine_axis_length(dyadic), ascending);
2939
2940 let permutations = affine_test_fixtures::appendix_d_v6_mean_permutations();
2946 let expected_length_bits = lengths.map(f64::to_bits);
2947 assert_eq!(
2948 affine_axis_lengths(permutations[0]).map(f64::to_bits),
2949 expected_length_bits
2950 );
2951 let tolerance = PositiveUniformAffineTolerance {
2952 equal_axis: 1.0e-5,
2953 relative_orthogonality: 1.0e-5,
2954 singular_determinant_relative: 1.0e-6,
2955 };
2956 for (permutation, linear) in permutations.into_iter().enumerate() {
2957 assert!(
2958 linear
2959 .x_axis
2960 .as_dvec3()
2961 .cross(linear.y_axis.as_dvec3())
2962 .dot(linear.z_axis.as_dvec3())
2963 > 0.0,
2964 "orientation for permutation {permutation}"
2965 );
2966 assert_eq!(
2967 average_affine_axis_length(affine_axis_lengths(linear)).to_bits(),
2968 expected.to_bits(),
2969 "mean for permutation {permutation}"
2970 );
2971 assert_eq!(
2972 classify_positive_uniform_affine(linear, tolerance),
2973 Err(AffineDomainViolation::NonUniformScale),
2974 "classification for permutation {permutation}"
2975 );
2976 }
2977 }
2978
2979 #[test]
2980 fn shared_affine_classifier_pins_f64_determinant_arithmetic() {
2981 let linear = Mat3::from_cols(
2986 Vec3::new(
2987 f32::from_bits(0x3ff3_5574),
2988 f32::from_bits(0x3f0e_fa3c),
2989 0.0,
2990 ),
2991 Vec3::new(
2992 f32::from_bits(0x3ff5_5e17),
2993 f32::from_bits(0x3f10_2c31),
2994 0.0,
2995 ),
2996 Vec3::Z,
2997 );
2998 let columns = [
2999 linear.x_axis.as_dvec3(),
3000 linear.y_axis.as_dvec3(),
3001 linear.z_axis.as_dvec3(),
3002 ];
3003 let determinant_f64 = columns[2].dot(columns[0].cross(columns[1]));
3004 let determinant_f32 = f64::from(linear.determinant());
3005 let lengths = affine_axis_lengths(linear);
3006 let threshold = (determinant_f64 + determinant_f32) / 2.0;
3007 assert!(determinant_f64 < threshold);
3008 assert!(determinant_f32 > threshold);
3009
3010 assert_eq!(
3011 classify_positive_uniform_affine(
3012 linear,
3013 PositiveUniformAffineTolerance {
3014 equal_axis: 10.0,
3015 relative_orthogonality: 10.0,
3016 singular_determinant_relative: threshold
3017 / (lengths[0] * lengths[1] * lengths[2]),
3018 },
3019 ),
3020 Err(AffineDomainViolation::Singular)
3021 );
3022
3023 let large_uniform = 2.0e19_f32;
3028 assert_eq!(
3029 classify_positive_uniform_affine(
3030 Mat3::from_diagonal(Vec3::splat(large_uniform)),
3031 PositiveUniformAffineTolerance {
3032 equal_axis: 1.0e-5,
3033 relative_orthogonality: 1.0e-5,
3034 singular_determinant_relative: 1.0e-6,
3035 },
3036 ),
3037 Ok(f64::from(large_uniform))
3038 );
3039 }
3040
3041 #[test]
3042 fn affine_geometry_facts_pin_every_widened_field_and_slot() {
3043 let linear = Mat3::from_cols(
3044 Vec3::new(1.0, 2.0, 3.0),
3045 Vec3::new(4.0, 5.0, 6.0),
3046 Vec3::new(7.0, 8.0, 10.0),
3047 );
3048
3049 let facts = AffineGeometryFacts::from_linear(linear).expect("finite widened facts");
3050 assert_eq!(
3051 facts.axis_lengths.map(f64::to_bits),
3052 [
3053 0x400d_eeea_1168_3f49,
3054 0x4021_8cc8_21d6_d3e3,
3055 0x402d_3064_dcc8_ae67,
3056 ]
3057 );
3058 assert_eq!(facts.mean_axis_length.to_bits(), 0x4022_12f7_d653_30b4);
3059 assert_eq!(facts.determinant.to_bits(), 0xc008_0000_0000_0000);
3060 assert_eq!(facts.axis_length_product.to_bits(), 0x407d_f2e3_88f2_1b01);
3061 assert_eq!(
3062 facts.cross_axis_dots.map(f64::to_bits),
3063 [
3064 0x4040_0000_0000_0000,
3065 0x404a_8000_0000_0000,
3066 0x4060_0000_0000_0000,
3067 ],
3068 "cross-axis slots are XY, XZ, YZ"
3069 );
3070 }
3071
3072 #[test]
3073 fn affine_geometry_facts_widen_every_dot_product_before_multiplying() {
3074 let x = Vec3::new(
3075 f32::from_bits(0x3ff3_5574),
3076 f32::from_bits(0x3f0e_fa3c),
3077 0.0,
3078 );
3079 let y = Vec3::new(
3080 f32::from_bits(0x3ff5_5e17),
3081 f32::from_bits(0x3f10_2c31),
3082 0.0,
3083 );
3084 let widened_dot = x.as_dvec3().dot(y.as_dvec3());
3085 let f32_then_widened = f64::from(x.dot(y));
3086
3087 for (slot, linear) in [
3088 (0, Mat3::from_cols(x, y, Vec3::Z)),
3089 (1, Mat3::from_cols(x, Vec3::Z, y)),
3090 (2, Mat3::from_cols(Vec3::Z, x, y)),
3091 ] {
3092 let facts = AffineGeometryFacts::from_linear(linear).expect("finite widened facts");
3093 assert_eq!(facts.cross_axis_dots[slot], widened_dot);
3094 assert_ne!(
3095 facts.cross_axis_dots[slot], f32_then_widened,
3096 "dot slot {slot} must multiply and add in f64, not widen an f32 result"
3097 );
3098 }
3099 }
3100
3101 #[test]
3102 fn weld_preserves_uv_seams_at_shared_positions() {
3103 let mut primitive = Primitive {
3104 positions: vec![Vec3::ZERO, Vec3::ZERO, Vec3::ZERO],
3105 uvs: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 0.0]],
3106 ..Primitive::default()
3107 };
3108
3109 primitive.weld();
3110
3111 assert_eq!(primitive.positions.len(), 2);
3112 let reconstructed_corners = primitive
3113 .indices
3114 .iter()
3115 .map(|&index| {
3116 let index = index as usize;
3117 (primitive.positions[index], primitive.uvs[index])
3118 })
3119 .collect::<Vec<_>>();
3120 assert_eq!(
3121 reconstructed_corners,
3122 vec![
3123 (Vec3::ZERO, [0.0, 0.0]),
3124 (Vec3::ZERO, [1.0, 0.0]),
3125 (Vec3::ZERO, [0.0, 0.0]),
3126 ]
3127 );
3128 }
3129}