Hello! Welcome back to our journey in building a C++ ray tracer.
In our last lesson, we explored the crucial why behind acceleration structures. You learned that for any reasonably complex scene, the brute-force method of testing every ray against every triangle is computationally impossible due to its O(N) complexity per ray. The solution, a Bounding Volume Hierarchy (BVH), promised to reduce this to an efficient O(log N), making complex scenes renderable.
Today, we'll build the fundamental component of that hierarchy. This lesson focuses on the learning outcome: Implement an axis-aligned bounding box (AABB) primitive. We will define the AABB as a geometric object in C++, implement a highly efficient ray-intersection test for it, and give it the ability to enclose other objects and boxes. This "primitive" will be the brick we use to build our entire acceleration structure.
Let's get started.
1. What is an Axis-Aligned Bounding Box?
An Axis-Aligned Bounding Box (AABB) is the simplest type of bounding volume. It's a 3D box whose faces are all parallel to the main coordinate planes (XY, YZ, and XZ).
Because of this alignment, we can define the entire box with just two points: a point3 for the minimum corner coordinates (e.g., the bottom-front-left) and a point3 for the maximum corner coordinates (e.g., the top-back-right).
A sphere perfectly enclosed by its Axis-Aligned Bounding Box. The box is defined by its minimum (p_min) and maximum (p_max) corner points.
While defining it with two points is intuitive, for the intersection logic, it's often more robust to think of an AABB as the intersection of three "slabs." A slab is the infinite space between two parallel planes.
- An x-slab is the space between planes
x=x_minandx=x_max. - A y-slab is the space between
y=y_minandy=y_max. - A z-slab is the space between
z=z_minandz=z_max.
The volume where all three of these slabs overlap is our AABB. This "slab" perspective is the key to creating a fast and elegant intersection algorithm.
2. The Ray-AABB Intersection Algorithm: The Slab Test
To determine if a ray hits an AABB, we don't test against its 6 faces or 8 vertices. Instead, we use the slab representation. The logic is simple: A ray intersects an AABB if and only if the t intervals of intersection for all three slabs overlap.
Let's break this down for a single axis, say the x-axis. A ray is defined by P(t) = Q + t*d, where Q is the origin and d is the direction. The x-coordinate at any point t is x(t) = Q_x + t*d_x.
We can find the t values where the ray crosses the two planes of the x-slab (x_0 and x_1):
These two values, t_0 and t_1, form a t-interval [t_min_x, t_max_x] during which the ray is inside the x-slab. We do the same for the y and z slabs to get [t_min_y, t_max_y] and [t_min_z, t_max_z].
The ray hits the box only if these three intervals overlap. The overlapping region is found by taking the latest start time and the earliest end time:
t_entry = max(t_min_x, t_min_y, t_min_z)t_exit = min(t_max_x, t_max_y, t_max_z)
An intersection occurs if the ray enters before it exits, i.e., t_entry < t_exit.
The book Ray Tracing: The Next Week explains this concept very clearly.
Let's read the conceptual overview from Peter Shirley's 'Ray Tracing: The Next Week'. It introduces AABBs and explains the slab method with helpful diagrams.
Please read the text and review the figures in the first section of Chapter 2, titled 'Axis-Aligned Bounding Boxes (AABBs)'. Focus on understanding the core idea that an AABB is an intersection of slabs and that a ray hits the box if the t-intervals for each slab overlap.
3. Implementing the aabb Class
Now let's translate this theory into C++. Our goal is to create a self-contained aabb.h file. Following the approach in Ray Tracing: The Next Week, we will represent the slabs using a small helper class, interval, which simply stores a min and max value.
This reading contains the complete C++ code for our aabb class, including its constructors and the all-important hit method that implements the slab test.
Let's examine a full C++ implementation. This code defines the aabb class and its intersection method.
Please read the code under the heading 'Now we have everything we need to implement the new AABB class.' This contains the full aabb.h file. As you read, pay close attention to: Data Members: The class uses interval x, y, z; to represent the three slabs. Constructors: Note the different ways an aabb can be created. The hit method: This is the core of the slab test. Trace how the loop iterates through the three axes, calculates t0 and t1 for each slab, and progressively shrinks the valid ray_t interval.
Dissecting the hit Method
The hit method in the provided code is a compact and efficient implementation of the slab test. Let's analyze its key features:
for (int axis = 0; axis < 3; axis++): It elegantly loops through the x, y, and z axes.const double adinv = 1.0 / ray_dir[axis];: It computes the inverse of the ray's direction component once per axis. This is an important optimization, as multiplication is much faster than division.auto t0 = (ax.min - ray_orig[axis]) * adinv;: This is the calculation for the first plane intersection.if (t0 < t1) { ... } else { ... }: Thisif/elseblock cleverly handles rays traveling in both positive and negative directions. If the ray direction for an axis is negative,t0will be greater thant1. This code correctly identifies the smallertas the entry and the largertas the exit from the slab, regardless of direction.if (t0 > ray_t.min) ray_t.min = t0;: This is where the overlap is computed. It's equivalent toray_t.min = max(ray_t.min, slab_t_min).if (ray_t.max <= ray_t.min) return false;: If at any point the overall interval becomes inverted (or zero-length), it means the slabs don't overlap, and we can immediately returnfalse. This is a crucial early-out optimization.
Test your understanding!
In the AABB hit method, what would happen if the ray is perfectly parallel to one of the axes, for example, the x-axis? How does the code handle the potential division by zero?
Show answer
If the ray is parallel to the x-axis, ray_dir.x would be zero. The calculation 1.0 / ray_dir[axis] would result in positive or negative infinity, depending on the sign of zero in floating-point math.
The subsequent multiplications will result in t0 and t1 also being +/- infinity. However, the logic still holds. The ray_t interval will either remain valid or collapse. For example, if the ray's origin is outside the slab (ray_orig[axis] < ax.min), the t interval becomes [+inf, +inf], and the ray_t interval won't be updated in a way that causes a false hit. If the origin is inside the slab, the t interval is [-inf, +inf], which effectively means the ray is always inside that slab, so the check passes and intersection depends only on the other two axes, which is correct.
The IEEE 754 floating-point standard's rules for infinity make this work robustly without special if (ray_dir[axis] == 0) checks.
4. Making AABBs Useful
An intersection test is only half the story. To build our BVH, we need two more pieces of functionality:
- A way for every
hittableobject to report its own bounding box. - A way to create a larger bounding box that encloses two smaller ones.
This involves modifying our abstract hittable class and adding a new constructor to our aabb class.
Let's extend our system to generate and combine bounding boxes. This will allow us to wrap our primitives in AABBs and then group those AABBs into a hierarchy.
Please read the section 'Constructing Bounding Boxes for Hittables'. Focus on three key changes: The new pure virtual method virtual aabb bounding_box() const = 0; added to the hittable interface. The implementation of this method for the sphere class. Notice how it calculates the min/max points from the center and radius. The new aabb constructor that takes two other aabbs as input and creates a new one that encloses both.
A final, subtle improvement is to ensure that our bounding boxes have a non-zero thickness on all axes. This prevents numerical issues with flat objects like quads or perfectly-aligned triangles. The Ray Tracing: The Next Week text (resource LINK) introduces a pad_to_minimums() function in Section 4 of the AABB chapter for this purpose, which is a good practice to adopt in your own implementation.
Conclusion
In this lesson, we have constructed a complete and robust AABB primitive. You now have a new tool in your ray tracing toolbox, ready for the next stage.
Key Takeaways:
- AABB Representation: An AABB is defined by its minimum and maximum corners, or equivalently, as the intersection of three axis-aligned slabs.
- The Slab Test: This is a highly efficient intersection algorithm that works by checking for an overlap in the ray's
t-intervals across the x, y, and z slabs. - Robust Implementation: A good implementation handles rays traveling in any direction and avoids division-by-zero issues through the properties of floating-point infinity.
- Class Functionality: A useful
aabbclass needs more than an intersection test. It requires constructors to be created from points, and importantly, from otheraabbs, to allow for hierarchical construction. - Hittable Interface: By adding
bounding_box()to thehittableabstract class, we ensure that any object in our scene can report its spatial bounds.
Next Steps
We now have our building blocks. In the next lesson, we will use this aabb primitive to implement the "H" in BVH: the Hierarchy. We'll learn how to take a list of scene objects, recursively partition them, and build the tree structure that will finally give us the massive performance boost we're looking for.