use crate::model::{
AffineDomainViolation, Bone, Document, MeshAsset, MeshInstance, PositiveUniformAffineTolerance,
SceneAsset, SceneAssets, Skeleton, SourceSkeletonAssets, Transform, WorldMatrixError,
affine_axis_lengths, classify_positive_uniform_affine, world_rest_matrices,
};
use glam::{Mat3, Mat4, Vec3};
use std::collections::BTreeSet;
const MATRIX_EPSILON: f32 = 1.0e-4;
const MIN_RELATIVE_DETERMINANT: f32 = 1.0e-6;
const COORDINATE_AFFINE_TOLERANCE: PositiveUniformAffineTolerance =
PositiveUniformAffineTolerance {
equal_axis: MATRIX_EPSILON as f64,
relative_orthogonality: MATRIX_EPSILON as f64,
singular_determinant_relative: 0.0,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SkinnedBindPosePlacement {
#[default]
Preserve,
GroundAndCenter,
}
#[derive(Debug, Clone, Copy)]
pub struct SkinnedBindPoseCanonicalizationOptions {
pub source_to_meters_y_up: Mat4,
pub placement: SkinnedBindPosePlacement,
}
impl Default for SkinnedBindPoseCanonicalizationOptions {
fn default() -> Self {
Self {
source_to_meters_y_up: Mat4::IDENTITY,
placement: SkinnedBindPosePlacement::Preserve,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SkinnedBindPoseCanonicalization {
pub document: Document,
pub source_world_to_canonical: Mat4,
pub converted_bounds_min: Vec3,
pub converted_bounds_max: Vec3,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SkinnedBindPoseCanonicalizationError {
#[error("cannot canonicalize skinned bind pose: document has no mesh instances")]
NoInstances,
#[error(
"cannot canonicalize skinned bind pose: clip {clip:?} animates node {node} ({property})"
)]
AnimationTrack {
clip: String,
node: usize,
property: &'static str,
},
#[error("cannot canonicalize skinned bind pose: source_to_meters_y_up is invalid ({reason})")]
InvalidCoordinateTransform {
reason: &'static str,
},
#[error(
"cannot canonicalize skinned bind pose: skeleton node {node} has invalid parent {parent}"
)]
InvalidParent {
node: usize,
parent: usize,
},
#[error(
"cannot canonicalize skinned bind pose: skeleton node {node} has a non-finite transform"
)]
NonFiniteTransform {
node: usize,
},
#[error(
"cannot canonicalize skinned bind pose: skeleton root {node} cannot be represented as TRS"
)]
NonTrsRoot {
node: usize,
},
#[error(
"cannot canonicalize skinned bind pose: source node {source_node_index} references missing {kind} {index}"
)]
MissingReference {
source_node_index: usize,
kind: &'static str,
index: usize,
},
#[error(
"cannot canonicalize skinned bind pose: source node {source_node_index} is not skinned"
)]
UnskinnedInstance {
source_node_index: usize,
},
#[error(
"cannot canonicalize skinned bind pose: source node {source_node_index} has {ibms} inverse bind matrices for {joints} joints"
)]
InverseBindCount {
source_node_index: usize,
ibms: usize,
joints: usize,
},
#[error(
"cannot canonicalize skinned bind pose: source node {source_node_index} joint {joint} has no inverse bind matrix"
)]
MissingInverseBind {
source_node_index: usize,
joint: usize,
},
#[error(
"cannot canonicalize skinned bind pose: source node {source_node_index} joint {joint} has an inconsistent inverse bind matrix"
)]
InconsistentInverseBind {
source_node_index: usize,
joint: usize,
},
#[error(
"cannot canonicalize skinned bind pose: joint {joint} has a singular or near-singular rest transform"
)]
SingularJoint {
joint: usize,
},
#[error(
"cannot canonicalize skinned bind pose: source node {source_node_index} has {reason} geometry transform"
)]
InvalidGeometryTransform {
source_node_index: usize,
reason: &'static str,
},
#[error(
"cannot canonicalize skinned bind pose: mesh {mesh} primitive {primitive} is invalid ({reason})"
)]
InvalidPrimitive {
mesh: usize,
primitive: usize,
reason: &'static str,
},
#[error(
"cannot canonicalize skinned bind pose: mesh {mesh} primitive {primitive} has non-finite {attribute} at vertex {vertex}"
)]
NonFiniteAttribute {
mesh: usize,
primitive: usize,
attribute: &'static str,
vertex: usize,
},
#[error(
"cannot canonicalize skinned bind pose: mesh {mesh} primitive {primitive} has zero normal at vertex {vertex}"
)]
ZeroNormal {
mesh: usize,
primitive: usize,
vertex: usize,
},
}
#[derive(Debug, Clone, Copy)]
struct Plan {
instance: usize,
geometry_world: Mat4,
}
pub fn canonicalize_skinned_bind_pose(
doc: &Document,
options: SkinnedBindPoseCanonicalizationOptions,
) -> Result<SkinnedBindPoseCanonicalization, SkinnedBindPoseCanonicalizationError> {
validate_coordinate_transform(options.source_to_meters_y_up)?;
let source_worlds = world_matrices(&doc.skeleton)?;
let plans = validate(doc, &source_worlds)?;
let (bounds_min, bounds_max) = converted_bounds(doc, &plans, options.source_to_meters_y_up)?;
let placement = placement_transform(options.placement, bounds_min, bounds_max);
let source_world_to_canonical = placement * options.source_to_meters_y_up;
let mut skeleton = canonical_skeleton(&doc.skeleton, source_world_to_canonical)?;
let canonical_worlds = world_matrices(&skeleton)?;
let mut meshes = Vec::with_capacity(plans.len());
let mut instances = Vec::with_capacity(plans.len());
let mut joints_with_skins = BTreeSet::new();
for plan in plans {
let input = &doc.assets.instances[plan.instance];
let output_mesh = meshes.len();
let geometry_transform = source_world_to_canonical * plan.geometry_world;
meshes.push(transform_mesh(
&doc.assets.meshes[input.mesh],
input.mesh,
geometry_transform,
)?);
let skin_joints: Vec<usize> = input.skin_joints.iter().map(|joint| joint + 1).collect();
let skin_ibms = skin_joints
.iter()
.map(|&joint| {
let inverse = inverse_joint_matrix(canonical_worlds[joint], joint)?;
joints_with_skins.insert(joint);
Ok(inverse)
})
.collect::<Result<Vec<_>, _>>()?;
instances.push(MeshInstance {
source_node_index: input.source_node_index,
node: 0,
mesh: output_mesh,
skin_joints,
skin_ibms,
});
}
for joint in joints_with_skins {
skeleton.bones[joint].inverse_bind =
Some(inverse_joint_matrix(canonical_worlds[joint], joint)?);
}
Ok(SkinnedBindPoseCanonicalization {
document: Document {
skeleton,
clips: doc.clips.clone(),
assets: SceneAssets {
meshes,
instances,
materials: doc.assets.materials.clone(),
material_resources: doc.assets.material_resources.clone(),
scenes: vec![SceneAsset {
source_scene_index: 0,
name: None,
roots: vec![0],
}],
default_scene: Some(0),
source_skeleton: SourceSkeletonAssets::default(),
},
source: doc.source.clone(),
},
source_world_to_canonical,
converted_bounds_min: bounds_min,
converted_bounds_max: bounds_max,
})
}
fn validate_coordinate_transform(
transform: Mat4,
) -> Result<(), SkinnedBindPoseCanonicalizationError> {
if !matrix4_is_finite(transform) {
return Err(
SkinnedBindPoseCanonicalizationError::InvalidCoordinateTransform {
reason: "non_finite",
},
);
}
if !transform.w_axis.abs_diff_eq(glam::Vec4::W, MATRIX_EPSILON) {
return Err(
SkinnedBindPoseCanonicalizationError::InvalidCoordinateTransform {
reason: "non_affine",
},
);
}
let linear = Mat3::from_mat4(transform);
let scale = affine_axis_lengths(linear)[0];
if !scale.is_finite() || scale <= f64::from(MATRIX_EPSILON) {
return Err(
SkinnedBindPoseCanonicalizationError::InvalidCoordinateTransform {
reason: "zero_scale",
},
);
}
classify_positive_uniform_affine(linear, COORDINATE_AFFINE_TOLERANCE)
.map(|_| ())
.map_err(
|violation| SkinnedBindPoseCanonicalizationError::InvalidCoordinateTransform {
reason: coordinate_transform_violation_reason(violation),
},
)
}
fn coordinate_transform_violation_reason(violation: AffineDomainViolation) -> &'static str {
match violation {
AffineDomainViolation::NonFinite => "non_finite",
AffineDomainViolation::NonUniformScale | AffineDomainViolation::Sheared => {
"non_uniform_or_sheared"
}
AffineDomainViolation::Reflected | AffineDomainViolation::Singular => {
"reflection_or_singular"
}
}
}
fn validate(
doc: &Document,
worlds: &[Mat4],
) -> Result<Vec<Plan>, SkinnedBindPoseCanonicalizationError> {
if doc.assets.instances.is_empty() {
return Err(SkinnedBindPoseCanonicalizationError::NoInstances);
}
for clip in &doc.clips {
if let Some(track) = clip.tracks.first() {
return Err(SkinnedBindPoseCanonicalizationError::AnimationTrack {
clip: clip.name.clone(),
node: track.bone,
property: track.property.as_str(),
});
}
}
let mut plans = Vec::with_capacity(doc.assets.instances.len());
for (instance_ordinal, instance) in doc.assets.instances.iter().enumerate() {
let mesh = doc.assets.meshes.get(instance.mesh).ok_or(
SkinnedBindPoseCanonicalizationError::MissingReference {
source_node_index: instance.source_node_index,
kind: "mesh",
index: instance.mesh,
},
)?;
worlds.get(instance.node).ok_or(
SkinnedBindPoseCanonicalizationError::MissingReference {
source_node_index: instance.source_node_index,
kind: "node",
index: instance.node,
},
)?;
if instance.skin_joints.is_empty() {
return Err(SkinnedBindPoseCanonicalizationError::UnskinnedInstance {
source_node_index: instance.source_node_index,
});
}
if !instance.skin_ibms.is_empty() && instance.skin_ibms.len() != instance.skin_joints.len()
{
return Err(SkinnedBindPoseCanonicalizationError::InverseBindCount {
source_node_index: instance.source_node_index,
ibms: instance.skin_ibms.len(),
joints: instance.skin_joints.len(),
});
}
let mut geometry_world = None;
for (slot, &joint) in instance.skin_joints.iter().enumerate() {
let joint_world = *worlds.get(joint).ok_or(
SkinnedBindPoseCanonicalizationError::MissingReference {
source_node_index: instance.source_node_index,
kind: "joint",
index: joint,
},
)?;
let actual = if instance.skin_ibms.is_empty() {
doc.skeleton.bones[joint].inverse_bind.ok_or(
SkinnedBindPoseCanonicalizationError::MissingInverseBind {
source_node_index: instance.source_node_index,
joint,
},
)?
} else {
instance.skin_ibms[slot]
};
let candidate = joint_world * actual;
if !matrix4_is_finite(actual)
|| !matrix4_is_finite(candidate)
|| geometry_world
.is_some_and(|geometry| !matrix_approximately_equal(candidate, geometry))
{
return Err(
SkinnedBindPoseCanonicalizationError::InconsistentInverseBind {
source_node_index: instance.source_node_index,
joint,
},
);
}
geometry_world.get_or_insert(candidate);
}
let Some(geometry_world) = geometry_world else {
return Err(SkinnedBindPoseCanonicalizationError::UnskinnedInstance {
source_node_index: instance.source_node_index,
});
};
validate_geometry_transform(geometry_world, instance.source_node_index)?;
validate_mesh(mesh, instance.mesh, instance)?;
plans.push(Plan {
instance: instance_ordinal,
geometry_world,
});
}
plans.sort_unstable_by_key(|plan| {
let instance = &doc.assets.instances[plan.instance];
(instance.source_node_index, plan.instance)
});
Ok(plans)
}
fn world_matrices(skeleton: &Skeleton) -> Result<Vec<Mat4>, SkinnedBindPoseCanonicalizationError> {
world_rest_matrices(skeleton).map_err(|error| match error {
WorldMatrixError::NonFiniteTransform { node } => {
SkinnedBindPoseCanonicalizationError::NonFiniteTransform { node }
}
WorldMatrixError::InvalidParent { node, parent } => {
SkinnedBindPoseCanonicalizationError::InvalidParent { node, parent }
}
})
}
fn inverse_joint_matrix(
world: Mat4,
joint: usize,
) -> Result<Mat4, SkinnedBindPoseCanonicalizationError> {
let linear = Mat3::from_mat4(world);
let scale = linear
.to_cols_array()
.into_iter()
.fold(0.0_f32, |largest, component| largest.max(component.abs()));
let determinant = linear.determinant();
if !determinant.is_finite()
|| scale == 0.0
|| determinant.abs() <= MIN_RELATIVE_DETERMINANT * scale * scale * scale
{
return Err(SkinnedBindPoseCanonicalizationError::SingularJoint { joint });
}
let inverse = world.inverse();
if !matrix4_is_finite(inverse) {
return Err(SkinnedBindPoseCanonicalizationError::SingularJoint { joint });
}
Ok(inverse)
}
fn validate_geometry_transform(
world: Mat4,
source_node_index: usize,
) -> Result<(), SkinnedBindPoseCanonicalizationError> {
let linear = Mat3::from_mat4(world);
let scale = linear
.to_cols_array()
.into_iter()
.fold(0.0_f32, |largest, component| largest.max(component.abs()));
let determinant = linear.determinant();
if !determinant.is_finite()
|| scale == 0.0
|| determinant.abs() <= MIN_RELATIVE_DETERMINANT * scale * scale * scale
{
return Err(
SkinnedBindPoseCanonicalizationError::InvalidGeometryTransform {
source_node_index,
reason: "singular_or_near_singular",
},
);
}
if determinant < 0.0 {
return Err(
SkinnedBindPoseCanonicalizationError::InvalidGeometryTransform {
source_node_index,
reason: "reflection",
},
);
}
Ok(())
}
fn validate_mesh(
mesh: &MeshAsset,
mesh_ordinal: usize,
instance: &MeshInstance,
) -> Result<(), SkinnedBindPoseCanonicalizationError> {
if mesh.primitives.is_empty() {
return Err(invalid_primitive(mesh_ordinal, 0, "empty_primitives"));
}
for (primitive_ordinal, primitive) in mesh.primitives.iter().enumerate() {
let invalid = |reason| invalid_primitive(mesh_ordinal, primitive_ordinal, reason);
if primitive.positions.is_empty() {
return Err(invalid("empty_positions"));
}
if !primitive.normals.is_empty() && primitive.normals.len() != primitive.positions.len() {
return Err(invalid("normal_count"));
}
if primitive.joints.len() != primitive.positions.len()
|| primitive.weights.len() != primitive.positions.len()
{
return Err(invalid("skin_attribute_count"));
}
if primitive.indices.is_empty() {
if !primitive.positions.len().is_multiple_of(3) {
return Err(invalid("unindexed_triangle_count"));
}
} else if !primitive.indices.len().is_multiple_of(3)
|| primitive
.indices
.iter()
.any(|&index| index as usize >= primitive.positions.len())
{
return Err(invalid("triangle_indices"));
}
for (vertex, position) in primitive.positions.iter().enumerate() {
if !position.is_finite() {
return Err(non_finite(
mesh_ordinal,
primitive_ordinal,
"position",
vertex,
));
}
}
for (vertex, normal) in primitive.normals.iter().enumerate() {
if !normal.is_finite() {
return Err(non_finite(
mesh_ordinal,
primitive_ordinal,
"normal",
vertex,
));
}
if normal.length_squared() == 0.0 {
return Err(SkinnedBindPoseCanonicalizationError::ZeroNormal {
mesh: mesh_ordinal,
primitive: primitive_ordinal,
vertex,
});
}
}
for (vertex, weights) in primitive.weights.iter().enumerate() {
if weights.iter().any(|weight| !weight.is_finite()) {
return Err(non_finite(
mesh_ordinal,
primitive_ordinal,
"weight",
vertex,
));
}
if weights.iter().any(|weight| *weight < 0.0)
|| (weights.iter().sum::<f32>() - 1.0).abs() > MATRIX_EPSILON
{
return Err(invalid("skin_weights"));
}
if primitive.joints[vertex]
.iter()
.zip(weights)
.any(|(&joint_slot, &weight)| {
weight != 0.0 && joint_slot as usize >= instance.skin_joints.len()
})
{
return Err(invalid("joint_index"));
}
}
}
Ok(())
}
fn converted_bounds(
doc: &Document,
plans: &[Plan],
source_to_meters_y_up: Mat4,
) -> Result<(Vec3, Vec3), SkinnedBindPoseCanonicalizationError> {
let mut min = Vec3::splat(f32::INFINITY);
let mut max = Vec3::splat(f32::NEG_INFINITY);
for plan in plans {
let instance = &doc.assets.instances[plan.instance];
let transform = source_to_meters_y_up * plan.geometry_world;
for (primitive_ordinal, primitive) in doc.assets.meshes[instance.mesh]
.primitives
.iter()
.enumerate()
{
for (vertex, position) in primitive.positions.iter().enumerate() {
let converted = transform.transform_point3(*position);
if !converted.is_finite() {
return Err(non_finite(
instance.mesh,
primitive_ordinal,
"converted_position",
vertex,
));
}
min = min.min(converted);
max = max.max(converted);
}
}
}
Ok((min, max))
}
fn placement_transform(placement: SkinnedBindPosePlacement, min: Vec3, max: Vec3) -> Mat4 {
match placement {
SkinnedBindPosePlacement::Preserve => Mat4::IDENTITY,
SkinnedBindPosePlacement::GroundAndCenter => Mat4::from_translation(Vec3::new(
-(min.x + max.x) * 0.5,
-min.y,
-(min.z + max.z) * 0.5,
)),
}
}
fn canonical_skeleton(
source: &Skeleton,
transform: Mat4,
) -> Result<Skeleton, SkinnedBindPoseCanonicalizationError> {
let mut bones = Vec::with_capacity(source.bones.len() + 1);
bones.push(Bone {
name: "animsmith-canonical-root".to_string(),
parent: None,
rest: Transform::IDENTITY,
inverse_bind: None,
});
for (node, source_bone) in source.bones.iter().enumerate() {
let (parent, rest) = match source_bone.parent {
Some(parent) => (Some(parent + 1), source_bone.rest),
None => (
Some(0),
transform_to_trs(transform * source_bone.rest.to_mat4())
.ok_or(SkinnedBindPoseCanonicalizationError::NonTrsRoot { node })?,
),
};
bones.push(Bone {
name: source_bone.name.clone(),
parent,
rest,
inverse_bind: None,
});
}
Ok(Skeleton { bones })
}
fn transform_mesh(
mesh: &MeshAsset,
mesh_ordinal: usize,
transform: Mat4,
) -> Result<MeshAsset, SkinnedBindPoseCanonicalizationError> {
let normal_transform = Mat3::from_mat4(transform).inverse().transpose();
if !matrix3_is_finite(normal_transform) {
return Err(
SkinnedBindPoseCanonicalizationError::InvalidCoordinateTransform {
reason: "normal_matrix",
},
);
}
let mut output = mesh.clone();
for (primitive_ordinal, primitive) in output.primitives.iter_mut().enumerate() {
for (vertex, position) in primitive.positions.iter_mut().enumerate() {
*position = transform.transform_point3(*position);
if !position.is_finite() {
return Err(non_finite(
mesh_ordinal,
primitive_ordinal,
"canonical_position",
vertex,
));
}
}
for (vertex, normal) in primitive.normals.iter_mut().enumerate() {
let Some(normalized) = (normal_transform * *normal).try_normalize() else {
return Err(SkinnedBindPoseCanonicalizationError::ZeroNormal {
mesh: mesh_ordinal,
primitive: primitive_ordinal,
vertex,
});
};
*normal = normalized;
}
}
Ok(output)
}
fn transform_to_trs(matrix: Mat4) -> Option<Transform> {
if !matrix4_is_finite(matrix) {
return None;
}
let (scale, rotation, translation) = matrix.to_scale_rotation_translation();
if !scale.is_finite() || !rotation.is_finite() || !translation.is_finite() {
return None;
}
let transform = Transform {
translation,
rotation,
scale,
};
matrix_approximately_equal(matrix, transform.to_mat4()).then_some(transform)
}
fn invalid_primitive(
mesh: usize,
primitive: usize,
reason: &'static str,
) -> SkinnedBindPoseCanonicalizationError {
SkinnedBindPoseCanonicalizationError::InvalidPrimitive {
mesh,
primitive,
reason,
}
}
fn non_finite(
mesh: usize,
primitive: usize,
attribute: &'static str,
vertex: usize,
) -> SkinnedBindPoseCanonicalizationError {
SkinnedBindPoseCanonicalizationError::NonFiniteAttribute {
mesh,
primitive,
attribute,
vertex,
}
}
fn matrix_approximately_equal(left: Mat4, right: Mat4) -> bool {
left.to_cols_array()
.into_iter()
.zip(right.to_cols_array())
.all(|(left, right)| {
(left - right).abs() <= MATRIX_EPSILON * left.abs().max(right.abs()).max(1.0)
})
}
fn matrix4_is_finite(matrix: Mat4) -> bool {
matrix
.to_cols_array()
.into_iter()
.all(|component| component.is_finite())
}
fn matrix3_is_finite(matrix: Mat3) -> bool {
matrix
.to_cols_array()
.into_iter()
.all(|component| component.is_finite())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::affine_test_fixtures;
#[test]
fn coordinate_transform_accepts_the_shared_fixtures_inside_its_1e4_bands() {
assert_eq!(
COORDINATE_AFFINE_TOLERANCE,
PositiveUniformAffineTolerance {
equal_axis: f64::from(1.0e-4_f32),
relative_orthogonality: f64::from(1.0e-4_f32),
singular_determinant_relative: 0.0,
},
"the canonicalization policy tuple is an explicit contract"
);
for basis in [
affine_test_fixtures::tolerance_divergence_basis(),
affine_test_fixtures::orthogonality_tolerance_divergence_basis(),
] {
validate_coordinate_transform(Mat4::from_mat3(basis))
.expect("skinned canonicalization keeps its declared 1e-4 policy");
}
}
}