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