Hello! Welcome back to our journey in building a C++ ray tracer.
In the last lesson, we designed and implemented the logic to construct a Bounding Volume Hierarchy. We took a flat list of objects and organized them into a bvh_node tree structure, where each node knows its bounding box and its children. Now, it's time to reap the rewards of that work.
This lesson focuses on implementing the hit() method for our bvh_node. This is the traversal algorithm that will navigate the tree, using our fast ray-AABB intersection test to rapidly discard huge portions of the scene. This is the core mechanism that makes rendering complex scenes feasible. By the end of this lesson, you'll have an efficient function that can find the closest intersection for a ray in a BVH-accelerated scene.
1. The Basic Traversal Algorithm: A Recursive Approach
Let's start with the most intuitive way to traverse a tree: recursion. The logic for our bvh_node::hit() method follows a simple depth-first search pattern:
- First, test if the incoming ray intersects the current node's bounding box. If it misses, we can immediately stop and return
false. There's no need to check any of the objects or child nodes contained within this volume. This is the fundamental culling step. - If the ray does hit the bounding box, we proceed:
- If the current node is a leaf node (meaning it directly holds geometric primitives), we intersect the ray against each of those primitives, updating our
hit_recordif we find a closer hit. - If the current node is an internal node, we recursively call the
hit()method on its left and right children.
- If the current node is a leaf node (meaning it directly holds geometric primitives), we intersect the ray against each of those primitives, updating our
A crucial detail is that the t_max parameter of our ray (or the t value in our hit_record) is continuously updated. When a recursive call finds an intersection, t_max is shortened. This new, shorter t_max is then used for all subsequent tests, allowing the algorithm to prune branches that are further away than an already-found intersection.
This article from Jacco's Blog provides a concise C++ implementation of this exact recursive traversal logic.
How to build a BVH – Part 1: Basics - Jacco's Blog
The article 'How to build a BVH – Part 1: Basics' provides a clean, recursive IntersectBVH function. This is a perfect starting point for understanding the core traversal logic.
Please read the section titled 'Traverse'. Focus on the IntersectBVH function. Note how it first calls IntersectAABB, and only if that succeeds does it proceed to either test triangles (for a leaf) or recurse into its children.
This image provides a great visualization of the process. For any given ray, it only traverses the "blue" paths where the ray hits the bounding box. It completely ignores branches where the ray misses the box (gray paths).

2. Implementing bvh_node::hit()
Let's translate this logic into our hittable class structure. Your bvh_node::hit() method will look something like this:
bool bvh_node::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
// 1. Check if the ray hits this node's bounding box.
// If not, we're done. Use the aabb::hit() method we already have.
if (!box.hit(r, t_min, t_max)) {
return false;
}
// 2. The ray hits the box. Now check the children.
// We need to check both children to find the CLOSEST intersection.
bool hit_left = left->hit(r, t_min, t_max, rec);
// 3. If the left child was hit, the hit_record 'rec' is now updated.
// The t_max for the right child check is now rec.t, ensuring we only
// look for intersections closer than what we've already found.
bool hit_right = right->hit(r, t_min, hit_left ? rec.t : t_max, rec);
return hit_left || hit_right;
}
Note: This simplified version is for an internal node. A complete implementation needs to handle leaf nodes, which contain the actual primitives.
The key insight here is how t_max propagates. When left->hit(...) returns true, it updates rec, and rec.t becomes our new, tighter t_max for the call to right->hit(...). This efficiently prunes the search space.
3. Optimization 1: Iterative Traversal with a Stack
While recursion is intuitive, it has drawbacks, such as potential stack overflow with very deep trees and function call overhead. Your CS background will recognize the standard solution: convert the recursion to an iterative algorithm using an explicit stack.
The logic remains a depth-first search:
- Create a stack and push the root node onto it.
- While the stack is not empty:
- Pop a node.
- Check if the ray hits its bounding box.
- If it hits:
- If it's a leaf, test its primitives.
- If it's an internal node, push its children onto the stack.
This conversion is not just a theoretical exercise. It's essential for platforms that have limited or no support for recursion, such as GPU shaders used heavily in VFX rendering.
Coding Adventure: More Ray Tracing!
Sebastian Lague provides an excellent walkthrough of converting the recursive BVH traversal to an iterative, stack-based one. This is a common and important transformation.
Please watch from 15:58 to 18:04. He explains why recursion isn't viable in his shader environment and clearly demonstrates how a stack can be used to achieve the same depth-first search iteratively.
4. Optimization 2: Ordered Traversal for Early Termination
Our current traversal checks children in a fixed order (e.g., left then right). This is suboptimal. Consider a ray passing through two child boxes. If we check the farther box first and find a hit, we still have to check the closer box to see if there's an even closer intersection.
However, if we check the closer box first, we are more likely to find a close intersection early. This shrinks our ray's t_max significantly, making it much more likely that the ray will miss the farther box entirely, allowing us to prune it without ever visiting it.
This leads to a powerful optimization: always traverse the closer child node first.
To implement this, our algorithm changes:
- Use an iterative, stack-based traversal.
- When an internal node is hit, test the ray against both child AABBs. Your
aabb::hitfunction should return the intersection distancet, not just a boolean. - Determine which child is closer.
- Push the farther child onto the stack (if it was hit and is closer than
t_max). - Immediately proceed to process the closer child in the next loop iteration (instead of pushing it).
This "closer first" strategy dramatically improves performance, especially for views where the ray passes through many layers of the BVH.
How to build a BVH – part 2: Faster Rays - Jacco's Blog
Jacco's Blog continues with 'Part 2: Faster Rays', where he implements this exact optimization. He refactors the recursive traversal into an iterative one that sorts children by distance.
Please read the section titled 'Order'. This section explains the problem with fixed-order traversal and presents the full C++ code for an iterative, stack-based IntersectBVH function that visits the closer child node first. Pay close attention to how it uses a stack and swaps the children based on their intersection distances.
Test your understanding!
In an iterative, ordered BVH traversal, you are at an internal node. Your ray hits both child AABBs.
- The left child's AABB is hit at distance
t=10. - The right child's AABB is hit at distance
t=5.
What does the algorithm do next?
Show answer
- It identifies the right child as being closer (
t=5). - It pushes the farther child (the left node) onto the stack to be processed later.
- It sets the current node to be the right child and continues the loop, effectively traversing down the closer path immediately.
Conclusion
Today, we've turned our static BVH data structure into a dynamic and highly efficient acceleration engine. You now have the complete algorithm to trace a ray through the hierarchy.
Key Takeaways:
- BVH traversal works by recursively (or iteratively) checking for ray intersections with node bounding boxes.
- If a ray misses a node's box, that entire branch of the scene hierarchy is culled, saving immense computation.
- The ray's maximum intersection distance (
t_max) is continuously updated, which is the key to pruning branches that are farther away than an already-found intersection. - For optimal performance, an iterative stack-based traversal should be used, and child nodes should be visited in front-to-back order.
Next Steps
We now have all the pieces: a way to build the BVH and a way to traverse it. In the next lesson, we'll integrate this into our main render loop. We will render a complex scene both with and without the BVH enabled to quantitatively measure the incredible performance gain our new acceleration structure provides.