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

  1. G-Buffer Pass: Standard deferred pass rendering geometry data and hardware depth to framebuffers.
  2. 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 (minZ and maxZ) 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).
  3. 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

  1. 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.
  2. 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).
  3. 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.