Exponential Cascaded Shadow Mapping with WebGL

Update: my implementation worked well on “native” OpenGL configuration but suffered from a GLSL-to-HLSL bug in the ANGLE shader cross-compiler used by Firefox and Chrome on Windows. It should now be fixed.

Update 2: you can toggle each cascade frustum using “L” and the camera frustum using “C”.

TL;DR

shadowmappingWebGL Cascaded Shadow Mapping demo powered by Minko

  • arrow keys: rotate around the scene or zoom in/out
  • C: toggle the debug display of the camera frustum
  • L: toggle the debug display of each cascade frustum
  • A: toggle shadow mapping for the 1st light
  • Z: toggle shadow mapping for the 2nd light

Motivations

Lighting is a very important part of rendering real-time convincing 3D scenes. Minko already provides a quite comprehensive set of components (DirectionalLight, SpotLight, AmbientLight, PointLight) and shaders to implement the Phong reflection model. Yet, without projected shadows, the human eye can hardly grasp the actual layout and depth of the scene.

My goal was to work on directional light projected shadows as part of a broader work to handle sun-light and a dynamic outdoor environment rendering setup (sun flares, sky, weather…).

Shadow Mapping is the preferred method for hardware accelerated dynamic projected shadows. Cascaded Shadow Mapping is a variation that makes it easier – and at some extend automagic – to render high-quality shadow maps especially for large (outdoor) scenes with one (or more) directional sun-like light(s).

Finally, shadow mapping is a complex rendering scheme that involves pre-render out-of-screen rendering and filtering phases and I wanted to make sure Minko’s effect pipeline was able to handle it.

Computing the split projections

The first step is to compute each shadow cascade (or split) projection. The goal is to split the original camera perspective projection frustum into multiple sub-frustum (1 split => 1 frustum). The sum of all sub-frustum must – of course – cover the exact same volume as the original camera frustum.

The Practical Split Scheme (Source: NVIDIA GPU Gems 3 - Chapter 10)
The Practical Split Scheme (Source: NVIDIA GPU Gems 3 – Chapter 10)

To do this, I used the  “Practical Split Scheme” provided in the GPU Gems 3 – Chapters 10 “Parallel Split Shadow Maps on Programmable GPUs” article.

Rendering the shadow maps

Rendering the shadow map is quite straight forward:

The only trick is that – in order to avoid using floating point textures that would require a WebGL extension – I pack the depth value in the R8G8B8A8 fragment color using the following function:

For performance reasons and in order to avoid branches in the final step when rendering shadow (see “Rendering the shadows” below), we have to render all the cascades in a single render target. This target will be our “shadow atlas”.

To do this, I simply setup 4 Renderers with 4 different viewports that will all render into the same render target:

Note that I also make sure only the 1st Renderer will clear the render target by calling renderer->clearBeforeRender(i == 0).

Rendering the shadows

Rendering the shadows themselves is a bit trickier. In order to sample the shadow map, one must first know which shadow map (/cascade) to sample. And with each shadow map comes a different projection, z-near and z-far values…

Because we render a shadow atlas, we just have to know which part/viewport to sample. In the end, selection that viewport, the light projection and the z near/far values can be done without branching using a classic weighting approach combining all possible values.

The following function will compute a vec4 with a single component set to 1 (and all the others set to 0). The component set to 1 will give you the proper cascade to use (x => cascade 1, y => cascade 2…):

Those weights can then be used to properly build the cascade parameters from all possible values:

I use those functions like this:

Exponential Shadow Mapping

To get smoother soft-like shadows, I use Exponential Shadow Mapping (ESM). ESM is done by doing mainly 2 things:

  • Filtering the shadow map: in my case I use a simple box-blur + linear texture filtering when sampling the result.
  • Use the exp() GLSL function before comparing the depths.

Beware! When you perform the box-blur, make sure you unpack the depth values before blurring them. My box-blur shader looks like this:

The ESM depth comparisong code is done with the following function:

Going further…

The final code will soon be available on the Minko repository in the dev branch. Next steps include:

  • Stabilizing the shadow map: lower resolution cascades tend to make the final shadow rendering “swim”. That’s a known issue fixed by tweaking the projection matrix.
  • Blending between cascade to get smoother transitions.
  • Implementing shadow mapping for point and spot lights.
  • -> works on linux intel but not windows nvidia. not lighting. Maybe a hint: shadowmap bg is black on nvidia win7 whereas it’s blue on the working intel linux. (might be something strange in undefined var in the shadowmap clear). Apart from black bg for shadowmap, that it does output a seemingly cascaded shadowmap on the nvidia/win7 (ffx & chrome)

    -> esm filtering with box filter may need log blur for correct small detail. (or output a exp(-cz) in shadowmap )
    http://www.olhovsky.com/2011/07/exponential-shadow-map-filtering-in-hlsl/

    -> Why not outputting and comparing Linearized depth ? Wouldn’t You’d gain much more float precision, especially for cascaded shadow ?
    (zNear an Zfar being the zNear/ZFar of each cascade, not the whole cast scene, which means 4*2 float for the receiver shader though)
    http://www.yosoygames.com.ar/wp/2014/01/linear-depth-buffer-my-ass/

    -> I don’t get why shadowmap is moving when cam move but light doesn’t, using clever technique for shadow & camera frustum intersection ?

  • Promethe

    Hello and thank you for your feedback,

    I’ve just fixed with a workaround for the GLSL-to-HLSL bug caused by ANGLE on Windows Firefox/Chrome. FYI don’t use global variables in your GLSL shaders if you expect them to work with ANGLE…

    Regarding ESM filtering you are right. I’ll fix this in a later update.

    About the linearized depth, I thought that’s what I was comparing. Is it not? The zFar and zNear I use to write the depth in the shadow map shaders are indeed the cascade ones.

    The shadowmap does move because I call DirectionalLight::computeShadowProjection() in order to fit my cascades according to the camera furstum. The only drawback is the shadow might “swim” or “jiggle” because the same pixel will not endup samping the same texel from one frame to another. I have to implement “stable” cascaded shadow mapping (see http://dice.se/wp-content/uploads/GDC09_ShadowAndDecals_Frostbite.ppt)

  • -> esm filtering with box filter may need log blur for correct small detail. (or output a exp(-cz) in shadowmap ).
    http://www.olhovsky.com/2011/07/exponential-shadow-map-filtering-in-hlsl/
    (and why not two pass box blur + using linear interpol from texture filter check http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/)

    -> Why not outputting and comparing Linearized depth ? Wouldn’t You’d gain much more float precision, especially for cascaded shadow ?
    (zNear an Zfar being the zNear/ZFar of each cascade, not the whole cast scene, which means 4*2 float for the receiver shader though)
    http://www.yosoygames.com.ar/wp/2014/01/linear-depth-buffer-my-ass/

    -> I don’t get why shadowmap is moving when cam move but light doesn’t, using clever technique for shadow & camera frustum intersection ?

    • Hello and thank you for your feedback,

      I’ve just fixed with a workaround for the GLSL-to-HLSL bug caused by
      ANGLE on Windows Firefox/Chrome. FYI don’t use global variables in your
      GLSL shaders if you expect them to work with ANGLE…

      Regarding ESM filtering you are right. I’ll fix this in a later update.

      About the linearized depth, I thought that’s what I was comparing. Is
      it not? The zFar and zNear I use to write the depth in the shadow map
      shaders are indeed the cascade ones.

      The shadowmap does move because I call
      DirectionalLight::computeShadowProjection() in order to fit my cascades
      according to the camera furstum. The only drawback is the shadow might
      “swim” or “jiggle” because the same pixel will not endup samping the
      same texel from one frame to another. I have to implement “stable”
      cascaded shadow mapping (see http://dice.se/wp-content/uploads/GDC09_ShadowAndDecals_Frostbite.ppt)

      • – working nicely here on all angle browser, thanks.

        – on linear depth maybe I’m reading the wrong code when looking at “linearDepthOrtho” (it should be doing something like
        return (depth – zNear) / (zFar – zNear) ? And the same in receiving shader to apply on the “shadowDepth” (if it’s a I think “objectDepthInShadowView”, so that you compare two linear depth)

        – ok, undersood better the optimizing the frustum & missing stabilization bits, thanks to you link http://fr.slideshare.net/repii/02-g-d-c09-shadow-and-decals-frostbite-final3flat/24

        • When I pack the value it has to be in [0..1]. Wouldn’t that cancel the point of using linear (view) space?

          • in
            depth = pos.z * pos.w / zFar
            depth = (depth – zNear) / (zFar -zNear)
            the *pos.w” re-linearize, the /zFar keep in it [0.1] but doesn’t “unlinearize” ?

            the last bit can be
            depth = depth / zFar
            to avoid precision loss near 0 (but in the cascaded case, may make less sense)
            Or you can also reverse far & near plane and make them 0,1 for that ( http://outerra.blogspot.fr/2009/08/logarithmic-z-buffer.html don’t forget to change depth check from less to greater in that case)

          • OK. I always thought an orthographic projection would give a linear depth. Does it not?

          • ouch, sorry, my bad, yes it does ! w=1 with ortho…

  • Lucas Naceri

    I really hate this look on my iPad