Summary
Add a second pass to the GPU culling compute shader that tests chunk AABBs against the depth pyramid from #383. Chunks fully occluded by closer terrain are culled, reducing draw calls by 30-50% in hilly/mountainous terrain.
Depends on: #383 (depth pyramid shader)
Current State (after #379)
GPU frustum culling eliminates chunks outside the camera's view frustum. But chunks inside the frustum that are completely hidden behind a hill or mountain are still rendered. In mountainous biomes this can be 30-50% of visible chunks.
How It Works
The depth pyramid (#383) stores hierarchical depth information from the previous frame. For each chunk AABB, we can test whether it's fully behind the closest surface already rendered:
- Project the AABB's 8 corners to screen space
- Find the bounding box of those projections in screen space
- Sample the depth pyramid at the appropriate mip level (matching the AABB's screen size)
- If the AABB's closest depth is further than the pyramid's maximum depth at that region → occluded
Conservative Testing
- Use the AABB's nearest corner depth for the test (conservative: err on the side of visibility)
- Sample the depth pyramid mip level that matches the AABB's screen-space coverage
- One sample at the appropriate mip level is sufficient (that's the beauty of the hierarchy)
Implementation Plan
Step 1: Add depth pyramid binding to culling shader
Update culling.comp:
- New binding:
layout(binding=3) uniform sampler2D depth_pyramid
- New push constant:
vec2 screen_size and float previous_frame_valid
- If
previous_frame_valid == 0: skip occlusion test (first frame, no previous depth)
Step 2: AABB projection + occlusion test
bool isOccluded(vec3 aabb_min, vec3 aabb_max, mat4 view_proj,
sampler2D depth_pyramid, vec2 screen_size) {
// Project all 8 AABB corners to clip space
// Find nearest depth and 2D screen bounds
// Select mip level based on screen-space AABB size
// Sample depth pyramid at mip level
// If nearest AABB depth > pyramid max depth → occluded
}
Step 3: Two-pass culling in compute
void main() {
uint chunk_idx = gl_GlobalInvocationID.x;
if (chunk_idx >= chunk_count) return;
// Pass 1: Frustum test (already exists)
if (!frustumTest(chunk_idx)) return;
// Pass 2: Occlusion test (new)
if (previous_frame_valid > 0 && isOccluded(chunk_idx, ...)) return;
// Visible: write draw command
writeDrawCommand(chunk_idx);
}
Step 4: Depth pyramid lifecycle
- Depth pyramid generated at end of frame N
- Used for occlusion in frame N+1
- First frame after startup: no occlusion (previous_frame_valid = 0)
- After camera cut/teleport: invalidate pyramid for one frame
Step 5: Debug visualization
- Add toggle to debug overlay showing occlusion culling statistics
- Display: chunks frustum-culled, chunks occlusion-culled, chunks rendered
- Color-coded wireframe: green=visible, red=frustum-culled, blue=occlusion-culled
Files to Modify
assets/shaders/vulkan/culling.comp — add occlusion test
src/engine/graphics/vulkan/culling_system.zig — bind depth pyramid, pass screen size
src/engine/graphics/vulkan/depth_pyramid.zig — expose texture for binding
src/engine/ui/debug_menu.zig — occlusion culling debug toggle
Testing
Roadmap: docs/PERFORMANCE_ROADMAP.md — Batch 5, Issue 3A-2
Summary
Add a second pass to the GPU culling compute shader that tests chunk AABBs against the depth pyramid from #383. Chunks fully occluded by closer terrain are culled, reducing draw calls by 30-50% in hilly/mountainous terrain.
Depends on: #383 (depth pyramid shader)
Current State (after #379)
GPU frustum culling eliminates chunks outside the camera's view frustum. But chunks inside the frustum that are completely hidden behind a hill or mountain are still rendered. In mountainous biomes this can be 30-50% of visible chunks.
How It Works
The depth pyramid (#383) stores hierarchical depth information from the previous frame. For each chunk AABB, we can test whether it's fully behind the closest surface already rendered:
Conservative Testing
Implementation Plan
Step 1: Add depth pyramid binding to culling shader
Update
culling.comp:layout(binding=3) uniform sampler2D depth_pyramidvec2 screen_sizeandfloat previous_frame_validprevious_frame_valid == 0: skip occlusion test (first frame, no previous depth)Step 2: AABB projection + occlusion test
Step 3: Two-pass culling in compute
Step 4: Depth pyramid lifecycle
Step 5: Debug visualization
Files to Modify
assets/shaders/vulkan/culling.comp— add occlusion testsrc/engine/graphics/vulkan/culling_system.zig— bind depth pyramid, pass screen sizesrc/engine/graphics/vulkan/depth_pyramid.zig— expose texture for bindingsrc/engine/ui/debug_menu.zig— occlusion culling debug toggleTesting
Roadmap:
docs/PERFORMANCE_ROADMAP.md— Batch 5, Issue 3A-2