As mentioned in my previous post, cascaded shadow mapping is a technique to shadow large areas at reasonable memory and run-time cost. The basic idea is simple: Split the view frustum into several sub frustums and render a shadow map for each split. Since, naturally, splits closer to the viewer cover less area in world space (i.e. under perspective projection) you get better shadow map resolution close to the camera and less resolution further away. Check out the image below: Each split is visualized in a different color and the corresponding shadow map has been filled with a block pattern. All shadow maps are of the same resolution. You can clearly see how shadow maps closer to the viewer (red and green) allocate much higher detail per world space unit than shadow maps further away from the viewer (blue and yellow). This gives us a highly desired quality: Lots of resolution/details close to the viewer and less far away – without the need for a huge shadow map.

So how can we get this done? First we have to take the same steps as in regular shadow mapping: Define the shadow casting light i.e. light direction and position and the corresponding projection into the shadow map. In my case I chose to simulate sun light which can be represented by a simple orthogonal projection. I create the shadow transform like so:

// shadow view matrix mShadowView = Matrix.CreateLookAt(mLightPosition, mLightPosition + mLightDirection, Vector3.Up); // determine shadow projection based on model bounding sphere { var center = Vector3.Transform(arena.BoundingSphere.Center, mShadowView); var min = center - new Vector3(arena.BoundingSphere.Radius); var max = center + new Vector3(arena.BoundingSphere.Radius); mShadowProjection = Matrix.CreateOrthographicOffCenter( min.X, max.X, min.Y, max.Y, -max.Z, -min.Z); } mShadowTransform = mShadowView * mShadowProjection;

So the shadow view matrix is a simple look at matrix placed at the position of the light `mLightPosition`

and looking towards `mLightDirection`

. As Up vector I just use the y-axis, which works fine as long as `mLightDirection`

is not pointing straight upwards. Using this definition I can then transform the model’s bounding sphere center into shadow space (i.e. compute the position of the bounding sphere center relative to the light) and get the minimum axis aligned bounding box (in shadow space) encompassing the sphere by just adding and subtracting the bounding sphere radius along each coordinate axis. The resulting vectors `min`

and `max`

give us thus the shadow frustum we need to project into our shadow maps in order to cover the whole model. Note that view space looks along the negative z axis so we need to negate the Z values.

The resulting matrix `mShadowTransform`

gives us thus the projection of world space coordinates into our shadow map. Now, in cascaded shadow mapping we have not one, but multiple shadows maps. We therefore need to define multiple versions of `mShadowTransform`

, one for each shadow split. And we also need to align the splits to the view frustum. So let’s start with the view frustum: Lets say we split the frustum at constant distances from the viewer, say the first split should range from -1 to -100, the second split from -100 to -300 and the third split from -300 to -600. Given these split distances we can use the functionality described in the post Frustum Splits to figure out the world space positions of the sub frustum corners for each split. Once we know the world space positions for each split we’re almost done: We just need to adjust the projection matrix to focus on the the split frustum.

// determine clip space split distances var splitDistances = new[] {-1, -100.0f, -300.0f, -600 } .Select(d => { var c = Vector4.Transform(new Vector3(0, 0, d), camera.projectionMatrix); return c.Z / c.W; }) .ToArray(); // determine split projections var splitData = Enumerable.Range(0, mNumShadowSplits).Select(i => { var n = splitDistances[i]; var f = splitDistances[i + 1]; // get frustum split corners and transform into shadow space var frustumCorners = splitFrustum(n, f) .Select(v => Vector3.Transform(v, mShadowView)); var min = frustumCorners.Aggregate((v1, v2) => Vector3.Min(v1, v2)); var max = frustumCorners.Aggregate((v1, v2) => Vector3.Max(v1, v2)); // determine the min/max z values based on arena bounding box var arenaBB = GeometryHelper.transformBoundingBox(arena.CollisionData.geometry.boundingBox, ShadowView); var minZ = -arenaBB.Max.Z; var maxZ = -arenaBB.Min.Z; // return orthographic projection return new { Distance = f, Projection = Matrix.CreateOrthographicOffCenter(min.X, max.X, min.Y, max.Y, minZ, maxZ) }; }).ToArray(); // compute final split transforms ShadowSplitProjections = splitData.Select(s => mShadowView * s.Projection).ToArray(); ShadowSplitDistances = splitData.Select(s => s.Distance).ToArray();

This gives us an array `ShadowSplitProjections`

of matrices where each matrix projects the scene into a shadow map, from the view of the light. Note that I also store the clip space split distances, measured in view space from the camera’s center of projection. Have a look at the images below:

The images on the left show the view frustum split into three sub frustums, while the images on the right show the shadow transform’s frustum for each split.

This concludes the first step of implementing cascaded shadow mapping. In the next post I’ll show how the finally render the shadow maps and how to project them onto the scene. Hope you find it as interesting as I do ðŸ™‚