How Does GLSL Shadow Sampler Depth Comparison Work?
In OpenGL Shading Language (GLSL), shadow samplers automate the
depth-testing stage of shadow mapping directly in hardware during
texture lookup. Instead of returning raw depth values for manual
software comparison, shadow samplers like sampler2DShadow
compare a reference depth coordinate against the depth recorded in the
shadow map. This article explores the internal mechanics of shadow
samplers, how hardware Percentage Closer Filtering (PCF) functions, and
how to configure both host OpenGL and GLSL shaders to use this
feature.
Understanding the Standard Depth Texture vs. Shadow Samplers
In a naive shadow mapping pipeline, depth information from the
light’s point of view is written to a standard depth texture and
accessed in shaders using a sampler2D. To determine
visibility, the fragment shader executes two distinct operations:
fetching the stored depth value via texture() and then
performing an explicit conditional check (such as
currentDepth > sampledDepth) to test if a fragment is
occluded.
Shadow samplers replace this manual process. By declaring the texture
uniform as a shadow type—such as sampler2DShadow or
samplerCubeShadow—the GPU combines the texture fetch and
the comparison operation into a single built-in hardware
instruction.
The Role of the Reference Coordinate
When sampling from a standard texture, GLSL requires coordinates corresponding to the texture’s dimensions (e.g., \((u, v)\) for 2D textures). When sampling with a shadow sampler, an additional coordinate component is required:
- For
sampler2DShadow, coordinates are passed as avec3(u, v, ref_z). - For projected coordinates,
textureProj()accepts avec4(u, v, z, w), which performs perspective division automatically to derive \(\frac{ref\_z}{w}\).
The extra component, \(ref\_z\), represents the reference depth: the distance from the light source to the currently processed surface fragment. The GPU compares this reference value directly against the texel values stored in the shadow map.
OpenGL Host State Configuration
Shadow samplers do not function automatically without setting the
appropriate texture comparison parameters on the OpenGL host side. Two
specific texture parameters must be configured using
glTexParameteri:
GL_TEXTURE_COMPARE_MODE: Must be set toGL_COMPARE_REF_TO_TEXTURE. This signals the hardware to activate its internal comparison logic instead of returning raw depth data.GL_TEXTURE_COMPARE_FUNC: Defines the condition under which a fragment is considered "in light" (evaluates to \(1.0\)). Typically, this is set toGL_LEQUAL(Less than or Equal), meaning if \(ref\_z \le depth_{map}\), the fragment is visible to the light.
If GL_TEXTURE_COMPARE_MODE is left as
GL_NONE (the default state), sampling a shadow sampler
yields undefined behavior or compilation warnings depending on the GLSL
version.
Hardware PCF and Bilinear Filtering
The most significant advantage of using shadow samplers over manual shader comparisons is hardware-level Percentage Closer Filtering (PCF).
When using standard sampler2D with bilinear filtering
(GL_LINEAR), the GPU linearly interpolates between raw
depth values. Interpolating depth values produces incorrect intermediate
distances, causing severe visual artifacts along silhouette edges.
Consequently, standard depth maps must rely on nearest-neighbor
filtering (GL_NEAREST).
When GL_COMPARE_REF_TO_TEXTURE and
GL_LINEAR are active simultaneously, the hardware changes
how interpolation executes:
- The GPU reads the four nearest depth texels surrounding the sampling coordinate.
- It compares the reference depth \(ref\_z\) against each of the four texels individually, yielding four binary results (\(0.0\) or \(1.0\)).
- It performs standard bilinear interpolation across the four comparison results.
The resulting output from
texture(shadowMap, vec3(uv, ref_z)) is a continuous
floating-point value in the range \([0.0,
1.0]\). This value represents the percentage of the sampled area
illuminated by the light, producing smoothly anti-aliased shadow edges
with zero shader overhead.
GLSL Implementation Example
Integrating a 2D shadow sampler into a fragment shader requires minimal code:
#version 330 core
in vec4 FragPosLightSpace;
uniform sampler2DShadow shadowMap;
out vec4 FragColor;
float CalculateShadow(vec4 fragPosLightSpace)
{
// Perform perspective divide to transform to Normalized Device Coordinates [-1, 1]
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
// Transform coordinates to the [0, 1] range for texture sampling
projCoords = projCoords * 0.5 + 0.5;
// Prevent shadowing outside the light frustum's far plane
if (projCoords.z > 1.0)
{
return 1.0;
}
// Built-in shadow comparison:
// projCoords.xy = UV coordinates
// projCoords.z = Reference depth to compare against the depth map
float shadowFactor = texture(shadowMap, projCoords);
return shadowFactor;
}
void main()
{
float visibility = CalculateShadow(FragPosLightSpace);
vec3 lightIntensity = vec3(1.0) * visibility;
FragColor = vec4(lightIntensity, 1.0);
}Shadow samplers provide an efficient, hardware-accelerated mechanism for depth testing and edge softening in modern graphics pipelines, eliminating boilerplate comparison logic and manual multi-tap filtering algorithms.