How SSAO Operates in a DirectX Pipeline

Screen-Space Ambient Occlusion (SSAO) is a popular real-time rendering technique designed to approximate the soft contact shadows that occur when ambient light is blocked by nearby geometry. In a DirectX post-processing pipeline, SSAO operates entirely on 2D screen-space buffers—primarily the depth buffer and surface normal textures—generated during the initial geometry pass. By evaluating depth disparities around reconstructed 3D points, a DirectX pixel or compute shader calculates an occlusion factor per pixel, filters the resulting noise, and multiplies the value into the final rendered image to dramatically enhance scene depth.

1. Resource Inputs and G-Buffer Setup

Before the SSAO pass executes, the DirectX rendering pipeline must render the scene's geometry into intermediate render targets (G-buffers) or depth-stencil views. The primary inputs bound to the SSAO shader as Shader Resource Views (SRVs) are:

  • Depth Texture: Stores the non-linear screen-space depth values (\(z\)-values) of the visible surfaces.
  • Normal Buffer: Contains view-space surface normal vectors. If normals are not rendered to a texture, they can alternatively be reconstructed from the depth buffer using partial derivatives (ddx and ddy) inside the shader.
  • Noise Texture or Rotation Matrix: A small (typically \(4\times4\)) tiling texture containing randomized tangent-space rotation vectors used to perturb sample directions and prevent visible banding patterns.

2. View-Space Position Reconstruction

Because the depth buffer stores non-linear depth values in normalized device coordinates (NDC), the SSAO shader must first reconstruct the surface's 3D position in view space. The shader samples the depth texture at the current screen pixel coordinate \((u, v)\), converts the coordinate into NDC space \([-1, 1]\), and multiplies it by the inverse projection matrix:

\[\text{ViewPosition} = \text{Unproject}(\text{UV}, \text{Depth})\]

This view-space coordinate forms the origin for the local occlusion queries.

3. Sampling the Kernel

To determine how occluded a surface point is, the shader samples a hemisphere oriented along the surface's normal vector.

  1. Hemisphere Generation: A set of pre-calculated sample points (usually 16 to 64 samples) distributed within a unit hemisphere is uploaded to the GPU via a Constant Buffer (cbuffer).
  2. Kernel Alignment: The shader samples the normal buffer to retrieve the surface normal and samples the noise texture to retrieve a random rotation vector. Using the Gram-Schmidt process, the shader builds an orthonormal basis (tangent, bitangent, normal) to align and randomly rotate the sample hemisphere to the surface.
  3. Position Evaluation: Each sample offset in the kernel is added to the reconstructed view-space position, scaled by an artist-defined occlusion radius.

4. Depth Comparison and Occlusion Accumulation

For every sample point generated within the hemisphere:

  1. Projection: The 3D sample point is projected back onto the screen using the camera's projection matrix, yielding a 2D texture coordinate \((u, v)\) and a sample view depth.
  2. Depth Lookup: The shader samples the depth buffer at these projected coordinates to find the actual surface depth at that screen location.
  3. Occlusion Test: If the actual geometry depth is closer to the camera than the generated sample point's depth, that sample is occluded.
  4. Range Check: A falloff function based on the distance between the origin and the sampled geometry prevents distant background geometry from falsely occluding foreground objects (a common artifact known as "haloing").

The occlusion results of all samples are summed and normalized, producing a grayscale value where \(1.0\) represents fully unoccluded areas and \(0.0\) indicates total occlusion.

5. Edge-Preserving Bilateral Blur

Because performance constraints limit the hemisphere to a low sample count, the raw SSAO output contains high-frequency noise from the randomized kernel rotation. To resolve this, the DirectX pipeline passes the raw SSAO texture through an edge-preserving bilateral blur shader (often implemented via separable horizontal and vertical compute shader passes).

The bilateral blur averages neighboring occlusion values while comparing their depths and normals. If the depth or normal difference between adjacent pixels exceeds a threshold, the blur weight drops to zero. This smooths out noise across flat surfaces while maintaining sharp contact shadow edges along geometry silhouettes.

6. Pipeline Integration

The blurred SSAO texture is finally passed to the scene composition stage. In a deferred rendering pipeline, it is bound as an SRV to the lighting pass, where the occlusion factor is multiplied directly with the ambient and indirect lighting terms. In a forward rendering pipeline, it is typically applied as a full-screen multiplicative blend pass over the rendered color target before subsequent post-processing effects, such as tone mapping, bloom, and color grading, are executed.