use crate::ray::{intersect_aabb, Ray};
use crate::scene::{mat4_affine_inverse, mat4_mul, ray_into_local, BoundingBox, MeshId, NodeId};
use crate::{Scene3D, SceneRayHit};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Instance {
pub node: NodeId,
pub mesh: MeshId,
pub bounds: BoundingBox,
pub world: [[f32; 4]; 4],
pub world_inv: [[f32; 4]; 4],
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct InstanceBvhNode {
pub bounds: BoundingBox,
pub left_or_first: u32,
pub right_child: u32,
pub instance_count: u32,
}
impl InstanceBvhNode {
pub fn is_leaf(&self) -> bool {
self.instance_count > 0
}
}
#[derive(Clone, Debug)]
pub struct InstanceBvh {
pub nodes: Vec<InstanceBvhNode>,
pub instances: Vec<Instance>,
}
impl InstanceBvh {
pub const LEAF_THRESHOLD: usize = 4;
pub fn build(scene: &Scene3D) -> Option<Self> {
let instances = gather_instances(scene);
if instances.is_empty() {
return None;
}
let mut indices: Vec<u32> = (0..instances.len() as u32).collect();
let mut bounds: Vec<BoundingBox> = instances.iter().map(|i| i.bounds).collect();
let mut centroids: Vec<[f32; 3]> = instances.iter().map(|i| i.bounds.center()).collect();
let total = indices.len();
let mut nodes: Vec<InstanceBvhNode> = Vec::new();
build_recursive(
&mut nodes,
&mut indices,
&mut bounds,
&mut centroids,
0,
total,
);
let permuted: Vec<Instance> = indices.iter().map(|&i| instances[i as usize]).collect();
Some(InstanceBvh {
nodes,
instances: permuted,
})
}
pub fn intersect_ray(&self, scene: &Scene3D, ray: Ray, t_max: f32) -> Option<SceneRayHit> {
if self.nodes.is_empty() {
return None;
}
intersect_aabb(
ray,
self.nodes[0].bounds.min,
self.nodes[0].bounds.max,
t_max,
)?;
let mut best: Option<SceneRayHit> = 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.instance_count as usize;
for inst in &self.instances[start..end] {
let aabb_hit = intersect_aabb(ray, inst.bounds.min, inst.bounds.max, best_t);
if aabb_hit.is_none() {
continue;
}
let Some(mesh) = scene.mesh(inst.mesh) else {
continue;
};
let local_ray = ray_into_local(inst.world_inv, ray);
if let Some((prim_idx, hit)) = mesh.intersect_ray(local_ray, best_t) {
if best.is_none() || hit.t < best_t {
best_t = hit.t;
best = Some(SceneRayHit {
node: inst.node,
primitive_index: prim_idx,
hit,
});
}
}
}
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, scene: &Scene3D, ray: Ray, t_max: f32) -> bool {
if self.nodes.is_empty() {
return false;
}
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.instance_count as usize;
for inst in &self.instances[start..end] {
if intersect_aabb(ray, inst.bounds.min, inst.bounds.max, t_max).is_none() {
continue;
}
let Some(mesh) = scene.mesh(inst.mesh) else {
continue;
};
let local_ray = ray_into_local(inst.world_inv, ray);
if mesh.intersect_ray(local_ray, 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 instance_count(&self) -> usize {
self.instances.len()
}
pub fn bounds(&self) -> Option<BoundingBox> {
self.nodes.first().map(|n| n.bounds)
}
}
fn gather_instances(scene: &Scene3D) -> Vec<Instance> {
let n_nodes = scene.nodes.len();
if n_nodes == 0 || scene.meshes.is_empty() {
return Vec::new();
}
let identity: [[f32; 4]; 4] = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
];
let mut visited = vec![false; n_nodes];
let mut out: Vec<Instance> = Vec::new();
let mut stack: Vec<(NodeId, [[f32; 4]; 4])> =
scene.roots.iter().rev().map(|r| (*r, identity)).collect();
while let Some((nid, parent)) = stack.pop() {
let idx = nid.0 as usize;
if idx >= n_nodes || visited[idx] {
continue;
}
visited[idx] = true;
let Some(node) = scene.node(nid) else {
continue;
};
let world = mat4_mul(parent, node.transform.to_matrix());
if let Some(m_id) = node.mesh {
if let Some(mesh) = scene.mesh(m_id) {
if let Some(local) = mesh.bounding_box() {
if let Some(world_inv) = mat4_affine_inverse(world) {
out.push(Instance {
node: nid,
mesh: m_id,
bounds: local.transform(world),
world,
world_inv,
});
}
}
}
}
for child in node.children.iter().rev() {
stack.push((*child, world));
}
}
out
}
fn build_recursive(
nodes: &mut Vec<InstanceBvhNode>,
indices: &mut [u32],
bounds: &mut [BoundingBox],
centroids: &mut [[f32; 3]],
start: usize,
end: usize,
) -> u32 {
let count = end - start;
debug_assert!(count > 0);
let mut node_bounds = bounds[start];
for b in &bounds[start + 1..end] {
node_bounds = node_bounds.union(*b);
}
if count <= InstanceBvh::LEAF_THRESHOLD {
let node_idx = nodes.len() as u32;
nodes.push(InstanceBvhNode {
bounds: node_bounds,
left_or_first: start as u32,
right_child: 0,
instance_count: count as u32,
});
return node_idx;
}
let mut cmin = centroids[start];
let mut cmax = cmin;
for c in ¢roids[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 centroids[left][axis] < mid_coord {
left += 1;
} else {
right -= 1;
indices.swap(left, right);
bounds.swap(left, right);
centroids.swap(left, right);
}
}
if left == start || left == end {
start + count / 2
} else {
left
}
};
let parent_idx = nodes.len() as u32;
nodes.push(InstanceBvhNode {
bounds: node_bounds,
left_or_first: 0,
right_child: 0,
instance_count: 0,
});
let left_child = build_recursive(nodes, indices, bounds, centroids, start, mid);
let right_child = build_recursive(nodes, indices, bounds, centroids, mid, end);
nodes[parent_idx as usize].left_or_first = left_child;
nodes[parent_idx as usize].right_child = right_child;
parent_idx
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::Topology;
use crate::scene::Transform;
use crate::{Mesh, Node, Primitive};
fn unit_cube_mesh() -> Mesh {
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, ]));
Mesh::new(Some("cube".to_owned())).with_primitive(p)
}
fn one_cube_scene() -> Scene3D {
let mut s = Scene3D::new();
let mid = s.add_mesh(unit_cube_mesh());
let nid = s.add_node(Node::new().with_mesh(mid));
s.add_root(nid);
s
}
fn grid_scene(n: usize, spacing: f32) -> Scene3D {
let mut s = Scene3D::new();
let mid = s.add_mesh(unit_cube_mesh());
for ix in 0..n {
let t = Transform::Trs {
translation: [ix as f32 * spacing, 0.0, 0.0],
rotation: [0.0, 0.0, 0.0, 1.0],
scale: [1.0, 1.0, 1.0],
};
let nid = s.add_node(Node::new().with_transform(t).with_mesh(mid));
s.add_root(nid);
}
s
}
#[test]
fn build_empty_scene_returns_none() {
let s = Scene3D::new();
assert!(InstanceBvh::build(&s).is_none());
}
#[test]
fn build_scene_with_node_but_no_mesh_returns_none() {
let mut s = Scene3D::new();
let nid = s.add_node(Node::new());
s.add_root(nid);
assert!(InstanceBvh::build(&s).is_none());
}
#[test]
fn build_one_cube_yields_one_leaf() {
let s = one_cube_scene();
let b = InstanceBvh::build(&s).expect("single instance builds");
assert_eq!(b.instance_count(), 1);
assert_eq!(b.leaf_count(), 1);
assert_eq!(b.node_count(), 1);
assert!(b.nodes[0].is_leaf());
assert_eq!(b.instances[0].bounds.min, [0.0, 0.0, 0.0]);
assert_eq!(b.instances[0].bounds.max, [1.0, 1.0, 1.0]);
}
#[test]
fn build_grid_yields_interior_nodes() {
let s = grid_scene(16, 3.0);
let b = InstanceBvh::build(&s).expect("16 instances builds");
assert_eq!(b.instance_count(), 16);
assert!(b.leaf_count() > 1);
assert!(b.node_count() > b.leaf_count(), "interior nodes present");
let root = b.bounds().unwrap();
assert!((root.min[0] - 0.0).abs() < 1e-5);
assert!((root.max[0] - 46.0).abs() < 1e-5);
}
#[test]
fn detached_node_does_not_appear() {
let mut s = Scene3D::new();
let mid = s.add_mesh(unit_cube_mesh());
let _detached = s.add_node(Node::new().with_mesh(mid));
let attached = s.add_node(Node::new().with_mesh(mid));
s.add_root(attached);
let b = InstanceBvh::build(&s).expect("one reachable instance");
assert_eq!(b.instance_count(), 1);
assert_eq!(b.instances[0].node, attached);
}
#[test]
fn singular_transform_is_skipped() {
let mut s = Scene3D::new();
let mid = s.add_mesh(unit_cube_mesh());
let bad = s.add_node(
Node::new()
.with_transform(Transform::Trs {
translation: [0.0, 0.0, 0.0],
rotation: [0.0, 0.0, 0.0, 1.0],
scale: [0.0, 1.0, 1.0],
})
.with_mesh(mid),
);
s.add_root(bad);
let good = s.add_node(
Node::new()
.with_transform(Transform::Trs {
translation: [5.0, 0.0, 0.0],
rotation: [0.0, 0.0, 0.0, 1.0],
scale: [1.0, 1.0, 1.0],
})
.with_mesh(mid),
);
s.add_root(good);
let b = InstanceBvh::build(&s).expect("one good instance");
assert_eq!(b.instance_count(), 1);
assert_eq!(b.instances[0].node, good);
}
#[test]
fn intersect_ray_matches_scene_walk_on_one_cube() {
let s = one_cube_scene();
let b = InstanceBvh::build(&s).unwrap();
let r = Ray::new([-1.0, 0.5, 0.5], [1.0, 0.0, 0.0]);
let bvh_hit = b.intersect_ray(&s, r, f32::INFINITY).unwrap();
let scene_hit = s.intersect_ray(r, f32::INFINITY).unwrap();
assert!((bvh_hit.hit.t - scene_hit.hit.t).abs() < 1e-5);
assert_eq!(bvh_hit.node, scene_hit.node);
}
#[test]
fn intersect_ray_miss_returns_none() {
let s = one_cube_scene();
let b = InstanceBvh::build(&s).unwrap();
let r = Ray::new([-1.0, 5.0, 5.0], [1.0, 0.0, 0.0]);
assert!(b.intersect_ray(&s, r, f32::INFINITY).is_none());
}
#[test]
fn intersect_ray_t_max_culls_hits() {
let s = one_cube_scene();
let b = InstanceBvh::build(&s).unwrap();
let r = Ray::new([-1.0, 0.5, 0.5], [1.0, 0.0, 0.0]);
assert!(b.intersect_ray(&s, r, 0.5).is_none());
}
#[test]
fn intersect_ray_picks_nearest_instance_in_grid() {
let s = grid_scene(8, 3.0);
let b = InstanceBvh::build(&s).unwrap();
let r = Ray::new([-1.0, 0.5, 0.5], [1.0, 0.0, 0.0]);
let hit = b.intersect_ray(&s, r, f32::INFINITY).unwrap();
assert!((hit.hit.t - 1.0).abs() < 1e-4);
}
#[test]
fn intersect_ray_matches_scene_walk_on_grid() {
let s = grid_scene(8, 3.0);
let b = InstanceBvh::build(&s).unwrap();
for iy in 0..5 {
let y = -1.0 + 0.5 * iy as f32;
let r = Ray::new([-1.0, y, 0.5], [1.0, 0.0, 0.0]);
let bf = s.intersect_ray(r, f32::INFINITY);
let bv = b.intersect_ray(&s, r, f32::INFINITY);
match (bf, bv) {
(None, None) => {}
(Some(a), Some(c)) => {
assert!((a.hit.t - c.hit.t).abs() < 1e-4);
assert_eq!(a.hit.front_face, c.hit.front_face);
}
(a, b) => panic!("mismatch: scene={:?} bvh={:?}", a, b),
}
}
}
#[test]
fn any_ray_intersection_agrees_with_scene_walk() {
let s = grid_scene(8, 3.0);
let b = InstanceBvh::build(&s).unwrap();
for iy in 0..5 {
let y = -1.0 + 0.5 * iy as f32;
let r = Ray::new([-1.0, y, 0.5], [1.0, 0.0, 0.0]);
assert_eq!(
b.any_ray_intersection(&s, r, f32::INFINITY),
s.any_ray_intersection(r, f32::INFINITY)
);
}
}
#[test]
fn any_ray_intersection_short_circuits_on_hit() {
let s = grid_scene(4, 3.0);
let b = InstanceBvh::build(&s).unwrap();
let r = Ray::new([-1.0, 0.5, 0.5], [1.0, 0.0, 0.0]);
assert!(b.any_ray_intersection(&s, r, f32::INFINITY));
}
#[test]
fn any_ray_intersection_miss_returns_false() {
let s = grid_scene(4, 3.0);
let b = InstanceBvh::build(&s).unwrap();
let r = Ray::new([-1.0, 5.0, 5.0], [1.0, 0.0, 0.0]);
assert!(!b.any_ray_intersection(&s, r, f32::INFINITY));
}
#[test]
fn instance_count_equals_reachable_meshed_nodes() {
let s = grid_scene(7, 3.0);
let b = InstanceBvh::build(&s).unwrap();
assert_eq!(b.instance_count(), 7);
let root = b.bounds().unwrap();
assert!((root.min[0] - 0.0).abs() < 1e-5);
assert!((root.max[0] - 19.0).abs() < 1e-5);
}
#[test]
fn leaf_threshold_constant_is_four() {
assert_eq!(InstanceBvh::LEAF_THRESHOLD, 4);
}
#[test]
fn small_scene_at_or_below_threshold_is_a_single_leaf() {
let s = grid_scene(4, 3.0);
let b = InstanceBvh::build(&s).unwrap();
assert_eq!(b.node_count(), 1);
assert_eq!(b.leaf_count(), 1);
assert!(b.nodes[0].is_leaf());
}
#[test]
fn shared_child_resolved_via_first_parent() {
let mut s = Scene3D::new();
let mid = s.add_mesh(unit_cube_mesh());
let shared = s.add_node(Node::new().with_mesh(mid));
let p1 = s.add_node(Node::new().with_transform(Transform::Trs {
translation: [0.0, 0.0, 0.0],
rotation: [0.0, 0.0, 0.0, 1.0],
scale: [1.0, 1.0, 1.0],
}));
let p2 = s.add_node(Node::new().with_transform(Transform::Trs {
translation: [10.0, 0.0, 0.0],
rotation: [0.0, 0.0, 0.0, 1.0],
scale: [1.0, 1.0, 1.0],
}));
if let Some(p1n) = s.node_mut(p1) {
p1n.children.push(shared);
}
if let Some(p2n) = s.node_mut(p2) {
p2n.children.push(shared);
}
s.add_root(p1);
s.add_root(p2);
let b = InstanceBvh::build(&s).unwrap();
assert_eq!(b.instance_count(), 1);
}
}