(Update: part 2)

Axis-aligned bounding boxes (AABBs) are universally used to bound finite objects in ray-tracing. Ray/AABB intersections are usually faster to calculate than exact ray/object intersections, and allow the construction of bounding volume hierarchies (BVHs) which reduce the number of objects that need to be considered for each ray. (More on BVHs in a later post.) This means that a ray-tracer spends a lot of its time calculating ray/AABB intersections, and therefore this code ought to be highly optimised.

The fastest method for performing ray/AABB intersections is the slab method. The idea is to treat the box as the space inside of three pairs of parallel planes. The ray is clipped by each pair of parallel planes, and if any portion of the ray remains, it intersected the box.

A simple implementation of this algorithm might look like this (in two dimensions for brevity):

bool intersection(box b, ray r) { double tmin = -INFINITY, tmax = INFINITY; if (ray.n.x != 0.0) { double tx1 = (b.min.x - r.x0.x)/r.n.x; double tx2 = (b.max.x - r.x0.x)/r.n.x; tmin = max(tmin, min(tx1, tx2)); tmax = min(tmax, max(tx1, tx2)); } if (ray.n.y != 0.0) { double ty1 = (b.min.y - r.x0.y)/r.n.y; double ty2 = (b.max.y - r.x0.y)/r.n.y; tmin = max(tmin, min(ty1, ty2)); tmax = min(tmax, max(ty1, ty2)); } return tmax >= tmin; } |

However, those divisions take quite a bit of time. Since when ray-tracing, the same ray is tested against many AABBs, it makes sense to pre-calculate the inverses of the direction components of the ray. If we can rely on the IEEE 754 floating-point properties, this also implicitly handles the edge case where a component of the direction is zero - the `tx1`

and `tx2`

values (for example) will be infinities of opposite sign if the ray is within the slabs, thus leaving `tmin`

and `tmax`

unchanged. If the ray is outside the slabs, `tx1`

and `tx2`

will be infinities with the same sign, thus making `tmin == +inf`

or `tmax == -inf`

, and causing the test to fail.

The final implementation would look like this:

bool intersection(box b, ray r) { double tx1 = (b.min.x - r.x0.x)*r.n_inv.x; double tx2 = (b.max.x - r.x0.x)*r.n_inv.x; double tmin = min(tx1, tx2); double tmax = max(tx1, tx2); double ty1 = (b.min.y - r.x0.y)*r.n_inv.y; double ty2 = (b.max.y - r.x0.y)*r.n_inv.y; tmin = max(tmin, min(ty1, ty2)); tmax = min(tmax, max(ty1, ty2)); return tmax >= tmin; } |

Since modern floating-point instruction sets can compute min and max without branches, this gives a ray/AABB intersection test with no branches or divisions.

My implementation of this in my ray-tracer Dimension can be seen here.

This doesn't seem to work out for me for negative direction vectors. Looking forward, I can see a box that's there, but looking backwards, I again see the same box mirrored, even though in this direction it's not "there". Following the algorithm step by step manually for both a positive and a negative z-dir (with x-dir and y-dir set to 0) gives the same near and far planes in both directions:

Right, because the test is only for whether the line intersects the box at all. The line extends both forwards and backwards. Just add a tmax >= 0 check. It's tmax, not tmin, since tmin will be < 0 if the ray originates inside the box.

Thanks. Works nicely and fast. I updated it a bit, to use SSE (though Vectormath), floats only.

https://gist.github.com/4412640#file-bbox-cpp-L14

You're welcome! I can't see that gist though (says "OAuth failure"). What kind of performance did the vectorisation give you?

To get the actual intersection, would I just use tmin, and multiply it with ray dir, adding ray origin? And what if I'm just interested in which face was intersected? x+,x-,y+,y-,z+ or z-?

Yes, that's what I'd do. Except if the ray origin is inside the box (

`tmin < 0`

), you need to use`tmax`

instead.To see what face was intersected, there's a few different ways. You can keep track of which slab is intersecting in the above algorithm at all times, but that slows it down.

For a cube centered at the origin, a neat trick is to take the component of the intersection point with the largest absolute value.

So if I return a vec3 with tmin, tmax and a float hit = step(tmin,tmax)*step(0,tmax), I basically know that if hit > 0 then ro+rd*tmin is my lower bound intersection point (entry point) and ro+rd*tmax is my higher bound intersection point (exit point), right? However, if tmin < 0, I'm inside the bounding box, which means I don't need the entry point and I can just use the ray origin as my starting point.

Sorry, not sure what you mean by

`step()`

. You can get the starting point as`ro + td*max(tmin, 0.0)`

.Thanks Tavianator,

Yeah, the cube-trick requires sorting.

So for now I test proximity to face within an epsilon.

If close enough to face, I assume that face was hit.

I can live with the few false positives, as I shoot over 100M photons each frame anyway.

Oops... that should be 100K photons of course.

Haha I was

reallyimpressed for a second :)The cube trick does not require sorting, just selecting the max from three candidates.

Something to consider here is that 0 * inf =nan which occurs when the ray starts exactly on the edge of a box

True, if you want consistent handling of that case while staying branch-free, you have to do a little more work. This is worth another post actually, I'll write one up.

Thanks for posting this (so long ago)! I used this in my own code, and wondered if it is possible to save 2 of the subtractions by taking advantage of the fact that min(x+a, y+a) = min(x,y)+a (for some constant a):

I think that should work, but there's a couple bugs in your example code. And actually for the test as written you can omit

`r.offset`

entirely since it doesn't affect the`tmax >= tmin`

check at the end. But since you probably want a`tmax >= 0.0`

check too, this should work:I'll try it out and see how much faster it is, thanks!