use crate::ray::{intersect_aabb, intersect_triangle, Ray, RayHit};
use crate::scene::BoundingBox;
use crate::Primitive;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BvhNode {
pub bounds: BoundingBox,
pub left_or_first: u32,
pub right_child: u32,
pub tri_count: u32,
}
impl BvhNode {
pub fn is_leaf(&self) -> bool {
self.tri_count > 0
}
}
#[derive(Clone, Debug)]
pub struct Bvh {
pub nodes: Vec<BvhNode>,
pub triangles: Vec<u32>,
}
impl Bvh {
pub const LEAF_THRESHOLD: usize = 4;
pub fn build(primitive: &Primitive) -> Option<Self> {
let n_pos = primitive.positions.len();
let all_tris = primitive.triangle_indices();
if all_tris.is_empty() {
return None;
}
let mut tri_bounds: Vec<BoundingBox> = Vec::with_capacity(all_tris.len());
let mut tri_centroids: Vec<[f32; 3]> = Vec::with_capacity(all_tris.len());
let mut tri_indices: Vec<u32> = Vec::with_capacity(all_tris.len());
for (idx, [ia, ib, ic]) in all_tris.iter().enumerate() {
let (ia, ib, ic) = (*ia as usize, *ib as usize, *ic as usize);
if ia >= n_pos || ib >= n_pos || ic >= n_pos {
continue;
}
let p0 = primitive.positions[ia];
let p1 = primitive.positions[ib];
let p2 = primitive.positions[ic];
if !finite_point(p0) || !finite_point(p1) || !finite_point(p2) {
continue;
}
let bbox = BoundingBox::from_point(p0).expand(p1).expand(p2);
let centroid = [
(p0[0] + p1[0] + p2[0]) / 3.0,
(p0[1] + p1[1] + p2[1]) / 3.0,
(p0[2] + p1[2] + p2[2]) / 3.0,
];
tri_bounds.push(bbox);
tri_centroids.push(centroid);
tri_indices.push(idx as u32);
}
if tri_indices.is_empty() {
return None;
}
let mut nodes: Vec<BvhNode> = Vec::new();
let total = tri_indices.len();
build_recursive(
&mut nodes,
&mut tri_indices,
&mut tri_bounds,
&mut tri_centroids,
0,
total,
);
Some(Bvh {
nodes,
triangles: tri_indices,
})
}
pub fn intersect_ray(&self, primitive: &Primitive, ray: Ray, t_max: f32) -> Option<RayHit> {
if self.nodes.is_empty() {
return None;
}
let all_tris = primitive.triangle_indices();
let n_pos = primitive.positions.len();
intersect_aabb(
ray,
self.nodes[0].bounds.min,
self.nodes[0].bounds.max,
t_max,
)?;
let mut best: Option<RayHit> = None;
let mut best_t = t_max;
let mut stack: Vec<(u32, f32)> = Vec::with_capacity(64);
stack.push((0, 0.0));
while let Some((node_idx, t_enter)) = stack.pop() {
if t_enter > best_t {
continue;
}
let node = &self.nodes[node_idx as usize];
if node.is_leaf() {
let start = node.left_or_first as usize;
let end = start + node.tri_count as usize;
for &tri_idx in &self.triangles[start..end] {
let tri = all_tris[tri_idx as usize];
let (ia, ib, ic) = (tri[0] as usize, tri[1] as usize, tri[2] as usize);
if ia >= n_pos || ib >= n_pos || ic >= n_pos {
continue;
}
let p0 = primitive.positions[ia];
let p1 = primitive.positions[ib];
let p2 = primitive.positions[ic];
if let Some((t, u, v, front)) = intersect_triangle(ray, p0, p1, p2, best_t) {
let w = 1.0 - u - v;
best = Some(RayHit {
t,
triangle_index: tri_idx as usize,
barycentric: [w, u, v],
front_face: front,
});
best_t = t;
}
}
continue;
}
let left = node.left_or_first;
let right = node.right_child;
let left_bbox = self.nodes[left as usize].bounds;
let right_bbox = self.nodes[right as usize].bounds;
let left_hit = intersect_aabb(ray, left_bbox.min, left_bbox.max, best_t);
let right_hit = intersect_aabb(ray, right_bbox.min, right_bbox.max, best_t);
match (left_hit, right_hit) {
(Some((tl, _)), Some((tr, _))) => {
if tl <= tr {
stack.push((right, tr));
stack.push((left, tl));
} else {
stack.push((left, tl));
stack.push((right, tr));
}
}
(Some((tl, _)), None) => stack.push((left, tl)),
(None, Some((tr, _))) => stack.push((right, tr)),
(None, None) => {}
}
}
best
}
pub fn any_ray_intersection(&self, primitive: &Primitive, ray: Ray, t_max: f32) -> bool {
if self.nodes.is_empty() {
return false;
}
let all_tris = primitive.triangle_indices();
let n_pos = primitive.positions.len();
if intersect_aabb(
ray,
self.nodes[0].bounds.min,
self.nodes[0].bounds.max,
t_max,
)
.is_none()
{
return false;
}
let mut stack: Vec<u32> = Vec::with_capacity(64);
stack.push(0);
while let Some(node_idx) = stack.pop() {
let node = &self.nodes[node_idx as usize];
if node.is_leaf() {
let start = node.left_or_first as usize;
let end = start + node.tri_count as usize;
for &tri_idx in &self.triangles[start..end] {
let tri = all_tris[tri_idx as usize];
let (ia, ib, ic) = (tri[0] as usize, tri[1] as usize, tri[2] as usize);
if ia >= n_pos || ib >= n_pos || ic >= n_pos {
continue;
}
let p0 = primitive.positions[ia];
let p1 = primitive.positions[ib];
let p2 = primitive.positions[ic];
if intersect_triangle(ray, p0, p1, p2, t_max).is_some() {
return true;
}
}
continue;
}
let left = node.left_or_first;
let right = node.right_child;
let left_bbox = self.nodes[left as usize].bounds;
let right_bbox = self.nodes[right as usize].bounds;
if intersect_aabb(ray, left_bbox.min, left_bbox.max, t_max).is_some() {
stack.push(left);
}
if intersect_aabb(ray, right_bbox.min, right_bbox.max, t_max).is_some() {
stack.push(right);
}
}
false
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn leaf_count(&self) -> usize {
self.nodes.iter().filter(|n| n.is_leaf()).count()
}
pub fn triangle_count(&self) -> usize {
self.triangles.len()
}
pub fn bounds(&self) -> Option<BoundingBox> {
self.nodes.first().map(|n| n.bounds)
}
}
fn build_recursive(
nodes: &mut Vec<BvhNode>,
tri_indices: &mut [u32],
tri_bounds: &mut [BoundingBox],
tri_centroids: &mut [[f32; 3]],
start: usize,
end: usize,
) -> u32 {
debug_assert!(end > start, "build_recursive called on an empty range");
let count = end - start;
let mut node_bounds = tri_bounds[start];
for b in &tri_bounds[start + 1..end] {
node_bounds = node_bounds.union(*b);
}
if count <= Bvh::LEAF_THRESHOLD {
let node_idx = nodes.len() as u32;
nodes.push(BvhNode {
bounds: node_bounds,
left_or_first: start as u32,
right_child: 0,
tri_count: count as u32,
});
return node_idx;
}
let mut cmin = tri_centroids[start];
let mut cmax = cmin;
for c in &tri_centroids[start + 1..end] {
for axis in 0..3 {
if c[axis] < cmin[axis] {
cmin[axis] = c[axis];
}
if c[axis] > cmax[axis] {
cmax[axis] = c[axis];
}
}
}
let extent = [cmax[0] - cmin[0], cmax[1] - cmin[1], cmax[2] - cmin[2]];
let axis = if extent[0] >= extent[1] && extent[0] >= extent[2] {
0
} else if extent[1] >= extent[2] {
1
} else {
2
};
let mid = if extent[axis] <= 0.0 {
start + count / 2
} else {
let mid_coord = 0.5 * (cmin[axis] + cmax[axis]);
let mut left = start;
let mut right = end;
while left < right {
if tri_centroids[left][axis] < mid_coord {
left += 1;
} else {
right -= 1;
tri_indices.swap(left, right);
tri_bounds.swap(left, right);
tri_centroids.swap(left, right);
}
}
if left == start || left == end {
start + count / 2
} else {
left
}
};
let parent_idx = nodes.len() as u32;
nodes.push(BvhNode {
bounds: node_bounds,
left_or_first: 0,
right_child: 0,
tri_count: 0,
});
let left_child = build_recursive(nodes, tri_indices, tri_bounds, tri_centroids, start, mid);
let right_child = build_recursive(nodes, tri_indices, tri_bounds, tri_centroids, mid, end);
nodes[parent_idx as usize].left_or_first = left_child;
nodes[parent_idx as usize].right_child = right_child;
parent_idx
}
#[inline]
fn finite_point(p: [f32; 3]) -> bool {
p[0].is_finite() && p[1].is_finite() && p[2].is_finite()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::Topology;
fn unit_triangle() -> Primitive {
let mut p = Primitive::new(Topology::Triangles);
p.positions = vec![[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [0.0, 1.0, 1.0]];
p
}
fn two_parallel_triangles() -> Primitive {
let mut p = Primitive::new(Topology::Triangles);
p.positions = vec![
[0.0, 0.0, 1.0],
[1.0, 0.0, 1.0],
[0.0, 1.0, 1.0],
[0.0, 0.0, 2.0],
[1.0, 0.0, 2.0],
[0.0, 1.0, 2.0],
];
p
}
fn unit_cube() -> Primitive {
let mut p = Primitive::new(Topology::Triangles);
p.positions = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[1.0, 0.0, 1.0],
[1.0, 1.0, 1.0],
[0.0, 1.0, 1.0],
];
p.indices = Some(crate::mesh::Indices::U32(vec![
0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, 0, 4, 7, 0, 7, 3, 1, 2, 6, 1, 6, 5, 0, 1, 5, 0, 5, 4, 3, 7, 6, 3, 6, 2,
]));
p
}
#[test]
fn build_single_triangle_one_leaf() {
let p = unit_triangle();
let bvh = Bvh::build(&p).expect("triangle builds");
assert_eq!(bvh.triangle_count(), 1);
assert_eq!(bvh.leaf_count(), 1);
assert_eq!(bvh.node_count(), 1);
assert!(bvh.nodes[0].is_leaf());
}
#[test]
fn build_empty_primitive_returns_none() {
let p = Primitive::new(Topology::Triangles);
assert!(Bvh::build(&p).is_none());
}
#[test]
fn build_non_triangle_topology_returns_none() {
let mut p = Primitive::new(Topology::Lines);
p.positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
assert!(Bvh::build(&p).is_none());
}
#[test]
fn build_all_nan_returns_none() {
let mut p = Primitive::new(Topology::Triangles);
p.positions = vec![[f32::NAN, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
assert!(Bvh::build(&p).is_none());
}
#[test]
fn build_skips_out_of_range_index() {
let mut p = Primitive::new(Topology::Triangles);
p.positions = vec![[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [0.0, 1.0, 1.0]];
p.indices = Some(crate::mesh::Indices::U32(vec![0, 1, 2, 0, 1, 99]));
let bvh = Bvh::build(&p).expect("one good triangle remains");
assert_eq!(bvh.triangle_count(), 1);
}
#[test]
fn intersect_matches_brute_force_two_triangles() {
let p = two_parallel_triangles();
let bvh = Bvh::build(&p).unwrap();
let r = Ray::new([0.3333, 0.3333, 0.0], [0.0, 0.0, 1.0]);
let bvh_hit = bvh.intersect_ray(&p, r, f32::INFINITY).unwrap();
let bf_hit = p.intersect_ray(r, f32::INFINITY).unwrap();
assert_eq!(bvh_hit, bf_hit);
assert!((bvh_hit.t - 1.0).abs() < 1e-5);
assert_eq!(bvh_hit.triangle_index, 0);
}
#[test]
fn intersect_matches_brute_force_cube_through_minus_x_face() {
let p = unit_cube();
let bvh = Bvh::build(&p).unwrap();
let r = Ray::new([-1.0, 0.5, 0.5], [1.0, 0.0, 0.0]);
let bvh_hit = bvh.intersect_ray(&p, r, f32::INFINITY).unwrap();
let bf_hit = p.intersect_ray(r, f32::INFINITY).unwrap();
assert_eq!(bvh_hit, bf_hit);
assert!((bvh_hit.t - 1.0).abs() < 1e-5);
assert!(bvh_hit.front_face);
}
#[test]
fn intersect_miss_returns_none() {
let p = unit_cube();
let bvh = Bvh::build(&p).unwrap();
let r = Ray::new([-1.0, 5.0, 0.5], [1.0, 0.0, 0.0]);
assert!(bvh.intersect_ray(&p, r, f32::INFINITY).is_none());
}
#[test]
fn intersect_t_max_culls_hits() {
let p = unit_cube();
let bvh = Bvh::build(&p).unwrap();
let r = Ray::new([-1.0, 0.5, 0.5], [1.0, 0.0, 0.0]);
assert!(bvh.intersect_ray(&p, r, 0.5).is_none());
}
#[test]
fn intersect_matches_brute_force_across_many_rays() {
let p = unit_cube();
let bvh = Bvh::build(&p).unwrap();
for ix in 0..7 {
for iy in 0..7 {
let x = -0.5 + 0.25 * ix as f32;
let y = -0.5 + 0.25 * iy as f32;
let r = Ray::new([x, y, 2.0], [0.0, 0.0, -1.0]);
let bf = p.intersect_ray(r, f32::INFINITY);
let bv = bvh.intersect_ray(&p, r, f32::INFINITY);
match (bf, bv) {
(None, None) => {}
(Some(a), Some(b)) => {
assert!(
(a.t - b.t).abs() < 1e-5,
"t mismatch at ({}, {}): bf={} bv={}",
x,
y,
a.t,
b.t
);
assert_eq!(a.front_face, b.front_face);
}
other => panic!("hit/miss disagreement at ({}, {}): {:?}", x, y, other),
}
}
}
}
#[test]
fn any_ray_intersection_true_through_cube() {
let p = unit_cube();
let bvh = Bvh::build(&p).unwrap();
let r = Ray::new([-1.0, 0.5, 0.5], [1.0, 0.0, 0.0]);
assert!(bvh.any_ray_intersection(&p, r, f32::INFINITY));
}
#[test]
fn any_ray_intersection_false_when_outside() {
let p = unit_cube();
let bvh = Bvh::build(&p).unwrap();
let r = Ray::new([-1.0, 5.0, 0.5], [1.0, 0.0, 0.0]);
assert!(!bvh.any_ray_intersection(&p, r, f32::INFINITY));
}
#[test]
fn any_ray_intersection_respects_t_max() {
let p = unit_cube();
let bvh = Bvh::build(&p).unwrap();
let r = Ray::new([-1.0, 0.5, 0.5], [1.0, 0.0, 0.0]);
assert!(!bvh.any_ray_intersection(&p, r, 0.5));
}
#[test]
fn leaf_threshold_is_respected() {
let mut p = Primitive::new(Topology::Triangles);
p.positions.push([0.5, 0.5, 1.0]);
let mut indices = Vec::new();
for i in 0..50u32 {
let theta = (i as f32) * std::f32::consts::TAU / 50.0;
p.positions
.push([0.5 + 0.4 * theta.cos(), 0.5 + 0.4 * theta.sin(), 1.0]);
let next = if i == 49 { 1 } else { i + 2 };
indices.extend_from_slice(&[0, i + 1, next]);
}
p.indices = Some(crate::mesh::Indices::U32(indices));
let bvh = Bvh::build(&p).unwrap();
assert_eq!(bvh.triangle_count(), 50);
for node in &bvh.nodes {
if node.is_leaf() {
assert!(
node.tri_count as usize <= Bvh::LEAF_THRESHOLD,
"leaf has {} > {}",
node.tri_count,
Bvh::LEAF_THRESHOLD
);
}
}
assert!(bvh.leaf_count() >= 2);
}
#[test]
fn coincident_centroids_still_build() {
let mut p = Primitive::new(Topology::Triangles);
p.positions = vec![[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [0.0, 1.0, 1.0]];
let mut indices = Vec::new();
for _ in 0..16 {
indices.extend_from_slice(&[0, 1, 2]);
}
p.indices = Some(crate::mesh::Indices::U32(indices));
let bvh = Bvh::build(&p).expect("degenerate centroids still build");
assert_eq!(bvh.triangle_count(), 16);
for node in &bvh.nodes {
if node.is_leaf() {
assert!(node.tri_count as usize <= Bvh::LEAF_THRESHOLD);
}
}
}
#[test]
fn root_bounds_are_tight() {
let p = unit_cube();
let bvh = Bvh::build(&p).unwrap();
let bounds = bvh.bounds().unwrap();
assert_eq!(bounds.min, [0.0, 0.0, 0.0]);
assert_eq!(bounds.max, [1.0, 1.0, 1.0]);
}
}