Implementing variance shadow mapping

In this recipe, we will cover a technique which gives a much better result, has better performance, and at the same time is easier to calculate. The technique is called variance shadow mapping. In conventional PCF-filtered shadow mapping, we compare the depth value of the current fragment to the mean depth value in the shadow map, and based on the outcome, we shadow the fragment.

In case of variance shadow mapping, the mean depth value (also called first moment) and the mean squared depth value (also called second moment) are calculated and stored. Then, rather than directly using the mean depth, the variance is used. The variance calculation requires both the mean depth as well as the mean of the squared depth. Using the variance, the probability of whether the given sample is shadowed is estimated. This probability is then compared to the maximum probability to determine if the current sample is shadowed.

Getting started

For this recipe, we will build on top of the shadow mapping recipe, Implementing shadow mapping with FBO. The code for this recipe is contained in the Chapter4/VarianceShadowMapping folder.

How to do it…

Let us start our recipe by following these simple steps:

  1. Set up the shadowmap texture as in the shadow map recipe, but this time remove the depth compare mode (glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_MODE,GL_COMPARE_REF_TO_TEXTURE) and glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_FUNC,GL_LEQUAL)). Also set the format of the texture to the GL_RGBA32F format. Also enable the mipmap generation for this texture. The mipmaps provide filtered textures across different scales and produces better alias-free shadows. We request five mipmap levels (by specifying the max level as 4).
    glGenTextures(1, &shadowMapTexID);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, shadowMapTexID);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR;
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_BORDER);
    glTexParameterfv(GL_TEXTURE_2D,GL_TEXTURE_BORDER_COLOR,border;
    glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA32F,SHADOWMAP_WIDTH,SHADOWMAP_HEIGHT,0,GL_RGBA,GL_FLOAT,NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 4);
    glGenerateMipmap(GL_TEXTURE_2D);
    
  2. Set up two FBOs: one for shadowmap generation and another for shadowmap filtering. The shadowmap FBO has a renderbuffer attached to it for depth testing. The filtering FBO does not have a renderbuffer attached to it but it has two texture attachments.
    glGenFramebuffers(1,&fboID);
      glGenRenderbuffers(1, &rboID);
      glBindFramebuffer(GL_FRAMEBUFFER,fboID);
      glBindRenderbuffer(GL_RENDERBUFFER, rboID);
      glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT32, SHADOWMAP_WIDTH, SHADOWMAP_HEIGHT);
    glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,shadowMapTexID,0);
      glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboID);
    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if(status == GL_FRAMEBUFFER_COMPLETE) {
      cout<<"FBO setup successful."<<endl;
    } else {
      cout<<"Problem in FBO setup."<<endl;
    }
    glBindFramebuffer(GL_FRAMEBUFFER,0);
    
    glGenFramebuffers(1,&filterFBOID);
    glBindFramebuffer(GL_FRAMEBUFFER,filterFBOID);
    glGenTextures(2, blurTexID);
    for(int i=0;i<2;i++) {
      glActiveTexture(GL_TEXTURE1+i);
      glBindTexture(GL_TEXTURE_2D, blurTexID[i]);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_BORDER);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_BORDER);
      glTexParameterfv(GL_TEXTURE_2D,GL_TEXTURE_BORDER_COLOR,border);
      glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA32F,SHADOWMAP_WIDTH,SHADOWMAP_HEIGHT,0,GL_RGBA,GL_FLOAT,NULL);
      glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0+i, GL_TEXTURE_2D,blurTexID[i],0);
    }
    status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if(status == GL_FRAMEBUFFER_COMPLETE) {
      cout<<"Filtering FBO setup successful."<<endl;
    } else {
      cout<<"Problem in Filtering FBO setup."<<endl;
    }
    glBindFramebuffer(GL_FRAMEBUFFER,0);
  3. Bind the shadowmap FBO, set the viewport to the size of the shadowmap texture, and render the scene from the point of view of the light, as in the Implementing shadow mapping with FBO recipe. In this pass, instead of storing the depth as in the shadow mapping recipe, we use a custom fragment shader (Chapter4/VarianceShadowmapping/shaders/firststep.frag) to output the depth and depth*depth values in the red and green channels of the fragment output color.
    glBindFramebuffer(GL_FRAMEBUFFER,fboID);   
    glViewport(0,0,SHADOWMAP_WIDTH, SHADOWMAP_HEIGHT);
    glDrawBuffer(GL_COLOR_ATTACHMENT0);
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
    DrawSceneFirstPass(MV_L, P_L);

    The shader code is as follows:

    #version 330 core
    layout(location=0) out vec4 vFragColor;
    smooth in vec4 clipSpacePos;
    void main()
    {
      vec3 pos = clipSpacePos.xyz/clipSpacePos.w; //-1 to 1
      pos.z += 0.001; //add some offset to remove the shadow acne
      float depth = (pos.z +1)*0.5; // 0 to 1
      float moment1 = depth;
      float moment2 = depth * depth; 
      vFragColor = vec4(moment1,moment2,0,0);
    }
  4. Bind the filtering FBO to filter the shadowmap texture generated in the first pass using separable Gaussian smoothing filters, which are more efficient and offer better performance. We first attach the vertical smoothing fragment shader (Chapter4/VarianceShadowmapping/shaders/GaussV.frag) to filter the shadowmap texture and then the horizontal smoothing fragment shader (Chapter4/VarianceShadowmapping/shaders/GaussH.frag) to smooth the output from the vertical Gaussian smoothing filter.
    glBindFramebuffer(GL_FRAMEBUFFER,filterFBOID);
    glDrawBuffer(GL_COLOR_ATTACHMENT0);
    glBindVertexArray(quadVAOID);
    gaussianV_shader.Use();
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);
    glDrawBuffer(GL_COLOR_ATTACHMENT1);
    gaussianH_shader.Use();
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);
    glBindFramebuffer(GL_FRAMEBUFFER,0);

    The horizontal Gaussian blur shader is as follows:

    #version 330 core
    layout(location=0) out vec4 vFragColor;
    smooth in vec2 vUV;
    uniform sampler2D textureMap;
    
    const float kernel[]=float[21] (0.000272337,  0.00089296, 0.002583865, 0.00659813,  0.014869116, 0.029570767, 0.051898313, 0.080381679, 0.109868729, 0.132526984, 0.14107424,  0.132526984, 0.109868729, 0.080381679, 0.051898313, 0.029570767, 0.014869116, 0.00659813, 0.002583865, 0.00089296, 0.000272337);
    
    void main()
    {
      vec2 delta = 1.0/textureSize(textureMap,0);
      vec4 color = vec4(0);
      int  index = 20;
    
      for(int i=-10;i<=10;i++) {
        color += kernel[index--]*texture(textureMap, vUV + (vec2(i*delta.x,0)));
      }
    
      vFragColor =  vec4(color.xy,0,0);
    }

    In the vertical Gaussian shader, the loop statement is modified, whereas the rest of the shader is the same.

    color += kernel[index--]*texture(textureMap, vUV + (vec2(0,i*delta.y)));
  5. Unbind the FBO, reset the default viewport, and then render the scene normally, as in the shadow mapping recipe.
    glDrawBuffer(GL_BACK_LEFT);
    glViewport(0,0,WIDTH, HEIGHT);
    DrawScene(MV, P);

How it works…

The variance shadowmap technique tries to represent the depth data such that it can be filtered linearly. Instead of storing the depth, it stores the depth and depth*depth value in a floating point texture, which is then filtered to reconstruct the first and second moments of the depth distribution. Using the moments, it estimates the variance in the filtering neighborhood. This helps in finding the probability of a fragment at a specific depth to be occluded using Chebyshev's inequality. For more mathematical details, we refer the reader to the See also section of this recipe.

From the implementation point of view, similar to the shadow mapping recipe, the method works in two passes. In the first pass, we render the scene from the point of view of light. Instead of storing the depth, we store the depth and the depth*depth values in a floating point texture using the custom fragment shader (see Chapter4/VarianceShadowmapping/shaders/firststep.frag).

The vertex shader outputs the clip space position to the fragment shader using which the fragment depth value is calculated. To reduce self-shadowing, a small bias is added to the z value.

vec3 pos = clipSpacePos.xyz/clipSpacePos.w;
pos.z += 0.001;
float depth = (pos.z +1)*0.5;
float moment1 = depth;
float moment2 = depth * depth;
vFragColor = vec4(moment1,moment2,0,0);

After the first pass, the shadowmap texture is blurred using a separable Gaussian smoothing filter. First the vertical and then the horizontal filter is applied to the shadowmap texture by applying the shadowmap texture to a full-screen quad and alternating the filter FBO's color attachment. Note that the shadowmap texture is bound to texture unit 0 whereas the textures used for filtering are bound to texture unit 1 (attached to GL_COLOR_ATTTACHMENT0 on the filtering FBO) and texture unit 2 (attached to GL_COLOR_ATTACHMENT1 on the filtering FBO).

glBindFramebuffer(GL_FRAMEBUFFER,fboID);
  glViewport(0,0,SHADOWMAP_WIDTH, SHADOWMAP_HEIGHT);
  glDrawBuffer(GL_COLOR_ATTACHMENT0);
  glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
  DrawSceneFirstPass(MV_L, P_L);
  
glBindFramebuffer(GL_FRAMEBUFFER,filterFBOID);
glDrawBuffer(GL_COLOR_ATTACHMENT0);
glBindVertexArray(quadVAOID);
  gaussianV_shader.Use();
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);
    
    glDrawBuffer(GL_COLOR_ATTACHMENT1);
  gaussianH_shader.Use();
  glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);
glBindFramebuffer(GL_FRAMEBUFFER,0);
glDrawBuffer(GL_BACK_LEFT);
glViewport(0,0,WIDTH, HEIGHT);

In the second pass, the scene is rendered from the point of view of the camera. The blurred shadowmap is used in the second pass as a texture to lookup the sample value (see Chapter4/VarianceShadowmapping/shaders/VarianceShadowMap.{vert, frag}). The variance shadow mapping vertex shader outputs the shadow texture coordinates, as in the shadow mapping recipe.

#version 330 core
layout(location=0) in vec3 vVertex;
layout(location=1) in vec3 vNormal;
uniform mat4 MVP;   //modelview projection matrix
uniform mat4 MV;    //modelview matrix
uniform mat4 M;      //model matrix
uniform mat3 N;     //normal matrix
uniform mat4 S;     //shadow matrix
smooth out vec3 vEyeSpaceNormal;
smooth out vec3 vEyeSpacePosition;
smooth out vec4 vShadowCoords;
void main()
{
  vEyeSpacePosition = (MV*vec4(vVertex,1)).xyz;
  vEyeSpaceNormal   = N*vNormal;
  vShadowCoords     = S*(M*vec4(vVertex,1));
  gl_Position       = MVP*vec4(vVertex,1);
}

The variance shadow mapping fragment shader operates differently. We first make sure that the shadow coordinates are in front of the light (to prevent back projection), that is, shadowCoord.w>1. Next, the shadowCoords.xyz values are divided by the homogeneous coordinate, shadowCoord.w, to get the depth value.

if(vShadowCoords.w>1) {
  vec3 uv = vShadowCoords.xyz/vShadowCoords.w;
  float depth = uv.z;

The texture coordinates after homogeneous division are used to lookup the shadow map storing the two moments. The two moments are used to estimate the variance. The variance is clamped and then the occlusion probability is estimated. The diffuse component is then modulated based on the obtained occlusion probability.

  vec4 moments = texture(shadowMap, uv.xy);
  float E_x2 = moments.y;
  float Ex_2 = moments.x*moments.x;
  float var = E_x2-Ex_2;
  var = max(var, 0.00002);
  float mD = depth-moments.x;
  float mD_2 = mD*mD;
  float p_max = var/(var+ mD_2);
  diffuse *= max(p_max, (depth<=moments.x)?1.0:0.2);
}

To recap, here is the complete variance shadow mapping fragment shader:

#version 330 core
layout(location=0) out vec4 vFragColor;
uniform sampler2D  shadowMap;
uniform vec3 light_position;  //light position in object space
uniform vec3 diffuse_color;
uniform mat4 MV;
smooth in vec3 vEyeSpaceNormal;
smooth in vec3 vEyeSpacePosition;
smooth in vec4 vShadowCoords;
const float k0 = 1.0;  //constant attenuation
const float k1 = 0.0;  //linear attenuation
const float k2 = 0.0;  //quadratic attenuation
void main() {   
  vec4 vEyeSpaceLightPosition = (MV*vec4(light_position,1));
  vec3 L = (vEyeSpaceLightPosition.xyz-vEyeSpacePosition);
  float d = length(L);
   L = normalize(L);
  float attenuationAmount = 1.0/(k0 + (k1*d) + (k2*d*d));
  float diffuse = max(0, dot(vEyeSpaceNormal, L)) * attenuationAmount;
  if(vShadowCoords.w>1) {
    vec3 uv = vShadowCoords.xyz/vShadowCoords.w;
    float depth = uv.z;
    vec4 moments = texture(shadowMap, uv.xy);
    float E_x2 = moments.y;
    float Ex_2 = moments.x*moments.x;
    float var = E_x2-Ex_2;
    var = max(var, 0.00002);
    float mD = depth-moments.x;
    float mD_2 = mD*mD;
    float p_max = var/(var+ mD_2);
    diffuse *= max(p_max, (depth<=moments.x)?1.0:0.2);
  }
  vFragColor = diffuse*vec4(diffuse_color, 1);
}

There's more…

Variance shadow mapping is an interesting idea. However, it does suffer from light bleeding artefacts. There have been several improvements to the basic technique, such as summed area variance shadow maps, layered variance shadow maps, and more recently, sample distribution shadow maps, that are referred to in the See also section of this recipe. After getting a practical insight into the basic variance shadow mapping idea, we invite the reader to try and implement the different variants of this algorithm, as detailed in the references in the See also section.

The demo application for this recipe shows the same scene (a cube and a sphere on a plane) lit by a point light source. Right-clicking the mouse button rotates the point light around the objects. The output result is shown in the following figure:

There's more…

Comparing this output to the previous shadow mapping recipes, we can see that the output quality is much better if compared to the conventional shadow mapping and the PCF-based technique. When comparing the outputs, variance shadow mapping gives a better output with a significantly less number of samples. Obtaining the same output using PCF or any other technique would require a very large neighborhood lookup with more samples. This makes this technique well-suited for real-time applications such as games.

See also

  • Proceedings of the 2006 symposium on Interactive 3D graphics and games, Variance Shadow Maps, pages 161-165 William Donnelly, Andrew Lauritzen
  • GPU Gems 3, Chapter 8, Summed-Area Variance Shadow Maps, Andrew Lauritzen: http://http.developer.nvidia.com/GPUGems3/gpugems3_ch08.html
  • Proceedings of the Graphics Interface 2008, Layered variance shadow maps, pages 139-146, Andrew Lauritzen, Michael McCool
  • Sample Distribution Shadow Maps, ACM SIGGRAPH Symposium on Interactive 3D Graphics and Games (I3D) 2011, February, Andrew Lauritzen, Marco Salvi, and Aaron Lefohn
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.14.1.136