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
- Depth Map: A texture that stores the depth information from the light's perspective.
- Light Space Transformation: Transforming world coordinates to the light's view space.
- 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
-
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.
-
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
- Set up a simple scene with a plane and a cube.
- Implement shadow mapping as described above.
- 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
- Modify the shadow calculation to create soft shadows.
- 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.
OpenGL Programming Course
Module 1: Introduction to OpenGL
- What is OpenGL?
- Setting Up Your Development Environment
- Creating Your First OpenGL Program
- Understanding the OpenGL Pipeline
Module 2: Basic Rendering
- Drawing Basic Shapes
- Understanding Coordinates and Transformations
- Coloring and Shading
- Using Buffers
Module 3: Intermediate Rendering Techniques
- Textures and Texture Mapping
- Lighting and Materials
- Blending and Transparency
- Depth Testing and Stencil Testing
Module 4: Advanced Rendering Techniques
Module 5: Performance Optimization
- Optimizing OpenGL Code
- Using Vertex Array Objects (VAOs)
- Efficient Memory Management
- Profiling and Debugging