Reflection

We have shiny objects. But can we have objects that actually behave like mirrors? We can, and in fact doing this in a ray tracer is remarkably simple, but it can also be mind-twisting the first time you see how it’s done.

Let’s see how mirrors work. When you look at a mirror, you’re seeing the rays of light that bounce off the mirror. Rays of light are reflected symmetrically respect to the surface normal:

Suppose we’re tracing a ray, and the closest intersection happens to be a mirror. What color is this ray of light? Obviously, not the color of the mirror, but whatever color the reflected ray is. All we’d have to do is to compute the direction of the reflected ray, and figure out what’s the color of the light coming from that direction. If only we had a function that given a ray returned the color of the light coming from its direction…

Oh, wait. We do have one; it’s called TraceRay.

So we start at the main loop, calling TraceRay to see what does the ray coming from the camera “see”. If TraceRay determines that the ray is seeing a reflective object, it just needs to compute the direction of the reflected ray, and call… itself.

At this point, I suggest you read the last three paragraphs until you get it. If this is the first time you read about recursive raytracing, it may take a couple of reads and some head scratching until you really get it.

Go on, I’ll wait.

Now that the euphoria of this beautiful aha! moment is starting to wane off, let’s formalize this a bit.

The trick with all recursive algorithms is to prevent an infinite loop. There’s an obvious exit condition in this algorithm: when the ray either hits a non-reflective object, or it hits nothing. But there’s a simple case where we could get trapped into an infinite loop: the infinite hall effect. This is what happens when you put a mirror in front of another mirror and look at it - infinite copies of yourself!

There are many ways to prevent this from happening. We’ll just introduce a recursion limit to the algorithm; it will control how “deep” we can go. Let’s call it $$r$$. When $$r = 0$$, we see objects, but no reflections. When $$r = 1$$, we see some objects and the reflections of some objects. When $$r = 2$$, we see some objects, the reflections of some objects, and the reflections of some reflections of some objects. And so on. In general, it doesn’t make much sense to go deeper than 2 or 3 levels, since the differences are barely noticeable at that point.

We’ll make another distinction. “Reflectiveness” doesn’t have to be an all-or-nothing proposition; objects may be partially reflective and partially colored. We’ll assign a number between $$0$$ and $$1$$ to every surface, specifying how reflective it is. Then we’ll blend the locally illuminated color and the reflected color proportionally to that number.

Finally, what parameters does the recursive call to TraceRay take? The ray starts at the surface of the object, $$P$$. The direction of the ray is the direction of the light bouncing off $$P$$; in TraceRay we have $$\vec{D}$$, the direction from the camera to $$P$$, which is the opposite of the direction of the light, so the direction of the reflected ray is $$\vec{-D}$$ reflected respect to $$\vec{N}$$. Similarly to what happened with the shadows, we don’t want objects to reflect themselves, so $$t_{min} = \epsilon$$. We want to see objects reflected no matter how far away they are, so $$t_{max} = +\infty$$. Last but not least, the recursion limit is one less than whatever recursion limit we’re currently in.

Rendering with reflection

Let’s add reflection to our ray tracer code.

As before, we modify the scene first:

sphere {
center = (0, -1, 3)
radius = 1
color = (255, 0, 0)  # Red
specular = 500  # Shiny
reflective = 0.2  # A bit reflective
}
sphere {
center = (-2, 1, 3)
radius = 1
color = (0, 0, 255)  # Blue
specular = 500  # Shiny
reflective = 0.3  # A bit more reflective
}
sphere {
center = (2, 1, 3)
radius = 1
color = (0, 255, 0)  # Green
specular = 10  # Somewhat shiny
reflective = 0.4  # Even more reflective
}
sphere {
color = (255, 255, 0)  # Yellow
center = (0, -5001, 0)
radius = 5000
specular = 1000  # Very shiny
reflective = 0.5  # Half reflective
}

We use the “reflect ray” formula in a couple of places, so we can factor it out. It takes a ray $$\vec{R}$$ and a normal $$\vec{N}$$ and returns $$\vec{R}$$ reflected respect to $$\vec{N}$$:

ReflectRay(R, N) {
return 2*N*dot(N, R) - R;
}

The only change in ComputeLighting is the replacement of the reflection equation with a call to this new ReflectRay.

There’s a small change in the main method - we need to pass a recursion limit to the top-level TraceRay:

        color = TraceRay(O, D, 1, inf, recursion_depth)

The constant recursion_depth can be set to a sensible value, e.g. 3 or 5.

The only significant changes happen near the end of TraceRay, where we compute the reflections recursively:

TraceRay(O, D, t_min, t_max, depth) {
closest_sphere, closest_t = ClosestIntersection(O, D, t_min, t_max)

if closest_sphere == NULL
return BACKGROUND_COLOR

# Compute local color
P = O + closest_t*D  # Compute intersection
N = P - closest_sphere.center  # Compute sphere normal at intersection
N = N / length(N)
local_color = closest_sphere.color*ComputeLighting(P, N, -D, sphere.specular)

# If we hit the recursion limit or the object is not reflective, we're done
r = closest_sphere.reflective
if depth <= 0 or r <= 0:
return local_color

# Compute the reflected color
R = ReflectRay(-D, N)
reflected_color = TraceRay(P, R, 0.001, inf, depth - 1)

return local_color*(1 - r) + reflected_color*r
}

I’ll let the results speak for themselves:

Source code and live demo >>

To better understand the recursion depth limit, here’s a close-up rendered with $$r = 1$$:

And here’s the same close-up of the same scene, this time rendered with $$r = 3$$:

As you can see, the difference is whether we see the reflections of the reflections of the reflections of the objects, or just the reflections of the objects.

Found an error? Everything is in Github.