use super::numeric::{
column_operand_magnitude, mat4_abs, matrix_magnitude, matrix_residual,
product_operand_magnitude, scale_translation_only,
};
use super::planning::{check_factor_narrows, validate_plan_document_inventory};
use super::reference::ScaleCandidate;
use super::validation::{
WorldBonePose, WorldPose, affected_skin_instance_indices, child_translation_rounding_magnitude,
instance_bind, local_rest_matrix, rest_world_pose, source_node_index_map, stored_instance_bind,
validate_candidate_structure, validate_scale_input,
};
use super::{
ProofResidualKind, ScaleError, ScaleFieldDisposition, ScaleFieldTarget, ScaleOperation,
ScalePlan, ScaleProofObligation, ScaleRewriteRule, ScaleSourceNodeKind, ScaleSourceRestField,
ScaleTolerancePolicy,
};
use crate::model::{
BoneId, Clip, Document, DocumentShapeError, Interpolation, MeshInstanceShapeViolation,
Primitive, Property, Skeleton, SourceNodeAsset, SourceNodeLocalRest, TrackValues, Transform,
affine_axis_lengths, average_affine_axis_length, mat4_is_finite,
};
use crate::sample::{TrackSample, sample_track};
use glam::{DMat3, DMat4, DVec3, Mat3, Mat4, Quat, Vec3, Vec4};
use std::collections::{BTreeMap, BTreeSet};
impl ScalePlan {
fn is_whole_document(&self) -> bool {
matches!(self.compiled, super::ScaleCompiledPlan::WholeDocument(_))
}
fn has_obligation(&self, expected: ScaleProofObligation) -> bool {
self.obligations().contains(&expected)
}
pub(super) fn rest_obligation(&self) -> Option<(&[BoneId], bool)> {
if self.has_obligation(ScaleProofObligation::RestWorldAndUnitScale) {
Some((self.affected_nodes(), true))
} else if self.has_obligation(ScaleProofObligation::RestWorld) {
Some((self.affected_nodes(), false))
} else {
None
}
}
fn transform_only_nodes(&self) -> Option<&[BoneId]> {
self.has_obligation(ScaleProofObligation::TransformOnlyAffine)
.then(|| self.transform_only_attachments())
}
fn has_key_translations(&self) -> bool {
self.has_obligation(ScaleProofObligation::KeyTranslations)
}
fn has_cubic_interiors(&self) -> bool {
self.has_obligation(ScaleProofObligation::CubicInteriors)
}
fn trajectory_nodes(&self) -> Option<&[BoneId]> {
self.has_obligation(ScaleProofObligation::Trajectories)
.then(|| self.affected_nodes())
}
fn has_skin_and_bounds(&self) -> bool {
self.has_obligation(ScaleProofObligation::SkinAndBounds)
}
fn has_unaffected_binds(&self) -> bool {
self.has_obligation(ScaleProofObligation::UnaffectedInverseBinds)
}
}
fn proof_connector_product(
connector_tail: usize,
by_source_index: &BTreeMap<usize, &SourceNodeAsset>,
connector_product_by_tail: &mut BTreeMap<usize, DMat4>,
) -> Result<DMat4, ScaleError> {
let mut suffix = Vec::new();
let mut visited = BTreeSet::new();
let mut cursor = connector_tail;
let mut product = loop {
if let Some(&cached) = connector_product_by_tail.get(&cursor) {
break cached;
}
if !visited.insert(cursor) {
return Err(ScaleError::IncompleteClosure {
reason: "cyclic_connector_source_parent_chain",
});
}
let node = by_source_index
.get(&cursor)
.ok_or(ScaleError::IncompleteClosure {
reason: "dangling_connector_source_node_index",
})?;
if node.bone.is_some() {
break DMat4::IDENTITY;
}
suffix.push(cursor);
cursor = node
.parent_source_node_index
.ok_or(ScaleError::IncompleteClosure {
reason: "connector_without_projected_ancestor",
})?;
};
while let Some(source) = suffix.pop() {
let node = by_source_index
.get(&source)
.ok_or(ScaleError::IncompleteClosure {
reason: "dangling_connector_source_node_index",
})?;
product *= local_rest_matrix(&node.local_rest).as_dmat4();
connector_product_by_tail.insert(source, product);
}
connector_product_by_tail
.get(&connector_tail)
.copied()
.ok_or(ScaleError::IncompleteClosure {
reason: "empty_connector_bridge",
})
}
fn proof_expected_bridged_source_local(
local_rest: &SourceNodeLocalRest,
connector: DMat4,
s_parent: f32,
s_node: f32,
bone: BoneId,
) -> Result<SourceNodeLocalRest, ScaleError> {
if s_parent == 1.0 && s_node == 1.0 {
return Ok(local_rest.clone());
}
let inverse = DMat3::from_cols(
connector.x_axis.truncate(),
connector.y_axis.truncate(),
connector.z_axis.truncate(),
)
.inverse();
let offset = inverse * (connector.w_axis.truncate() * (f64::from(s_parent) - 1.0));
if !inverse.x_axis.is_finite()
|| !inverse.y_axis.is_finite()
|| !inverse.z_axis.is_finite()
|| !offset.is_finite()
{
return Err(ScaleError::NonFiniteTransform { node: bone });
}
let expected = match local_rest {
SourceNodeLocalRest::Trs {
translation,
rotation,
scale,
} => SourceNodeLocalRest::Trs {
translation: (translation.as_dvec3() * f64::from(s_parent) + offset).as_vec3(),
rotation: *rotation,
scale: (scale.as_dvec3() * (f64::from(s_parent) / f64::from(s_node))).as_vec3(),
},
SourceNodeLocalRest::Matrix(matrix) => {
let ratio = f64::from(s_parent) / f64::from(s_node);
let rebase_linear_column = |column: Vec4| {
(column.truncate().as_dvec3() * ratio)
.as_vec3()
.extend(column.w)
};
let translation =
(matrix.w_axis.truncate().as_dvec3() * f64::from(s_parent) + offset).as_vec3();
SourceNodeLocalRest::Matrix(Mat4::from_cols(
rebase_linear_column(matrix.x_axis),
rebase_linear_column(matrix.y_axis),
rebase_linear_column(matrix.z_axis),
translation.extend(matrix.w_axis.w),
))
}
};
if !mat4_is_finite(local_rest_matrix(&expected)) {
return Err(ScaleError::NonFiniteTransform { node: bone });
}
Ok(expected)
}
fn vec3_bits_equal(left: Vec3, right: Vec3) -> bool {
left.to_array().map(f32::to_bits) == right.to_array().map(f32::to_bits)
}
fn quat_bits_equal(left: Quat, right: Quat) -> bool {
left.to_array().map(f32::to_bits) == right.to_array().map(f32::to_bits)
}
fn source_rest_field_bits_equal(
left: &SourceNodeLocalRest,
right: &SourceNodeLocalRest,
field: ScaleSourceRestField,
) -> bool {
match (left, right, field) {
(
SourceNodeLocalRest::Trs {
translation: left, ..
},
SourceNodeLocalRest::Trs {
translation: right, ..
},
ScaleSourceRestField::Translation,
)
| (
SourceNodeLocalRest::Trs { scale: left, .. },
SourceNodeLocalRest::Trs { scale: right, .. },
ScaleSourceRestField::Scale,
) => vec3_bits_equal(*left, *right),
(
SourceNodeLocalRest::Trs { rotation: left, .. },
SourceNodeLocalRest::Trs {
rotation: right, ..
},
ScaleSourceRestField::Rotation,
) => quat_bits_equal(*left, *right),
(
SourceNodeLocalRest::Matrix(left),
SourceNodeLocalRest::Matrix(right),
ScaleSourceRestField::MatrixLinear,
) => {
vec3_bits_equal(left.x_axis.truncate(), right.x_axis.truncate())
&& vec3_bits_equal(left.y_axis.truncate(), right.y_axis.truncate())
&& vec3_bits_equal(left.z_axis.truncate(), right.z_axis.truncate())
}
(
SourceNodeLocalRest::Matrix(left),
SourceNodeLocalRest::Matrix(right),
ScaleSourceRestField::MatrixTranslation,
) => vec3_bits_equal(left.w_axis.truncate(), right.w_axis.truncate()),
(
SourceNodeLocalRest::Matrix(left),
SourceNodeLocalRest::Matrix(right),
ScaleSourceRestField::MatrixHomogeneous,
) => {
[left.x_axis.w, left.y_axis.w, left.z_axis.w, left.w_axis.w].map(f32::to_bits)
== [
right.x_axis.w,
right.y_axis.w,
right.z_axis.w,
right.w_axis.w,
]
.map(f32::to_bits)
}
_ => false,
}
}
fn f32_values_within_scale_tolerance<const N: usize>(
left: [f32; N],
right: [f32; N],
tolerance: &ScaleTolerancePolicy,
) -> bool {
left.into_iter().zip(right).all(|(left, right)| {
let left = f64::from(left);
let right = f64::from(right);
let residual = (left - right).abs();
let limit = tolerance.scalar_tolerance(left, right);
residual.is_finite() && limit.is_finite() && residual <= limit
})
}
fn rewritten_source_rest_field_within_tolerance(
expected: &SourceNodeLocalRest,
actual: &SourceNodeLocalRest,
field: ScaleSourceRestField,
tolerance: &ScaleTolerancePolicy,
) -> bool {
match (expected, actual, field) {
(
SourceNodeLocalRest::Trs {
translation: expected,
..
},
SourceNodeLocalRest::Trs {
translation: actual,
..
},
ScaleSourceRestField::Translation,
)
| (
SourceNodeLocalRest::Trs {
scale: expected, ..
},
SourceNodeLocalRest::Trs { scale: actual, .. },
ScaleSourceRestField::Scale,
) => f32_values_within_scale_tolerance(expected.to_array(), actual.to_array(), tolerance),
(
SourceNodeLocalRest::Matrix(expected),
SourceNodeLocalRest::Matrix(actual),
ScaleSourceRestField::MatrixLinear,
) => f32_values_within_scale_tolerance(
[
expected.x_axis.x,
expected.x_axis.y,
expected.x_axis.z,
expected.y_axis.x,
expected.y_axis.y,
expected.y_axis.z,
expected.z_axis.x,
expected.z_axis.y,
expected.z_axis.z,
],
[
actual.x_axis.x,
actual.x_axis.y,
actual.x_axis.z,
actual.y_axis.x,
actual.y_axis.y,
actual.y_axis.z,
actual.z_axis.x,
actual.z_axis.y,
actual.z_axis.z,
],
tolerance,
),
(
SourceNodeLocalRest::Matrix(expected),
SourceNodeLocalRest::Matrix(actual),
ScaleSourceRestField::MatrixTranslation,
) => f32_values_within_scale_tolerance(
expected.w_axis.truncate().to_array(),
actual.w_axis.truncate().to_array(),
tolerance,
),
_ => false,
}
}
fn proof_expected_rewritten_source_local(
source: &Document,
plan: &ScalePlan,
affected: &BTreeSet<BoneId>,
source_node: &SourceNodeAsset,
rule: ScaleRewriteRule,
connector_products: &mut BTreeMap<usize, DMat4>,
source_nodes: &BTreeMap<usize, &SourceNodeAsset>,
) -> Result<SourceNodeLocalRest, ScaleError> {
match rule {
ScaleRewriteRule::WholeDocumentLength => {
let q = check_factor_narrows(plan.common_factor(), plan.common_factor())?;
Ok(match &source_node.local_rest {
SourceNodeLocalRest::Trs {
translation,
rotation,
scale,
} => SourceNodeLocalRest::Trs {
translation: Vec3::new(translation.x * q, translation.y * q, translation.z * q),
rotation: *rotation,
scale: *scale,
},
SourceNodeLocalRest::Matrix(matrix) => {
SourceNodeLocalRest::Matrix(Mat4::from_cols(
matrix.x_axis,
matrix.y_axis,
matrix.z_axis,
Vec4::new(
matrix.w_axis.x * q,
matrix.w_axis.y * q,
matrix.w_axis.z * q,
matrix.w_axis.w,
),
))
}
})
}
ScaleRewriteRule::RestBindSourceLocal { connector_tail } => {
let bone = source_node
.bone
.ok_or(ScaleError::SourceNodeNotNormalized {
source_node_index: source_node.source_node_index,
})?;
let parent = source
.skeleton
.bones
.get(bone)
.ok_or(ScaleError::BoneIndexOutOfRange { index: bone })?
.parent;
let s = check_factor_narrows(plan.common_factor(), plan.common_factor())?;
let s_parent = if parent.is_some_and(|parent| affected.contains(&parent)) {
s
} else {
1.0
};
let s_node = if affected.contains(&bone) { s } else { 1.0 };
if let Some(connector_tail) = connector_tail {
let connector =
proof_connector_product(connector_tail, source_nodes, connector_products)?;
return proof_expected_bridged_source_local(
&source_node.local_rest,
connector,
s_parent,
s_node,
bone,
);
}
Ok(match &source_node.local_rest {
SourceNodeLocalRest::Trs {
translation,
rotation,
scale,
} => SourceNodeLocalRest::Trs {
translation: Vec3::new(
translation.x * s_parent,
translation.y * s_parent,
translation.z * s_parent,
),
rotation: *rotation,
scale: Vec3::new(
scale.x * (s_parent / s_node),
scale.y * (s_parent / s_node),
scale.z * (s_parent / s_node),
),
},
SourceNodeLocalRest::Matrix(matrix) => {
let inverse_node = 1.0 / s_node;
let linear = |column: Vec4| {
Vec4::new(
column.x * s_parent * inverse_node,
column.y * s_parent * inverse_node,
column.z * s_parent * inverse_node,
column.w * inverse_node,
)
};
SourceNodeLocalRest::Matrix(Mat4::from_cols(
linear(matrix.x_axis),
linear(matrix.y_axis),
linear(matrix.z_axis),
Vec4::new(
matrix.w_axis.x * s_parent,
matrix.w_axis.y * s_parent,
matrix.w_axis.z * s_parent,
matrix.w_axis.w,
),
))
}
})
}
ScaleRewriteRule::RestBindParentBasis
| ScaleRewriteRule::RestBindLocalScale
| ScaleRewriteRule::RestBindNodeBasis => Err(ScaleError::PlanDocumentMismatch {
reason: "invalid_source_local_rewrite_rule",
}),
}
}
fn check_rewritten_source_field_dispositions(
source: &Document,
candidate: &Document,
plan: &ScalePlan,
affected: &BTreeSet<BoneId>,
tolerance: &ScaleTolerancePolicy,
discharged: &mut BTreeSet<usize>,
) -> Result<(), ScaleError> {
let source_nodes = source_node_index_map(source);
let candidate_nodes = source_node_index_map(candidate);
let mut connector_products = BTreeMap::new();
for (row_index, row) in plan.field_rows().iter().enumerate() {
let (
ScaleFieldTarget::SourceNodeRest {
source_node_index,
field,
},
ScaleFieldDisposition::Rewrite(rule),
) = (row.target, row.disposition)
else {
continue;
};
let before =
source_nodes
.get(&source_node_index)
.ok_or(ScaleError::CandidateStructureMismatch {
reason: "rewritten_source_node_missing",
})?;
let after = candidate_nodes.get(&source_node_index).ok_or(
ScaleError::CandidateStructureMismatch {
reason: "rewritten_source_node_missing",
},
)?;
let expected = proof_expected_rewritten_source_local(
source,
plan,
affected,
before,
rule,
&mut connector_products,
&source_nodes,
)?;
let bridged = matches!(
rule,
ScaleRewriteRule::RestBindSourceLocal {
connector_tail: Some(_)
}
);
let matches = if bridged {
source_rest_field_bits_equal(&expected, &after.local_rest, field)
} else {
rewritten_source_rest_field_within_tolerance(
&expected,
&after.local_rest,
field,
tolerance,
)
};
if !matches {
return Err(ScaleError::CandidateStructureMismatch {
reason: if bridged {
"bridged_source_local_mismatch"
} else {
"field_disposition_mismatch"
},
});
}
mark_field_row_discharged(discharged, row_index)?;
}
Ok(())
}
fn check_preserved_field_dispositions(
source: &Document,
candidate: &Document,
plan: &ScalePlan,
discharged: &mut BTreeSet<usize>,
) -> Result<(), ScaleError> {
let source_nodes = source_node_index_map(source);
let candidate_nodes = source_node_index_map(candidate);
let mut connector_sources = BTreeSet::new();
let mut bridged_successors = BTreeSet::new();
for row in plan.ledger().source_topology() {
match row.kind() {
ScaleSourceNodeKind::Connector => {
connector_sources.insert(row.source_node_index());
}
ScaleSourceNodeKind::Projected {
incoming_connector_tail: Some(_),
..
} => {
bridged_successors.insert(row.source_node_index());
}
ScaleSourceNodeKind::Projected { .. } | ScaleSourceNodeKind::OutsideDomain { .. } => {}
}
}
for (row_index, row) in plan.field_rows().iter().enumerate() {
let (
ScaleFieldTarget::SourceNodeRest {
source_node_index,
field,
},
ScaleFieldDisposition::PreserveExact,
) = (row.target, row.disposition)
else {
continue;
};
let exact = match (
source_nodes.get(&source_node_index),
candidate_nodes.get(&source_node_index),
) {
(Some(before), Some(after)) => {
source_rest_field_bits_equal(&before.local_rest, &after.local_rest, field)
}
(None, None) => true,
_ => false,
};
if !exact {
return Err(ScaleError::CandidateStructureMismatch {
reason: if connector_sources.contains(&source_node_index) {
"connector_source_local_mismatch"
} else if bridged_successors.contains(&source_node_index) {
"bridged_source_local_mismatch"
} else {
"field_disposition_mismatch"
},
});
}
mark_field_row_discharged(discharged, row_index)?;
}
Ok(())
}
fn mark_field_row_discharged(
discharged: &mut BTreeSet<usize>,
row_index: usize,
) -> Result<(), ScaleError> {
if !discharged.insert(row_index) {
return Err(ScaleError::PlanDocumentMismatch {
reason: "field_row_discharged_twice",
});
}
Ok(())
}
fn finish_field_row_discharge(
plan: &ScalePlan,
discharged: &BTreeSet<usize>,
) -> Result<(), ScaleError> {
let expected: BTreeSet<_> = (0..plan.field_rows().len()).collect();
if *discharged != expected {
return Err(ScaleError::PlanDocumentMismatch {
reason: "field_row_not_discharged",
});
}
Ok(())
}
mod residual {
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct ScaleProofResidual {
max: f64,
comparisons: usize,
}
impl ScaleProofResidual {
#[must_use]
pub fn max(self) -> f64 {
self.max
}
#[must_use]
pub fn comparisons(self) -> usize {
self.comparisons
}
#[must_use]
pub fn evaluated(self) -> bool {
self.comparisons != 0
}
pub(super) const EMPTY: Self = Self {
max: 0.0,
comparisons: 0,
};
pub(super) fn record(&mut self, observed: f64) {
self.max = self.max.max(observed);
self.comparisons += 1;
}
}
#[cfg(doctest)]
mod api_contract {
struct RemovedSplitFields;
}
}
pub use residual::ScaleProofResidual;
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct ScaleProof {
pub tolerance_policy: ScaleTolerancePolicy,
pub rest_translation: ScaleProofResidual,
pub rest_rotation: ScaleProofResidual,
pub unit_scale: ScaleProofResidual,
pub transform_only_affine: ScaleProofResidual,
pub track_value: ScaleProofResidual,
pub mesh_position: ScaleProofResidual,
pub key_translation: ScaleProofResidual,
pub cubic_interior: ScaleProofResidual,
pub trajectory: ScaleProofResidual,
pub skin_matrix: ScaleProofResidual,
pub bounds: ScaleProofResidual,
pub unaffected_inverse_bind: ScaleProofResidual,
pub observed_factor: f64,
pub planned_observed_factor: f64,
pub observed_factor_divergence: f64,
pub sample_time_count: usize,
#[cfg(test)]
pub(super) rest_translation_f32_rounding_demand: f64,
#[cfg(test)]
pub(super) trajectory_f32_rounding_demand: f64,
#[cfg(test)]
pub(super) skin_matrix_f32_rounding_demand: f64,
#[cfg(test)]
pub(super) bounds_f32_rounding_demand: f64,
#[cfg(test)]
pub(super) unaffected_inverse_bind_f32_rounding_demand: f64,
}
pub fn prove_scale(
source: &Document,
candidate: &ScaleCandidate,
plan: &ScalePlan,
) -> Result<ScaleProof, ScaleError> {
let candidate = candidate.document();
validate_plan_document_inventory(source, plan)?;
validate_scale_input(candidate)?;
validate_candidate_structure(source, candidate)?;
let mut discharged_field_rows = BTreeSet::new();
let tol = plan.tolerance_policy;
let affected = plan.affected_set();
let affected_skin_instances = if plan.has_skin_and_bounds() {
affected_skin_instance_indices(source, &affected)
} else {
Vec::new()
};
let source_worlds = rest_world_pose(&source.skeleton)?;
let candidate_worlds = rest_world_pose(&candidate.skeleton)?;
if plan.has_obligation(ScaleProofObligation::ExactUnchangedWorldRest) {
for node in (0..source.skeleton.bones.len()).filter(|node| !affected.contains(node)) {
let before = source_worlds.bone(node)?.matrix;
let after = candidate_worlds.bone(node)?.matrix;
if before != after {
return Err(ScaleError::CandidateStructureMismatch {
reason: "unaffected_world_rest_mismatch",
});
}
}
}
let observed_factor = observed_factor_from_source(source, &source_worlds, plan)?;
let mut proof = ScaleProof {
tolerance_policy: tol,
rest_translation: ScaleProofResidual::EMPTY,
rest_rotation: ScaleProofResidual::EMPTY,
unit_scale: ScaleProofResidual::EMPTY,
transform_only_affine: ScaleProofResidual::EMPTY,
track_value: ScaleProofResidual::EMPTY,
mesh_position: ScaleProofResidual::EMPTY,
key_translation: ScaleProofResidual::EMPTY,
cubic_interior: ScaleProofResidual::EMPTY,
trajectory: ScaleProofResidual::EMPTY,
skin_matrix: ScaleProofResidual::EMPTY,
bounds: ScaleProofResidual::EMPTY,
unaffected_inverse_bind: ScaleProofResidual::EMPTY,
observed_factor,
planned_observed_factor: plan.observed_factor,
observed_factor_divergence: relative_divergence(plan.observed_factor, observed_factor),
sample_time_count: 0,
#[cfg(test)]
rest_translation_f32_rounding_demand: 0.0,
#[cfg(test)]
trajectory_f32_rounding_demand: 0.0,
#[cfg(test)]
skin_matrix_f32_rounding_demand: 0.0,
#[cfg(test)]
bounds_f32_rounding_demand: 0.0,
#[cfg(test)]
unaffected_inverse_bind_f32_rounding_demand: 0.0,
};
check_candidate_values(
source,
candidate,
&affected,
plan,
&tol,
&mut proof,
&mut discharged_field_rows,
)?;
if let Some((rest_nodes, prove_unit_scale)) = plan.rest_obligation() {
for &node in rest_nodes {
let before = source_worlds.bone(node)?.matrix;
let after_pose = candidate_worlds.bone(node)?;
let after = after_pose.matrix;
let after_chain = after_pose.translation_rounding_magnitude;
let (translation_residual, before_mag, after_mag) = rest_node_residual(
before,
after,
plan.is_whole_document(),
plan.common_factor(),
);
check_and_track_f32_rounded(
ProofResidualKind::RestTranslation,
translation_residual,
before_mag,
after_mag,
after_chain,
&tol,
&mut proof,
)?;
let source_rotation = source
.skeleton
.bones
.get(node)
.ok_or(ScaleError::BoneIndexOutOfRange { index: node })?
.rest
.rotation;
let candidate_rotation = candidate
.skeleton
.bones
.get(node)
.ok_or(ScaleError::BoneIndexOutOfRange { index: node })?
.rest
.rotation;
let rotation_residual =
quat_residual_radians(quat_equality_residual(source_rotation, candidate_rotation));
record_and_check(
ProofResidualKind::RestRotation,
rotation_residual,
tol.rotation_residual_radians,
&mut proof,
)?;
if prove_unit_scale {
let (after_scale, ..) = after.to_scale_rotation_translation();
let residual = (after_scale.x as f64 - 1.0)
.abs()
.max((after_scale.y as f64 - 1.0).abs())
.max((after_scale.z as f64 - 1.0).abs());
record_and_check(
ProofResidualKind::UnitScale,
residual,
tol.postcondition_unit_scale_residual,
&mut proof,
)?;
}
}
}
for (row_index, row) in plan.field_rows().iter().enumerate() {
if matches!(row.target, ScaleFieldTarget::BoneRest { .. }) {
mark_field_row_discharged(&mut discharged_field_rows, row_index)?;
}
}
if let Some(transform_only_nodes) = plan.transform_only_nodes() {
let correction = Mat4::from_scale(Vec3::splat((1.0 / plan.common_factor()) as f32));
let probe = Vec3::ONE;
for &node in transform_only_nodes {
let before = source_worlds.bone(node)?.matrix;
let after = candidate_worlds.bone(node)?.matrix;
let expected_point = (before * correction).transform_point3(probe).as_dvec3();
let actual_point = after.transform_point3(probe).as_dvec3();
let residual = (actual_point - expected_point).length();
check_and_track(
ProofResidualKind::TransformOnlyAffine,
residual,
expected_point.length(),
actual_point.length(),
&tol,
&mut proof,
)?;
}
}
check_skin_and_bounds(
source,
candidate,
&source_worlds,
&candidate_worlds,
&affected_skin_instances,
plan,
&tol,
&mut proof,
)?;
if plan.has_unaffected_binds() {
check_unaffected_instance_binds(source, candidate, &affected, &tol, &mut proof)?;
}
for (row_index, row) in plan.field_rows().iter().enumerate() {
if matches!(
row.target,
ScaleFieldTarget::BoneInverseBind { .. }
| ScaleFieldTarget::InstanceInverseBind { .. }
| ScaleFieldTarget::MeshNormals { .. }
) {
mark_field_row_discharged(&mut discharged_field_rows, row_index)?;
}
}
let any_sampled_obligation = plan.has_key_translations()
|| plan.has_cubic_interiors()
|| plan.trajectory_nodes().is_some()
|| plan.has_skin_and_bounds();
if any_sampled_obligation {
let mut clip_times = Vec::with_capacity(source.clips.len());
let mut sample_times: u64 = 0;
for clip in &source.clips {
let times = clip_sample_times(clip, &affected);
sample_times = sample_times
.saturating_add(times.0.len() as u64)
.saturating_add(times.1.len() as u64);
clip_times.push(times);
}
let per_sample_cost = per_sample_work_units(source, &affected_skin_instances);
check_sampling_budget(&tol, sample_times, per_sample_cost)?;
for (clip_index, clip) in source.clips.iter().enumerate() {
let candidate_clip =
candidate
.clips
.get(clip_index)
.ok_or(ScaleError::MissingProofEvidence {
kind: ProofResidualKind::KeyTranslation,
detail: "candidate_clip_missing",
})?;
let (key_times, interior_times) = &clip_times[clip_index];
for &t in key_times {
proof.sample_time_count += 1;
if plan.has_key_translations() {
check_track_value_residual(
ProofResidualKind::KeyTranslation,
source,
clip,
candidate_clip,
&affected,
t,
plan,
&tol,
&mut proof,
)?;
}
sample_time_obligations(
source,
candidate,
clip,
candidate_clip,
t,
&affected_skin_instances,
plan,
&tol,
&mut proof,
)?;
}
for &t in interior_times {
proof.sample_time_count += 1;
if plan.has_cubic_interiors() {
check_track_value_residual(
ProofResidualKind::CubicInterior,
source,
clip,
candidate_clip,
&affected,
t,
plan,
&tol,
&mut proof,
)?;
}
sample_time_obligations(
source,
candidate,
clip,
candidate_clip,
t,
&affected_skin_instances,
plan,
&tol,
&mut proof,
)?;
}
}
}
check_rewritten_source_field_dispositions(
source,
candidate,
plan,
&affected,
&tol,
&mut discharged_field_rows,
)?;
check_preserved_field_dispositions(source, candidate, plan, &mut discharged_field_rows)?;
finish_field_row_discharge(plan, &discharged_field_rows)?;
Ok(proof)
}
#[allow(clippy::too_many_arguments)]
fn sample_time_obligations(
source: &Document,
candidate: &Document,
source_clip: &Clip,
candidate_clip: &Clip,
t: f32,
affected_skin_instances: &[usize],
plan: &ScalePlan,
tol: &ScaleTolerancePolicy,
proof: &mut ScaleProof,
) -> Result<(), ScaleError> {
if plan.trajectory_nodes().is_none() && !plan.has_skin_and_bounds() {
return Ok(());
}
let source_worlds = world_at_time(&source.skeleton, source_clip, t)?;
let candidate_worlds = world_at_time(&candidate.skeleton, candidate_clip, t)?;
if let Some(nodes) = plan.trajectory_nodes() {
check_trajectory_residual_at(&source_worlds, &candidate_worlds, nodes, plan, tol, proof)?;
}
check_skin_and_bounds(
source,
candidate,
&source_worlds,
&candidate_worlds,
affected_skin_instances,
plan,
tol,
proof,
)
}
const PROOF_SIDES: u64 = 2;
pub(super) fn check_sampling_budget(
tol: &ScaleTolerancePolicy,
sample_times: u64,
per_sample_cost: u64,
) -> Result<(), ScaleError> {
let work = sample_times.saturating_mul(per_sample_cost);
if work > tol.proof_sample_work_budget {
return Err(ScaleError::ProofSamplingBudgetExceeded {
policy_id: tol.id,
sample_times,
per_sample_cost,
work,
budget: tol.proof_sample_work_budget,
});
}
Ok(())
}
pub(super) fn per_sample_work_units(document: &Document, affected_skin_instances: &[usize]) -> u64 {
let mut units = PROOF_SIDES.saturating_mul(document.skeleton.bones.len() as u64);
for &instance_index in affected_skin_instances {
let instance = &document.assets.instances[instance_index];
let slots = instance.skin_joints.len() as u64;
units = units.saturating_add(PROOF_SIDES.saturating_mul(slots));
units = units.saturating_add(slots);
let Some(mesh) = document.assets.meshes.get(instance.mesh) else {
continue;
};
for primitive in &mesh.primitives {
units =
units.saturating_add(PROOF_SIDES.saturating_mul(primitive.positions.len() as u64));
}
}
units
}
pub(super) fn check_residual(
kind: ProofResidualKind,
observed: f64,
tolerance: f64,
) -> Result<(), ScaleError> {
if !observed.is_finite() || !tolerance.is_finite() || observed > tolerance {
return Err(ScaleError::ProofResidualExceeded {
kind,
observed,
tolerance,
});
}
Ok(())
}
impl ScaleProof {
#[cfg(test)]
pub(super) fn record_f32_rounding_demand(
&mut self,
kind: ProofResidualKind,
observed: f64,
magnitude: f64,
) {
let demand = if magnitude > 0.0 {
observed / (magnitude * f64::from(f32::EPSILON))
} else if observed == 0.0 {
0.0
} else {
f64::INFINITY
};
let slot = match kind {
ProofResidualKind::RestTranslation => &mut self.rest_translation_f32_rounding_demand,
ProofResidualKind::Trajectory => &mut self.trajectory_f32_rounding_demand,
ProofResidualKind::SkinMatrix => &mut self.skin_matrix_f32_rounding_demand,
ProofResidualKind::Bounds => &mut self.bounds_f32_rounding_demand,
ProofResidualKind::UnaffectedInverseBind => {
&mut self.unaffected_inverse_bind_f32_rounding_demand
}
_ => unreachable!("only f32-rounded residual kinds record a raw rounding demand"),
};
*slot = slot.max(demand);
}
fn tally(&mut self, kind: ProofResidualKind) -> Option<&mut ScaleProofResidual> {
let tally = match kind {
ProofResidualKind::RestTranslation => &mut self.rest_translation,
ProofResidualKind::RestRotation => &mut self.rest_rotation,
ProofResidualKind::UnitScale => &mut self.unit_scale,
ProofResidualKind::TransformOnlyAffine => &mut self.transform_only_affine,
ProofResidualKind::TrackValue => &mut self.track_value,
ProofResidualKind::MeshPosition => &mut self.mesh_position,
ProofResidualKind::KeyTranslation => &mut self.key_translation,
ProofResidualKind::CubicInterior => &mut self.cubic_interior,
ProofResidualKind::Trajectory => &mut self.trajectory,
ProofResidualKind::SkinMatrix => &mut self.skin_matrix,
ProofResidualKind::Bounds => &mut self.bounds,
ProofResidualKind::UnaffectedInverseBind => &mut self.unaffected_inverse_bind,
ProofResidualKind::ObservedFactor => return None,
};
Some(tally)
}
}
fn record_and_check(
kind: ProofResidualKind,
observed: f64,
tolerance: f64,
proof: &mut ScaleProof,
) -> Result<(), ScaleError> {
if let Some(tally) = proof.tally(kind) {
tally.record(observed);
}
check_residual(kind, observed, tolerance)
}
fn check_and_track(
kind: ProofResidualKind,
observed: f64,
before: f64,
after: f64,
tol: &ScaleTolerancePolicy,
proof: &mut ScaleProof,
) -> Result<(), ScaleError> {
record_and_check(kind, observed, tol.scalar_tolerance(before, after), proof)
}
fn check_and_track_f32_rounded(
kind: ProofResidualKind,
observed: f64,
before: f64,
after: f64,
magnitude: f64,
tol: &ScaleTolerancePolicy,
proof: &mut ScaleProof,
) -> Result<(), ScaleError> {
#[cfg(test)]
proof.record_f32_rounding_demand(kind, observed, magnitude);
record_and_check(
kind,
observed,
tol.f32_rounded_tolerance(before, after, magnitude),
proof,
)
}
fn rest_node_residual(
before: Mat4,
after: Mat4,
whole_document: bool,
factor: f64,
) -> (f64, f64, f64) {
let (_, _, before_translation) = before.to_scale_rotation_translation();
let (_, _, after_translation) = after.to_scale_rotation_translation();
let expected_translation = if whole_document {
before_translation.as_dvec3() * factor
} else {
before_translation.as_dvec3()
};
let actual_translation = after_translation.as_dvec3();
let translation_residual = (actual_translation - expected_translation).length();
(
translation_residual,
expected_translation.length(),
actual_translation.length(),
)
}
fn translation_multiplier(
document: &Document,
node: BoneId,
affected: &BTreeSet<BoneId>,
plan: &ScalePlan,
) -> f64 {
if plan.is_whole_document() {
return plan.common_factor();
}
if !affected.contains(&node) {
return 1.0;
}
match document
.skeleton
.bones
.get(node)
.and_then(|bone| bone.parent)
{
Some(parent) if affected.contains(&parent) => plan.common_factor(),
_ => 1.0,
}
}
fn proof_scale_animation_root(
source: &Document,
plan: &ScalePlan,
) -> Result<Option<BoneId>, ScaleError> {
let ScaleOperation::RestBindUniformScale {
source_root_node_index,
..
} = plan.operation()
else {
return Ok(None);
};
let selected_root = source
.assets
.source_skeleton
.nodes
.iter()
.find(|asset| asset.source_node_index == source_root_node_index)
.and_then(|asset| asset.bone)
.ok_or(ScaleError::PlanDocumentMismatch {
reason: "selected_root_projection_mismatch",
})?;
Ok(Some(selected_root))
}
fn check_candidate_values(
source: &Document,
candidate: &Document,
affected: &BTreeSet<BoneId>,
plan: &ScalePlan,
tol: &ScaleTolerancePolicy,
proof: &mut ScaleProof,
discharged: &mut BTreeSet<usize>,
) -> Result<(), ScaleError> {
let proof_scale_root = proof_scale_animation_root(source, plan)?;
for (row_index, row) in plan.field_rows().iter().enumerate() {
match row.target {
ScaleFieldTarget::AnimationValues {
clip_index,
track_index,
bone,
property,
} => {
let track = &source.clips[clip_index].tracks[track_index];
let candidate_track = &candidate.clips[clip_index].tracks[track_index];
if track.bone != bone || track.property != property {
return Err(ScaleError::PlanDocumentMismatch {
reason: "compiled_animation_target_mismatch",
});
}
match (&track.values, &candidate_track.values) {
(TrackValues::Vec3s(before), TrackValues::Vec3s(after)) => {
let multiplier = match row.disposition {
ScaleFieldDisposition::PreserveExact => 1.0,
ScaleFieldDisposition::Rewrite(
ScaleRewriteRule::WholeDocumentLength,
) => plan.common_factor(),
ScaleFieldDisposition::Rewrite(
ScaleRewriteRule::RestBindParentBasis,
) => translation_multiplier(source, bone, affected, plan),
ScaleFieldDisposition::Rewrite(
ScaleRewriteRule::RestBindLocalScale,
) => {
if proof_scale_root == Some(bone) {
1.0 / plan.common_factor()
} else {
1.0
}
}
ScaleFieldDisposition::Rewrite(
ScaleRewriteRule::RestBindNodeBasis
| ScaleRewriteRule::RestBindSourceLocal { .. },
) => {
return Err(ScaleError::PlanDocumentMismatch {
reason: "invalid_animation_rewrite_rule",
});
}
};
for (before, after) in before.iter().zip(after.iter()) {
let expected = before.as_dvec3() * multiplier;
let actual = after.as_dvec3();
let residual = (actual - expected).length();
check_and_track(
ProofResidualKind::TrackValue,
residual,
expected.length(),
actual.length(),
tol,
proof,
)?;
}
}
(TrackValues::Quats(before), TrackValues::Quats(after)) => {
for (before, after) in before.iter().zip(after.iter()) {
let residual = quat_equality_residual(*before, *after);
check_and_track(
ProofResidualKind::TrackValue,
residual,
before.length() as f64,
after.length() as f64,
tol,
proof,
)?;
}
}
_ => {
return Err(ScaleError::CandidateStructureMismatch {
reason: "track_value_variant_mismatch",
});
}
}
mark_field_row_discharged(discharged, row_index)?;
}
ScaleFieldTarget::MeshPositions {
mesh_index,
primitive_index,
} => {
let source_primitive =
&source.assets.meshes[mesh_index].primitives[primitive_index];
let candidate_primitive =
&candidate.assets.meshes[mesh_index].primitives[primitive_index];
let position_multiplier = match row.disposition {
ScaleFieldDisposition::PreserveExact => 1.0,
ScaleFieldDisposition::Rewrite(ScaleRewriteRule::WholeDocumentLength) => {
plan.common_factor()
}
ScaleFieldDisposition::Rewrite(_) => {
return Err(ScaleError::PlanDocumentMismatch {
reason: "invalid_mesh_position_rewrite_rule",
});
}
};
for (before, after) in source_primitive
.positions
.iter()
.zip(candidate_primitive.positions.iter())
{
let expected = before.as_dvec3() * position_multiplier;
let actual = after.as_dvec3();
let residual = (actual - expected).length();
check_and_track(
ProofResidualKind::MeshPosition,
residual,
expected.length(),
actual.length(),
tol,
proof,
)?;
}
mark_field_row_discharged(discharged, row_index)?;
}
_ => {}
}
}
Ok(())
}
fn quat_equality_residual(before: Quat, after: Quat) -> f64 {
let before = before.as_dquat();
let after = after.as_dquat();
(before - after).length().min((before + after).length())
}
fn quat_residual_radians(chord: f64) -> f64 {
4.0 * (chord / 2.0).min(1.0).asin()
}
fn clip_sample_times(clip: &Clip, affected: &BTreeSet<BoneId>) -> (Vec<f32>, Vec<f32>) {
let mut keys = Vec::new();
let mut interiors = Vec::new();
for track in &clip.tracks {
if !affected.contains(&track.bone) {
continue;
}
keys.extend_from_slice(&track.times);
if track.interpolation == Interpolation::CubicSpline {
for window in track.times.windows(2) {
interiors.push((window[0] + window[1]) * 0.5);
}
}
}
keys.sort_by(f32::total_cmp);
keys.dedup();
interiors.sort_by(f32::total_cmp);
interiors.dedup();
(keys, interiors)
}
#[allow(clippy::too_many_arguments)]
fn check_track_value_residual(
kind: ProofResidualKind,
source: &Document,
source_clip: &Clip,
candidate_clip: &Clip,
affected: &BTreeSet<BoneId>,
t: f32,
plan: &ScalePlan,
tol: &ScaleTolerancePolicy,
proof: &mut ScaleProof,
) -> Result<(), ScaleError> {
for (track, candidate_track) in source_clip.tracks.iter().zip(candidate_clip.tracks.iter()) {
if track.property != Property::Translation || !affected.contains(&track.bone) {
continue;
}
let multiplier = translation_multiplier(source, track.bone, affected, plan);
let TrackSample::Vec3(before) = sample_track(track, t) else {
return Err(ScaleError::MissingProofEvidence {
kind,
detail: "source_sample_not_vec3",
});
};
let TrackSample::Vec3(after) = sample_track(candidate_track, t) else {
return Err(ScaleError::MissingProofEvidence {
kind,
detail: "candidate_sample_not_vec3",
});
};
let expected = before.as_dvec3() * multiplier;
let actual = after.as_dvec3();
let residual = (actual - expected).length();
check_and_track(
kind,
residual,
expected.length(),
actual.length(),
tol,
proof,
)?;
}
Ok(())
}
fn check_trajectory_residual_at(
source_worlds: &WorldPose,
candidate_worlds: &WorldPose,
affected_nodes: &[BoneId],
plan: &ScalePlan,
tol: &ScaleTolerancePolicy,
proof: &mut ScaleProof,
) -> Result<(), ScaleError> {
for &node in affected_nodes {
let before = source_worlds.bone(node)?.matrix;
let after_pose = candidate_worlds.bone(node)?;
let after = after_pose.matrix;
let after_chain = after_pose.translation_rounding_magnitude;
let (translation_residual, before_mag, after_mag) = rest_node_residual(
before,
after,
plan.is_whole_document(),
plan.common_factor(),
);
check_and_track_f32_rounded(
ProofResidualKind::Trajectory,
translation_residual,
before_mag,
after_mag,
after_chain,
tol,
proof,
)?;
}
Ok(())
}
pub(super) fn world_at_time(
skeleton: &Skeleton,
clip: &Clip,
t: f32,
) -> Result<WorldPose, ScaleError> {
let bone_count = skeleton.bones.len();
let mut locals = vec![Transform::IDENTITY; bone_count];
for (index, bone) in skeleton.bones.iter().enumerate() {
locals[index] = bone.rest;
}
for track in &clip.tracks {
if track.bone >= bone_count {
return Err(ScaleError::BoneIndexOutOfRange { index: track.bone });
}
match sample_track(track, t) {
TrackSample::Vec3(value) => {
if !value.is_finite() {
return Err(ScaleError::NonFiniteTransform { node: track.bone });
}
match track.property {
Property::Translation => locals[track.bone].translation = value,
Property::Scale => locals[track.bone].scale = value,
Property::Rotation => {}
}
}
TrackSample::Quat(value) => {
if !value.is_finite() {
return Err(ScaleError::NonFiniteTransform { node: track.bone });
}
locals[track.bone].rotation = value;
}
}
}
let mut bones: Vec<WorldBonePose> = Vec::with_capacity(bone_count);
for (index, bone) in skeleton.bones.iter().enumerate() {
let local = locals[index].to_mat4();
if !mat4_is_finite(local) {
return Err(ScaleError::NonFiniteTransform { node: index });
}
let pose = match bone.parent {
Some(parent) if parent < index => {
let parent_pose = bones[parent];
let matrix = parent_pose.matrix * local;
WorldBonePose {
matrix,
translation_rounding_magnitude: child_translation_rounding_magnitude(
parent_pose,
local,
),
}
}
Some(parent) => {
return Err(ScaleError::InvalidParent {
node: index,
parent,
});
}
None => WorldBonePose {
matrix: local,
translation_rounding_magnitude: 0.0,
},
};
if !mat4_is_finite(pose.matrix) {
return Err(ScaleError::NonFiniteTransform { node: index });
}
bones.push(pose);
}
Ok(WorldPose { bones })
}
fn relative_divergence(planned: f64, proved: f64) -> f64 {
(planned - proved).abs() / planned.abs().max(proved.abs())
}
pub(super) fn observed_factor_from_source(
source: &Document,
source_worlds: &WorldPose,
plan: &ScalePlan,
) -> Result<f64, ScaleError> {
let ScaleOperation::RestBindUniformScale {
source_root_node_index,
..
} = plan.operation()
else {
return Ok(plan.common_factor());
};
let bone = source_node_index_map(source)
.get(&source_root_node_index)
.and_then(|asset| asset.bone)
.ok_or(ScaleError::MissingProofEvidence {
kind: ProofResidualKind::ObservedFactor,
detail: "scaled_root_not_projected",
})?;
let world = source_worlds.bone(bone)?.matrix;
Ok(average_affine_axis_length(affine_axis_lengths(
Mat3::from_mat4(world),
)))
}
fn check_unaffected_instance_binds(
source: &Document,
candidate: &Document,
affected: &BTreeSet<BoneId>,
tol: &ScaleTolerancePolicy,
proof: &mut ScaleProof,
) -> Result<(), ScaleError> {
for (instance, candidate_instance) in source
.assets
.instances
.iter()
.zip(candidate.assets.instances.iter())
{
if instance
.skin_joints
.iter()
.any(|joint| affected.contains(joint))
{
continue;
}
for (slot, &joint) in instance.skin_joints.iter().enumerate() {
let before = stored_instance_bind(source, instance, slot, joint)?;
let after = stored_instance_bind(candidate, candidate_instance, slot, joint)?;
let (before, after) = match (before, after) {
(None, None) => continue,
(Some(_), None) => {
return Err(ScaleError::MissingProofEvidence {
kind: ProofResidualKind::UnaffectedInverseBind,
detail: "candidate_slot_bind_missing",
});
}
(None, Some(_)) => {
return Err(ScaleError::MissingProofEvidence {
kind: ProofResidualKind::UnaffectedInverseBind,
detail: "source_slot_bind_missing",
});
}
(Some(before), Some(after)) => (before, after),
};
let expected = before;
let residual = matrix_residual(expected, after);
let magnitude = matrix_magnitude(expected).max(matrix_magnitude(after));
check_and_track_f32_rounded(
ProofResidualKind::UnaffectedInverseBind,
residual,
matrix_magnitude(expected),
matrix_magnitude(after),
magnitude,
tol,
proof,
)?;
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn check_skin_and_bounds(
source: &Document,
candidate: &Document,
source_worlds: &WorldPose,
candidate_worlds: &WorldPose,
affected_skin_instances: &[usize],
plan: &ScalePlan,
tol: &ScaleTolerancePolicy,
proof: &mut ScaleProof,
) -> Result<(), ScaleError> {
if !plan.has_skin_and_bounds() {
return Ok(());
}
let mut source_bounds = BoundsAccumulator::default();
let mut candidate_bounds = BoundsAccumulator::default();
let q = if plan.is_whole_document() {
plan.common_factor()
} else {
1.0
};
for &instance_index in affected_skin_instances {
let instance = &source.assets.instances[instance_index];
let candidate_instance = candidate.assets.instances.get(instance_index).ok_or(
ScaleError::MissingProofEvidence {
kind: ProofResidualKind::SkinMatrix,
detail: "candidate_instance_missing",
},
)?;
let mut source_slots = Vec::with_capacity(instance.skin_joints.len());
let mut candidate_slots = Vec::with_capacity(instance.skin_joints.len());
for (slot, &joint) in instance.skin_joints.iter().enumerate() {
let before_pose = source_worlds.bone(joint)?;
let after_pose = candidate_worlds.bone(joint)?;
let before_world = before_pose.matrix;
let before_chain = before_pose.translation_rounding_magnitude;
let after_world = after_pose.matrix;
let after_chain = after_pose.translation_rounding_magnitude;
let before_ibm = instance_bind(source, instance, slot, joint)?;
let after_ibm = instance_bind(candidate, candidate_instance, slot, joint)?;
source_slots.push(SkinSlot::compose(before_world, before_ibm, before_chain));
candidate_slots.push(SkinSlot::compose(after_world, after_ibm, after_chain));
}
for (before, after) in source_slots.iter().zip(candidate_slots.iter()) {
let expected = if plan.is_whole_document() {
scale_translation_only(before.matrix, plan.common_factor() as f32)
} else {
before.matrix
};
let residual = matrix_residual(expected, after.matrix);
let magnitude = after.rounding_magnitude.max(q * before.rounding_magnitude);
check_and_track_f32_rounded(
ProofResidualKind::SkinMatrix,
residual,
matrix_magnitude(expected),
matrix_magnitude(after.matrix),
magnitude,
tol,
proof,
)?;
}
let mesh = source.assets.meshes.get(instance.mesh).ok_or(
DocumentShapeError::MeshInstanceShape {
instance_index,
violation: MeshInstanceShapeViolation::MeshIndexOutOfRange,
},
)?;
let candidate_mesh = candidate.assets.meshes.get(candidate_instance.mesh).ok_or(
DocumentShapeError::MeshInstanceShape {
instance_index,
violation: MeshInstanceShapeViolation::MeshIndexOutOfRange,
},
)?;
for (primitive_index, (primitive, candidate_primitive)) in mesh
.primitives
.iter()
.zip(candidate_mesh.primitives.iter())
.enumerate()
{
accumulate_skinned_bounds(
instance_index,
primitive_index,
primitive,
&source_slots,
&mut source_bounds,
)?;
accumulate_skinned_bounds(
instance_index,
primitive_index,
candidate_primitive,
&candidate_slots,
&mut candidate_bounds,
)?;
}
}
let source_bounds_magnitude = source_bounds.rounding_magnitude();
let candidate_bounds_magnitude = candidate_bounds.rounding_magnitude();
let (before_min, before_max) =
source_bounds
.finish()
.ok_or(ScaleError::MissingProofEvidence {
kind: ProofResidualKind::Bounds,
detail: "source_bounds_missing",
})?;
let (after_min, after_max) =
candidate_bounds
.finish()
.ok_or(ScaleError::MissingProofEvidence {
kind: ProofResidualKind::Bounds,
detail: "candidate_bounds_missing",
})?;
let magnitude = candidate_bounds_magnitude.max(q * source_bounds_magnitude);
for (before, after) in [(before_min, after_min), (before_max, after_max)] {
let before = before.to_array();
let after = after.to_array();
for axis in 0..3 {
let b = before[axis] as f64;
let a = after[axis] as f64;
let expected = b * q;
let residual = (a - expected).abs();
check_and_track_f32_rounded(
ProofResidualKind::Bounds,
residual,
expected,
a,
magnitude,
tol,
proof,
)?;
}
}
Ok(())
}
#[derive(Debug, Clone, Copy)]
pub(super) struct SkinSlot {
pub(super) matrix: Mat4,
pub(super) absolute: Mat4,
pub(super) rounding_magnitude: f64,
}
impl SkinSlot {
pub(super) fn compose(
world: Mat4,
inverse_bind: Mat4,
world_translation_rounding_magnitude: f64,
) -> Self {
let matrix = world * inverse_bind;
Self {
matrix,
absolute: mat4_abs(matrix),
rounding_magnitude: product_operand_magnitude(world, inverse_bind)
.max(world_translation_rounding_magnitude),
}
}
}
pub(super) struct BoundsAccumulator {
min: Vec3,
max: Vec3,
touched: bool,
rounding_magnitude: f64,
}
impl Default for BoundsAccumulator {
fn default() -> Self {
Self {
min: Vec3::splat(f32::INFINITY),
max: Vec3::splat(f32::NEG_INFINITY),
touched: false,
rounding_magnitude: 0.0,
}
}
}
impl BoundsAccumulator {
pub(super) fn finish(self) -> Option<(Vec3, Vec3)> {
self.touched.then_some((self.min, self.max))
}
pub(super) fn rounding_magnitude(&self) -> f64 {
self.rounding_magnitude
}
}
pub(super) fn accumulate_skinned_bounds(
instance_index: usize,
primitive_index: usize,
primitive: &Primitive,
slots: &[SkinSlot],
bounds: &mut BoundsAccumulator,
) -> Result<(), ScaleError> {
if primitive.joints.len() != primitive.positions.len()
|| primitive.weights.len() != primitive.positions.len()
{
return Err(ScaleError::InvalidSkinnedPrimitive {
instance_index,
primitive_index,
reason: "joints_or_weights_length_mismatch",
});
}
for (vertex, &position) in primitive.positions.iter().enumerate() {
if !position.is_finite() {
return Err(ScaleError::InvalidSkinnedPrimitive {
instance_index,
primitive_index,
reason: "non_finite_position",
});
}
let joints = primitive.joints[vertex];
let weights = primitive.weights[vertex];
let mut weight_sum = 0.0f64;
for weight in weights {
if weight == 0.0 {
continue;
}
if !weight.is_finite() {
return Err(ScaleError::InvalidSkinnedPrimitive {
instance_index,
primitive_index,
reason: "non_finite_weight",
});
}
weight_sum += f64::from(weight);
}
if weight_sum == 0.0 {
continue;
}
let mut skinned_numerator = DVec3::ZERO;
let mut weighted_magnitude = 0.0f64;
for slot_index in 0..4 {
let stored_weight = weights[slot_index];
if stored_weight == 0.0 {
continue;
}
let Some(slot) = slots.get(joints[slot_index] as usize) else {
return Err(ScaleError::InvalidSkinnedPrimitive {
instance_index,
primitive_index,
reason: "joint_influence_slot_out_of_range",
});
};
let weight = f64::from(stored_weight);
skinned_numerator += weight * slot.matrix.transform_point3(position).as_dvec3();
let influence_magnitude = skin_influence_magnitude(slot, position);
weighted_magnitude += weight * influence_magnitude;
}
let skinned = (skinned_numerator / weight_sum).as_vec3();
if !skinned.is_finite() {
return Err(ScaleError::InvalidSkinnedPrimitive {
instance_index,
primitive_index,
reason: if skinned.is_nan() {
"non_finite_result"
} else {
"skinned_magnitude_overflow"
},
});
}
bounds.min = bounds.min.min(skinned);
bounds.max = bounds.max.max(skinned);
let vertex_magnitude = weighted_magnitude / weight_sum;
bounds.rounding_magnitude = bounds.rounding_magnitude.max(vertex_magnitude);
bounds.touched = true;
}
Ok(())
}
pub(super) fn skin_influence_magnitude(slot: &SkinSlot, position: Vec3) -> f64 {
column_operand_magnitude(slot.absolute, position.extend(1.0)).max(slot.rounding_magnitude)
}