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:
Greatercompare withClear(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_depthcompensates 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)