I've been trying to get my cascaded shadow maps looking right for a while. I managed to fix the shimmering and most of the quantization artifacts, but for some reason the shadows still look really blocky. I'm using a resolution of 1024x1024. Here are images of the bounding boxes used for the orthographic matrices, the camera frustum, and the resulting shadows:

Bounding boxes for frustum

Camera frustum

Resulting shadows

This is a snippet of the code I'm using to generate the bounds for the orthographic matrix:

       for (int i = 0; i < numShadowCascades; i++) {
                //transform camera frustum split corners to world space
                tempShadowBounds[4 * i + 0] = cameraViewToWorld * glm::vec4(cascadedShadowBounds[4*i+0], 1.0f); //upper left
                tempShadowBounds[4 * i + 1] = cameraViewToWorld * glm::vec4(cascadedShadowBounds[4*i+1], 1.0f); //upper right
                tempShadowBounds[4 * i + 2] = cameraViewToWorld * glm::vec4(cascadedShadowBounds[4*i+2], 1.0f); //bottom left
                tempShadowBounds[4 * i + 3] = cameraViewToWorld * glm::vec4(cascadedShadowBounds[4*i+3], 1.0f); //bottom right
                tempShadowBounds[4 * i + 4] = cameraViewToWorld * glm::vec4(cascadedShadowBounds[4*i+4], 1.0f); //next upper left
                tempShadowBounds[4 * i + 5] = cameraViewToWorld * glm::vec4(cascadedShadowBounds[4*i+5], 1.0f); //next upper right
                tempShadowBounds[4 * i + 6] = cameraViewToWorld * glm::vec4(cascadedShadowBounds[4*i+6], 1.0f); //next bottom left
                tempShadowBounds[4 * i + 7] = cameraViewToWorld * glm::vec4(cascadedShadowBounds[4*i+7], 1.0f); //next bottom right
                glm::vec3 frustumCenter = glm::vec3(0.0f);
                for (int j = 4 * i; j < ((4 * i) + 8); j++) {
                    frustumCenter += tempShadowBounds[j];
                frustumCenter /= 8.0f;

                dirLightViews[i] = glm::lookAt(frustumCenter, frustumCenter - lightDir, glm::vec3(0.0f, 1.0f, 0.0f));
                float maxX = std::numeric_limits<float>::lowest(); float maxY = maxX; 
                float maxZ = maxX;
                float minX = std::numeric_limits<float>::max(); float minY = minX; 
                float minZ = minX;
                using std::max; using std::min;
                for (int j = 4 * i; j < ((4 * i) + 8); j++) {
                    glm::vec3 temp = dirLightViews[i] * glm::vec4(tempShadowBounds[j], 1.0f);
                    maxX = max(maxX, temp.x);
                    minX = min(minX, temp.x);
                    maxY = max(maxY, temp.y);
                    minY = min(minY, temp.y);
                    maxZ = max(maxZ, temp.z);
                    minZ = min(minZ, temp.z);

                glm::mat4 cascadedOrtho = glm::ortho(minX, maxX, minY, maxY, minZ, maxZ);
                cascadedShadowMatrices[i] = cascadedOrtho * dirLightViews[i];

The code is somewhat rough right now, but that's the gist of how I'm generating the bounding boxes. Another big problem is that you can see the transition between the shadow maps as you move the camera, and I'm not sure how to smooth that transition.

The seams seem hard to eliminate. On a side note why don't people divide the world space into directional light chunks where each shadow map is assigned a section of world space, as opposed to cascaded shadow maps? Or am I missing something?


Getting high quality shadows from Cascade Shadow maps with (relatively) low resolution shadow maps is a process. I recommend taking on different aspects one at a time.

Here are few generic suggestions... Shadow transitions between cascade zones can be handled by allowing the cascades to overlap by a small amount then computing a lerp value for each zone. The zone near the camera goes from 1 to 0 inside the overlap region and the next zone goes from 0 to 1 inside the overlap region. Then tweaking the size of overlap to get the best visuals to performance.

Allow the number of cascades to vary depending on the region, and/or quality setting. Once you have the basic algorithm working, changing the number of cascades (usually between 3 and 6) can help and isn't to hard to add. This is especially helpful with large open world areas.

PCSS can be implemented reasonably easily with cascade maps, enabling hardware interpolation and hardware bias settings will help and can get you down to the minimum samples while still getting good performance. This is another place that a quality setting can be handy.

Computing a best fit for the cascade frustums can help considerably, but there are as many ways to do this as there are people writing cascade shadow maps.

Here is a link to a Microsoft web site that has some good tips on dealing with aliasing issues.

Also, I recommend asking specific question regarding specific issues with some of the techniques described above (or others that you find). It's hard to give a good answer to such a broad question.

  • $\begingroup$ My point light omnidirectional shadows looked way sharper at lower resolutions in comparison to the orthographic projection, so I was just wondering if this was how it was supposed to be or if I was doing something wrong. I was looking into shadow filtering methods and I think I'm going to go with variance shadow maps and linearly interpolate between depth values in the overlap regions. I usually use a gradient with PCF to mitigate shadow acne, but I'm not sure how to use that at the edges of the cascade where the view and orthographic matrices are different $\endgroup$
    – Josh Lakin
    Jul 7 at 5:10
  • $\begingroup$ There are 6 1024 shadow maps for omnidirectional vs 1 shadow map for each cascade region, and that 1 map is covering a much larger area then any 1 of the omni maps. This puts the camera much farther away from the objects so it can "see" the entire area for the map. Play with the area covered by the cascades, usually small changes to the cascade sizes can result in large differences in the aliasing. (especially for a 16:9 aspect ratio). $\endgroup$
    – pmw1234
    Jul 7 at 11:33

The blockiness of the shadows in your screenshot looks to me like the expected results from using the hardware bilinear PCF to sample the shadow map. Unfortunately this is just not a very good filter. It's necessary to use a wider filter to get smooth shadows. This could be as simple as doing multiple bilinear PCF samples at offset locations, and averaging the results. This will blur out the shadows a lot, but you could then apply a sharpening function to the result to obtain sharper shadows with smoother boundaries.

Shadow map filtering is a big topic and can get very complicated (just google "shadow map filtering" and you'll find plenty of reading material).

  • $\begingroup$ I implemented omnidirectional shadows before I programmed the shadow cascades, and the perspective shadows seemed way sharper at lower resolutions. I just wasn't expecting the orthographic projection to make shadows look this blocky. $\endgroup$
    – Josh Lakin
    Jul 7 at 4:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.