Charles Petzold

Let There Be Shadows

March 12, 2007
New York, N.Y.

I want to have an example of rendering shadows in my WPF 3D book, and I've been exploring some techniques.

This one, however, I'm going to have to classify as a failure. You can try it out here:


I don't want to make the source code available because it's rather sloppy and way too specific to this particular program, but I can describe how it works:

The "chopper" is just two 8-vertex boxes stuck together and moved around with three animations: rotating around its axis, revolving around the Y axis, and moving up and down.

The "ground" is a plane surface composed of 200 by 200 rectangles, or 80,000 triangles. Interestingly, even without the shadow logic, increasing the number of triangles in the ground made the animation of the chopper increasingly choppy.

The illumination is a combination of AmbientLight with a color of 40-40-40 and DirectionalLight with a color of C0-C0-C0 and a direction of (2, -3, -1).

I render the shadow by manipulating the Normals collection of the ground during a CompositionTarget.Rendering event. For each vertex on the ground, I set the vertex normal as follows: If no shadow is to appear at a vertex, I set the normal to the vector (0, 1, 0), which is the default straight-up surface normal for the floor. For a shadow, I set the normal to (3, 2, -1), which is at right angles to the directional light. The setting of this normal eliminates the directional light but not the ambient light from the shadow.

Originally, I called the 3D version of VisualTreeHelper.HitTest for every vertex on the floor, using a RayHitTestParameters object based on the vertex and the negative Direction vector of the DirectionalLight. If the ray from the floor in the negative direction of the directional light hit the chopper, then that vertex was part of the shadow. At that point I could set the vertex normal.

I then realized I could reduce the calls to VisualTreeHelper.HitTest considerably by first determining a 2D bounding box for the shadow. I did this by looking at rays from the vertices of the chopper — and there are only 16 of those — to the floor using the positive Direction vector of the DirectionalLight. Because this particular hit-test calculation only involves an intersection of a line and a plane, I did it in code rather than calling VisualTreeHelper.HitTest. This bounding box establishes minimum and maximum X and Z coordinates on the floor within which the shadow appears. The calls to VisualTreeHelper.HitTest are then restricted to that box.

I thought this was an interesting approach, and I was disappointed it didn't work better. The jaggies on the shadow can be eliminated only by increasing the resolution of the floor, but, of course, that tends to slow down the whole show.