1use crate::model::{
9 Bone, Document, MeshAsset, MeshInstance, Primitive, SceneAsset, SceneAssets, Skeleton,
10 SourceSkeletonAssets, Transform, WorldMatrixError, world_rest_matrices,
11};
12use glam::{Mat3, Mat4};
13use serde::Serialize;
14use std::collections::HashSet;
15
16const MIN_RELATIVE_DETERMINANT: f32 = 1.0e-6;
19
20#[derive(Debug, Clone)]
22#[non_exhaustive]
23pub struct StaticMeshBake {
24 pub document: Document,
27 pub evidence: StaticMeshBakeEvidence,
29}
30
31#[derive(Debug, Clone, Serialize)]
33#[non_exhaustive]
34pub struct StaticMeshBakeEvidence {
35 pub entries: Vec<StaticMeshBakeInstanceEvidence>,
37}
38
39#[derive(Debug, Clone, Serialize)]
41#[non_exhaustive]
42pub struct StaticMeshBakeInstanceEvidence {
43 pub source_node_index: usize,
45 pub source_node_name: String,
47 pub source_mesh_ordinal: usize,
49 pub source_mesh_index: usize,
51 pub source_mesh_name: String,
53 pub output_node_index: usize,
55 pub output_mesh_index: usize,
57 pub world_transform: [f32; 16],
59 pub linear_determinant: f32,
61 pub primitive_count: usize,
63 pub position_count: usize,
65 pub normal_count: usize,
67}
68
69#[derive(Debug, thiserror::Error)]
71#[non_exhaustive]
72pub enum StaticMeshBakeError {
73 #[error("cannot bake static transforms: document has no mesh instances")]
75 NoInstances,
76 #[error(
78 "cannot bake static transforms: mesh {mesh} (source mesh {source_mesh_index}) has {instances} instances; each mesh definition must have exactly one"
79 )]
80 MeshInstanceCount {
81 mesh: usize,
83 source_mesh_index: usize,
85 instances: usize,
87 },
88 #[error(
90 "cannot bake static transforms: source node {source_node_index} references missing mesh {mesh}"
91 )]
92 MissingMesh {
93 source_node_index: usize,
95 mesh: usize,
97 },
98 #[error(
100 "cannot bake static transforms: source node {source_node_index} maps to missing skeleton node {node}"
101 )]
102 MissingNode {
103 source_node_index: usize,
105 node: usize,
107 },
108 #[error(
110 "cannot bake static transforms: source node {source_node_index} has duplicate mesh attachments"
111 )]
112 DuplicateNodeAttachment {
113 source_node_index: usize,
115 },
116 #[error("cannot bake static transforms: skeleton node {node} has duplicate mesh attachments")]
118 DuplicateSkeletonNodeAttachment {
119 node: usize,
121 },
122 #[error(
124 "cannot bake static transforms: clip {clip:?} animates skeleton node {node} ({property})"
125 )]
126 AnimationTrack {
127 clip: String,
129 node: usize,
131 property: &'static str,
133 },
134 #[error("cannot bake static transforms: skeleton node {node} carries an inverse bind matrix")]
136 SkeletonSkin {
137 node: usize,
139 },
140 #[error("cannot bake static transforms: source node {source_node_index} is skinned")]
142 SkinnedInstance {
143 source_node_index: usize,
145 },
146 #[error(
148 "cannot bake static transforms: mesh {mesh} primitive {primitive} carries skin attributes"
149 )]
150 SkinnedPrimitive {
151 mesh: usize,
153 primitive: usize,
155 },
156 #[error("cannot bake static transforms: skeleton node {node} has invalid parent {parent}")]
158 InvalidParent {
159 node: usize,
161 parent: usize,
163 },
164 #[error("cannot bake static transforms: skeleton node {node} has a non-finite transform")]
166 NonFiniteTransform {
167 node: usize,
169 },
170 #[error(
172 "cannot bake static transforms: mesh {mesh} primitive {primitive} is invalid: {reason}"
173 )]
174 InvalidPrimitive {
175 mesh: usize,
177 primitive: usize,
179 reason: &'static str,
181 },
182 #[error("cannot bake static transforms: material {material} has non-finite {factor}")]
184 NonFiniteMaterial {
185 material: usize,
187 factor: &'static str,
189 },
190 #[error(
192 "cannot bake static transforms: mesh {mesh} primitive {primitive} has non-finite {attribute} at vertex {vertex}"
193 )]
194 NonFiniteAttribute {
195 mesh: usize,
197 primitive: usize,
199 attribute: &'static str,
201 vertex: usize,
203 },
204 #[error(
206 "cannot bake static transforms: mesh {mesh} primitive {primitive} has zero normal at vertex {vertex}"
207 )]
208 ZeroNormal {
209 mesh: usize,
211 primitive: usize,
213 vertex: usize,
215 },
216 #[error(
218 "cannot bake static transforms: source node {source_node_index} has singular or near-singular transform (determinant {determinant})"
219 )]
220 SingularTransform {
221 source_node_index: usize,
223 determinant: f32,
225 },
226 #[error(
228 "cannot bake static transforms: source node {source_node_index} has reflection transform (determinant {determinant})"
229 )]
230 ReflectionTransform {
231 source_node_index: usize,
233 determinant: f32,
235 },
236}
237
238#[derive(Debug, Clone, Copy)]
239struct BakePlan {
240 instance: usize,
241 mesh: usize,
242 node: usize,
243 world: Mat4,
244 normal: Mat3,
245 determinant: f32,
246}
247
248pub fn bake_static_mesh_transforms(doc: &Document) -> Result<StaticMeshBake, StaticMeshBakeError> {
268 let plans = validate(doc)?;
269 let mut meshes = Vec::with_capacity(plans.len());
270 let mut instances = Vec::with_capacity(plans.len());
271 let mut nodes = Vec::with_capacity(plans.len() + 1);
272 let mut evidence = Vec::with_capacity(plans.len());
273
274 nodes.push(Bone {
275 name: "animsmith-static-root".to_string(),
276 parent: None,
277 rest: Transform::IDENTITY,
278 inverse_bind: None,
279 });
280
281 for (ordinal, plan) in plans.iter().enumerate() {
282 let input_mesh = &doc.assets.meshes[plan.mesh];
283 let input_instance = &doc.assets.instances[plan.instance];
284 let output_node = ordinal + 1;
285 let output_mesh = ordinal;
286 let baked_mesh = bake_mesh(input_mesh, plan, plan.mesh)?;
287 let position_count = baked_mesh
288 .primitives
289 .iter()
290 .map(|primitive| primitive.positions.len())
291 .sum();
292 let normal_count = baked_mesh
293 .primitives
294 .iter()
295 .map(|primitive| primitive.normals.len())
296 .sum();
297
298 nodes.push(Bone {
299 name: format!("animsmith-static-mesh-{ordinal}"),
300 parent: Some(0),
301 rest: Transform::IDENTITY,
302 inverse_bind: None,
303 });
304 meshes.push(baked_mesh);
305 instances.push(MeshInstance {
306 source_node_index: input_instance.source_node_index,
307 node: output_node,
308 mesh: output_mesh,
309 skin_joints: Vec::new(),
310 skin_ibms: Vec::new(),
311 });
312 evidence.push(StaticMeshBakeInstanceEvidence {
313 source_node_index: input_instance.source_node_index,
314 source_node_name: doc.skeleton.bones[plan.node].name.clone(),
315 source_mesh_ordinal: plan.mesh,
316 source_mesh_index: input_mesh.source_mesh_index,
317 source_mesh_name: input_mesh.name.clone(),
318 output_node_index: output_node,
319 output_mesh_index: output_mesh,
320 world_transform: plan.world.to_cols_array(),
321 linear_determinant: plan.determinant,
322 primitive_count: input_mesh.primitives.len(),
323 position_count,
324 normal_count,
325 });
326 }
327
328 Ok(StaticMeshBake {
329 document: Document {
330 skeleton: Skeleton { bones: nodes },
331 clips: Vec::new(),
332 assets: SceneAssets {
333 meshes,
334 instances,
335 materials: doc.assets.materials.clone(),
336 material_resources: doc.assets.material_resources.clone(),
337 scenes: vec![SceneAsset {
338 source_scene_index: 0,
339 name: None,
340 roots: vec![0],
341 }],
342 default_scene: Some(0),
343 source_skeleton: SourceSkeletonAssets::default(),
344 },
345 source: doc.source.clone(),
346 },
347 evidence: StaticMeshBakeEvidence { entries: evidence },
348 })
349}
350
351fn validate(doc: &Document) -> Result<Vec<BakePlan>, StaticMeshBakeError> {
352 if doc.assets.instances.is_empty() {
353 return Err(StaticMeshBakeError::NoInstances);
354 }
355 for clip in &doc.clips {
356 if let Some(track) = clip.tracks.first() {
357 return Err(StaticMeshBakeError::AnimationTrack {
358 clip: clip.name.clone(),
359 node: track.bone,
360 property: track.property.as_str(),
361 });
362 }
363 }
364 for (node, bone) in doc.skeleton.bones.iter().enumerate() {
365 if bone.inverse_bind.is_some() {
366 return Err(StaticMeshBakeError::SkeletonSkin { node });
367 }
368 }
369
370 let worlds = world_matrices(&doc.skeleton)?;
371 validate_materials(&doc.assets)?;
372 let mut mesh_counts = vec![0usize; doc.assets.meshes.len()];
373 let mut source_nodes = HashSet::with_capacity(doc.assets.instances.len());
374 let mut skeleton_nodes = vec![false; doc.skeleton.bones.len()];
375 for instance in &doc.assets.instances {
376 let Some(count) = mesh_counts.get_mut(instance.mesh) else {
377 return Err(StaticMeshBakeError::MissingMesh {
378 source_node_index: instance.source_node_index,
379 mesh: instance.mesh,
380 });
381 };
382 *count += 1;
383 if instance.node >= doc.skeleton.bones.len() {
384 return Err(StaticMeshBakeError::MissingNode {
385 source_node_index: instance.source_node_index,
386 node: instance.node,
387 });
388 }
389 if !source_nodes.insert(instance.source_node_index) {
390 return Err(StaticMeshBakeError::DuplicateNodeAttachment {
391 source_node_index: instance.source_node_index,
392 });
393 }
394 if skeleton_nodes[instance.node] {
395 return Err(StaticMeshBakeError::DuplicateSkeletonNodeAttachment {
396 node: instance.node,
397 });
398 }
399 skeleton_nodes[instance.node] = true;
400 if !instance.skin_joints.is_empty() || !instance.skin_ibms.is_empty() {
401 return Err(StaticMeshBakeError::SkinnedInstance {
402 source_node_index: instance.source_node_index,
403 });
404 }
405 }
406 for (mesh, &instances) in mesh_counts.iter().enumerate() {
407 if instances != 1 {
408 return Err(StaticMeshBakeError::MeshInstanceCount {
409 mesh,
410 source_mesh_index: doc.assets.meshes[mesh].source_mesh_index,
411 instances,
412 });
413 }
414 }
415
416 let mut plans = Vec::with_capacity(doc.assets.instances.len());
417 for (instance_ordinal, instance) in doc.assets.instances.iter().enumerate() {
418 let mesh = &doc.assets.meshes[instance.mesh];
419 validate_mesh(mesh, instance.mesh, doc.assets.materials.len())?;
420 let world = worlds[instance.node];
421 let linear = Mat3::from_mat4(world);
422 let determinant = linear.determinant();
423 let scale = linear
424 .to_cols_array()
425 .into_iter()
426 .fold(0.0_f32, |largest, component| largest.max(component.abs()));
427 let threshold = MIN_RELATIVE_DETERMINANT * scale * scale * scale;
428 if !determinant.is_finite() || scale == 0.0 || determinant.abs() <= threshold {
429 return Err(StaticMeshBakeError::SingularTransform {
430 source_node_index: instance.source_node_index,
431 determinant,
432 });
433 }
434 if determinant < 0.0 {
435 return Err(StaticMeshBakeError::ReflectionTransform {
436 source_node_index: instance.source_node_index,
437 determinant,
438 });
439 }
440 let normal = linear.inverse().transpose();
441 if !matrix3_is_finite(normal) {
442 return Err(StaticMeshBakeError::SingularTransform {
443 source_node_index: instance.source_node_index,
444 determinant,
445 });
446 }
447 validate_baked_mesh(mesh, instance.mesh, world, normal)?;
448 plans.push(BakePlan {
449 instance: instance_ordinal,
450 mesh: instance.mesh,
451 node: instance.node,
452 world,
453 normal,
454 determinant,
455 });
456 }
457 plans.sort_unstable_by_key(|plan| doc.assets.instances[plan.instance].source_node_index);
458 Ok(plans)
459}
460
461fn validate_materials(assets: &SceneAssets) -> Result<(), StaticMeshBakeError> {
462 for (material, value) in assets.materials.iter().enumerate() {
463 if !value
464 .base_color
465 .iter()
466 .all(|component| component.is_finite())
467 {
468 return Err(StaticMeshBakeError::NonFiniteMaterial {
469 material,
470 factor: "base_color",
471 });
472 }
473 if !value.metallic.is_finite() {
474 return Err(StaticMeshBakeError::NonFiniteMaterial {
475 material,
476 factor: "metallic",
477 });
478 }
479 if !value.roughness.is_finite() {
480 return Err(StaticMeshBakeError::NonFiniteMaterial {
481 material,
482 factor: "roughness",
483 });
484 }
485 if value
486 .normal_texture
487 .as_ref()
488 .is_some_and(|normal| !normal.scale.is_finite())
489 {
490 return Err(StaticMeshBakeError::NonFiniteMaterial {
491 material,
492 factor: "normal_texture_scale",
493 });
494 }
495 }
496 Ok(())
497}
498
499fn world_matrices(skeleton: &Skeleton) -> Result<Vec<Mat4>, StaticMeshBakeError> {
500 world_rest_matrices(skeleton).map_err(|error| match error {
501 WorldMatrixError::NonFiniteTransform { node } => {
502 StaticMeshBakeError::NonFiniteTransform { node }
503 }
504 WorldMatrixError::InvalidParent { node, parent } => {
505 StaticMeshBakeError::InvalidParent { node, parent }
506 }
507 })
508}
509
510fn validate_mesh(
511 mesh: &MeshAsset,
512 mesh_ordinal: usize,
513 materials: usize,
514) -> Result<(), StaticMeshBakeError> {
515 if mesh.primitives.is_empty() {
516 return Err(StaticMeshBakeError::InvalidPrimitive {
517 mesh: mesh_ordinal,
518 primitive: 0,
519 reason: "empty_primitives",
520 });
521 }
522 for (primitive_ordinal, primitive) in mesh.primitives.iter().enumerate() {
523 validate_primitive(primitive, mesh_ordinal, primitive_ordinal, materials)?;
524 }
525 Ok(())
526}
527
528fn validate_primitive(
529 primitive: &Primitive,
530 mesh: usize,
531 primitive_ordinal: usize,
532 materials: usize,
533) -> Result<(), StaticMeshBakeError> {
534 let invalid = |reason| StaticMeshBakeError::InvalidPrimitive {
535 mesh,
536 primitive: primitive_ordinal,
537 reason,
538 };
539 if primitive
540 .material
541 .is_some_and(|material| material >= materials)
542 {
543 return Err(invalid("material_index"));
544 }
545 if primitive.positions.is_empty() {
546 return Err(invalid("empty_positions"));
547 }
548 if !primitive.normals.is_empty() && primitive.normals.len() != primitive.positions.len() {
549 return Err(invalid("normal_count"));
550 }
551 if !primitive.uvs.is_empty() && primitive.uvs.len() != primitive.positions.len() {
552 return Err(invalid("uv_count"));
553 }
554 if !primitive.joints.is_empty() || !primitive.weights.is_empty() {
555 return Err(StaticMeshBakeError::SkinnedPrimitive {
556 mesh,
557 primitive: primitive_ordinal,
558 });
559 }
560 if primitive.indices.is_empty() {
561 if !primitive.positions.len().is_multiple_of(3) {
562 return Err(invalid("unindexed_triangle_count"));
563 }
564 } else {
565 if !primitive.indices.len().is_multiple_of(3) {
566 return Err(invalid("indexed_triangle_count"));
567 }
568 if primitive.indices.iter().any(|&index| {
569 usize::try_from(index).map_or(true, |index| index >= primitive.positions.len())
570 }) {
571 return Err(invalid("triangle_index"));
572 }
573 }
574 for (vertex, position) in primitive.positions.iter().enumerate() {
575 if !position.is_finite() {
576 return Err(StaticMeshBakeError::NonFiniteAttribute {
577 mesh,
578 primitive: primitive_ordinal,
579 attribute: "position",
580 vertex,
581 });
582 }
583 }
584 for (vertex, normal) in primitive.normals.iter().enumerate() {
585 if !normal.is_finite() {
586 return Err(StaticMeshBakeError::NonFiniteAttribute {
587 mesh,
588 primitive: primitive_ordinal,
589 attribute: "normal",
590 vertex,
591 });
592 }
593 if normal.length_squared() == 0.0 {
594 return Err(StaticMeshBakeError::ZeroNormal {
595 mesh,
596 primitive: primitive_ordinal,
597 vertex,
598 });
599 }
600 }
601 for (vertex, uv) in primitive.uvs.iter().enumerate() {
602 if !uv.iter().all(|component| component.is_finite()) {
603 return Err(StaticMeshBakeError::NonFiniteAttribute {
604 mesh,
605 primitive: primitive_ordinal,
606 attribute: "uv",
607 vertex,
608 });
609 }
610 }
611 Ok(())
612}
613
614fn validate_baked_mesh(
615 mesh: &MeshAsset,
616 mesh_ordinal: usize,
617 world: Mat4,
618 normal_matrix: Mat3,
619) -> Result<(), StaticMeshBakeError> {
620 for (primitive_ordinal, primitive) in mesh.primitives.iter().enumerate() {
621 for (vertex, position) in primitive.positions.iter().enumerate() {
622 if !world.transform_point3(*position).is_finite() {
623 return Err(StaticMeshBakeError::NonFiniteAttribute {
624 mesh: mesh_ordinal,
625 primitive: primitive_ordinal,
626 attribute: "baked_position",
627 vertex,
628 });
629 }
630 }
631 for (vertex, normal) in primitive.normals.iter().enumerate() {
632 let transformed = normal_matrix * *normal;
633 if !transformed.is_finite() {
634 return Err(StaticMeshBakeError::NonFiniteAttribute {
635 mesh: mesh_ordinal,
636 primitive: primitive_ordinal,
637 attribute: "baked_normal",
638 vertex,
639 });
640 }
641 if transformed.try_normalize().is_none() {
642 return Err(StaticMeshBakeError::ZeroNormal {
643 mesh: mesh_ordinal,
644 primitive: primitive_ordinal,
645 vertex,
646 });
647 }
648 }
649 }
650 Ok(())
651}
652
653fn bake_mesh(
654 mesh: &MeshAsset,
655 plan: &BakePlan,
656 mesh_ordinal: usize,
657) -> Result<MeshAsset, StaticMeshBakeError> {
658 let mut baked = mesh.clone();
659 for (primitive_ordinal, primitive) in baked.primitives.iter_mut().enumerate() {
660 for position in &mut primitive.positions {
661 *position = plan.world.transform_point3(*position);
662 }
663 for (vertex, normal) in primitive.normals.iter_mut().enumerate() {
664 if plan.world == Mat4::IDENTITY && normal.is_normalized() {
667 continue;
668 }
669 let transformed = plan.normal * *normal;
670 let Some(normalized) = transformed.try_normalize() else {
671 return Err(StaticMeshBakeError::ZeroNormal {
672 mesh: mesh_ordinal,
673 primitive: primitive_ordinal,
674 vertex,
675 });
676 };
677 *normal = normalized;
678 }
679 }
680 Ok(baked)
681}
682
683fn matrix3_is_finite(matrix: Mat3) -> bool {
684 matrix
685 .to_cols_array()
686 .into_iter()
687 .all(|component| component.is_finite())
688}