Real-Time shadows : Cascaded Shadow Maps

Cascaded Shadow Maps is so far the best solution to tackle Perspective and Projective aliasing in shadows. The basic idea is to use multiple shadow maps (mostly 3 or 4) to cover different areas of view camera frustum. The root cause of these shadow artifacts is lack of one to one mapping between shadow map texels and pixels on the screen (in view space). The Cascaded Shadow mapping provides higher resolution shadow maps  to objects closer to the eye solving the root cause of artifacts.

(Screenshot – JustAnotherGameEngine)

Over the years, a lot of different ways to split cascades (while generating shadow maps), calculate light view frustums and to select the right cascade map have been proposed. I tried few different variations before reaching my final result of stable flicker/shimmer free high-quality soft shadows. [1] [2] [3] [4] [5]

For splitting the cascade, I am using a combination of linear and logarithmic schemes. For more details check [4] and [5].

After splitting up the eye frustum into 3-4 smaller ones we calculate their bounds in light space which are then used by the light camera while rendering to respective cascade maps. Our Objective is to calculate smallest possible bounds that will cover each cascade. One way to do it is to calculate a bounding box using 8 points making up the frustum for any cascade. To calculate nearZ and farZ we can simple transform scene bounding box into light space and take minZ and maxZ but that will again waste a lot of precision which can cause the shadow acne.

sahdow acne

(Image taken from MSDN)

A better way to calculate nearZ and farZ is to cull scene bounding box with the four planes of light view frustum in light space. This method has been explained in great detail over MSDN. For selecting the right cascade while rendering I tested 2 methods – one based one distance of pixel being rendered and compare it to cascade splitting distances and second based on finding the highest resolution cascade map having depth at that pixel location. And I found 2nd method gives better results in my test cases. Both the methods have been explained in detail in [3].

Method 1 – Cascade Splitting interval based selection

(Image taken from MSDN)

Method 2 – Map based selection

 

(Image taken from MSDN)

CSM Artifacts

One of the most common artifact in cascaded shadow map is shimmering. It is caused by 2 reasons –

  1. Shimmering due to camera movement – This happens due to pixels coming in and out of the shadow when camera moves. Fix is to move the orthographic projection bounds in pixel size increments.
  2. Shimmering due to change in the size of light camera frustum – If you are calculation cascade bounds as explained earlier you will notice this type of shimmering when rotating the camera. This happens because cascade bounds size changes as we rotate our eye camera while leads the change in the area covered by each shadow map and hence the shadow map resolution. To fix this, we have to keep the size of light view frustum same for respective cascades independent of the orientation of eye camera. There is couple of ways to calculate such bounds –
  • Method1 – Use the splitting distances to calculate a bounding box around eye camera position in light space. But this scheme will waste a lot of memory since it will also cover the area behind eye camera.[5][3]
  • Method2 – Use bounding sphere instead of bounding box in the previous method. Calculate a bounding sphere that will cover each cascade frustum. Since a sphere is rotational invariant it will keep the size of the orthographic projection frustum same independent of eye view direction.[7][8]   
  • Method3 – Another method I am using is a variation of method 2 to calculate tighter bounds. I using a sphere with the diameter of size equal to the splitting distance for each cascade and calculate a light space bounding box around that. So if splitting distance is d, I am using d/sqrtf(2.0f) as sphere radius. sphere center is preCascadeOffset + d/2.0f. With this scheme, I have to use map selection mode at rendering time since shadow map for Cascade 1 doesn’t necessarily covers all the pixels for cascade1 but those pixels will be present in cascade 2 for sure. Result is a much tighter bound and hence much more resolution for nearby objects where it’s needed. (There’s a small bug in my current implementation where some area near border b/w cascade 1 and 2 appears black when eye camera is in the same direction as light direction. I am still debugging it and will post the results later.)

 Final Result

Here’s how final version with all the fixes, various filtering techniques (will post details later in a separate post), etc. As we can see in below video shadows are quite stable and are of high quality.

Performance

I am using mostly 3 or 4 cascades which are updated every frame ( for now ) but in a single pass using geometry shaders. Results posted below are for 1080p resolution without MSAA. Using Tiled Deferred rendering with deferred shadow pass.

Test Scene 1: PowerPlant

Scene rendered with 3 dynamic cascades and a static shadow map, each with a resolution of 2048 x 2048. Used Texture2DArray for cascades and Texture2D for static map. Using map selection mode, Normal offsets, derivatives transition blur and 8 randomly rotated poison disk samples. Timings – ShadowMapsUpdate – 3 ms Depth Pass – 1.2 ms ShadowBuffer pass (with 1 pcf sample) – 0.502 ms ShadowBuffer pass (with 8 randomly rotated Poisson disk samples) – 0.684 ms More detailed timings can be checked below –

csmPowerPlant1

Timings –

  • ShadowMapsUpdate – 3 ms
  • Depth Pass – 1.2 ms
  • ShadowBuffer pass (with 1 pcf sample) – 0.502 ms
  • ShadowBuffer pass (with 8 randomly rotated Poisson disk samples) – 0.684 ms

More detailed timings can be checked below –

Note – This scene is not the original sdkmesh scene that comes with DXSDK. I ripped it of using a software called NinjaRipper from DXSDK example exe and used 3dsmax to convert it into FBX. Even though that software ripped of the whole scenes but it comes out pretty bad form. So rendering quality/performance may not be as good as DXSDK example scenes.

Test Scene 2 : Sponza

Scene rendered with 3 dynamic cascades and a static shadow map, each with a resolution of 2048 x 2048. Used Texture2DArray for cascades and Texture2D for static map. Using map selection mode, Normal offsets, derivatives transition blur and 8 randomly rotated poison disk samples.

csmSponza1

Timings –

  • ShadowMapsUpdate – 3 ms
  • Depth Pass – 0.605 ms
  • ShadowBuffer pass (with 1 pcf sample) – 0.442 ms
  • ShadowBuffer pass (with 8 randomly rotated poisson disk samples) – 0.666 ms

 

More detailed timings can be checked below –

 

Shoutout to the developers of RenderDoc which has been very useful throughout the development and have saved countless hours while debugging. It’s an open source gfx debugger available at – https://github.com/baldurk/renderdoc

 

References :

  1. RealTimeRendering (Book)
  2. Playing with Real Time Shadows , by Nikolas Kasyan, Crytek
  3. Cascaded Shadow Maps article on MSDN
  4. GPU Gems3 : Parallel-Split Shadow Maps on Programmable GPUs
  5. Shadows Decals: D3D10 techniques from Frostbite  by Daniel Johansson Johan Andersson, DICE
  6. “Stable Rendering of Cascaded Shadow Maps” in ShaderX 6
  7. Shimmering Fix – GameDev.net
  8. Graphics Tech: Shadow Maps – Jonathan Blow
Advertisements

About chetanjags

Game Developer

Posted on February 5, 2015, in Game Development, Graphics, JustAnotherGameEngine, Shadows, Tutorials and tagged , , , , . Bookmark the permalink. 1 Comment.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: