use crate::interval::{eval_interval, Vec3Interval};
use crate::types::{Aabb, SdfNode};
use glam::Vec3;
use rayon::prelude::*;
#[derive(Debug, Clone, Copy)]
pub struct TightAabbConfig {
initial_half_size: f32,
bisection_iterations: u32,
coarse_subdivisions: u32,
}
#[derive(Debug, thiserror::Error)]
pub enum TightAabbConfigError {
#[error("initial_half_size must be > 0, got {0}")]
InvalidHalfSize(f32),
#[error("bisection_iterations must be > 0")]
ZeroIterations,
#[error("coarse_subdivisions must be > 0")]
ZeroSubdivisions,
}
impl TightAabbConfig {
#[must_use]
pub const fn preset_small() -> Self {
Self {
initial_half_size: 10.0,
bisection_iterations: 20,
coarse_subdivisions: 8,
}
}
#[must_use]
pub const fn preset_medium() -> Self {
Self {
initial_half_size: 100.0,
bisection_iterations: 22,
coarse_subdivisions: 12,
}
}
#[must_use]
pub const fn preset_large() -> Self {
Self {
initial_half_size: 500.0,
bisection_iterations: 24,
coarse_subdivisions: 16,
}
}
pub fn try_new(
initial_half_size: f32,
bisection_iterations: u32,
coarse_subdivisions: u32,
) -> Result<Self, TightAabbConfigError> {
if initial_half_size.is_nan() || initial_half_size <= 0.0 {
return Err(TightAabbConfigError::InvalidHalfSize(initial_half_size));
}
if bisection_iterations == 0 {
return Err(TightAabbConfigError::ZeroIterations);
}
if coarse_subdivisions == 0 {
return Err(TightAabbConfigError::ZeroSubdivisions);
}
Ok(Self {
initial_half_size,
bisection_iterations,
coarse_subdivisions,
})
}
#[must_use]
#[inline]
pub const fn initial_half_size(&self) -> f32 {
self.initial_half_size
}
#[must_use]
#[inline]
pub const fn bisection_iterations(&self) -> u32 {
self.bisection_iterations
}
#[must_use]
#[inline]
pub const fn coarse_subdivisions(&self) -> u32 {
self.coarse_subdivisions
}
}
pub fn compute_tight_aabb(node: &SdfNode) -> Aabb {
compute_tight_aabb_with_config(node, &TightAabbConfig::preset_small())
}
pub fn compute_tight_aabb_with_config(node: &SdfNode, config: &TightAabbConfig) -> Aabb {
let h = config.initial_half_size();
let initial_min = Vec3::splat(-h);
let initial_max = Vec3::splat(h);
let full_bounds = Vec3Interval::from_bounds(initial_min, initial_max);
let full_interval = eval_interval(node, full_bounds);
if full_interval.lo > 0.0 || full_interval.hi < 0.0 {
return Aabb::new(Vec3::ZERO, Vec3::ZERO);
}
let results: Vec<f32> = (0..6u8)
.into_par_iter()
.map(|face| {
let axis = (face / 2) as usize; let is_max = face % 2 == 1;
find_tight_bound(node, axis, is_max, initial_min, initial_max, config)
})
.collect();
Aabb::new(
Vec3::new(results[0], results[2], results[4]),
Vec3::new(results[1], results[3], results[5]),
)
}
fn find_tight_bound(
node: &SdfNode,
axis: usize,
is_max: bool,
initial_min: Vec3,
initial_max: Vec3,
config: &TightAabbConfig,
) -> f32 {
let lo = get_axis(initial_min, axis);
let hi = get_axis(initial_max, axis);
let (search_lo, search_hi) = coarse_scan(
node,
axis,
is_max,
lo,
hi,
initial_min,
initial_max,
config.coarse_subdivisions(),
);
bisect_bound(
node,
axis,
is_max,
search_lo,
search_hi,
initial_min,
initial_max,
config.bisection_iterations(),
)
}
#[allow(clippy::too_many_arguments)]
fn coarse_scan(
node: &SdfNode,
axis: usize,
is_max: bool,
lo: f32,
hi: f32,
initial_min: Vec3,
initial_max: Vec3,
subdivisions: u32,
) -> (f32, f32) {
let step = (hi - lo) / subdivisions as f32;
if is_max {
for i in 0..subdivisions {
let slab_hi = (i as f32).mul_add(-step, hi);
let slab_lo = slab_hi - step;
let bounds = make_slab_bounds(axis, slab_lo, slab_hi, initial_min, initial_max);
let interval = eval_interval(node, bounds);
if may_contain_surface(&interval) {
return (slab_lo, slab_hi);
}
}
(lo, lo)
} else {
for i in 0..subdivisions {
let slab_lo = (i as f32).mul_add(step, lo);
let slab_hi = slab_lo + step;
let bounds = make_slab_bounds(axis, slab_lo, slab_hi, initial_min, initial_max);
let interval = eval_interval(node, bounds);
if may_contain_surface(&interval) {
return (slab_lo, slab_hi);
}
}
(hi, hi)
}
}
#[allow(clippy::too_many_arguments)]
fn bisect_bound(
node: &SdfNode,
axis: usize,
is_max: bool,
search_lo: f32,
search_hi: f32,
initial_min: Vec3,
initial_max: Vec3,
iterations: u32,
) -> f32 {
let mut lo = search_lo;
let mut hi = search_hi;
for _ in 0..iterations {
let mid = f32::midpoint(lo, hi);
if is_max {
let bounds = make_slab_bounds(axis, mid, hi, initial_min, initial_max);
let interval = eval_interval(node, bounds);
if may_contain_surface(&interval) {
lo = mid;
} else {
hi = mid;
}
} else {
let bounds = make_slab_bounds(axis, lo, mid, initial_min, initial_max);
let interval = eval_interval(node, bounds);
if may_contain_surface(&interval) {
hi = mid;
} else {
lo = mid;
}
}
}
if is_max {
lo
} else {
hi
}
}
#[inline(always)]
fn make_slab_bounds(
axis: usize,
slab_lo: f32,
slab_hi: f32,
initial_min: Vec3,
initial_max: Vec3,
) -> Vec3Interval {
match axis {
0 => Vec3Interval::from_bounds(
Vec3::new(slab_lo, initial_min.y, initial_min.z),
Vec3::new(slab_hi, initial_max.y, initial_max.z),
),
1 => Vec3Interval::from_bounds(
Vec3::new(initial_min.x, slab_lo, initial_min.z),
Vec3::new(initial_max.x, slab_hi, initial_max.z),
),
_ => Vec3Interval::from_bounds(
Vec3::new(initial_min.x, initial_min.y, slab_lo),
Vec3::new(initial_max.x, initial_max.y, slab_hi),
),
}
}
#[inline(always)]
fn may_contain_surface(interval: &crate::interval::Interval) -> bool {
interval.lo <= 0.0 && interval.hi >= 0.0
}
#[inline(always)]
const fn get_axis(v: Vec3, axis: usize) -> f32 {
match axis {
0 => v.x,
1 => v.y,
_ => v.z,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sphere_tight_aabb() {
let sphere = SdfNode::sphere(1.0);
let aabb = compute_tight_aabb(&sphere);
for i in 0..3 {
let lo = get_axis(aabb.min, i);
let hi = get_axis(aabb.max, i);
assert!(
lo < -0.9 && lo > -1.2,
"min[{}] = {} should be near -1.0",
i,
lo
);
assert!(
hi > 0.9 && hi < 1.2,
"max[{}] = {} should be near 1.0",
i,
hi
);
}
}
#[test]
fn test_box_tight_aabb() {
let box3 = SdfNode::box3d(1.0, 2.0, 0.5);
let aabb = compute_tight_aabb(&box3);
assert!(
aabb.min.x < -0.4 && aabb.min.x > -0.7,
"min.x = {}",
aabb.min.x
);
assert!(
aabb.max.x > 0.4 && aabb.max.x < 0.7,
"max.x = {}",
aabb.max.x
);
assert!(
aabb.min.y < -0.9 && aabb.min.y > -1.2,
"min.y = {}",
aabb.min.y
);
assert!(
aabb.max.y > 0.9 && aabb.max.y < 1.2,
"max.y = {}",
aabb.max.y
);
assert!(
aabb.min.z < -0.15 && aabb.min.z > -0.4,
"min.z = {}",
aabb.min.z
);
assert!(
aabb.max.z > 0.15 && aabb.max.z < 0.4,
"max.z = {}",
aabb.max.z
);
}
#[test]
fn test_translated_sphere() {
let shape = SdfNode::sphere(1.0).translate(3.0, -2.0, 1.0);
let aabb = compute_tight_aabb(&shape);
assert!(
aabb.min.x > 1.5 && aabb.min.x < 2.2,
"min.x = {}",
aabb.min.x
);
assert!(
aabb.max.x > 3.8 && aabb.max.x < 4.5,
"max.x = {}",
aabb.max.x
);
assert!(
aabb.min.y > -3.2 && aabb.min.y < -2.5,
"min.y = {}",
aabb.min.y
);
assert!(
aabb.max.y > -1.5 && aabb.max.y < -0.8,
"max.y = {}",
aabb.max.y
);
}
#[test]
fn test_scaled_sphere() {
let shape = SdfNode::sphere(1.0).scale(3.0);
let aabb = compute_tight_aabb(&shape);
for i in 0..3 {
let lo = get_axis(aabb.min, i);
let hi = get_axis(aabb.max, i);
assert!(lo < -2.7 && lo > -3.5, "min[{}] = {}", i, lo);
assert!(hi > 2.7 && hi < 3.5, "max[{}] = {}", i, hi);
}
}
#[test]
fn test_union_aabb() {
let shape = SdfNode::sphere(1.0)
.translate(-3.0, 0.0, 0.0)
.union(SdfNode::sphere(1.0).translate(3.0, 0.0, 0.0));
let aabb = compute_tight_aabb(&shape);
assert!(aabb.min.x < -3.5, "min.x = {}", aabb.min.x);
assert!(aabb.max.x > 3.5, "max.x = {}", aabb.max.x);
assert!(
aabb.min.y > -1.5 && aabb.min.y < -0.5,
"min.y = {}",
aabb.min.y
);
}
#[test]
fn test_subtraction_aabb() {
let shape = SdfNode::sphere(2.0).subtract(SdfNode::box3d(0.5, 0.5, 0.5));
let aabb = compute_tight_aabb(&shape);
assert!(aabb.min.x < -1.5, "min.x = {}", aabb.min.x);
assert!(aabb.max.x > 1.5, "max.x = {}", aabb.max.x);
}
#[test]
fn test_no_surface_returns_zero() {
let shape = SdfNode::Plane {
normal: Vec3::Y,
distance: 100.0,
};
let aabb = compute_tight_aabb(&shape);
assert_eq!(aabb.min, Vec3::ZERO);
assert_eq!(aabb.max, Vec3::ZERO);
}
#[test]
fn test_custom_config() {
let sphere = SdfNode::sphere(5.0);
let config = TightAabbConfig::try_new(8.0, 15, 4).expect("valid config");
let aabb = compute_tight_aabb_with_config(&sphere, &config);
for i in 0..3 {
let lo = get_axis(aabb.min, i);
let hi = get_axis(aabb.max, i);
assert!(lo < -4.5 && lo > -5.5, "min[{}] = {}", i, lo);
assert!(hi > 4.5 && hi < 5.5, "max[{}] = {}", i, hi);
}
}
#[test]
fn test_torus_tight_aabb() {
let torus = SdfNode::torus(2.0, 0.5);
let aabb = compute_tight_aabb(&torus);
assert!(
aabb.min.x < -2.3 && aabb.min.x > -2.8,
"min.x = {}",
aabb.min.x
);
assert!(
aabb.max.x > 2.3 && aabb.max.x < 2.8,
"max.x = {}",
aabb.max.x
);
assert!(
aabb.min.y > -0.8 && aabb.min.y < -0.3,
"min.y = {}",
aabb.min.y
);
assert!(
aabb.max.y > 0.3 && aabb.max.y < 0.8,
"max.y = {}",
aabb.max.y
);
}
}