Raymarched SDF Ambient Occlusion in GLSL
Estimating ambient occlusion on raymarched Signed Distance Field (SDF) surfaces provides realistic contact shadows and depth cues without the heavy performance cost of secondary visibility rays. Instead of tracing multiple distributed rays across the hemisphere, SDF ambient occlusion relies on stepping along the surface normal vector at predefined intervals and sampling the distance field. By comparing the actual distance returned by the SDF against the expected distance traveled along the normal, geometry occluding the surface is detected and accumulated into a single ambient shadowing coefficient directly inside a GLSL fragment shader.
The Underlying Principle
In a flat, unoccluded plane, moving outward from a surface point along its normal vector \(\vec{n}\) by a distance \(h\) yields an SDF distance value equal to \(h\):
\[\text{SDF}(p + \vec{n} \cdot h) = h\]
If neighboring geometry curves over the point, enters the space, or forms a crevice, the true distance to the nearest surface will be less than \(h\):
\[\text{SDF}(p + \vec{n} \cdot h) < h\]
The discrepancy between the expected distance (\(h\)) and the sampled distance indicates the presence of nearby occluding surfaces. The greater the difference \((h - \text{SDF}(p + \vec{n} \cdot h))\), the more occluded that point is.
The Step Accumulation Algorithm
The algorithm samples the SDF along the normal over several discrete steps. Rather than using constant weighting, each successive sample is scaled by a decaying weight factor (typically powers of two). This prioritizes tight crevices close to the surface—producing sharp contact shadows—while allowing broader features further out to contribute softer shading.
At each step \(i\) (from 1 to \(N\)):
- Compute the sample distance \(h = i \times \text{stepSize}\).
- Query the scene distance at the offset position: \(d = \text{map}(p + \vec{n} \cdot h)\).
- Calculate the occlusion difference: \((h - d)\).
- Weight the difference by a falloff factor \(w^i\) and add it to the total occlusion accumulator.
- Invert the total occlusion at the end so that \(1.0\) represents fully open space and \(0.0\) represents fully occluded space.
GLSL Implementation
Below is a standard GLSL implementation of SDF ambient occlusion based on step accumulation:
// Function prototype for the global scene SDF
float map(vec3 p);
/**
* Calculates ambient occlusion at a given surface point.
* @param p Surface position.
* @param n Normalized surface normal.
* @param stepSize Spacing between consecutive normal offsets.
* @param k Occlusion intensity scale factor.
* @return Ambient occlusion factor between 0.0 (occluded) and 1.0 (exposed).
*/
float calculateAO(vec3 p, vec3 n, float stepSize, float k) {
float occlusion = 0.0;
float weight = 1.0;
const int STEPS = 5;
for (int i = 1; i <= STEPS; ++i) {
float h = float(i) * stepSize;
float d = map(p + n * h);
// Accumulate the positive difference between expected and sampled distance
occlusion += (h - d) * weight;
// Decay the weight for subsequent samples
weight *= 0.5;
}
// Map the result to an ambient visibility scalar
return clamp(1.0 - k * occlusion, 0.0, 1.0);
}Parameter Tuning and Optimization
- Step Count (
STEPS): Typically between 4 and 6 iterations. Because SDF evaluations can be computationally intensive, keeping the loop iteration count low preserves interactive frame rates while capturing sufficient occlusion detail. - Step Size (
stepSize): Defines the spatial radius of the occlusion effect. Small values (e.g.,0.01to0.05) isolate micro-details and crevice shadows, whereas larger values (e.g.,0.1to0.3) create broader, room-scale ambient shading. - Intensity Factor (
k): A scaling multiplier used to emphasize or soften the darkening effect depending on lighting conditions and aesthetic requirements. - Weight Falloff: Scaling the weight by
0.5per iteration guarantees that the infinite sum of weights converges, preventing distant features from artificially overpowering immediate geometric contact.