Implementing shadow mapping with FBO

Shadows give important cues about the relative positioning of graphical objects. There are myriads of shadow generation techniques, including shadow volumes, shadow maps, cascaded shadow maps, and so on. An excellent reference on several shadow generation techniques is given in the See also section. We will now see how to carry out basic shadow mapping using FBO.

Getting started

For this recipe, we will use the previous scene but instead of a grid object, we will use a plane object so that the generated shadows can be seen. The code for this recipe is contained in the Chapter4/ShadowMapping directory.

How to do it…

Let us start with this recipe by following these simple steps:

  1. Create an OpenGL texture object which will be our shadow map texture. Make sure to set the clamp mode to GL_CLAMP_TO_BORDER, set the border color to {1,0,0,0}, give the texture comparison mode to GL_COMPARE_REF_TO_TEXTURE, and set the compare function to GL_LEQUAL. Set the texture internal format to GL_DEPTH_COMPONENT24.
    glGenTextures(1, &shadowMapTexID);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, shadowMapTexID);
    GLfloat border[4]={1,0,0,0};
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_MODE,GL_COMPARE_REF_TO_TEXTURE);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_FUNC,GL_LEQUAL);
    glTexParameterfv(GL_TEXTURE_2D,GL_TEXTURE_BORDER_COLOR,border);
    glTexImage2D(GL_TEXTURE_2D,0,GL_DEPTH_COMPONENT24,SHADOWMAP_WIDTH,SHADOWMAP_HEIGHT,0,GL_DEPTH_COMPONENT,GL_UNSIGNED_BYTE,NULL);
    
  2. Set up an FBO and use the shadow map texture as a single depth attachment. This will store the scene's depth from the point of view of light.
    glGenFramebuffers(1,&fboID);
    glBindFramebuffer(GL_FRAMEBUFFER,fboID);
    glFramebufferTexture2D(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,GL_TEXTURE_2D,shadowMapTexID,0);
    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);
  3. Using the position and the direction of the light, set up the shadow matrix (S) by combining the light modelview matrix (MV_L), projection matrix (P_L), and bias matrix (B). For reducing runtime calculation, we store the combined projection and bias matrix (BP) at initialization.
    MV_L = glm::lookAt(lightPosOS,glm::vec3(0,0,0), glm::vec3(0,1,0));
    P_L  = glm::perspective(50.0f,1.0f,1.0f, 25.0f);
    B    = glm::scale(glm::translate(glm::mat4(1), glm::vec3(0.5,0.5,0.5)),glm::vec3(0.5,0.5,0.5));
    BP   = B*P_L;
    S    = BP*MV_L;
  4. Bind the FBO and render the scene from the point of view of the light. Make sure to enable front-face culling (glEnable(GL_CULL_FACE) and glCullFace(GL_FRONT)) so that the back-face depth values are rendered. Otherwise our objects will suffer from shadow acne.

    Tip

    Normally, a simple shader could be used for rendering of a scene in the depth texture. This may also be achieved by disabling writing to the color buffer (glDrawBuffer(GL_NONE)) and then enabling it for normal rendering. In addition, an offset bias can also be added in the shader code to reduce shadow acne.

    glBindFramebuffer(GL_FRAMEBUFFER,fboID);
    glClear(GL_DEPTH_BUFFER_BIT);
    glViewport(0,0,SHADOWMAP_WIDTH, SHADOWMAP_HEIGHT);
    glCullFace(GL_FRONT);
    DrawScene(MV_L, P_L);
    glCullFace(GL_BACK);
  5. Disable FBO, restore default viewport, and render the scene normally from the point of view of the camera.
    glBindFramebuffer(GL_FRAMEBUFFER,0);
    glViewport(0,0,WIDTH, HEIGHT);
    DrawScene(MV, P, 0 );
  6. In the vertex shader, multiply the world space vertex positions (M*vec4(vVertex,1)) with the shadow matrix (S) to obtain the shadow coordinates. These will be used for lookup of the depth values from the shadowmap texture in the fragment shader.
    #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);
    }
  7. In the fragment shader, use the shadow coordinates to lookup the depth value in the shadow map sampler which is of the sampler2Dshadow type. This sampler can be used with the textureProj function to return a comparison outcome. We then use the comparison result to darken the diffuse component, simulating shadows.
    #version 330 core
    layout(location=0) out vec4 vFragColor;
    uniform sampler2DShadow shadowMap;
    uniform vec3 light_position;  //light position in eye space
    uniform vec3 diffuse_color;
    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
    uniform bool bIsLightPass; //no shadows in light pass
    void main() {
      if(bIsLightPass)
      return;
      vec3 L = (light_position.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) {
        float shadow = textureProj(shadowMap, vShadowCoords);
        diffuse = mix(diffuse, diffuse*shadow, 0.5);
      }
      vFragColor = diffuse*vec4(diffuse_color, 1);
    }

How it works…

The shadow mapping algorithm works in two passes. In the first pass, the scene is rendered from the point of view of light, and the depth buffer is stored into a texture called shadowmap. We use a single FBO with a depth attachment for this purpose. Apart from the conventional minification/magnification texture filtering, we set the texture wrapping mode to GL_CLAMP_TO_BORDER, which ensures that the values are clamped to the specified border color. Had we set this as GL_CLAMP or GL_CLAMP_TO_EDGE, the border pixels forming the shadow map would produce visible artefacts.

The shadowmap texture has some additional parameters. The first is the GL_TEXTURE_COMPARE_MODE parameter, which is set as the GL_COMPARE_REF_TO_TEXTURE value. This enables the texture to be used for depth comparison in the shader. Next, we specify the GL_TEXTURE_COMPARE_FUNC parameter, which is set as GL_LEQUAL. This compares the currently interpolated texture coordinate value (r) with the depth texture's sample value (D). It returns 1 if r<=D, otherwise it returns 0. This means that if the depth of the current sample is less than or equal to the depth from the shadowmap texture, the sample is not in shadow; otherwise, it is in shadow. The textureProj GLSL shader function performs this comparison for us and returns 0 or 1 based on whether the point is in shadow or not. These are the texture parameters required for the shadowmap texture.

To ensure that we do not have any shadow acne, we enable front-face culling (glEnable(GL_CULL_FACE) and glCullFace(GL_FRONT)) so that the back-face depth values get written to the shadowmap texture. In the second pass, the scene is rendered normally from the point of view of the camera and the shadow map is projected on the scene geometry using shaders.

To render the scene from the point of view of light, the modelview matrix of the light (MV_L), the projection matrix (P_L), and the bias matrix (B) are calculated. After multiplying with the projection matrix, the coordinates are in clip space (that is, they range from [-1,-1,-1]). to [1,1,1]. The bias matrix rescales this range to bring the coordinates from [0,0,0] to [1,1,1] range so that the shadow lookup can be carried out.

If we have the object's vertex position in the object space given as Vobj, the shadow coordinates (UVproj) for the lookup in the shadow map can be given by multiplying the shadow matrix (S) with the world space position of the object (M*Vobj). The whole series of transformations is given as follows:

How it works…

Here, B is the bias matrix, P L is the projection matrix of light, and MV L is the modelview matrix of light. For efficiency, we precompute the bias matrix of the light and the projection matrix, since they are unchanged for the lifetime of the application. Based on the user input, the light's modelview is modified and then the shadow matrix is recalculated. This is then passed to the shader.

In the vertex shader, the shadowmap texture coordinates are obtained by multiplying the world space vertex position (M*Vobj) with the shadow matrix (S). In the fragment shader, the shadow map is looked up using the projected texture coordinate to find if the current fragment is in shadow. Before the texture lookup, we check the value of the w coordinate of the projected texture coordinate. We only do our calculations if the w coordinate is greater than 1. This ensures that we only accept the forward projection and reject the back projection. Try removing this condition to see what we mean.

The shadow map lookup computation is facilitated by the textureProj GLSL function. The result from the shadow map lookup returns 1 or 0. This result is multiplied with the shading computation. As it happens in the real world, we never have coal black shadows. Therefore, we combine the shadow outcome with the shading computation by using the mix GLSL function.

There's more…

The demo application for this recipe shows a plane, a cube, and a sphere. A point light source, which can be rotated using the right mouse button, is placed. The distance of the light source can be altered using the mouse wheel. The output result from the demo is displayed in the following figure:

There's more…

This recipe detailed the shadow mapping technique for a single light source. With each additional light source, the processing, as well as storage requirements, increase.

See also

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

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