Introduction
The Black Engine uses Cascaded Shadow Maps (CSMs) to create shadows from Directional Lights (eg. the Sun or moon). The algorithm splits the camera’s view frustum into several sub-frustums, and applies a shadow map to each independently. This reduces perspective aliasing by effectively providing higher resolution shadow maps near the camera.
The following image shows a camera’s view frustum (represented by the white triangle) broken into 3 sub-frustums. The black boxes represent the area the corresponding shadow map covers. Note that each shadow map wastes some space on areas outside of the view.
At a high level, shadow mapping is broken up into the following Shadow Map Generation and Shadow Projection steps.
Shadow Map Generation
For each sub-frustum, the engine determines which scene models cast shadows onto it. Shadow culling is currently unimplemented in the Black Engine, however, octree and frustum culling are good first steps towards future improvements. See “Fast Extraction of Viewing Frustum Planes from the WorldView-Projection Matrix” by Gil Gribb and Klaus Hartmann for a clever trick to calculate camera frustum planes.
Shadow casters are rendered into the shadow map by transforming them by the light’s view-projection matrix aimed at the center of the sub-frustum. A LookAt(at, from, up) style function generates the light’s view matrix where at is the center of the sub-frustum. Then from = at + -LightDirection* frustumExtent. This view matrix is then concatenated with an orthographic projection matrix with width/height large enough to cover the sub-frustum.
The above image shows a scene that is split up into 4 shadow maps. For convenience, a single 2048×2048 shadow map is partitioned into 4 sections, one for each map. The projection step is a single, full screen pass where the pixel shader samples the correct shadow map texel based on the current pixel’s distance from the light’s view position.
Performance
It’s important to keep in mind that a shadow caster will be rendered for each shadow map it affects. So, for example, a building with a long shadow may end up being rendered for each cascaded shadow map!
Also note, shadow map depth rendering typically involves simple pixel shaders that only write depth and not color. Keep your shadow map resolution as small as possible to improve performance.
Shadow Projection
Next, the engine draws a single full-screen quad that projects the shadows onto the scene. The shader uses the gbuffer’s scene depth and screen position to reconstruct each pixel’s world position. This position is transformed by the light’s world-view-projection matrix. The results are converted from homogeneous clip space to texture space (ex, [0,0], [1,1]).
Finally, the shader samples the shadow map using the texture space values calculated above. If the shadow map value is greater than the z component of our projected value, the pixel is in shadow.
Swimming edges
The edges of view dependent dynamic shadows can “swim” as the camera moves and turns. This is fixed by keeping projected texels aligned with world coordinates.
Black Engine achieves this by transforming the zero vector by the light’s view-projection matrix and creating an xy delta to the center of the nearest texel. We use this delta on all post view-projected pixels to keep them aligned with the shadow map’s texels. Thus, they no longer slide around in texture space as the camera moves.
The code for this follows:
const f32 texel_size = 2.0f / (g_shadow_tex_dimensions * 0.5f); Vec4 proj_center(0.0f, 0.0f, 0.0f, 1.0f); proj_center = proj_center.transform_point(light_view_proj, true); const f32 far_corner_dist = g_far_clip_plane / (cam_dir.dot(ul)); const f32 near_corner_dist = (g_near_clip_plane * far_corner_dist) / g_far_clip_plane; const float fracX = fmod(proj_center.x, texel_size); const float fracY = fmod(proj_center.y, texel_size); Mat4 offset; offset.make_identity(); offset[3][0] = -fracX; offset[3][1] = -fracY; Mat4 texture_matrix; texture_matrix.make_identity(); texture_matrix[0].x = 0.5f; texture_matrix[1].y = -0.5f; texture_matrix[3].x = 0.5f + (0.5f / g_shadow_tex_dimensions); texture_matrix[3].y = 0.5f + (0.5f / g_shadow_tex_dimensions); |