Chasing Shadows
June 22nd, 2008During this weekend’s heatwave, I’ve been tracking a shadow problem that’s been lurking in my ray tracer for a while. It occurs when a light source is at a grazing angle to some of the facets in a mesh object. I’ve ignored it with Utah Teapot and images, but now that I’m playing with cloth simulation, it became more prominent.
As is often the case, what I thought was one bug turned out to be two (or three, depending on how you count them).
Here’s an example of the problems. The shadows marked with red ellipses are clearly artifacts of the underlying mesh approximation. Also, the top of the knob on the teapot lid shows subtle, but clearly wrong, undulations. I thought these were two aspects of the same bug. They weren’t. I’ve solved the latter problem, and I now understand the former—I’m just not sure what to do about it.
A little background. The teapot is defined with Bezier patches, which are smooth two-dimensional curved surfaces. My ray tracer teselates the patches into a triangle mesh that approximates the curves. Tracing the patches directly is harder and generally slower.
To avoid the blocky appearance, we tweak the surface normals used in the shading calculations to more closely match the round object. This approach can give a nice rounded appearance despite the coarse triangle mesh (as long as you don’t scrutinize the profiles too closely). The is done by precomputing the desired surface normal at each vertex in the mesh and then by interpolating the from the nearest vertices.
The problems stem from the fact that the mesh is just an approximation. You can reduce the artifacts by increasing the resolution of the mesh, but they still persist, just smaller. If I were to add the ability to trace the patches directly, I’d still have the problem with other objects, like the bunny, that are natively defined as a mesh.
My instincts told me that I had messed up the interpolation of the normals. And indeed, that was the cause of the shading problem on the knob at the top of the lid. Lots of long, very skinny triangles come together there. To compute a vertex normal, I accumulated the surface normal of each triangle that touched the vertex and then normalized the sum.
It turns out that this is not the best way to compute the vertex normals. About half of the skinny triangles on top each have a side that is approximately zero in length. (Ideally, they would be exactly zero except for the imprecision of floating point arithmetic. Also, their normals would be very close to the correct value, but their very skinny nature introduces error into the cross product of the edges.) These almost non-existent triangles were contributing as much to the normal calculation as the larger triangles whose points meet at the center of the knob.
A did a search and found a page of course notes from Ken Perlin that explains how to compute normals by weighing the contributions of the larger facets more than those of the smaller ones. I changed my program to use this algorithm, and the strange shading on the top of the knob vanished.
But the other shadows remained.
And that is the key. The remaining problem had to do with shadows not shading.
Like scanline renderers, the ray tracer shades a point with calculations based on the surface normal and the direction to the light source. Surfaces that face the light are brighter than those that are angled away. This is shading.
Shadows depend on the visibility of the light source from the point in question. Scanline renderers require tricks (such as light buffers or shadow volumes) to determine if a light source actually reaches a particular point. A ray tracer, on the other hand, can simply trace the path back to the light source to see if anything stands in the way. If you forgive the perfect point light source approximation, this gives very realistic shadows without any of the complications the scanline renderers use.
And that’s the problem. We’re using a physically accurate test in order to check for a shadow on a physically inaccurate model of the teapot.

The thick straight lines represent the facets of our mesh (edge on), and the curved line is the actual contour we’re trying to approximate. We want to compute the shading for the point at the base of the blue arrow, which represents the interpolated surface normal. The light is at a grazing angle. Although the angle between the vector to the light source and the interpolated surface normal is less than 90 degrees, the shadow test fails. The corner where the two facets meet essentially casts a shadow across the entire left facet. This is the artifact seen in the teapot.
My first few debugging runs suggested that the problem was self shadowing. That is, the facet was casting a shadow across itself. I modified my code to disallow self-shadowing, but it didn’t help much. It turns out that the shadow, in most cases, is actually cast by an adjacent facet.
I’m not sure how to fix it at this point. I was hoping that writing this down might give me some ideas. Perhaps you have a suggestion.
