Now that we have basic shadowing working, let’s look into improving shadow quality. When loading up the current state of the sample, the first thing you might notice is that shadows are showing this weird ‘shimmering’ or ‘swimming’ effect. The boundaries where shadowed area meets non-shadowed area seem to be in constant flux with pixels coming in and out of the shadow as soon as the camera moves. The reason for this type of artifact is the discretization of the scene into shadow map pixels. Imagine the shadow map as a regular grid moving with the camera: All parts of the scene that fall within one grid cell will look up the same depth value. Now let’s think about some shadow casting edge somewhere in the scene and assume we are moving towards it. As soon as any shadow map texel comes in contact with the edge it’s stored depth value will be overwritten by the edge depth, hence putting the whole scene area covered by said texel into shadow. If we continue moving towards the edge, the depth value of the shadow map texel will not change – but the scene area covered by it will continue to move, effectively making it look like the shadow boundary is moving away from the viewer. And as soon as the next pixel comes in contact with the edge the whole game starts over. A similar effect happens when the shadow map changes size every frame.
So… How can we fix this? First, lets agree to make the shadow map size in world space constant, say
shadowMapSize, and decouple it from light and camera rotation. In my case I chose to align the shadow map to world space and axis. I do so by exchanging the shadow view matrix for a matrix where the and axis point in direction of world space and , positioned at
mLightPosition like so:
// Remember: XNA uses a right handed coordinate system, i.e. -Z goes into the screen var look = Vector3.Normalize(arena.BoundingSphere.Center - mLightPosition); mShadowView = Matrix.Invert( new Matrix( 1, 0, 0, 0, 0, 0, -1, 0, -look.X, -look.Y, -look.Z, 0, mLightPosition.X, mLightPosition.Y, mLightPosition.Z, 1 ) );
Note that the axis is flipped in order to preserve culling order in the final view transform. Also note that this approach only works as long as
mLightPosition does not lie in the plane in world space as then the resulting matrix becomes singular.
Now lets tackle camera movement: As outlined before, the problem is that even the slightest movement of the shadow map will affect all the scene as each scene position will change position in the shadow map (subpixel-wise speaking). What we need, however, is that the scene positions stay constant (at least relative to their corresponding pixel). So instead of moving the shadow map continuously, lets move it in fixed increments of one shadow map pixel. When moving the shadow map this way, each world space position might fall into a different shadow map texel than the frame before – but the relative position within the shadow map texel will stay the same, which means no more moving shadow boundaries.
So how can we implement this? Given our view transform defined like above, all we need to do is adjust the shadow projections: We want to place the shadow map corners at discrete positions only, separated by some value, e.g.
quantizationStep. Remember, in one of my previous posts Cascaded Shadow Mapping (1), we defined the extent of the shadow projection matrices based on values
max which were determined from the view frustum. All we need to do now is make sure the and coordinates of
max are properly discretized:
var quantizationStep = 1.0f / shadowMapSize; var qx = (float)Math.IEEERemainder(min.X, quantizationStep); var qy = (float)Math.IEEERemainder(min.Y, quantizationStep); min.X -= qx; min.Y -= qy; max.X += shadowMapSize; max.Y += shadowMapSize;
Using the adjusted
max values we create the shadow projection matrix as described before:
Projection = Matrix.CreateOrthographicOffCenter(min.X, max.X, min.Y, max.Y, minZ, maxZ);
The effect of these few lines of code is dramatic. Check out the video below:
You can clearly see how the scene shadows are stabilized, almost all artifacts during camera movements and rotations are gone. The remaining artifacts stem from transitions between the different shadow splits, as shown in the last part of the video.