Bounding Volume Hierarchies and Iterated Function Systems
2010, Dr. Lawlor, CS
481/681, CS, UAF
Basically, every raytracer looks like this code:
for (each pixel) {
for (each ray bounce) {
for (each chunk of geometry) {
if (ray from pixel hits geometry)
shade pixel with geometry
}
}
}
The performance is clearly going to be
O(pixels*bounces*geometry). The number of pixels is in the
millions, the amount of geometry is in the thousands, although luckily
we can get away with only a few bounces (on average). Searching
thousands of chunks of geometry for each of the millions of pixels per
frame is not going to be efficient.
One simple way to improve performance above is to reduce the number of
pixels--that is, reduce the screen resolution. Another simple
trick is to reduce the geometric complexity of the scene.
But one surprising way to reduce the average geometric complexity per ray without changing the final image is to use a "bounding volume hierarchy".
The basic idea behind bounding volumes is before digging into a
complicated block of geometry, we first intersect our ray with a simple
bounding volume like a sphere. If the ray misses the sphere, then
we can skip the entire block of geometry. Because misses are
common (a ray can only hit one object in the end!), this can be a huge
savings in complex scenes. You can even have a nested tree of
bounding volumes, and raytracing then boils down to something like a
binary search: does the ray hit anything on the left? No, how
about the right? Yes! Then is it the left-right, or the
right-right? And so on.
Common bounding volumes include:
- Planes. Intersecting a ray with a plane only takes one dot
product, but you need to load the plane normal and offset, which is
four floats. Planes aren't closed, so you need several planes to
bound small objects.
- Bounding boxes. Intersecting a ray with an axis-aligned bounding box takes three dot products, and six floats.
- Spheres. Intersecting a ray with a sphere takes three dot products, and four floats.
In pseudocode, you traverse the bounding volume tree to find all the geometry that intersects a ray like this:
ray_intersection intersect(ray r,geometry_tree_node g) {
ray_intersection ret=miss;
for (c in child geometry of g) {
if (r hits c's bounding volume)
add_intersection(ret,intersect(r,c));
}
return ret;
}
Of course, "intersect" is a recursive function, and recursion isn't
supported on the GPU yet (no hardware stack). Instead of
recursing, you can:
- Ignore bounding volumes, and just traverse all the geometry
linearly. The bounding volumes give you a log-factor speedup,
like binary search, so this is quite expensive for scenes with any
complexity.
- Expand the functions out to a fixed recursion depth. The code grows exponentially as you do this, though!
- Convert the recursion to some form of iteration. For example, you can add links to "thread" a binary tree, which allows you to traverse the tree without using any stack space.
Building a bounding volume hierarchy requires the scene geometry to be
pre-processed into a search tree. But we can see much of the same
behavior with "recursive geometry" such as an iterated function system.
Iterated Function Systems: Recursively Defined Geometry
An iterated function system (IFS) is a set of "map" functions that
transform a parent bounding volume into a set of child bounding
volumes. You can draw an IFS by recursively applying the map
functions, which gives smaller and smaller bounding volumes. For
example, here's a Sierpinski Triangle, which is defined as three
smaller Sierpinski triangles:
You can read much more about iterated function systems online, for example the Wikipedia Sierpinksi page.
I wrote a little 2D IFS manipulation program back in 2003.