How Three.js SSAOPass Calculates Contact Shadows
Screen-space ambient occlusion (SSAO) is a post-processing technique
used in 3D computer graphics to calculate real-time contact shadows and
soft ambient lighting. In Three.js, the SSAOPass simulates
this effect by analyzing the depth and normal buffers of a rendered
scene in screen space, rather than performing expensive 3D ray-tracing.
This article explains the step-by-step rendering pipeline that
SSAOPass uses to determine pixel occlusion and add
depth-enhancing shadows where surfaces meet.
1. Generating Depth and Normal Buffers
Before calculating occlusion, SSAOPass requires
information about the scene’s geometry from the camera’s perspective. It
performs a pre-pass to render the scene’s depth and camera-space normals
into separate textures (often referred to as a G-buffer).
- Depth Buffer: Stores the distance of each pixel from the camera.
- Normal Buffer: Stores the direction that each surface point is facing.
These two textures provide the mathematical foundation for calculating spatial relationships in screen space.
2. Creating the Sample Kernel and Noise Texture
To determine if a point on a surface is occluded (blocked by nearby geometry), the shader needs to sample the surrounding area.
- Sample Kernel:
SSAOPassgenerates a hemisphere of random 3D sample points (kernel) oriented along the surface normal. By using a hemisphere instead of a sphere, samples only project outward from the surface, preventing self-occlusion. - Noise Texture: To prevent banding artifacts and simulate smooth shadows without using thousands of samples, a small, repeating random rotation texture (usually 4x4 pixels) is tiled across the screen. This rotates the sample kernel randomly at each pixel, turning harsh banding into high-frequency noise that can be easily blurred later.
3. Projecting and Comparing Samples
For every pixel on the screen, the SSAO shader performs the following calculations:
- Reconstruct 3D Position: Using the pixel’s screen coordinates and its corresponding value from the depth buffer, the shader reconstructs the 3D coordinate of the point in view-space.
- Orient the Kernel: The shader retrieves the surface normal from the normal texture and uses the noise texture to rotate the sample hemisphere so it aligns with the surface.
- Sample the Surrounding Space: The shader loops through the random sample points in the hemisphere. For each sample point, it projects the 3D position back onto the 2D screen coordinates.
- Depth Comparison: The shader compares the depth of
the sample point with the actual depth stored in the depth buffer at
those coordinates:
- If the sample point is deeper than the geometry in the depth buffer, it is inside an object (occluded).
- If the sample point is closer to the camera than the geometry, it is in open space (unoccluded).
4. Applying a Range Check
If a sample is occluded by geometry that is extremely far behind or
far in front of the target pixel, it could falsely contribute to the
occlusion value. To prevent this, SSAOPass implements a
range check. If the depth difference between the sample point and the
geometry exceeds a set threshold (the occlusion radius), the occlusion
contribution is discarded or heavily scaled down.
5. Blur and Composition
The raw occlusion factor is calculated as the ratio of occluded samples to total samples. Because random sampling is used to keep performance high, this raw output looks highly pixelated and noisy.
To resolve this, the SSAOPass applies a bilateral blur
filter. Unlike a standard blur, a bilateral blur preserves sharp edges
by checking depth and normal differences, ensuring that shadows do not
bleed across depth discontinuities (like the edge of a foreground object
against a distant background).
Finally, the blurred ambient occlusion map is multiplied by the original scene color, darkening crevices, corners, and contact points to produce realistic contact shadows.