| 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),
..
}),
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)