SDF-based rendering effects go beyond the sphere-traced primary surfaces covered in the hub article. Distance fields also drive two lighting effects that are hard to do well with traditional shadow maps: soft shadows with smooth penumbra, and ambient occlusion without a screen-space pass. This article explains the math behind both, walks through the GLSL, and compares the results against the standard shadow mapping pipeline.
For the fundamentals of sphere tracing and the SDF gradient, start with the Signed Distance Fields overview . For the engine-wide context, see SDFs in game development .
Distance Field Soft Shadows
Soft shadows from area lights are expensive with shadow mapping because you need to sample the light source at multiple positions and average the results. Shadow maps also alias badly at shallow angles and need cascades for large scenes.
Distance field soft shadows approximate area light penumbra by marching a secondary ray from each surface point toward the light source and recording how closely the ray passes by occluding geometry. The minimum distance-to-occluder along the ray determines the shadow softness:
float sdfSoftShadow(vec3 origin, vec3 lightDir, float maxDist, float softness,
SDF scene) {
float res = 1.0;
float t = 0.02; // Start slightly off the surface to avoid self-occlusion
for (int i = 0; i < MAX_SHADOW_STEPS; i++) {
float d = evaluateSDF(origin + lightDir * t, scene);
// The closer the ray passes to an occluder, the darker the shadow
res = min(res, softness * d / t);
t += clamp(d, 0.01, 0.5);
if (res < 0.001 || t > maxDist) break;
}
return clamp(res, 0.0, 1.0);
}
The key insight is in the line softness * d / t. When the ray passes far from any surface, d remains large and res stays near 1.0 (fully lit). When the ray grazes close to an occluder, d becomes small, d / t shrinks, and res drops toward 0.0 near the contact. The softness parameter controls how wide the penumbra is: larger values spread the shadow edge over a wider region.
A worked example makes the ratio concrete. At step distance along the light ray, the field reads , meaning the ray passes 0.1 units from a surface. With softness = 8.0, the contribution is , so that step dims the result to 80% brightness. If the ray passes twice as close, , the contribution is , and the point falls to 40% brightness. The penumbra is the region where this ratio interpolates between the fully lit and fully shadowed values, and the softness parameter stretches or compresses that region.
This produces smooth, physically plausible shadows from a single ray per surface point, with no shadow map resolution limits or cascade seams. The cost is the extra ray-march steps inside the shadow loop, but those steps evaluate the same SDF that the primary ray already used, so no new data structures are needed. When the scene is already an SDF, the shadow pass is just a second loop over the same function.
SDF Ambient Occlusion
Ambient occlusion measures how occluded a surface point is by nearby geometry. In a mesh pipeline, this requires screen-space sampling (SSAO) or pre-baked occlusion maps. With an SDF, ambient occlusion can be computed directly during the shading pass by sampling the distance field in a hemisphere around the surface point:
float sdfAmbientOcclusion(vec3 position, vec3 normal, float radius,
SDF scene) {
float occlusion = 0.0;
float weightSum = 1e-6;
for (int i = 0; i < AO_SAMPLES; i++) {
vec3 dir = sampleHemisphere(normal, i, AO_SAMPLES);
float t = 0.01;
float sampleOcclusion = 0.0;
// March a short ray into the hemisphere
for (int j = 0; j < AO_STEPS; j++) {
float d = evaluateSDF(position + dir * t, scene);
// If we are inside nearby geometry, accumulate occlusion
sampleOcclusion += max(0.0, -d) / (1.0 + t);
t += max(abs(d), 0.01);
if (t > radius) break;
}
float weight = 1.0 / (1.0 + t);
occlusion += sampleOcclusion * weight;
weightSum += weight;
}
return 1.0 - occlusion / weightSum;
}
Each sample direction marches a short ray outward from the surface and accumulates occlusion whenever the distance goes negative (inside nearby geometry). The 1.0 + t denominator makes nearby occluders contribute more than distant ones. A sample ray that hits geometry right next to the surface point accumulates a large occlusion term; a ray that exits into open space accumulates nothing and contributes a large weight to the final average, keeping the result bright.
A worked example: a sample ray at step distance reads , meaning the ray is inside nearby geometry. The term contributed is and the weight for this direction is , so this direction drags the average toward occluded. On open ground, every sample direction reads positive distances, every occlusion term is zero, and the result stays 1.0.
This produces contact shadows in crevices and under overhangs without needing a separate screen-space pass or pre-computation. It shares the renderer’s existing SDF, so dynamic geometry is handled automatically: move a box and the AO response moves with it, with no re-bake and no screen-space artifacts.
Comparison to Shadow Maps
| Property | Shadow Maps | SDF Shadows |
|---|---|---|
| Setup cost | One render pass per light | None (uses existing SDF) |
| Per-pixel cost | One texture sample (hard) / several (soft) | Multiple SDF evaluations |
| Aliasing | Resolution-limited, needs cascades | Smooth by construction |
| Dynamic geometry | Free (re-render shadow map) | Free (SDF updates with scene) |
| Large worlds | Needs cascaded shadow maps | Single SDF covers everything |
| Semi-transparent shadows | Hard | Natural (distance-based) |
SDF shadows are not a universal replacement for shadow maps. Shadow maps are faster for hard shadows from distant directional lights in static scenes. SDF shadows excel when you need soft penumbra, when the scene is already represented as an SDF, or when dynamic geometry changes the occlusion every frame.
The limitations are worth stating plainly. The shadow and AO loops add ray-march cost on top of the primary ray, so a scene that is fully mesh-rasterized with screen-space shadows can be cheaper overall. And the quality of both effects depends on the field’s distance guarantee: a sampled or smoothed field that overestimates distance can let shadow rays step through thin occluders, so effects like these are most reliable on exact analytical fields or carefully baked grids with conservative step clamps.
Summary
Two lighting effects fall out of the same SDF contract:
- Soft shadows march a ray toward the light and record the closest approach to geometry,
softness * d / t, which estimates penumbra width from a single ray. - Ambient occlusion marches short rays into a hemisphere and accumulates
-d / (1 + t)wherever the field goes negative, producing contact shadows without a screen-space pass.
Both effects reuse the scene SDF, so dynamic geometry is free and no shadow map resolution limits apply. When the rest of the scene is already field-based, they are the natural lighting path; the destructible terrain deep-dive shows the same field driving the geometry that these passes light.