From 99f409280a8a39f58861fd2943af856e1ede0ca3 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 15 Apr 2026 17:46:47 +1000 Subject: [PATCH] ifcviewer: disable HiZ cull when camera has moved MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HiZ from last frame encodes depth from last frame's viewpoint. When the camera moves, projecting a current-frame AABB through the stored VP answers 'was this occluded last frame?' rather than 'is it occluded now?' — a self-reinforcing feedback loop where objects culled in prior frames never appear in any depth buffer and stay permanently hidden at certain camera angles. Fix: require hiz_vp_ == current VP for the HiZ test to apply. HiZ still helps static views (kicks in one frame after camera stops) but no longer produces false occlusions during orbit. The correct fix for orbit coverage is a depth pre-pass feeding fresh HiZ — planned as part of Phase 3E GPU compute cull. Co-Authored-By: Claude Opus 4.6 --- src/ifcviewer/ViewportWindow.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 66525e28cf..e96b45f909 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -1388,7 +1388,21 @@ void ViewportWindow::cullModelCpu(ModelGpuData& m, const float planes[6][4], // HiZ occlusion is skipped entirely when the pick pass runs // (min_pixel_radius == 0 on that path), when the user disables it via // env var, or before the first pyramid has been built. - const bool hiz_on = hizEnabled() && min_pixel_radius > 0.0f && hiz_vp_valid_; + // + // Crucially, HiZ is also skipped when the stored VP (hiz_vp_, captured at + // the end of the previous frame) differs from this frame's VP — i.e. + // whenever the camera has moved. The stored depth buffer encodes what + // was visible from hiz_vp_'s viewpoint; projecting a current-frame AABB + // through that VP answers "was this occluded LAST frame?", which is only + // a correct proxy for "is this occluded NOW?" when the camera is static. + // Orbiting past a wall would otherwise leave objects persistently culled + // because prior frames' depth buffers only ever contained the wall (the + // objects behind it were themselves HiZ-culled, never drawn, so never in + // the buffer — a self-reinforcing feedback loop). On static views HiZ + // kicks in after a single frame of lag. + const QMatrix4x4 current_vp = proj_matrix_ * view_matrix_; + const bool hiz_vp_matches = hiz_vp_valid_ && hiz_vp_ == current_vp; + const bool hiz_on = hizEnabled() && min_pixel_radius > 0.0f && hiz_vp_matches; // Hot path: read the AABB from the compact bvh_items array (28 B stride) // rather than the wide InstanceCpu (104 B stride). Most instances fail