Skip to content

Commit

Permalink
improvement: BVHNode hit method optimization
Browse files Browse the repository at this point in the history
earlier returns based on the distances to the child nodes' axis-aligned bounding boxes. rays no longer need to traverse all the way through objects to return the closest hitpoint
  • Loading branch information
Walther committed Jun 23, 2024
1 parent 6898b32 commit a0831be
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 34 deletions.
4 changes: 2 additions & 2 deletions clovers-cli/src/debug_visualizations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ use rand::rngs::SmallRng;

/// Visualizes the BVH traversal count - how many BVH nodes needed to be tested for intersection?
#[must_use]
pub fn bvh_testcount(ray: &Ray, scene: &Scene) -> LinSrgb {
pub fn bvh_testcount(ray: &Ray, scene: &Scene, rng: &mut SmallRng) -> LinSrgb {
let mut depth = 0;
scene
.hitables
.bvh_testcount(&mut depth, ray, EPSILON_SHADOW_ACNE, Float::MAX);
.bvh_testcount(&mut depth, ray, EPSILON_SHADOW_ACNE, Float::MAX, rng);

bvh_testcount_to_color(depth)
}
Expand Down
2 changes: 1 addition & 1 deletion clovers-cli/src/draw_cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ fn render_pixel_bvhtestcount(
.camera
.get_ray(pixel_location, lens_offset, time, wavelength);

let color: LinSrgb = { bvh_testcount(&ray, scene) };
let color: LinSrgb = { bvh_testcount(&ray, scene, rng) };
let color: Srgb = color.into_color_unclamped();
let color: Srgb<u8> = color.into_format();
color
Expand Down
113 changes: 91 additions & 22 deletions clovers/src/bvh/bvh_testcount.rs
Original file line number Diff line number Diff line change
@@ -1,41 +1,110 @@
use crate::{hitable::Hitable, ray::Ray, Float};
use rand::rngs::SmallRng;

use crate::{
hitable::{HitRecord, Hitable, HitableTrait},
ray::Ray,
Float,
};

use super::BVHNode;

impl<'scene> BVHNode<'scene> {
// NOTE: this must be kept in close alignment with the implementation of BVHNode::hit()!
// TODO: maybe move the statistics counting to the method itself? Measure the impact?
/// Alternate hit method that maintains a test count for the BVH traversals.
pub fn bvh_testcount(
&'scene self,
depth: &mut usize,
ray: &Ray,
distance_min: Float,
distance_max: Float,
) {
rng: &mut SmallRng,
) -> Option<HitRecord> {
*depth += 1;

// If we do not hit the bounding box of current node, early return None
if !self.bounding_box.hit(ray, distance_min, distance_max) {
return;
return None;
}

Self::bvh_testcount_recurse_condition(&self.left, depth, ray, distance_min, distance_max);
Self::bvh_testcount_recurse_condition(&self.right, depth, ray, distance_min, distance_max);
}
// Check the distance to the bounding boxes
let (left_aabb_distance, right_aabb_distance) =
match (self.left.bounding_box(), self.right.bounding_box()) {
// Early returns, if there's no bounding box
(None, None) => return None,
(Some(_l), None) => {
return recurse(&self.left, depth, ray, distance_min, distance_max, rng)
}
(None, Some(_r)) => {
return recurse(&self.right, depth, ray, distance_min, distance_max, rng)
}
// If we have bounding boxes, get the distances
(Some(l), Some(r)) => (l.distance(ray), r.distance(ray)),
};
let (_closest_aabb_distance, furthest_aabb_distance) =
match (left_aabb_distance, right_aabb_distance) {
// Early return: neither child AABB can be hit with the ray
(None, None) => return None,
// Early return: only one child can be hit with the ray
(Some(_d), None) => {
return recurse(&self.left, depth, ray, distance_min, distance_max, rng)
}
(None, Some(_d)) => {
return recurse(&self.right, depth, ray, distance_min, distance_max, rng)
}
// Default case: both children can be hit with the ray, check the distance
(Some(l), Some(r)) => (Float::min(l, r), Float::max(l, r)),
};

fn bvh_testcount_recurse_condition(
bvhnode: &'scene Hitable, // BVHNode
depth: &mut usize,
ray: &Ray,
distance_min: Float,
distance_max: Float,
) {
match bvhnode {
Hitable::BVHNode(bvh) => bvh.bvh_testcount(depth, ray, distance_min, distance_max),
Hitable::STL(s) => s
.bvhnode
.bvh_testcount(depth, ray, distance_min, distance_max),
Hitable::GLTF(g) => g
.bvhnode
.bvh_testcount(depth, ray, distance_min, distance_max),
_ => (),
// Check the closest first
let (closest_bvh, furthest_bvh) = if left_aabb_distance < right_aabb_distance {
(&self.left, &self.right)
} else {
(&self.right, &self.left)
};
let closest_bvh_hit = recurse(closest_bvh, depth, ray, distance_min, distance_max, rng);

// Is the hit closer than the closest point of the other AABB?
if let Some(ref hit_record) = closest_bvh_hit {
if hit_record.distance < furthest_aabb_distance {
return Some(hit_record.clone());
}
}
// Otherwise, check the other child too
let furthest_bvh_hit = recurse(furthest_bvh, depth, ray, distance_min, distance_max, rng);

// Did we hit neither of the child nodes, one of them, or both?
// Return the closest thing we hit
match (&closest_bvh_hit, &furthest_bvh_hit) {
(None, None) => None,
(None, Some(_)) => furthest_bvh_hit,
(Some(_), None) => closest_bvh_hit,
(Some(left), Some(right)) => {
if left.distance < right.distance {
return closest_bvh_hit;
}
furthest_bvh_hit
}
}
}
}

fn recurse<'scene>(
bvhnode: &'scene Hitable, // BVHNode
depth: &mut usize,
ray: &Ray,
distance_min: Float,
distance_max: Float,
rng: &mut SmallRng,
) -> Option<HitRecord<'scene>> {
match bvhnode {
Hitable::BVHNode(bvh) => bvh.bvh_testcount(depth, ray, distance_min, distance_max, rng),
Hitable::STL(s) => s
.bvhnode
.bvh_testcount(depth, ray, distance_min, distance_max, rng),
Hitable::GLTF(g) => g
.bvhnode
.bvh_testcount(depth, ray, distance_min, distance_max, rng),
hitable => hitable.hit(ray, distance_min, distance_max, rng),
}
}
50 changes: 42 additions & 8 deletions clovers/src/bvh/hitable_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,55 @@ impl<'scene> HitableTrait for BVHNode<'scene> {
return None;
}

// Otherwise we have hit the bounding box of this node, recurse to child nodes
let hit_left = self.left.hit(ray, distance_min, distance_max, rng);
let hit_right = self.right.hit(ray, distance_min, distance_max, rng);
// Check the distance to the bounding boxes
let (left_aabb_distance, right_aabb_distance) =
match (self.left.bounding_box(), self.right.bounding_box()) {
// Early returns, if there's no bounding box
(None, None) => return None,
(Some(_l), None) => return self.left.hit(ray, distance_min, distance_max, rng),
(None, Some(_r)) => return self.right.hit(ray, distance_min, distance_max, rng),
// If we have bounding boxes, get the distances
(Some(l), Some(r)) => (l.distance(ray), r.distance(ray)),
};
let (_closest_aabb_distance, furthest_aabb_distance) =
match (left_aabb_distance, right_aabb_distance) {
// Early return: neither child AABB can be hit with the ray
(None, None) => return None,
// Early return: only one child can be hit with the ray
(Some(_d), None) => return self.left.hit(ray, distance_min, distance_max, rng),
(None, Some(_d)) => return self.right.hit(ray, distance_min, distance_max, rng),
// Default case: both children can be hit with the ray, check the distance
(Some(l), Some(r)) => (Float::min(l, r), Float::max(l, r)),
};

// Check the closest first
let (closest_bvh, furthest_bvh) = if left_aabb_distance < right_aabb_distance {
(&self.left, &self.right)
} else {
(&self.right, &self.left)
};
let closest_bvh_hit = closest_bvh.hit(ray, distance_min, distance_max, rng);

// Is the hit closer than the closest point of the other AABB?
if let Some(ref hit_record) = closest_bvh_hit {
if hit_record.distance < furthest_aabb_distance {
return Some(hit_record.clone());
}
}
// Otherwise, check the other child too
let furthest_bvh_hit = furthest_bvh.hit(ray, distance_min, distance_max, rng);

// Did we hit neither of the child nodes, one of them, or both?
// Return the closest thing we hit
match (&hit_left, &hit_right) {
match (&closest_bvh_hit, &furthest_bvh_hit) {
(None, None) => None,
(None, Some(_)) => hit_right,
(Some(_), None) => hit_left,
(None, Some(_)) => furthest_bvh_hit,
(Some(_), None) => closest_bvh_hit,
(Some(left), Some(right)) => {
if left.distance < right.distance {
return hit_left;
return closest_bvh_hit;
}
hit_right
furthest_bvh_hit
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion clovers/src/hitable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use enum_dispatch::enum_dispatch;
use rand::rngs::SmallRng;

/// Represents a ray-object intersection, with plenty of data about the intersection.
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct HitRecord<'a> {
/// Distance from the ray origin to the hitpoint
pub distance: Float,
Expand Down

0 comments on commit a0831be

Please sign in to comment.