Imitating the Canvas Engine (8): Constructing the Shadow Map

Previous Post:
Imitating the Canvas Engine (7): Basic Shadow Mapping - Memories of Melon Pan

The first thing to do is to render the scene from the point of view of the light. We've got everything in the scene in world space already, but we're missing the two components that make up the camera: the view matrix and the projection matrix. All we have to do is generate them.

At its most basic, the shadow map's view matrix and projection matrix has to contain all the points you could possibly see in the scene, or we won't get shadows in certain areas of the scene.

So first, let's check out what we need to know if we're going to make these matrices.

To figure out what the view matrix looks like, we need three things.

  • Position
  • Direction, or look at position
  • Up vector

If we're looking to make a projection matrix... well, first off I'm going to say that we're using orthographic projection for this. And to make this orthographic projection, we need:

  • Length of projection in the x axis
  • Length of projection in the y axis
  • Near plane
  • Far plane

All four of these really boil down to the same number, though.

Let's think about the view matrix for a little bit first. If you look at the volume of space that the original scene renders (we call this its projection space), it takes the shape of a 3D frustum. We can start by pointing our light at the centroid of that frustum. We already know its direction since we've defined it beforehand, and can reuse it as the direction for our shadow view matrix. But since our light is a directional light, it doesn't actually have a position. We need one to make a shadow view matrix, so we're going to have to invent one.

But let's think about the projection matrix now. It's an orthographic projection matrix, and we need a near and far plane for it. What happens when the near plane is negative? The projection will reach backwards and include space behind the camera's position. Now this makes finding the view matrix position much easier.

Our only goal is to render the scene from the point of view from the light, and all we got to do is get matrices that work. We can set the position of the view matrix to be the centroid of the scene's projection space, and point it in the direction of the light. Then, when we create our shadow projection matrix, we can just have it reach back as far as it needs to include all of the space we need.

So, first things first: get the centroid of the scene's projection space. It's pretty easy once you get the corners of that frustum, and XNA actually has the functions to make this nice and painless.

BoundingFrustum Frustum = new BoundingFrustum(View * Projection);
Vector3[] FrustumCorners = Frustum.GetCorners();

Average all the corners up, and you get the centroid. Easy fizzin' peazy.

Now that we've got the centroid, and we know the direction of the light, the last part is the up vector. This can just be the standard up vector (0.0, 1.0, 0.0). If the direction is already the up vector, you can put something else in there instead, like the backwards vector (0.0, 0.0, 1.0). Toss all that to Matrix.CreateLookAt (if you're using XNA), and you'll have your shadow view matrix.

Matrix ShadowViewMatrix = Matrix.CreateLookAt(Centroid, Centroid + LightDirection, (LightDirection != Vector3.Up) ? Vector3.Up : Vector3.Backward);

Moving on to the projection matrix, the only thing we need to know is how wide and tall it needs to be, as well as how far back (for the near plane) and how far forward (for the far plane) it needs to go. And for this, there's really only one number we need to know - what is the maximum distance from the centroid to any point on the viewing frustum?

Intuitively, this would be the distance from the centroid to any of the viewing frustum corners on the far plane. The orthographic projection has to go left this amount and right this amount, so its width should be double this distance. Its height should be the same, since it has to go both up and down that far. The near plane has to stretch back that distance, and the far plane has to stretch forward that distance.

float Length = Vector3.Distance(Centroid, FrustumCorners[i]);
Matrix ShadowProjMatrix = Matrix.CreateOrthographic(Length * 2, Length * 2, -Length, Length);


Where i is the index of a viewing frustum corner on the far plane.

Some people might also say that the width and height of the shadow projection matrix should be the same as the diagonal of the box which perfectly fits the viewing frustum. These turn out to be the same number, though.

And with this, you have a view matrix and a projection matrix. You can save some time and multiply them now before passing them to the shader, so the shader doesn't have to do that calculation for every vertex or pixel it shades.

... So, I've talked a lot about how to get a view and projection matrix for the shadow map, but when we render it, what should it look like? Just like the normal depth map, we just need it to store the depth values of each pixel from the point of view of the light. This makes the vertex shader real simple.

output.Position = mul(mul(input.Position, World), ShadowViewProjection);
output.Depth = output.Position.z / output.Position.w;


For the variables:

  • input.Position, the position in object space of the vertex
  • World, the world space transform of the object
  • ShadowViewProjection, the shadow view matrix multiplied by the shadow projection matrix

And our pixel shader even simpler.

return float4(input.Depth, input.Depth, input.Depth, 1.0);


Where the input to the pixel shader is the same as the output from the vertex shader.

With the dust settled... for now... our scene from the point of view of the light looks like this. The darker the pixel, the closer it is to the light.

We still have to convert this to screen space coordinates, but I'll get to that in a later post.

Next Post:
Imitating the Canvas Engine (9): Transforming the Shadow Map to Screen Space - Memories of Melon Pan