Implemeting shadow mapping with percentage closer filtering (PCF)

The shadow mapping algorithm, though simple to implement, suffers from aliasing artefacts, which are due to the shadowmap resolution. In addition, the shadows produced using this approach are hard. These can be minimized either by increasing the shadowmap resolution or taking more samples. The latter approach is called percentage closer filtering (PCF), where more samples are taken for the shadowmap lookup and the percentage of the samples is used to estimate if a fragment is in shadow. Thus, in PCF, instead of a single lookup, we sample an n×n neighborhood of shadowmap and then average the values.

Getting started

The code for this recipe is contained in the Chapter4/ShadowMappingPCF directory. It builds on top of the previous recipe, Implementing shadow mapping with FBO. We use the same scene but augment it with PCF.

How to do it…

Let us see how to extend the basic shadow mapping with PCF.

  1. Change the shadowmap texture minification/magnification filtering modes to GL_LINEAR. Here, we exploit the texture filtering capabilities of the GPU to reduce aliasing artefacts during sampling of the shadow map. Even with the linear filtering support, we have to take additional samples to reduce the artefacts.
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
  2. In the fragment shader, instead of a single texture lookup as in the shadow map recipe, we use a number of samples. GLSL provides a convenient function, textureProjOffset, to allow calculation of samples using an offset. For this recipe, we look at a 3×3 neighborhood around the current shadow map point. Hence, we use a large offset of 2. This helps to reduce sampling artefacts.
    if(vShadowCoords.w>1) {
      float sum = 0;
      sum += textureProjOffset(shadowMap,vShadowCoords,ivec2(-2,-2));
      sum += textureProjOffset(shadowMap,vShadowCoords,ivec2(-2, 0));
      sum += textureProjOffset(shadowMap,vShadowCoords,ivec2(-2, 2));
      sum += textureProjOffset(shadowMap,vShadowCoords,ivec2( 0,-2));
      sum += textureProjOffset(shadowMap,vShadowCoords,ivec2( 0, 0));
      sum += textureProjOffset(shadowMap,vShadowCoords,ivec2( 0, 2));
      sum += textureProjOffset(shadowMap,vShadowCoords,ivec2( 2,-2));
      sum += textureProjOffset(shadowMap,vShadowCoords,ivec2( 2, 0));
      sum += textureProjOffset(shadowMap,vShadowCoords,ivec2( 2, 2));
      float shadow = sum/9.0;
      diffuse = mix(diffuse, diffuse*shadow, 0.5);
    }

How it works…

In order to implement PCF, the first change we need is to set the texture filtering mode to linear filtering. This change enabled the GPU to bilinearly interpolate the shadow value. This gives smoother edges since the hardware does PCF filtering underneath. However it is not enough for our purpose. Therefore, we have to take additional samples to improve the result.

Fortunately, we can use a convenient function, textureProjOffset, which accepts an offset that is added to the given shadow map texture coordinate. Note that the offset given to this function must be a constant literal. Thus, we cannot use a loop variable for dynamic sampling of the shadow map sampler. We, therefore, have to unroll the loop to sample the neighborhood.

We use an offset of 2 units because we wanted to sample at a value of 1.5. However, since the textureProjOffset function does not accept a floating point value, we round it to the nearest integer. The offset is then modified to move to the next sample point until the entire 3×3 neighborhood is sampled. We then average the sampling result for the entire neighborhood. The obtained sampling result is then multiplied to the lighting contribution, thus, producing shadows if the current sample happens to be in an occluded region.

Even with adding additional samples, we get sampling artefacts. These can be reduced by shifting the sampling points randomly. To achieve this, we first implement a pseudo-random function in GLSL as follows:

float random(vec4 seed) {
  float dot_product = dot(seed, vec4(12.9898,78.233, 45.164, 94.673));
  return fract(sin(dot_product) * 43758.5453);
}

Then, the sampling for PCF uses the noise function to shift the shadow offset, as shown in the following shader code:

for(int i=0;i<16;i++) {
  float indexA = (random(vec4(gl_FragCoord.xyx, i))*0.25);
  float indexB = (random(vec4(gl_FragCoord.yxy, i))*0.25);
  sum += textureProj(shadowMap, vShadowCoords + 
         vec4(indexA, indexB, 0, 0));
}
shadow = sum/16.0;

In the given code, three macros are defined, STRATIFIED_3x3 (for 3x3 stratified sampling), STRATIFIED_5x5 (for 5x5 stratified sampling), and RANDOM_SAMPLING (for 4x4 random sampling).

There's more…

Making these changes, we get a much better result, as shown in the following figure. If we take a bigger neighborhood, we get a better result. However, the computational requirements also increase.

There's more…

The following figure compares this result of the PCF-filtered shadow map (right) with a normal shadow map (left). We can see that the PCF-filtered result gives softer shadows with reduced aliasing artefacts.

There's more…

The following figure compares the result of the stratified PCF-filtered image (left) against the random PCF-filtered image (right). As can be seen, the noise-filtered image gives a much better result.

There's more…

See also

..................Content has been hidden....................

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