Shadow mapping would not be an accepted technique if the problems you see in this renderer could not be fixed:
The controls at the top right allow you to independently toggle the four potential fixes described below. The preview panel on the bottom left shows the contents of the depth texture, which is how the light views the scene.
A distracting pattern appears on the surfaces. If you move around the scene, you will find that it only appears on the fragments that are closest to the light source, which is a clue to their existence. These dark spots occur when closestDepth
and fragmentDepth
should theoretically be equal, and therefore the fragments should not be in shadow. However, the two depths are computed in different ways and stored with different amounts of precision, so they are slightly off from each other. Checking for equality is a bad idea.
A possible fix is to nudge the fragment depth back a bit by a very small bias:
float fragmentDepth = texPosition.z - 0.0005;
Use the controls to apply this bias. The self-shadowing stops.
A slightly less magical way of removing the self-shadowing caused by precision issues is to store in the texture not the depths of the closest fragments, but the depths of the closest backfaces. When the meshes are rendered to the default framebuffer, the fragments closest to the light will be compared to the backfaces that are strictly behind them. These nearer fragments win the depth test without any arbitrary nudging.
Use the controls to disable the bias and cull the front faces. You can see the one-sided plane disappear from the depth texture preview. With this fix, one-sided surfaces will not cast shadows.
This renderer uses by default a depth texture with a resolution of 128x128, which is quite small. Since WebGL doesn't allow linear interpolation of depth textures, the shadows appear pixelated. This pixelation can be minimized by increasing the resolution of the texture. Use the controls to switch to a 2048x2048 texture.
Increasing the resolution of the depth texture means you'll have a lot more fragments to process and less efficient texture lookups. Generally you want to keep textures small.
Additionally, the shadows have a very hard edge, which only happens in the physical world when the light is a focused beam. Shadows are more generally surrounded by a soft penumbra, an area where some light is occluded but not all.
You can get by with a smaller texture and get softer edges by performing your own linear filtering of the depth texture. One filtering method is to perform the depth tests not just for a single fragment but for the 3x3 neighborhood of texels around the fragment and count how many are not occluded:
float percentage = 0.0;
for (int y = -1; y <= 1; y += 1) {
for (int x = -1; x <= 1; x += 1) {
vec2 offset = vec2(x, y) / resolution;
vec2 neighborPosition = texPosition.xy + offset;
float closestDepth = texture(depthTexture, neighborPosition).r;
percentage += closestDepth < fragmentDepth ? 0.0 : 1.0;
}
}
shadowFactor = percentage / 9.0;
Within the shadow's umbra, all neighbors will be occluded, the percentage will be 0, and the shadow will be dark. At the edges, some neighbors will not be occluded, the percentage will be greater, and the shadow will not be as dark. This technique is called percentage-closer filtering. The bigger the neighborhood, the softer and larger the penumbra will be.
Use the controls to enable percentage-closer filtering. The effect of the filtering is more pronounced on the low-resolution texture. In fact, that's the point. The pixelation of the smaller texture gets smoothed out.