use alloc::vec::Vec;
use core::fmt;
use super::ValidateConfig;
use super::tri_grid::TriangleGrid;
use crate::{Real, Sdf, Shape3, vec3};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AccuracyConfig {
cell_size: f64,
band_radius: f64,
residual_tolerance: f64,
}
impl AccuracyConfig {
pub const BAND_RADIUS_REL: f64 = 1.0;
pub const RESIDUAL_TOLERANCE_REL: f64 = 1e-4;
pub const MAX_NEWTON_ITERATIONS: u32 = 8;
pub fn from_cell_size(cell_size: f64) -> crate::Result<Self> {
if !cell_size.is_finite() || cell_size <= 0.0 {
return Err(crate::Error::InvalidCellSize { value: cell_size });
}
Ok(Self {
cell_size,
band_radius: cell_size * Self::BAND_RADIUS_REL,
residual_tolerance: cell_size * Self::RESIDUAL_TOLERANCE_REL,
})
}
#[must_use]
pub fn cell_size(&self) -> f64 {
self.cell_size
}
#[must_use]
pub fn band_radius(&self) -> f64 {
self.band_radius
}
#[must_use]
pub fn residual_tolerance(&self) -> f64 {
self.residual_tolerance
}
#[must_use]
pub fn cell_diagonal(&self) -> f64 {
self.cell_size * libm::sqrt(3.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DistanceStats<R: Real> {
pub samples: u64,
pub max: R,
pub mean: R,
}
impl<R: Real> DistanceStats<R> {
const EMPTY: Self = Self {
samples: 0,
max: R::ZERO,
mean: R::ZERO,
};
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AccuracyReport<R: Real> {
pub mesh_to_field: DistanceStats<R>,
pub field_to_mesh: DistanceStats<R>,
pub vertex_samples: u64,
pub centroid_samples: u64,
pub unconverged_mesh_samples: u64,
pub seeds: u64,
pub seeds_out_of_band: u64,
pub seeds_non_finite: u64,
pub seeds_unconverged: u64,
pub seeds_outside_domain: u64,
pub triangles: u64,
pub faces_skipped: u64,
pub degenerate_triangles: u64,
pub non_finite_positions: u64,
pub config: AccuracyConfig,
}
impl<R: Real> AccuracyReport<R> {
#[must_use]
pub fn symmetric_hausdorff(&self) -> R {
self.mesh_to_field.max.max(self.field_to_mesh.max)
}
#[must_use]
pub fn mean_absolute_error(&self) -> R {
self.mesh_to_field.mean
}
#[must_use]
pub fn has_coverage(&self) -> bool {
self.mesh_to_field.samples > 0 && self.field_to_mesh.samples > 0
}
pub fn panic_if_worse_than(&self, limit: R) {
assert!(self.has_coverage(), "no accuracy coverage\n{self}");
assert!(
self.symmetric_hausdorff() <= limit,
"mesh is further than {limit:?} from the surface\n{self}"
);
}
}
impl<R: Real> fmt::Display for AccuracyReport<R> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "accuracy")?;
writeln!(f, " h {:?}", self.config.cell_size())?;
writeln!(f, " ---------------------------")?;
writeln!(
f,
" mesh->field max {:?} mean {:?} n {}",
self.mesh_to_field.max, self.mesh_to_field.mean, self.mesh_to_field.samples
)?;
writeln!(
f,
" field->mesh max {:?} mean {:?} n {}",
self.field_to_mesh.max, self.field_to_mesh.mean, self.field_to_mesh.samples
)?;
writeln!(f, " symmetric {:?}", self.symmetric_hausdorff())?;
writeln!(f, " ---------------------------")?;
writeln!(
f,
" mesh samples {} vertices + {} centroids, {} unconverged",
self.vertex_samples, self.centroid_samples, self.unconverged_mesh_samples
)?;
writeln!(
f,
" seeds {} total: {} out of band, {} non-finite, {} unconverged, {} outside",
self.seeds,
self.seeds_out_of_band,
self.seeds_non_finite,
self.seeds_unconverged,
self.seeds_outside_domain
)?;
writeln!(
f,
" triangles {} used, {} skipped, {} degenerate, {} non-finite",
self.triangles,
self.faces_skipped,
self.degenerate_triangles,
self.non_finite_positions
)?;
if self.has_coverage() {
write!(f, " measured both directions")
} else {
write!(
f,
" !! NO COVERAGE — one or both directions sampled nothing"
)
}
}
}
pub fn accuracy<R, S>(
positions: &[[R; 3]],
indices: &[u32],
sdf: &S,
shape: &impl Shape3,
origin: [R; 3],
cfg: &AccuracyConfig,
) -> crate::Result<AccuracyReport<R>>
where
R: Real,
S: Sdf<Scalar = R>,
{
let size = shape.size();
if size.iter().any(|&n| n < 2) {
return Err(crate::Error::GridTooSmall { size });
}
let h = R::from_f64(cfg.cell_size());
let band = R::from_f64(cfg.band_radius());
let tol = R::from_f64(cfg.residual_tolerance());
let mut report = AccuracyReport {
mesh_to_field: DistanceStats::EMPTY,
field_to_mesh: DistanceStats::EMPTY,
vertex_samples: 0,
centroid_samples: 0,
unconverged_mesh_samples: 0,
seeds: 0,
seeds_out_of_band: 0,
seeds_non_finite: 0,
seeds_unconverged: 0,
seeds_outside_domain: 0,
triangles: 0,
faces_skipped: 0,
degenerate_triangles: 0,
non_finite_positions: 0,
config: *cfg,
};
let two_area_limit =
R::from_f64(2.0 * ValidateConfig::AREA_EPSILON_REL * cfg.cell_size() * cfg.cell_size());
let limit_sq = two_area_limit * two_area_limit;
let whole = indices.len() - indices.len() % 3;
report.faces_skipped = u64::from(!indices.len().is_multiple_of(3));
let mut tris: Vec<[u32; 3]> = Vec::with_capacity(whole / 3);
let mut referenced = alloc::vec![false; positions.len()];
for face in indices[..whole].as_chunks::<3>().0 {
let in_range = face.iter().all(|&i| (i as usize) < positions.len());
let distinct = face[0] != face[1] && face[1] != face[2] && face[0] != face[2];
if !in_range || !distinct {
report.faces_skipped += 1;
continue;
}
let a = positions[face[0] as usize];
let b = positions[face[1] as usize];
let c = positions[face[2] as usize];
if !finite(a) || !finite(b) || !finite(c) {
report.non_finite_positions += 1;
continue;
}
let cross = vec3::cross(vec3::sub(b, a), vec3::sub(c, a));
if vec3::length_squared(cross) <= limit_sq {
report.degenerate_triangles += 1;
continue;
}
for &i in face {
referenced[i as usize] = true;
}
tris.push([face[0], face[1], face[2]]);
}
report.triangles = tris.len() as u64;
let mut forward = Accumulator::<R>::new();
for (i, &p) in positions.iter().enumerate() {
if !referenced[i] {
continue;
}
report.vertex_samples += 1;
match project(
sdf,
p,
R::INFINITY,
tol,
AccuracyConfig::MAX_NEWTON_ITERATIONS,
) {
Projected::Converged(q) => forward.push(vec3::length(vec3::sub(p, q))),
_ => report.unconverged_mesh_samples += 1,
}
}
let third = R::ONE / R::from_f64(3.0);
for t in &tris {
let a = positions[t[0] as usize];
let b = positions[t[1] as usize];
let c = positions[t[2] as usize];
let centroid = vec3::scale(
[a[0] + b[0] + c[0], a[1] + b[1] + c[1], a[2] + b[2] + c[2]],
third,
);
report.centroid_samples += 1;
match project(
sdf,
centroid,
R::INFINITY,
tol,
AccuracyConfig::MAX_NEWTON_ITERATIONS,
) {
Projected::Converged(q) => forward.push(vec3::length(vec3::sub(centroid, q))),
_ => report.unconverged_mesh_samples += 1,
}
}
report.mesh_to_field = forward.finish();
let grid = TriangleGrid::build(positions, &tris, cfg.cell_size())?;
let lo_accept = [origin[0] + h, origin[1] + h, origin[2] + h];
let hi_accept = [
origin[0] + h * R::from_f64(f64::from(size[0] - 2)),
origin[1] + h * R::from_f64(f64::from(size[1] - 2)),
origin[2] + h * R::from_f64(f64::from(size[2] - 2)),
];
let mut reverse = Accumulator::<R>::new();
for z in 0..size[2] {
for y in 0..size[1] {
for x in 0..size[0] {
report.seeds += 1;
let seed = [
origin[0] + h * R::from_f64(f64::from(x)),
origin[1] + h * R::from_f64(f64::from(y)),
origin[2] + h * R::from_f64(f64::from(z)),
];
let q = match project(sdf, seed, band, tol, AccuracyConfig::MAX_NEWTON_ITERATIONS) {
Projected::Converged(q) => q,
Projected::OutOfBand => {
report.seeds_out_of_band += 1;
continue;
}
Projected::NonFinite => {
report.seeds_non_finite += 1;
continue;
}
Projected::Unconverged => {
report.seeds_unconverged += 1;
continue;
}
};
let inside = (0..3).all(|a| q[a] >= lo_accept[a] && q[a] <= hi_accept[a]);
if !inside {
report.seeds_outside_domain += 1;
continue;
}
let d2 = grid.nearest_distance_squared(q, positions, &tris);
if d2.is_finite() {
reverse.push(d2.sqrt());
} else {
report.seeds_outside_domain += 1;
}
}
}
}
report.field_to_mesh = reverse.finish();
debug_assert_eq!(
report.seeds,
report.seeds_out_of_band
+ report.seeds_non_finite
+ report.seeds_unconverged
+ report.seeds_outside_domain
+ report.field_to_mesh.samples,
"a seed was dropped without landing in a bucket"
);
Ok(report)
}
#[inline]
fn finite<R: Real>(p: [R; 3]) -> bool {
p[0].is_finite() && p[1].is_finite() && p[2].is_finite()
}
enum Projected<R: Real> {
Converged([R; 3]),
OutOfBand,
NonFinite,
Unconverged,
}
fn project<R, S>(
sdf: &S,
start: [R; 3],
max_first_step: R,
tol: R,
max_iterations: u32,
) -> Projected<R>
where
R: Real,
S: Sdf<Scalar = R>,
{
let mut p = start;
for i in 0..max_iterations {
let f = sdf.sample(p);
let g = sdf.gradient(p);
let gg = vec3::dot(g, g);
if gg <= R::ZERO || !gg.is_finite() || !f.is_finite() {
return Projected::NonFinite;
}
let step = vec3::scale(g, f / gg);
let len = vec3::length(step);
if !len.is_finite() {
return Projected::NonFinite;
}
if i == 0 && len > max_first_step {
return Projected::OutOfBand;
}
p = vec3::sub(p, step);
if !finite(p) {
return Projected::NonFinite;
}
if len <= tol {
return Projected::Converged(p);
}
}
Projected::Unconverged
}
struct Accumulator<R: Real> {
count: u64,
max: R,
sum: R,
compensation: R,
}
impl<R: Real> Accumulator<R> {
fn new() -> Self {
Self {
count: 0,
max: R::ZERO,
sum: R::ZERO,
compensation: R::ZERO,
}
}
fn push(&mut self, d: R) {
self.count += 1;
if d > self.max {
self.max = d;
}
let t = self.sum + d;
if self.sum.abs() >= d.abs() {
self.compensation += (self.sum - t) + d;
} else {
self.compensation += (d - t) + self.sum;
}
self.sum = t;
}
fn finish(self) -> DistanceStats<R> {
if self.count == 0 {
return DistanceStats::EMPTY;
}
let total = self.sum + self.compensation;
DistanceStats {
samples: self.count,
max: self.max,
mean: total / R::from_f64(self.count as f64),
}
}
}
#[cfg(test)]
mod tests;