use crate::interval::{eval_interval, Vec3Interval};
use crate::types::{Aabb, SdfNode};
use glam::{Mat3, 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.is_positive() || full_interval.is_negative() {
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();
let searched = Aabb::new(
Vec3::new(results[0], results[2], results[4]),
Vec3::new(results[1], results[3], results[5]),
);
match analytic_aabb(node) {
AnalyticAabb::Bounded(analytic) => {
let lo = searched.min.max(analytic.min);
let hi = searched.max.min(analytic.max);
if lo.x <= hi.x && lo.y <= hi.y && lo.z <= hi.z {
Aabb::new(lo, hi)
} else {
searched
}
}
AnalyticAabb::Empty | AnalyticAabb::Unsupported => searched,
}
}
#[derive(Debug, Clone, Copy)]
pub enum AnalyticAabb {
Empty,
Bounded(Aabb),
Unsupported,
}
#[derive(Clone, Copy)]
struct Affine {
a: Mat3,
t: Vec3,
}
#[derive(Clone, Copy)]
enum Region {
Empty,
Box(Vec3, Vec3),
Unknown,
}
impl Region {
fn hull(self, other: Self) -> Self {
match (self, other) {
(Self::Unknown, _) | (_, Self::Unknown) => Self::Unknown,
(Self::Empty, r) | (r, Self::Empty) => r,
(Self::Box(a0, a1), Self::Box(b0, b1)) => Self::Box(a0.min(b0), a1.max(b1)),
}
}
fn meet(self, other: Self) -> Self {
match (self, other) {
(Self::Unknown, r) | (r, Self::Unknown) => r,
(Self::Empty, _) | (_, Self::Empty) => Self::Empty,
(Self::Box(a0, a1), Self::Box(b0, b1)) => {
let (lo, hi) = (a0.max(b0), a1.min(b1));
if lo.x > hi.x || lo.y > hi.y || lo.z > hi.z {
Self::Empty
} else {
Self::Box(lo, hi)
}
}
}
}
}
fn leaf_box(xf: &Affine, center: Vec3, half: Vec3) -> Region {
if !center.is_finite() || !half.is_finite() {
return Region::Unknown;
}
if half.min_element() < 0.0 {
return Region::Empty;
}
let abs = Mat3::from_cols(xf.a.x_axis.abs(), xf.a.y_axis.abs(), xf.a.z_axis.abs());
let c = xf.a * center + xf.t;
let h = abs * half;
Region::Box(c - h, c + h)
}
fn row(a: &Mat3, i: usize) -> Vec3 {
Vec3::new(a.x_axis[i], a.y_axis[i], a.z_axis[i])
}
fn round_leaf(xf: &Affine, center: Vec3, seg: Vec3, disc: f32, ball: f32) -> Region {
if !center.is_finite() || !seg.is_finite() || !disc.is_finite() || !ball.is_finite() {
return Region::Unknown;
}
if disc < 0.0 || ball < 0.0 {
return Region::Empty;
}
let c = xf.a * center + xf.t;
let s = (xf.a * seg).abs();
let mut h = s;
for i in 0..3 {
let r = row(&xf.a, i);
h[i] += disc * r.x.hypot(r.z) + ball * r.length();
}
Region::Box(c - h, c + h)
}
fn level_region(node: &SdfNode, xf: &Affine, delta: f32) -> Region {
if !delta.is_finite() {
return Region::Unknown;
}
match node {
SdfNode::Sphere { radius } => round_leaf(xf, Vec3::ZERO, Vec3::ZERO, 0.0, radius + delta),
SdfNode::Box3d { half_extents } => {
leaf_box(xf, Vec3::ZERO, *half_extents + Vec3::splat(delta))
}
SdfNode::Cylinder {
radius,
half_height,
} => {
let (r, h) = (radius + delta, half_height + delta);
if h < 0.0 {
return Region::Empty;
}
round_leaf(xf, Vec3::ZERO, Vec3::new(0.0, h, 0.0), r, 0.0)
}
SdfNode::Torus {
major_radius,
minor_radius,
} => {
let minor = minor_radius + delta;
if minor < 0.0 || *major_radius < 0.0 {
return Region::Empty;
}
round_leaf(xf, Vec3::ZERO, Vec3::ZERO, *major_radius, minor)
}
SdfNode::Capsule {
point_a,
point_b,
radius,
} => {
let center = (*point_a + *point_b) * 0.5;
let seg = (*point_b - *point_a) * 0.5;
round_leaf(xf, center, seg, 0.0, radius + delta)
}
SdfNode::RoundedBox {
half_extents,
round_radius,
} => leaf_box(
xf,
Vec3::ZERO,
*half_extents + Vec3::splat(round_radius + delta),
),
SdfNode::RoundedCylinder {
radius,
round_radius,
half_height,
} => {
let h = half_height + round_radius + delta;
if h < 0.0 {
return Region::Empty;
}
round_leaf(xf, Vec3::ZERO, Vec3::new(0.0, h, 0.0), radius + delta, 0.0)
}
SdfNode::Union { a, b } => level_region(a, xf, delta).hull(level_region(b, xf, delta)),
SdfNode::Intersection { a, b } => {
level_region(a, xf, delta).meet(level_region(b, xf, delta))
}
SdfNode::Subtraction { a, .. } => level_region(a, xf, delta),
SdfNode::SmoothUnion { a, b, k } if k.is_finite() && *k >= 0.0 => {
let d = delta + k * 0.25;
level_region(a, xf, d).hull(level_region(b, xf, d))
}
SdfNode::SmoothIntersection { a, b, k } if k.is_finite() && *k >= 0.0 => {
level_region(a, xf, delta).meet(level_region(b, xf, delta))
}
SdfNode::SmoothSubtraction { a, k, .. } if k.is_finite() && *k >= 0.0 => {
level_region(a, xf, delta)
}
SdfNode::WithMaterial { child, .. } => level_region(child, xf, delta),
SdfNode::Translate { child, offset } => {
let moved = Affine {
a: xf.a,
t: xf.t + xf.a * *offset,
};
level_region(child, &moved, delta)
}
SdfNode::Rotate { child, rotation } => {
let turned = Affine {
a: xf.a * Mat3::from_quat(rotation.normalize()),
t: xf.t,
};
level_region(child, &turned, delta)
}
SdfNode::Scale { child, factor } if factor.is_finite() && *factor > 0.0 => {
let scaled = Affine {
a: xf.a * *factor,
t: xf.t,
};
level_region(child, &scaled, delta / factor)
}
SdfNode::Round { child, radius } => level_region(child, xf, delta + radius),
SdfNode::Onion { child, thickness } => level_region(child, xf, delta + thickness),
SdfNode::Elongate { child, amount } => {
if !amount.is_finite() || amount.min_element() < 0.0 {
return Region::Unknown;
}
match level_region(child, xf, delta) {
Region::Box(lo, hi) => {
let abs =
Mat3::from_cols(xf.a.x_axis.abs(), xf.a.y_axis.abs(), xf.a.z_axis.abs());
let grow = abs * *amount;
Region::Box(lo - grow, hi + grow)
}
other => other,
}
}
SdfNode::Mirror { child, axes } => {
let identity = Affine {
a: Mat3::IDENTITY,
t: Vec3::ZERO,
};
match level_region(child, &identity, delta) {
Region::Box(mut lo, hi) => {
for i in 0..3 {
if axes[i] != 0.0 {
if hi[i] < 0.0 {
return Region::Empty;
}
lo[i] = -hi[i];
}
}
leaf_box(xf, (lo + hi) * 0.5, (hi - lo) * 0.5)
}
other => other,
}
}
SdfNode::ScaleNonUniform { child, factors }
if factors.is_finite()
&& factors.min_element() > 0.0
&& factors.recip().is_finite() =>
{
let scaled = Affine {
a: xf.a * Mat3::from_diagonal(*factors),
t: xf.t,
};
level_region(child, &scaled, delta / factors.min_element())
}
_ => Region::Unknown,
}
}
#[must_use]
pub fn analytic_aabb(node: &SdfNode) -> AnalyticAabb {
let identity = Affine {
a: Mat3::IDENTITY,
t: Vec3::ZERO,
};
match level_region(node, &identity, 0.0) {
Region::Empty => AnalyticAabb::Empty,
Region::Unknown => AnalyticAabb::Unsupported,
Region::Box(lo, hi) => {
let pad = (lo.abs().max(hi.abs()).max_element() * 4.0 + 1.0) * f32::EPSILON * 4.0;
AnalyticAabb::Bounded(Aabb::new(lo - Vec3::splat(pad), hi + Vec3::splat(pad)))
}
}
}
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.contains(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
);
}
}