Software Occlusion Culling to Reduce Draw Calls
Software occlusion culling is a CPU-side rendering optimization that identifies and discards hidden geometry before issuing draw calls to the GPU. By rasterizing a simplified representation of the scene into a low-resolution CPU depth buffer, developers can test object bounding boxes against this buffer in real time. This article explores the architecture of software occlusion systems, detailing the generation of occluder meshes, SIMD-accelerated software rasterization, hierarchical depth tests, and how to integrate this pipeline to maximize rendering efficiency.
The Problem with Hardware Occlusion Queries
Hardware occlusion queries rely on the GPU to render bounding boxes and report back how many pixels passed the depth test. While accurate, retrieving these results incurs significant CPU-GPU synchronization latency. If the CPU stalls waiting for results, frame rate drops. If the engine uses the results on the subsequent frame, visual artifacts (occlusion popping) can occur during rapid camera movements. Software occlusion solves this by performing the entire culling phase on the CPU, making visibility results available immediately within the same frame before draw calls are submitted.
1. Selecting and Preparing Occluders
Rasterizing complex high-poly geometry on the CPU is too expensive. Developers separate scene assets into two categories: occluders (objects that hide other things) and occludees (objects that might be hidden).
- Occluder Geometry: Developers select large, opaque objects such as terrain, walls, and buildings. These meshes are simplified into low-polygon proxy representations, often consisting of tens to hundreds of triangles rather than thousands.
- Occludee Geometry: Any potentially visible dynamic or static mesh is represented by its conservative bounding volume, typically an Axis-Aligned Bounding Box (AABB) or Oriented Bounding Box (OBB).
2. Software Rasterization of the Depth Buffer
The engine initializes a low-resolution software depth buffer on the CPU, commonly ranging from 256x144 to 480x270 pixels. The occluder proxy meshes are transformed into clip space using the current camera’s view-projection matrix and rasterized into this buffer.
- Backface Culling and Clipping: Standard geometric clipping and backface culling discard non-visible polygons before pixel processing.
- Depth Calculation: The software rasterizer computes the interpolated depth values (\(Z\)) for each covered pixel. Unlike standard rendering, pixel colors, materials, and textures are ignored entirely; only the nearest depth value is written.
3. SIMD and Multithreading Optimization
Because CPU software rasterization can be computationally demanding, developers optimize the pipeline using data-oriented designs and modern processor vector instructions (such as SSE4.1, AVX2, or ARM Neon).
- Vectorized Triangle Traversal: Engines test and rasterize groups of pixels simultaneously. Using AVX2, an algorithm can calculate edge functions and depth interpolation for 8 or 16 pixels per instruction.
- Job System Distribution: The rasterization of occluders and the querying of occludees are divided into discrete jobs distributed across worker threads, executing in parallel with other engine tasks such as animation updates or physics simulation.
4. Hierarchical Z-Buffering (Hi-Z)
To avoid sampling every pixel covered by an occludee’s bounding box, developers generate a Hierarchical Z-Buffer (a CPU depth mip-pyramid).
- Each subsequent mip level downsamples the depth buffer, storing the maximum (farthest) depth value of the corresponding \(2\times2\) pixel footprint.
- When testing an object's bounding box, the system projects the box to screen space to calculate its 2D area.
- The system selects the mip level where the projected box covers roughly a \(2\times2\) to \(4\times4\) pixel region and compares the minimum depth of the bounding box against the maximum depth recorded in that mip level.
- If the nearest point of the bounding box is farther than the farthest recorded occluder depth in that cell, the entire object is guaranteed to be hidden and is culled instantly.
5. Culling Integration into the Render Pipeline
The final step connects query results to the GPU command generation stage:
- Camera Setup: View-projection matrices are updated at the start of the frame.
- Occluder Rasterization: Proxy meshes are rasterized into the CPU depth buffer.
- Depth Pyramid Generation: The software Hi-Z buffer is generated.
- Batch Bounding Box Queries: The engine iterates through the scene's spatial partition structures (such as BVH or Octrees). Hierarchical volumes that fail the occlusion test cull entire branches of the scene graph at once.
- Command Generation: Only instances whose bounding boxes pass both the view-frustum test and the occlusion test are appended to the GPU draw list or indirect draw buffers.
By leveraging low-resolution rasterization, conservative proxies, and SIMD-driven depth tests, software occlusion culling eliminates non-visible objects prior to the rendering stage, heavily reducing GPU vertex processing, fragment shading, and draw call overhead.