Clustered Frustum Partitioning in DirectX Compute
Clustered forward shading divides the camera view frustum into a three-dimensional grid of sub-frustums, known as clusters or froxels, to efficiently cull and assign lights in modern rendering pipelines. This article details how DirectX compute passes construct this three-dimensional spatial acceleration structure. It covers the mathematical formulation of screen-space and depth-space slicing, the implementation of DirectX compute shaders to generate bounding volumes, and how these clusters are stored in GPU memory to decouple scene complexity from dynamic light counts.
1. The 3D Cluster Grid Structure
Standard tiled forward shading partitions only the two-dimensional screen space (\(X\) and \(Y\)), which causes high depth complexity to result in false-positive light assignments along the view ray. Clustered forward shading resolves this by adding depth (\(Z\)) partitioning.
A typical cluster allocation consists of:
- \(X\) and \(Y\) Dimensions: The viewport is subdivided into uniform pixel blocks (commonly \(16 \times 16\) or \(32 \times 32\) pixels).
- \(Z\) Dimension: The depth range between the near clipping plane (\(Z_{near}\)) and far clipping plane (\(Z_{far}\)) is subdivided into discrete slices (typically 16 to 64 slices).
Because perspective projection causes frustum volume to expand rapidly with distance, linear depth subdivision results in excessively large clusters near the camera and tiny clusters far away. To maintain a balanced ratio between cluster dimensions, the depth slices are partitioned exponentially:
\[Z_i = Z_{near} \cdot \left(\frac{Z_{far}}{Z_{near}}\right)^{\frac{i}{N}}\]
Where \(i\) is the slice index (\(0 \le i < N\)) and \(N\) is the total number of depth slices.
2. Generating Cluster Bounds via Compute Pass
In DirectX (Direct3D 11 or Direct3D 12), building the cluster hierarchy is executed as a standalone compute pass prior to light culling.
A compute shader is dispatched with a three-dimensional thread group configuration matching the cluster grid dimensions:
[numthreads(1, 1, 1)] // Or grouped into configurations like (8, 8, 1)
void CS_BuildClusters(uint3 dispatchThreadId : SV_DispatchThreadID)
{
uint3 clusterId = dispatchThreadId;
// Calculate AABB for clusterId
}
Within this compute shader, the cluster volume is bounded by determining its Axis-Aligned Bounding Box (AABB) or an oriented frustum in view space:
- Screen-to-View Space Unprojection: The shader computes the normalized device coordinates (NDC) for the four screen-space corners of the current \((X, Y)\) tile.
- Ray Generation: Using the inverse projection matrix, the NDC points are unprojected to create four directional rays originating from the camera position \((0, 0, 0)\) in view space.
- Depth Extrusion: The shader calculates the cluster's specific near and far depth bounds (\(Z_{min}\) and \(Z_{max}\)) using the exponential formula. The four rays are intersected with the near and far view planes of the cluster to produce eight corner vertices in view space.
- Bounding Box Construction: An AABB is derived from the minimum and maximum component-wise coordinates of these eight view-space vertices.
3. GPU Memory Storage and Layout
Once the bounds are generated, the compute shader writes the results to an unordered access view (UAV) bound to a structured buffer.
struct ClusterAABB
{
float4 minPoint;
float4 maxPoint;
};
RWStructuredBuffer<ClusterAABB> RWClusterAABBs : register(u0);
Because camera-relative cluster volumes do not change dynamically unless the camera field of view or clip planes change, this compute pass can be executed once on scene initialization or window resize, avoiding per-frame reconstruction overhead.
4. Downstream Light Culling and Shading
Once the clusters are established, a subsequent compute pass assigns lights to each cluster:
- Light Culling Compute Pass: Threads map to
clusters, loading the stored
ClusterAABBand testing it against active point and spot lights (using sphere-AABB or cone-AABB intersection tests). - Linked Lists or Grid Indirection: Intersecting
light indices are written to a global light index list, with each
cluster recording an offset and count into this list stored in a 3D
texture (
RWTexture3D<uint2>) or flat structured buffer. - Forward Pixel Shader Execution: During the geometry rendering pass, the pixel shader reads its screen coordinate and linear view-space depth, computes the corresponding cluster index using inverse mathematical functions, reads the assigned light range, and evaluates only the lights affecting that specific cluster.