From 406b0ae3a07a964e8bf9b21a1f5295813b79456a Mon Sep 17 00:00:00 2001 From: Sam Hislop-Lynch Date: Wed, 17 Dec 2025 19:51:46 +1000 Subject: [PATCH] Fix horizon culling causing terrain tiles to disappear Visual symptoms: - At certain camera orientations, large portions of the visible Earth (often an entire hemisphere) would suddenly disappear - The issue was orientation-sensitive: a small rotation of the camera would cause the missing terrain to reappear - Most noticeable when viewing the globe from orbital distances Root cause: The horizon culling in SurfaceNode::isVisible() was calling Horizon::isVisible() with radius=0, treating each tile's horizon culling point as an infinitesimal point rather than accounting for the tile's actual spatial extent. This caused tiles near the horizon boundary to be incorrectly culled even when part of the tile should have been visible. Additionally, Horizon::isVisible() had a potential numerical precision issue where the calculation `sqrt(dot(VT,VT) - a*a)` could produce NaN if floating-point errors caused the value under the sqrt to go negative. The fix: 1. SurfaceNode.h: Pass the tile's bounding sphere radius to the horizon visibility test, making culling more conservative by accounting for the tile's actual size 2. Horizon.cpp: Add numerical safeguards to prevent NaN propagation: - Clamp cSquared to >= 0 before taking sqrt - If the final result is NaN, default to treating tile as visible --- src/rocky/Horizon.cpp | 15 ++++++++++++++- src/rocky/vsg/terrain/SurfaceNode.h | 7 +++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/rocky/Horizon.cpp b/src/rocky/Horizon.cpp index 211151e8e..d589101af 100644 --- a/src/rocky/Horizon.cpp +++ b/src/rocky/Horizon.cpp @@ -4,6 +4,7 @@ * MIT License */ #include "Horizon.h" +#include using namespace ROCKY_NAMESPACE; @@ -123,9 +124,21 @@ Horizon::isVisible(double x, double y, double z, double radius) const VT = target - _eye; double a = glm::dot(VT, -_eyeUnit); double b = a * _coneTan; - double c = sqrt(glm::dot(VT, VT) - a * a); + double VTdotVT = glm::dot(VT, VT); + double cSquared = VTdotVT - a * a; + + // Guard against numerical precision issues that could produce negative value + if (cSquared < 0.0) + cSquared = 0.0; + + double c = sqrt(cSquared); double d = c - b; double e = d * _coneCos; + + // Check for NaN in the calculation (indicates numerical instability) + if (std::isnan(e)) + return true; // Default to visible if we can't determine + if (e > -radius) return true; diff --git a/src/rocky/vsg/terrain/SurfaceNode.h b/src/rocky/vsg/terrain/SurfaceNode.h index cccd70915..2741fd547 100644 --- a/src/rocky/vsg/terrain/SurfaceNode.h +++ b/src/rocky/vsg/terrain/SurfaceNode.h @@ -113,16 +113,19 @@ namespace ROCKY_NAMESPACE auto viewID = rv.getCommandBuffer()->viewID; auto& horizon = (*_horizon)[viewID]; + // Use the bounding sphere radius as margin for more conservative culling + double cullRadius = worldBoundingSphere.radius; + if (_horizonCullingPoint_valid) { - return horizon.isVisible(_horizonCullingPoint); + return horizon.isVisible(_horizonCullingPoint, cullRadius); } else { for (int p = 0; p < 4; ++p) { auto& wp = _worldPoints[p]; - if (horizon.isVisible(wp.x, wp.y, wp.z)) + if (horizon.isVisible(wp.x, wp.y, wp.z, cullRadius)) return true; } return false;