-
Notifications
You must be signed in to change notification settings - Fork 0
9. Shadow Mapping
- Literally create a "map" of the shadows made by a light.
- Use this map to determine where not to apply light.
- The map is held as a 2D Texture (sampler2D in the shader)
- Map is created using a "Framebuffer"
- Framebuffer then writes to texture
- Therefore: At least two rendering passes needed!
- One for creating shadow map, second for drawing scene.
-
For first pass: Render the scene from perspective of a light source

-
Shaders don't just create colour output
-
Recall from Rendering Pipeline: Per-Sample Operations
-
Depth Tests using Depth Buffer values
-
Depth Buffer is another buffer along with Colour Buffer that holds a value between 0 and 1 how deep in to frustum a fragment is
-
0 is on the Near Plane (close to the camera)
-
1 is on the Far Plane (far from the camera)
-
How to extract depth buffer data?
-
Framebuffer Object!
-
Normally, Framebuffer bound is '0'
-
This is the default buffer (the one drawn to the screen when buffer swap is called)
-
We can find a separate Framebuffer and draw to that...
-
Then use the data as we wish
-
glGenFramebuffers(1, &FBO);
-
Create a texture the usual way, but...
-
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
-
GL_DEPTH_COMPONENT: Single float value, unlike RGB which had three.
-
Data is NULL, so we have created an empty texture with dimensions width x height
-
Set Framebuffer to write to texture with:
- glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, textureID, 0);
-
GL_DEPTH_ATTACHMENT: Tells Framebuffer to only write Depth Buffer data
-
glDrawBuffer(GL_NONE);
-
glReadBuffer(GL_NONE);
-
These override color data draw/read operations. We don't want to output colour with our shadow map!
-
Shader itself is simple:
- Apply Projection and View matrices as if light source is the camera
- Apply model matrix of each object
- Fragment Shader isn't even needed: Depth buffer is written automatically.
- Directional light shadow map works differently to Point/Spot Light shadow maps!
- View Matrix position should consist of reverse of Directional Light's Direction (Simulating light in that direction_
- View Matrix direction is simply the direction of the light
- Project Matrix is different: Frustum of Perspective Projection fans out! Directional Light rays are all parallel, they must not fan out.
- Solution: Orthographic Projection Matrix
- glm::ortho(-20.0f, 20.0f, -20.0f, 20.0f, 0.01f, 100.0f);
-
After rendering the scene with the Shadow Map shader, the texture bound to it is occupied with Shadow Map Data.
-
Make sure to unbind the Framebuffer used for the shadow map!
-
Now we need to bind the texture to our main shader and use it.
-
Need access to the View Matrix used in the Shadow Map Shader (the one using the light's perspective)
-
Use this to get the current fragment position in relation to the light source
-
Need to create a way to access points on the Shadow Map with the light source perspective's fragment co-ordinates...
-
Therefore, need to convert light source perspective fragment's co-ordinates to "Normalized Device Co-ordinates" (values between -1 and 1, like when we started)
-
Need to perform a "perspective divide".
-
Similar to how co-ordinates are created when moving to the Fragment Shader anyway...
-
However this is only applied to gl_Position.
-
We need to do it manually for the position relative to the light source.
-
Easy calculation: Divide vector by its 'w' component. This is why we use a vec4!
-
vec3 projCoords = LightSpacePos.xyz / LightSpacePos.w;
-
Then we need to scale the projCoors to 0,1, to math the 0, 1 values of a texture (recall textures use u and v axis between 0 and 1)
-
projCoords = (projCoords * 0.5) + 0.5;
-
Now use texture function to get closet depth measure during Shadow Map pass
-
float closest = texture(shadowMap, projCoors.xy).r;
-
Grab z value from projCoors.
-
z-axis on normalised co-ordinates is between 0 and 1, just like depth, and so can be treated as such
-
Compare current and closest depth...
-
If current larger than closest: It is further away than the first point the light hits at that fragment! So it must be in shadow
-
Otherwise: It is the same point, so it must be getting lit by the light
-
To apply shadow, simply add or remove diffuse and specular (retain ambient, remember: Ambient Light is ALWAYS present)
-
colour = fragColour * (ambient + (1.0 - shadow) * (diffuse + specular));
- Shadow Acne occurs due to resolution issues
- Imagine lighting a surface at an angle...
- When rendering from a less slanted angle, two pixels may converge to one texel on the shadow map
- one point could be mistaken as being behind a point next to it.

- Solution: Add a slight bias.
- Effectively moving everything slightly towards the camera to fake a closer depth.
- Try to keep the bias small or...
- "Peter Panning" occurs.
- Bias offset causes areas close to shadow source to disappear because depth values are close
- What about areas outside of the Projection Frustum used to create the shadow map?
- Values will be outside 0,1 range and therefore always create shadows!
- Solution:
- Set texture type to use border with values all consisting of 0 (always lowest depth value so always lit)
- For values beyond far plane and therefore greater than 1: Initialize to 0
-
Edges of shadows are limited to resolution of texture shadow map is written to.
-
THis causes unsightly pixelated edges
-
Solution: Sample surrounding texels and calculate average. Apply one partial shadows for shadowed areas
-
Also known as: Percentage-Closer Filtering (PCF)
-
Can get dangerously intensive if not used correctly
-
Get depth values of surrounding texels, such as the 8 immediate surrounding
-
Determine if in shadow
-
If yes: Increment shadow value
-
When done, divide shadow value by number of samples taken
-
Apply percentage of shadow using this value
-
E.g Shadow value calculated as 3 and 9 samples are taken. 3/9 = 0.333... So apply 33% shadow to that pixel
-
more samples: Better fade effect, but...
-
Keep in mind, this set of samples will be taken for EVERY fragment, so instead of being one calculation, it becomes 9x calculations just for using immediate surrounding texels!.
- Shadows created by texture maps of depth data.
- Depth data created by rendering scene from point of view of light source
- Do two passes: One to create shadow maps and one to render scene
- Compare depth of fragment from light's perspective to value on shadow map texture
- Add bias to remove shadow acne
- Set values from beyond sampling region to '0' (no shadow)
- Use PCF algorithms to fade shadow edges