use nalgebra::RealField;
use num_traits::ToPrimitive;
use crate::{
bvh::{Bvh, BvhConfig, BvhNode},
error::{BvhError, Result},
geometry::Aabb,
traits::{Bounded, FallibleNumeric},
};
struct SplitCandidate<T: RealField + Copy> {
axis: usize,
position: T,
cost: T,
}
#[derive(Debug)]
pub struct BvhBuilder<T: RealField + Copy> {
config: BvhConfig<T>,
indices: Vec<usize>,
nodes: Vec<BvhNode<T>>,
nodes_used: usize,
}
impl<T: RealField + Copy + ToPrimitive> BvhBuilder<T> {
pub fn new(config: &BvhConfig<T>) -> Self {
Self {
config: config.clone(),
indices: Vec::new(),
nodes: Vec::new(),
nodes_used: 0,
}
}
pub fn build<B: Bounded<T>>(mut self, shapes: &[B]) -> Result<Bvh<T>> {
if shapes.is_empty() {
return Err(BvhError::EmptyGeometry.into());
}
self.indices = (0..shapes.len()).collect();
self.nodes = vec![
BvhNode {
aabb: Aabb::empty()?,
left_child: 0,
count: 0,
};
(shapes.len() * 2) - 1
];
self.nodes[0].left_child = 0;
self.nodes[0].count = shapes.len();
self.nodes_used = 1;
self.update_bounds(0, shapes)?;
let depth = self.subdivide(0, shapes, 0)?;
self.nodes.truncate(self.nodes_used);
self.nodes.shrink_to_fit();
Bvh::construct_directly(self.indices, self.nodes, depth)
}
fn update_bounds<B: Bounded<T>>(&mut self, index: usize, shapes: &[B]) -> Result<()> {
let start = self.nodes[index].aabb.clone();
let new_aabb = (0..self.nodes[index].count)
.map(|i| shapes[self.indices[self.nodes[index].left_child + i]].aabb())
.try_fold(start, |acc, aabb| acc.merge(&*aabb?))?;
self.nodes[index].aabb = new_aabb;
Ok(())
}
fn subdivide<B: Bounded<T>>(&mut self, index: usize, shapes: &[B], current_depth: usize) -> Result<usize> {
if (self.nodes[index].count <= self.config.max_shapes_per_node) || (current_depth >= self.config.max_depth) {
return Ok(current_depth);
}
let Ok(Some(best_split)) = self.find_best_split(index, shapes) else {
return Ok(current_depth);
};
let leaf_cost = T::try_from_usize(self.nodes[index].count)? * self.config.intersect_cost;
if best_split.cost >= leaf_cost {
return Ok(current_depth);
}
let mut i = self.nodes[index].left_child;
let mut j = i + self.nodes[index].count - 1;
while i <= j {
let shape_index = self.indices[i];
let centroid = shapes[shape_index].aabb()?.centre()?;
if centroid[best_split.axis] < best_split.position {
i += 1;
} else {
self.indices.swap(i, j);
if j == 0 {
return Ok(current_depth);
}
j -= 1;
}
}
let left_count = i - self.nodes[index].left_child;
if (left_count == 0) || (left_count == self.nodes[index].count) {
return Ok(current_depth);
}
let left_child_index = self.nodes_used;
self.nodes_used += 1;
let right_child_index = self.nodes_used;
self.nodes_used += 1;
self.nodes[left_child_index].left_child = self.nodes[index].left_child;
self.nodes[left_child_index].count = left_count;
self.nodes[right_child_index].left_child = i;
self.nodes[right_child_index].count = self.nodes[index].count - left_count;
self.nodes[index].left_child = left_child_index;
self.nodes[index].count = 0;
self.update_bounds(left_child_index, shapes)?;
self.update_bounds(right_child_index, shapes)?;
let left_depth = self.subdivide(left_child_index, shapes, current_depth + 1)?;
let right_depth = self.subdivide(right_child_index, shapes, current_depth + 1)?;
Ok(left_depth.max(right_depth))
}
fn find_best_split<B: Bounded<T>>(&self, node_index: usize, shapes: &[B]) -> Result<Option<SplitCandidate<T>>> {
let node = &self.nodes[node_index];
let node_surface_area = node.aabb.surface_area()?;
if node_surface_area <= T::zero() {
return Ok(None);
}
let mut best_split: Option<SplitCandidate<T>> = None;
for axis in 0..3 {
let extent = node.aabb.maxs[axis] - node.aabb.mins[axis];
if extent <= T::zero() {
continue;
}
let mut buckets = vec![(0, Aabb::empty()?); self.config.sah_buckets];
for i in 0..node.count {
let shape_index = self.indices[node.left_child + i];
let shape_aabb = shapes[shape_index].aabb()?;
let centroid = shape_aabb.centre()?;
let bucket_index = ((centroid[axis] - node.aabb.mins[axis]) / extent
* T::try_from_usize(self.config.sah_buckets)?)
.floor()
.to_usize()
.unwrap_or(0)
.min(self.config.sah_buckets - 1);
buckets[bucket_index].0 += 1;
if buckets[bucket_index].0 == 1 {
buckets[bucket_index].1 = shape_aabb.into_owned();
} else {
buckets[bucket_index].1 = buckets[bucket_index].1.merge(&shape_aabb)?;
}
}
for split_bucket in 1..self.config.sah_buckets {
let mut left_count = 0;
let mut left_aabb: Option<Aabb<T>> = None;
for bucket in &buckets[..split_bucket] {
if bucket.0 > 0 {
left_count += bucket.0;
left_aabb = Some(left_aabb.map_or_else(|| Ok(bucket.1.clone()), |aabb| aabb.merge(&bucket.1))?);
}
}
let mut right_count = 0;
let mut right_aabb: Option<Aabb<T>> = None;
for bucket in &buckets[split_bucket..] {
if bucket.0 > 0 {
right_count += bucket.0;
right_aabb = Some(right_aabb.map_or_else(|| Ok(bucket.1.clone()), |aabb| aabb.merge(&bucket.1))?);
}
}
if left_count == 0 || right_count == 0 {
continue;
}
let left_surface_area = left_aabb
.as_ref()
.map(Aabb::surface_area)
.transpose()?
.unwrap_or_else(T::zero);
let right_surface_area = right_aabb
.as_ref()
.map(Aabb::surface_area)
.transpose()?
.unwrap_or_else(T::zero);
let cost = self.config.traverse_cost
+ (left_surface_area / node_surface_area) * T::try_from_usize(left_count)? * self.config.intersect_cost
+ (right_surface_area / node_surface_area) * T::try_from_usize(right_count)? * self.config.intersect_cost;
let split_position = node.aabb.mins[axis]
+ extent * T::try_from_usize(split_bucket)? / T::try_from_usize(self.config.sah_buckets)?;
if best_split.as_ref().is_none_or(|best| cost < best.cost) {
best_split = Some(SplitCandidate {
axis,
position: split_position,
cost,
});
}
}
}
Ok(best_split)
}
}