Tiled vs Clustered Deferred Shading in GLSL
This article breaks down the architectural and implementation differences between tiled deferred shading and clustered deferred shading within GLSL lighting passes. While both techniques modernize traditional deferred rendering by culling lights before evaluating surface illumination, they diverge significantly in how they partition space, manage depth discontinuities, and resolve light lists in shaders. Understanding these differences is critical for optimizing rendering engines dealing with high dynamic light counts in OpenGL or Vulkan.
The Motivation: Scaling Beyond Classic Deferred Rendering
Classic deferred rendering evaluates geometry into a G-buffer (position/depth, normals, albedo, material properties) and subsequently computes lighting. Using light bounding volumes (spheres or cones) requires drawing geometry proxies, which incurs excessive draw call overhead and rasterization cost. Rendering a full-screen quad per light degrades fill-rate performance linearly as light counts increase.
Both tiled and clustered deferred shading decouple light evaluation from individual draw calls by assigning lights to discrete spatial bins, allowing a single lighting pass to iterate only over lights that affect local geometry.
Tiled Deferred Shading in GLSL
Tiled deferred shading divides the 2D viewport into a uniform grid of screen-space tiles, typically \(16 \times 16\) or \(32 \times 32\) pixels.
Implementation Pipeline
- G-Buffer Pass: Standard deferred pass rendering geometry data and hardware depth to framebuffers.
- Tile Culling (Compute Shader):
- A compute shader dispatches one workgroup per tile (e.g.,
layout (local_size_x = 16, local_size_y = 16) in;). - Threads cooperatively sample the tile’s depth buffer to find the
minimum and maximum depth values (
minZandmaxZ) using atomic operations or parallel reductions in shared memory (shared uint minDepth, maxDepth;). - A frustum bounding volume is derived for the tile using the tile's
screen-space extents and the calculated
minZ/maxZ. - Visible lights are tested (usually sphere-frustum or sphere-AABB intersection) against this tile frustum.
- Intersecting light indices are appended to a list stored in a Shader Storage Buffer Object (SSBO).
- A compute shader dispatches one workgroup per tile (e.g.,
- Lighting Pass:
Executed via a full-screen compute shader or a single full-screen fragment shader pass.
The pixel identifies its tile:
uvec2 tileID = uvec2(gl_FragCoord.xy) / uvec2(TILE_SIZE); uint tileIndex = tileID.x + tileID.y * numTilesX;The shader reads the offset and count from the tile's light index list in the SSBO and loops over only the lights affecting that 2D tile.
The Depth Discontinuity Problem
Tiled deferred shading relies entirely on screen-space tiles bounded
by min/max depth. When a tile captures a foreground object (e.g., a
weapon or pillar close to the camera) and distant background geometry
simultaneously, minZ is very close to zero and
maxZ approaches the far plane. This expands the tile
frustum into an elongated corridor, causing lights located anywhere in
the empty space between the foreground and background to pass the
intersection test. Consequently, pixels in that tile waste cycles
iterating over lights that contribute no illumination.
Clustered Deferred Shading in GLSL
Clustered deferred shading addresses the depth discontinuity problem by sub-dividing the view frustum along all three dimensions: X, Y, and Z. The view volume is split into a 3D grid of volumetric cells, often called "froxels" or clusters.
Implementation Pipeline
- Cluster Generation (One-time or View-dependent Compute
Pass):
- The view frustum is split into screen-space slices (\(X, Y\)) and logarithmic depth slices (\(Z\)).
- Logarithmic depth distribution allocates smaller clusters near the near-plane and larger clusters farther away, matching perspective projection behavior: \[\text{slice} = \left\lfloor \frac{\log(-z_{view} / z_{near})}{\log(1 + \frac{2 \cdot \tan(\text{fov} / 2)}{N_y})} \right\rfloor\]
- A compute shader generates an axis-aligned bounding box (AABB) in view space for every cluster and writes them to an SSBO.
- Light Culling (Compute Shader):
- Unlike tiled culling, light culling does not depend on current-frame scene depth values.
- A compute shader tests scene lights against the fixed 3D cluster AABBs.
- Output light indices are stored using a global index list and a grid pointing to offsets/counts per cluster (e.g., using a compact grid or linked list in SSBOs).
- Lighting Pass:
The lighting shader reconstructs the 3D cluster index directly from the fragment coordinate and the linear depth of the current pixel:
uint clusterZ = uint(max(log(-viewPos.z / nearZ) * scale + bias, 0.0)); uvec3 clusterCoord = uvec3(gl_FragCoord.xy / tileSize, clusterZ); uint clusterIndex = clusterCoord.x + clusterCoord.y * gridDimX + clusterCoord.z * gridDimX * gridDimY;The pixel loads the light list assigned strictly to its specific 3D volume, completely bypassing lights residing in the empty space along the view ray.
Direct Comparison in GLSL Passes
| Feature | Tiled Deferred Shading | Clustered Deferred Shading |
|---|---|---|
| Partitioning Geometry | 2D Screen Space (\(X, Y\)) bounded by dynamic depth | 3D View Space (\(X, Y, Z\)) with predefined depth slicing |
| Depth Dependency for Culling | Dependent on scene depth buffer; requires per-tile min/max reduction. | Independent of scene depth; clusters are defined purely by view frustum mathematics. |
| Depth Discontinuity Handling | Poor. Elongated frustums cause heavy light leakage and false-positive illumination loops. | Optimal. Pixels only check the light slice corresponding to their exact depth segment. |
| GLSL Fetch Complexity | Fast 2D tile indexing via
gl_FragCoord.xy / TILE_SIZE. |
Requires linearizing depth and evaluating a logarithmic or exponential equation to determine the \(Z\)-slice. |
| SSBO Memory Layout | 2D array of light offsets/counts
(width * height / tileSize^2). |
3D flattened array of light offsets/counts
(gridX * gridY * gridZ). Higher memory footprint. |
| Flexibility (Forward+) | Can be adapted to Forward+, but geometry overlapping deep depth ranges incurs high overhead. | Directly transferable to Clustered Forward rendering, allowing seamless handling of transparent surfaces and MSAA. |
Conclusion
Tiled deferred shading simplifies the culling pipeline and uses less buffer memory, making it an efficient choice for scenes with uniform depth, such as top-down isometric perspectives. Clustered deferred shading requires a slightly more complex mathematical conversion in GLSL to derive the 3D cluster index and demands more memory for 3D cluster structures. However, it completely eliminates performance drops caused by depth discontinuities, scaling far more predictably in complex 3D environments with dense, multi-layered point lights.