depth-prepass-greater-compare

star 0

When building a depth prepass for reverse-Z HZB occlusion culling, use Greater compare (not Always) — the closest fragment must win so the HZB stores the nearest occluding surface, not the farthest

Solidor777 By Solidor777 schedule Updated 6/16/2026

name: depth-prepass-greater-compare description: When building a depth prepass for reverse-Z HZB occlusion culling, use Greater compare (not Always) — the closest fragment must win so the HZB stores the nearest occluding surface, not the farthest source: auto-skill extracted_at: '2026-06-16T02:20:23.020Z'

When building a depth prepass to feed an HZB occlusion pyramid under reverse-Z, the depth compare function must be Greater, not Always.

Why: Under reverse-Z, larger depth = closer to camera. With Always, every fragment passes and the LAST one drawn overwrites the depth buffer — the farthest object at each pixel wins. When a back-face cube self-occludes against the depth prepass, it writes its own (farther) depth OVER the closer occluder's depth. The HZB min-fold then captures the farthest depth, making every candidate pass the occlusion test: the cull reads the candidate's own depth from the HZB.

With Greater, the CLOSEST fragment at each pixel passes and later (farther) fragments are rejected. The HZB stores the nearest occluding surface at every pixel. A candidate behind that surface correctly fails the reverse-Z test nearest_depth + epsilon < hzb_depth.

Pipeline construction:

depth_stencil: Some(wgpu::DepthStencilState {
    format: wgpu::TextureFormat::Depth32Float,
    depth_write_enabled: Some(true),
    depth_compare: Some(wgpu::CompareFunction::Greater),  // NOT Always
    ..
}),

Render pass clear value: Clear(0.0) — reverse-Z far plane. With Greater, all initial fragments pass (depth > 0.0 is always true for rendered geometry).

Literature: Greene, Kass, Miller 1993 "Hierarchical Z-buffer visibility" — the depth prepass is the HZB source; the HZB must represent the closest visible surface at each pixel for conservative occlusion. Lapidous 2016 "Reverse Z (and why it's awesome)" — reverse-Z mip construction with min-fold.

Important: Titan uses standard projection, not reverse-Z

Titan's build_frame_uniforms constructs the projection via glam::Mat4::perspective_rh — standard right-handed projection (near=0, far=1), not reverse-Z. The scene_cull.wgsl shader comments and the original commit messages refer to reverse-Z, but the actual projection matrix is standard.

Under standard projection:

  • Greater compare with Clear(0.0): the FARTHEST fragment at each pixel wins (larger depth = farther). This is the opposite of what a depth prepass normally needs (closest fragment).
  • The HZB min-fold therefore captures the CLOSEST depth among those farthest values.
  • The occlusion test candidate_nearest > hzb_depth compensates for this double inversion.

If you switch to Less compare with Clear(1.0), you must also revisit the HZB fold direction and the occlusion test comparison to keep the three consistent.

Verification: After this fix, the bench CSV should show hzb_occluded for objects behind occluders. If all entries remain visible, also check:

  • Camera UBO layout matches WGSL (flags.y = candidate_count, not screen.y)
  • Camera height matches scene geometry Y-level (bench-camera-scene-alignment)
  • The depth prepass actually renders geometry (check candidate_count > 0)
Install via CLI
npx skills add https://github.com/Solidor777/Titan --skill depth-prepass-greater-compare
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator