5
\$\begingroup\$

I am currently implementing basic shadow mapping in my C++ Custom Engine using GLSL 4.10. It is currently working with basic PCF anti-aliasing and very minimal reduction for unwanted artifacts. Here is a screenshot for reference:

enter image description here

Recently, I have started modifying my shadow map pipeline to reduce shadow acne. After reading through various articles, many suggest either using a bias value, which is subtracted from the depth value being compared to the shadow map

// Shadows
float bias = 0.0005;
vec4 shadowCoordinate = u_worldToShadowMapSpaceTransform * vec4( worldPosition.xyz, 1.0 );
shadowCoordinate.z -= bias;
float shadowValue = textureProj( u_shadowMap, shadowCoordinate );

and or to cull front faces when rendering to the shadow map.

glCullFace( GL_FRONT )
// Render to ShadowMap FBO
glCullFace( GL_BACK )
// Render Scene  

Culling front faces reduces most of the shadow acne but creates another problem that none of the articles have mentioned. The screenshot below shows the issue. If you look at the white box in front of horned creature, you will notice the creatures's shadow is cast upon the other side of the box. This is because the front faces of the box are culled which leads the back faces to compare their depth values against geometry to the north of the box.

enter image description here

Naturally, this also leads to these shadows also being cast below the floor as well as above. Here is a screenshot of the bottom of the floor. It is difficult to see but you can see the shadows being cast.

enter image description here

I believe many 3D engines utilize variations of these artifact reduction techniques. So, how do they avoid the shadow casting issue described above?

\$\endgroup\$
  • \$\begingroup\$ Why does that face have any direct illumination at all? Shouldn't it all be shadowed? \$\endgroup\$ – GuyRT Dec 5 '14 at 13:47
  • \$\begingroup\$ The illumination you see is global ambient light. I tweaked the shadows to render without ambient light for demonstrating the issue. \$\endgroup\$ – Paul Renton Dec 5 '14 at 15:47
5
\$\begingroup\$

try using a multiplicative "bias" instead of additive:

shadowCoordinate.z *= 0.98;

If you're doing the sampling yourself rather than using the shadow comparator interpolating the shadow map helps reduce acne a lot.

There shouldn't be any shadow visible behind the object as the light should not affect it with the light being completely occluded by the object itself.

Global ambient light should not be affected by shadows, shadows should only cuts off the light that is casting the shadow not the other lights.

vec3 output_color = ambient * material_diffuse;
foreach(light){
  float shadow_masking = light.CalculateShadow();
  output_color += light.Calculate(material_diffuse, material_specular) * shadow_masking;
}

Here's 3 colored point lights using cubemap shadow casting and 1 directional. Shadow maps are rendered front-face (the usual back-face culling). Single-pass forward-shading. And a multiplicative "bias" of 1.02 applied to the shadow depth. Shadow map depth is interpolated. The columns grooves are actually modeled (not normal maps) to stress test self-shadowing and shadow acne issues. The 3 small spheres are the point light sources. Rendered on an nVidia GTX 560M. The minimum requirement is GLSL 1.1 (OpenGL 2.0). multiple light sources 3 points, 1 directional

Edit: Adding pseudo-code of the internals of sample2DShadow vs sampler2D

sample2DShadow:

float A = Nearest(tex, floor(coord.xy + vec2(0, 0))).r < coord.z ? 1.0 : 0.0;
float B = Nearest(tex, floor(coord.xy + vec2(1, 0))).r < coord.z ? 1.0 : 0.0;
float C = Nearest(tex, floor(coord.xy + vec2(0, 1))).r < coord.z ? 1.0 : 0.0;
float D = Nearest(tex, floor(coord.xy + vec2(1, 1))).r < coord.z ? 1.0 : 0.0;
return Lerp( Lerp(A, B, fract(coord.x)), Lerp(C, D, fract(coord.x)), fract(coord.y));

sampler2D on depth texture:

vec4 A = Nearest(tex, floor(coord.xy + vec2(0, 0))).rrrr;
vec4 B = Nearest(tex, floor(coord.xy + vec2(1, 0))).rrrr;
vec4 C = Nearest(tex, floor(coord.xy + vec2(0, 1))).rrrr;
vec4 D = Nearest(tex, floor(coord.xy + vec2(1, 1))).rrrr;
return Lerp( Lerp(A, B, fract(coord.x)), Lerp(C, D, fract(coord.x)), fract(coord.y));

Both types of samplers do 1 bilinear interpolation, but the sampler2DShadow does it on the boolean result.

enter image description here

\$\endgroup\$
  • \$\begingroup\$ Would you mind elaborating what you mean by interpolating the shadow map depth? As mentioned in a previous comment, I purposefully ignore ambient to show the shadow casting issue. Normally, I add ambient light * diffuse after applying shadows. Unless I am misunderstanding, the shadow on the opposite side of the box is a result of front face culling applied before rendering the shadow depth map. The depth values from the pixel in shadow map space compared to shadow map depth are nearly equal, which would not occur with back face culling. Hope that makes sense. \$\endgroup\$ – Paul Renton Dec 10 '14 at 4:05
  • 1
    \$\begingroup\$ GPUs have hardware shadow map comparators and use the interpolation unit to do PCF which forces the shadow map to be sampled using NEAREST = lots of shadow acne. But instead of using a shadow sampler you can use a regular sampler and sample the depth texture yourself with interpolation enabled which give smoother depth values, which you can compare in the shader code. The issue with this is that you don't get PCF in the sampling unit and must take multiple samples to do PCF in the shader if you need it but it nearly eliminates shadow acne thanks to the interpolated depth. \$\endgroup\$ – Stephane Hockenhull Dec 10 '14 at 5:01
  • \$\begingroup\$ Okay, thanks for elaborating. I now realize what you meant by interpolation. I was under the impression the textureProj built in function performed PCF when the shadow map's texture MIN/MAG filter is set to LINEAR ( Returns a value [ 0 - 1 ] based on samples of four neighboring depth texels ). I also have a shader where I did the math without textureProj and a shadowSampler, I will try sampling it with LINEAR interpolation and doing PCF manually to compare the results. Thanks! \$\endgroup\$ – Paul Renton Dec 10 '14 at 5:12
  • 2
    \$\begingroup\$ The shadow samplers do a linear interpolation on the depth comparison boolean RESULTS of 4 pixels, but the depth values used for those 4 comparisons are not interpolated. There's only 1 interpolation unit on the texture sampler, interpolating the 4 depth values then interpolating the boolean results would require 5 interpolation units in the texture sampler circuits. Doing PCF in the shader code might be a lot slower on some GPUs if they're code-bound rather than memory bandwidth bound. \$\endgroup\$ – Stephane Hockenhull Dec 10 '14 at 16:03
  • 2
    \$\begingroup\$ I did a quick google search and there aren't many tutorial that explain why one would want to NOT use a shadow-sampler. nVidia covers it somewhat but its explained in a way that people need to already know what they're talking about, very academic. So I added a quick graph. \$\endgroup\$ – Stephane Hockenhull Dec 10 '14 at 17:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.