Shadow mapping is a technique used in 3D computer graphics to add shadows to a scene. It involves rendering the scene from the perspective of the light source to create a depth map, which is then used to determine whether a pixel is in shadow or not when rendering the scene from the camera's perspective.

Key Concepts

  1. Depth Map: A texture that stores the depth information from the light's perspective.
  2. Light Space Transformation: Transforming world coordinates to the light's view space.
  3. Shadow Comparison: Comparing the depth of a fragment with the depth stored in the depth map to determine if it is in shadow.

Steps to Implement Shadow Mapping

  1. Render the Scene from the Light's Perspective:

    • Create a depth map by rendering the scene from the light's point of view.
    • Store the depth information in a texture.
  2. Render the Scene from the Camera's Perspective:

    • Use the depth map to determine if a fragment is in shadow.
    • Apply shadow calculations to the final color of the fragment.

Practical Example

Step 1: Create a Depth Map

First, we need to set up a framebuffer and a texture to store the depth information.

// Create a framebuffer object
GLuint depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);

// Create a texture to store the depth map
GLuint depthMap;
glGenTextures(1, &depthMap);
glBindTexture(GL_TEXTURE_2D, depthMap);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_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);
float borderColor[] = { 1.0, 1.0, 1.0, 1.0 };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

// Attach the texture as the depth buffer
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

Step 2: Render the Scene from the Light's Perspective

Render the scene to the depth map from the light's point of view.

// Set the viewport to the size of the depth map
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);

// Configure the light's view and projection matrices
glm::mat4 lightProjection, lightView;
glm::mat4 lightSpaceMatrix;
float near_plane = 1.0f, far_plane = 7.5f;
lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);
lightView = glm::lookAt(lightPos, glm::vec3(0.0f), glm::vec3(0.0, 1.0, 0.0));
lightSpaceMatrix = lightProjection * lightView;

// Render the scene
shadowShader.use();
shadowShader.setMat4("lightSpaceMatrix", lightSpaceMatrix);
renderScene(shadowShader);

glBindFramebuffer(GL_FRAMEBUFFER, 0);

Step 3: Render the Scene from the Camera's Perspective

Use the depth map to determine if a fragment is in shadow.

// Set the viewport back to the original size
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// Configure the camera's view and projection matrices
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
shader.use();
shader.setMat4("projection", projection);
shader.setMat4("view", view);

// Set the light space matrix and depth map
shader.setMat4("lightSpaceMatrix", lightSpaceMatrix);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, depthMap);
shader.setInt("shadowMap", 1);

// Render the scene
renderScene(shader);

Shadow Calculation in the Fragment Shader

In the fragment shader, compare the depth of the current fragment with the depth stored in the depth map.

#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec3 Normal;
in vec4 FragPosLightSpace;

uniform sampler2D shadowMap;
uniform vec3 lightPos;
uniform vec3 viewPos;

float ShadowCalculation(vec4 fragPosLightSpace)
{
    // Perform perspective divide
    vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
    // Transform to [0,1] range
    projCoords = projCoords * 0.5 + 0.5;
    // Get closest depth value from light's perspective (using [0,1] range fragPosLight as coords)
    float closestDepth = texture(shadowMap, projCoords.xy).r; 
    // Get depth of current fragment from light's perspective
    float currentDepth = projCoords.z;
    // Check whether current frag pos is in shadow
    float shadow = currentDepth > closestDepth  ? 1.0 : 0.0;

    return shadow;
}

void main()
{
    // Ambient
    vec3 ambient = 0.15 * vec3(1.0);

    // Diffuse
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * vec3(1.0);

    // Specular
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
    vec3 specular = spec * vec3(1.0);

    // Calculate shadow
    float shadow = ShadowCalculation(FragPosLightSpace);       
    vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular));

    FragColor = vec4(lighting, 1.0);
}

Practical Exercises

Exercise 1: Basic Shadow Mapping

  1. Set up a simple scene with a plane and a cube.
  2. Implement shadow mapping as described above.
  3. Adjust the light's position and observe the shadows.

Solution:

Follow the steps provided in the practical example to set up the depth map, render the scene from the light's perspective, and then render the scene from the camera's perspective using the depth map for shadow calculations.

Exercise 2: Soft Shadows

  1. Modify the shadow calculation to create soft shadows.
  2. Implement Percentage Closer Filtering (PCF) to smooth the shadow edges.

Solution:

In the fragment shader, sample multiple points around the current fragment's position in the depth map and average the results to create soft shadows.

float ShadowCalculation(vec4 fragPosLightSpace)
{
    vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
    projCoords = projCoords * 0.5 + 0.5;
    float currentDepth = projCoords.z;
    float shadow = 0.0;
    vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
    for(int x = -1; x <= 1; ++x)
    {
        for(int y = -1; y <= 1; ++y)
        {
            float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r; 
            shadow += currentDepth > pcfDepth  ? 1.0 : 0.0;        
        }    
    }
    shadow /= 9.0;

    return shadow;
}

Common Mistakes and Tips

  • Incorrect Depth Map Setup: Ensure the depth map is correctly set up and the framebuffer is properly configured.
  • Precision Issues: Use a high precision depth buffer to avoid artifacts.
  • Light Space Matrix: Make sure the light space matrix is correctly calculated and passed to the shaders.
  • Shadow Acne: Use a small bias to prevent shadow acne, which occurs due to precision issues.

Conclusion

Shadow mapping is a powerful technique for adding realistic shadows to a scene. By understanding the key concepts and following the steps to implement shadow mapping, you can create visually appealing shadows in your OpenGL applications. Practice with different scenes and lighting conditions to master shadow mapping and explore advanced techniques like soft shadows for even more realistic results.

© Copyright 2024. All rights reserved