use super::aabb::AabbPacked;
use super::compiler::{CompileError, CompiledSdf};
use super::instruction::Instruction;
use super::refit::{self, RefitError};
use crate::types::SdfNode;
use glam::Vec3;
#[derive(Clone, Debug)]
pub struct CompiledSdfBvh {
pub instructions: Vec<Instruction>,
pub aux_data: Vec<f32>,
pub aabbs: Vec<AabbPacked>,
pub node_count: usize,
pub scene_aabb: AabbPacked,
pub parent_indices: Vec<Option<u32>>,
pub subtree_end: Vec<u32>,
}
impl CompiledSdfBvh {
pub fn compile(node: &SdfNode) -> Self {
Self::try_compile(node)
.expect("CompiledSdfBvh::compile() failed: unsupported primitive in SDF tree")
}
pub fn try_compile(node: &SdfNode) -> Result<Self, CompileError> {
let compiled = CompiledSdf::try_compile(node)?;
let n = compiled.instructions.len();
let mut bvh = Self {
instructions: compiled.instructions,
aux_data: compiled.aux_data,
aabbs: vec![AabbPacked::empty(); n],
node_count: compiled.node_count,
scene_aabb: AabbPacked::empty(),
parent_indices: Vec::new(),
subtree_end: Vec::new(),
};
refit::refit_all(&mut bvh).map_err(refit_to_compile_error)?;
bvh.parent_indices = refit::build_parent_indices(&bvh).map_err(refit_to_compile_error)?;
bvh.subtree_end = refit::build_subtree_ends(&bvh).map_err(refit_to_compile_error)?;
Ok(bvh)
}
pub fn refit_all_from_bytecode(&mut self) -> Result<usize, RefitError> {
refit::refit_all(self)
}
pub fn refit_partial_from_bytecode(&mut self, dirty: &[usize]) -> Result<usize, RefitError> {
refit::refit_partial(self, dirty)
}
#[inline]
pub fn instruction_count(&self) -> usize {
self.instructions.len()
}
#[inline]
pub fn memory_size(&self) -> usize {
self.instructions.len() * std::mem::size_of::<Instruction>()
+ self.aabbs.len() * std::mem::size_of::<AabbPacked>()
+ self.aux_data.len() * std::mem::size_of::<f32>()
}
}
fn refit_to_compile_error(e: RefitError) -> CompileError {
CompileError::UnsupportedPrimitive(format!("BVH annotation failed: {e}"))
}
#[inline]
pub fn eval_compiled_bvh(sdf: &CompiledSdfBvh, point: Vec3) -> f32 {
super::eval_scalar_core::eval_bytecode(&sdf.instructions, &sdf.aux_data, point)
}
pub const fn get_scene_aabb(sdf: &CompiledSdfBvh) -> AabbPacked {
sdf.scene_aabb
}
#[cfg(test)]
mod tests {
use super::*;
use crate::eval::eval;
#[test]
fn test_compile_sphere_bvh() {
let node = SdfNode::sphere(1.0);
let compiled = CompiledSdfBvh::compile(&node);
assert_eq!(compiled.node_count, 1);
assert_eq!(compiled.instructions.len(), 2); assert_eq!(compiled.aabbs.len(), 2);
let aabb = &compiled.aabbs[0];
assert_eq!(aabb.min(), Vec3::new(-1.0, -1.0, -1.0));
assert_eq!(aabb.max(), Vec3::new(1.0, 1.0, 1.0));
}
#[test]
fn test_compile_union_bvh() {
let node = SdfNode::sphere(1.0).union(SdfNode::sphere(1.0).translate(3.0, 0.0, 0.0));
let compiled = CompiledSdfBvh::compile(&node);
let scene_aabb = get_scene_aabb(&compiled);
assert!(scene_aabb.min_x <= -1.0);
assert!(scene_aabb.max_x >= 4.0); }
#[test]
fn test_eval_bvh_sphere() {
let node = SdfNode::sphere(1.0);
let compiled = CompiledSdfBvh::compile(&node);
let d = eval_compiled_bvh(&compiled, Vec3::ZERO);
assert!((d + 1.0).abs() < 0.001);
let d = eval_compiled_bvh(&compiled, Vec3::new(1.0, 0.0, 0.0));
assert!(d.abs() < 0.001);
}
#[test]
fn test_eval_bvh_vs_interpreted() {
let node = SdfNode::sphere(1.0)
.smooth_union(SdfNode::box3d(0.8, 0.8, 0.8), 0.1)
.translate(0.5, 0.0, 0.0);
let compiled = CompiledSdfBvh::compile(&node);
let test_points = [
Vec3::ZERO,
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.5, 0.5, 0.5),
Vec3::new(-1.0, -1.0, -1.0),
];
for p in test_points {
let d_interp = eval(&node, p);
let d_bvh = eval_compiled_bvh(&compiled, p);
assert!(
(d_interp - d_bvh).abs() < 0.001,
"Mismatch at {:?}: interp={}, bvh={}",
p,
d_interp,
d_bvh
);
}
}
#[test]
fn test_scene_aabb() {
let node = SdfNode::sphere(1.0).union(SdfNode::sphere(1.0).translate(5.0, 0.0, 0.0));
let compiled = CompiledSdfBvh::compile(&node);
let aabb = get_scene_aabb(&compiled);
assert!(aabb.min_x <= -1.0);
assert!(aabb.max_x >= 6.0);
assert!(aabb.min_y <= -1.0);
assert!(aabb.max_y >= 1.0);
}
fn assert_bvh_matches_interp(node: &SdfNode, test_points: &[Vec3], tolerance: f32) {
let compiled = CompiledSdfBvh::compile(node);
for &p in test_points {
let d_interp = eval(node, p);
let d_bvh = eval_compiled_bvh(&compiled, p);
assert!(
(d_interp - d_bvh).abs() < tolerance,
"Mismatch at {:?}: interp={}, bvh={} (diff={})",
p,
d_interp,
d_bvh,
(d_interp - d_bvh).abs()
);
}
}
const STANDARD_TEST_POINTS: [Vec3; 8] = [
Vec3::ZERO,
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
Vec3::new(0.5, 0.5, 0.5),
Vec3::new(-1.0, -1.0, -1.0),
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(0.3, -0.7, 1.2),
];
#[test]
fn test_eval_bvh_cone() {
let node = SdfNode::cone(1.0, 1.5);
assert_bvh_matches_interp(&node, &STANDARD_TEST_POINTS, 0.001);
}
#[test]
fn test_eval_bvh_cone_translated() {
let node = SdfNode::cone(0.8, 1.0).translate(1.0, 0.5, 0.0);
assert_bvh_matches_interp(&node, &STANDARD_TEST_POINTS, 0.001);
}
#[test]
fn test_eval_bvh_cone_aabb() {
let node = SdfNode::cone(1.0, 4.0); let compiled = CompiledSdfBvh::compile(&node);
let aabb = &compiled.aabbs[0];
assert!((aabb.min_x - (-1.0)).abs() < 0.001);
assert!((aabb.max_x - 1.0).abs() < 0.001);
assert!((aabb.min_y - (-2.0)).abs() < 0.001);
assert!((aabb.max_y - 2.0).abs() < 0.001);
}
#[test]
fn test_eval_bvh_ellipsoid() {
let node = SdfNode::ellipsoid(1.0, 0.5, 0.75);
assert_bvh_matches_interp(&node, &STANDARD_TEST_POINTS, 0.01);
}
#[test]
fn test_eval_bvh_ellipsoid_union() {
let node =
SdfNode::ellipsoid(1.0, 0.5, 0.75).union(SdfNode::sphere(0.5).translate(2.0, 0.0, 0.0));
assert_bvh_matches_interp(&node, &STANDARD_TEST_POINTS, 0.01);
}
#[test]
fn test_eval_bvh_ellipsoid_aabb() {
let node = SdfNode::ellipsoid(2.0, 1.0, 1.5);
let compiled = CompiledSdfBvh::compile(&node);
let aabb = &compiled.aabbs[0];
assert!((aabb.min_x - (-2.0)).abs() < 0.001);
assert!((aabb.max_x - 2.0).abs() < 0.001);
assert!((aabb.min_y - (-1.0)).abs() < 0.001);
assert!((aabb.max_y - 1.0).abs() < 0.001);
assert!((aabb.min_z - (-1.5)).abs() < 0.001);
assert!((aabb.max_z - 1.5).abs() < 0.001);
}
#[test]
fn test_eval_bvh_mirror() {
let node = SdfNode::box3d(1.0, 0.5, 0.5)
.translate(1.0, 0.0, 0.0)
.mirror(true, false, false);
assert_bvh_matches_interp(&node, &STANDARD_TEST_POINTS, 0.001);
}
#[test]
fn test_eval_bvh_mirror_xyz() {
let node = SdfNode::sphere(0.5)
.translate(1.0, 1.0, 1.0)
.mirror(true, true, true);
let extra_points = [
Vec3::new(1.0, 1.0, 1.0),
Vec3::new(-1.0, 1.0, 1.0),
Vec3::new(1.0, -1.0, -1.0),
Vec3::new(-1.0, -1.0, -1.0),
];
assert_bvh_matches_interp(&node, &extra_points, 0.001);
}
#[test]
fn test_eval_bvh_revolution() {
let node = SdfNode::sphere(0.3).revolution(1.0);
assert_bvh_matches_interp(&node, &STANDARD_TEST_POINTS, 0.01);
}
#[test]
fn test_eval_bvh_revolution_aabb() {
let node = SdfNode::sphere(0.5).revolution(2.0);
let compiled = CompiledSdfBvh::compile(&node);
let aabb = &compiled.aabbs[0]; assert!(aabb.min_x <= -2.5, "min_x={}", aabb.min_x);
assert!(aabb.max_x >= 2.5, "max_x={}", aabb.max_x);
assert!(aabb.min_z <= -2.5, "min_z={}", aabb.min_z);
assert!(aabb.max_z >= 2.5, "max_z={}", aabb.max_z);
}
#[test]
fn test_eval_bvh_extrude() {
let node = SdfNode::sphere(1.0).extrude(0.5);
assert_bvh_matches_interp(&node, &STANDARD_TEST_POINTS, 0.01);
}
#[test]
fn test_eval_bvh_extrude_box() {
let node = SdfNode::box3d(1.0, 0.5, 0.0).extrude(2.0);
let test_points = [
Vec3::ZERO,
Vec3::new(0.5, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
Vec3::new(0.0, 0.0, 3.0),
Vec3::new(1.5, 0.0, 0.5),
];
assert_bvh_matches_interp(&node, &test_points, 0.01);
}
#[test]
fn test_eval_bvh_noise() {
let node = SdfNode::sphere(1.0).noise(0.1, 2.0, 42);
let compiled_bvh = CompiledSdfBvh::compile(&node);
let compiled_flat = crate::compiled::CompiledSdf::compile(&node);
for &p in &STANDARD_TEST_POINTS {
let d_bvh = eval_compiled_bvh(&compiled_bvh, p);
let d_flat = crate::compiled::eval::eval_compiled(&compiled_flat, p);
assert!(
(d_bvh - d_flat).abs() < 0.001,
"Noise mismatch at {:?}: bvh={}, flat={} (diff={})",
p,
d_bvh,
d_flat,
(d_bvh - d_flat).abs()
);
}
}
#[test]
fn test_eval_bvh_noise_on_union() {
let node = SdfNode::sphere(1.0)
.union(SdfNode::box3d(0.5, 0.5, 0.5).translate(1.5, 0.0, 0.0))
.noise(0.05, 3.0, 7);
let compiled_bvh = CompiledSdfBvh::compile(&node);
let compiled_flat = crate::compiled::CompiledSdf::compile(&node);
for &p in &STANDARD_TEST_POINTS {
let d_bvh = eval_compiled_bvh(&compiled_bvh, p);
let d_flat = crate::compiled::eval::eval_compiled(&compiled_flat, p);
assert!(
(d_bvh - d_flat).abs() < 0.001,
"Noise mismatch at {:?}: bvh={}, flat={} (diff={})",
p,
d_bvh,
d_flat,
(d_bvh - d_flat).abs()
);
}
}
#[test]
fn test_eval_bvh_combined_new_features() {
let node = SdfNode::cone(0.5, 1.0).mirror(true, false, true);
assert_bvh_matches_interp(&node, &STANDARD_TEST_POINTS, 0.001);
}
#[test]
fn test_scene_aabb_transform_root() {
let node = SdfNode::sphere(1.0).translate(5.0, 3.0, 0.0);
let compiled = CompiledSdfBvh::compile(&node);
let aabb = get_scene_aabb(&compiled);
assert!((aabb.min_x - 4.0).abs() < 0.001, "min_x={}", aabb.min_x);
assert!((aabb.max_x - 6.0).abs() < 0.001, "max_x={}", aabb.max_x);
assert!((aabb.min_y - 2.0).abs() < 0.001, "min_y={}", aabb.min_y);
assert!((aabb.max_y - 4.0).abs() < 0.001, "max_y={}", aabb.max_y);
}
#[test]
fn test_scene_aabb_modifier_root() {
let node = SdfNode::sphere(1.0).noise(0.1, 2.0, 42);
let compiled = CompiledSdfBvh::compile(&node);
let aabb = get_scene_aabb(&compiled);
assert!(aabb.min_x <= -1.0);
assert!(aabb.max_x >= 1.0);
}
#[test]
fn test_noise_interpreted_vs_compiled() {
let node = SdfNode::sphere(1.0).noise(0.1, 2.0, 42);
assert_bvh_matches_interp(&node, &STANDARD_TEST_POINTS, 0.001);
}
}