Fixing Shadow Acne with Slope-Scaled Depth Bias in GLSL

Shadow acne is a common visual artifact in shadow mapping caused by depth-buffer quantization and surface orientation, leading to false self-shadowing patterns across surfaces. This article explains the primary causes of shadow acne, why simple constant biasing often fails, and how to implement slope-scaled depth bias directly within a GLSL fragment shader to maintain clean shadows without introducing detachment artifacts.

What Causes Shadow Acne?

Shadow acne appears as alternating dark and light bands across surfaces that should be smoothly illuminated. It is primarily caused by two issues:

  1. Resolution and Quantization: A shadow map discretizes continuous 3D space into a grid of texels, where each texel stores a single scalar depth value. When scene fragments are projected into shadow map space, multiple fragments usually map to the same shadow map texel.
  2. Surface Slope: When a surface is angled relative to the light direction, the true depth varies continuously across the area represented by a single shadow map texel. Because the shadow map only records depth at the center of the texel, fragments closer to the light than the texel center pass the depth test, while fragments further away fail it. The fragments that fail the test consider themselves occluded by their own surface, generating false self-shadowing (acne).

Constant Bias vs. Slope-Scaled Bias

The traditional fix is adding a small depth offset (bias) during the shadow depth comparison:

float shadow = currentDepth - bias > sampledDepth ? 1.0 : 0.0;

A constant bias applies the same offset regardless of surface orientation. If the constant bias is too small, acne persists on steep surfaces. If it is increased enough to fix steep angles, it introduces "Peter Panning"—an artifact where shadows detach from the caster because the bias pushes the shadow receiver too far back.

A slope-scaled bias solves this trade-off by scaling the offset based on the slope of the surface relative to the incoming light ray. Surfaces perpendicular to the light require almost no bias, while surfaces nearly parallel to the light receive a larger bias.

Implementing Slope-Scaled Depth Bias in GLSL

Slope can be evaluated geometrically using the dot product between the normalized surface normal (\(\mathbf{N}\)) and the normalized light direction (\(\mathbf{L}\)).

When \(\mathbf{N} \cdot \mathbf{L} \approx 1\), the surface directly faces the light, requiring minimal bias. As \(\mathbf{N} \cdot \mathbf{L}\) approaches 0, the angle between the surface and light increases, requiring a larger bias.

1. Simple Dot-Product Bias

A robust and lightweight approximation scales the bias linearly with the inclination:

// In your lighting/shadow calculation pass:
vec3 normal = normalize(v_Normal);
vec3 lightDir = normalize(u_LightPos - v_FragPos);

// Calculate bias based on surface slope
float minBias = 0.0005;
float maxBias = 0.005;
float bias = max(maxBias * (1.0 - dot(normal, lightDir)), minBias);

// Shadow comparison
float currentDepth = projCoords.z;
float shadow = (currentDepth - bias > sampledDepth) ? 1.0 : 0.0;

2. Trigonometric Slope Scaling

For a physically accurate representation of the slope, use the tangent of the angle between the normal and the light vector (\(\tan(\theta) = \frac{\sin(\theta)}{\cos(\theta)}\)):

vec3 normal = normalize(v_Normal);
vec3 lightDir = normalize(u_LightPos - v_FragPos);

float cosTheta = clamp(dot(normal, lightDir), 0.0, 1.0);
// tan(theta) = sqrt(1 - cos^2(theta)) / cos(theta)
float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
float tanTheta = sinTheta / max(cosTheta, 0.0001);

// Scale a base factor by the tangent of the surface angle
float baseFactor = 0.0005;
float bias = clamp(baseFactor * tanTheta, 0.0005, 0.01);

float currentDepth = projCoords.z;
float shadow = (currentDepth - bias > sampledDepth) ? 1.0 : 0.0;

Clamping the maximum bias prevents extreme offsets at grazing angles (\(\mathbf{N} \cdot \mathbf{L} \to 0\)), which controls Peter Panning while completely eliminating shadow acne on curved and slanted geometry.