From 3e2869b6aaf39fad73939856cd6509f565f694e2 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 30 Apr 2026 15:05:32 +1000 Subject: [PATCH] ifcviewer: re-enable contribution culling in ortho mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous projection-toggle commit short-circuited contribution culling when projection_ortho_ was set — the formula r_px = focal_px * r / dist looks like it depends on per-instance distance, which doesn't apply in ortho. Result: every frustum- visible object drew, including sub-pixel ones, and FPS tanked on top-down plan views. In ortho the projected pixel size of a bounding sphere is constant: r_px = pixels_per_world * r, where pixels_per_world equals the existing focal_px / camera_distance_ (the ortho box was sized to match perspective at the pivot's distance). So the same formula gives the right answer if we replace per-instance dist with camera_distance_. cullModelCpu now does that substitution for both contributionPasses and pixelRadius (the latter feeds LOD1 selection too — sub-pixel objects pick LOD1 in ortho the same way they do in perspective). The "camera inside AABB" early-return is kept; it only fires in perspective where dist→0 would otherwise blow up r_px, and is harmless in ortho. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer/ViewportWindow.cpp | 49 +++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 69fb587088..d9aaa1f733 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -2189,9 +2189,20 @@ void ViewportWindow::cullModelCpu(ModelGpuData& m, const float planes[6][4], const float cx = camera_eye_.x(); const float cy = camera_eye_.y(); const float cz = camera_eye_.z(); + // In ortho the projected pixel size of a bounding sphere doesn't depend + // on per-instance distance — the ortho box scales the entire scene by a + // constant. Substitute that constant (camera_distance_, which is + // exactly the half-height/tan(fovy/2) used to size the box) for the + // per-instance dist so the same `r_px = focal_px * r / dist` formula + // and threshold survive both modes. Snapshot now so the worker threads + // see a consistent value. + const bool ortho_mode = projection_ortho_; + const float ortho_dist = camera_distance_; + auto contributionPasses = [&](const float mn[3], const float mx[3]) -> bool { if (min_pixel_radius <= 0.0f) return true; - // Camera inside AABB? Always keep. + // Camera inside AABB? Always keep — only relevant in perspective + // where dist→0 would make r_px blow up; harmless in ortho too. if (cx >= mn[0] && cx <= mx[0] && cy >= mn[1] && cy <= mx[1] && cz >= mn[2] && cz <= mx[2]) { @@ -2201,10 +2212,15 @@ void ViewportWindow::cullModelCpu(ModelGpuData& m, const float planes[6][4], float ey = 0.5f * (mx[1] - mn[1]); float ez = 0.5f * (mx[2] - mn[2]); float radius = std::sqrt(ex*ex + ey*ey + ez*ez); - float dx = 0.5f * (mx[0] + mn[0]) - cx; - float dy = 0.5f * (mx[1] + mn[1]) - cy; - float dz = 0.5f * (mx[2] + mn[2]) - cz; - float dist = std::sqrt(dx*dx + dy*dy + dz*dz); + float dist; + if (ortho_mode) { + dist = ortho_dist; + } else { + float dx = 0.5f * (mx[0] + mn[0]) - cx; + float dy = 0.5f * (mx[1] + mn[1]) - cy; + float dz = 0.5f * (mx[2] + mn[2]) - cz; + dist = std::sqrt(dx*dx + dy*dy + dz*dz); + } // r_px = focal_px * radius / dist; compare r_px >= min_pixel_radius, // rearranged to avoid the divide. return focal_px * radius >= min_pixel_radius * dist; @@ -2223,10 +2239,15 @@ void ViewportWindow::cullModelCpu(ModelGpuData& m, const float planes[6][4], float ey = 0.5f * (mx[1] - mn[1]); float ez = 0.5f * (mx[2] - mn[2]); float radius = std::sqrt(ex*ex + ey*ey + ez*ez); - float dx = 0.5f * (mx[0] + mn[0]) - cx; - float dy = 0.5f * (mx[1] + mn[1]) - cy; - float dz = 0.5f * (mx[2] + mn[2]) - cz; - float dist = std::sqrt(dx*dx + dy*dy + dz*dz); + float dist; + if (ortho_mode) { + dist = ortho_dist; + } else { + float dx = 0.5f * (mx[0] + mn[0]) - cx; + float dy = 0.5f * (mx[1] + mn[1]) - cy; + float dz = 0.5f * (mx[2] + mn[2]) - cz; + dist = std::sqrt(dx*dx + dy*dy + dz*dz); + } return dist > 0.0f ? focal_px * radius / dist : std::numeric_limits::infinity(); }; @@ -2533,11 +2554,11 @@ void ViewportWindow::render() { // depth would normally populate the pyramid), causing false occlusion. if (needs_settle_recull) hiz_vp_valid_ = false; - // Contribution culling assumes perspective (r_px = focal_px * r / dist); - // in ortho the per-instance distance is irrelevant, so disable it rather - // than ship wrong results. Frustum + HiZ culling still run. - const float min_pixel_radius = projection_ortho_ ? 0.0f - : (use_motion_threshold ? motion_min_pixel_radius : base_min_pixel_radius); + // Contribution culling works in both modes: cullModelCpu substitutes + // camera_distance_ for the per-instance dist when projection_ortho_ is + // set, which matches the ortho box's constant pixels-per-world. + const float min_pixel_radius = use_motion_threshold + ? motion_min_pixel_radius : base_min_pixel_radius; if (cull_this_frame) { hiz_reject_count_.store(0, std::memory_order_relaxed); last_cull_was_motion_ = camera_moving;