From da4440b28d50ccf45f52ec0ef95c36a73d7c4ce4 Mon Sep 17 00:00:00 2001 From: sailro Date: Fri, 12 Jun 2026 21:30:01 +0200 Subject: [PATCH 1/3] fix(terrain): Cover the visible ground at high camera zoom With the default settings (DrawEntireTerrain=No) the terrain is drawn only within a window of tiles around the view center. At high camera zoom this window no longer covers the visible ground, so any map water renders behind it. Maps that have no real water often still keep a map-sized "Default Water" polygon, which then makes the whole map look flooded. Players worked around this with DrawEntireTerrain=Yes, but that always draws the entire map and is very slow even at normal zoom (#2743). This change instead sizes the normal draw window to the actual visible footprint: it projects the four view corners onto the ground plane (the same method updateCenter() uses to position the window) and grows the draw size just enough to span them, bounded by the map extent and snapped to whole vertex-buffer tiles. At normal zoom the footprint is smaller than the normal window, so this is a no-op and behavior is unchanged; the window only grows as the camera zooms out. This does not change the DrawEntireTerrain path, but removes the need for it in the common case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Source/W3DDevice/GameClient/W3DView.cpp | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp index c029fa4880a..a3c7f4f31f3 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp @@ -3715,6 +3715,89 @@ void W3DView::updateTerrain() drawHeight = WorldHeightMap::LOW_ANGLE_DRAW_HEIGHT; } + // TheSuperHackers @bugfix sailro 13/06/2026 Grow the terrain draw window to cover the visible ground + // when the camera is zoomed far out, for all maps. + // + // Terrain is only drawn within a window of tiles around the view center (the NORMAL/LOW_ANGLE sizes + // chosen above). When the camera is zoomed far out the visible ground reaches past that window, so the + // terrain there is not drawn and any map water shows through behind it. Many maps that have no real + // water still keep a map-sized "Default Water" polygon, which then makes the whole map look flooded. + // The legacy workaround was DrawEntireTerrain=Yes, which always draws the entire map and is very slow + // even at normal zoom (see TheSuperHackers/GeneralsGameCode#2743). + // + // Instead we size the window to the actual visible footprint: project the four view corners onto the + // ground plane (the same method updateCenter() uses to position the window) and grow the draw size just + // enough to span them. At normal zoom the footprint is smaller than the normal window, so this is a + // no-op and behavior is unchanged; the window only grows - bounded by the map extent - as the camera + // zooms out. The size is snapped to whole vertex-buffer tiles so the terrain reallocates only when a + // zoom threshold is crossed, and a single square size keeps it stable as the camera rotates. + if (WorldHeightMap *map = TheTerrainRenderObject->getMap()) + { + const Matrix3D &cameraTransform = m_3DCamera->Get_Transform(); + const Vector3 cameraLocation = m_3DCamera->Get_Position(); + Vector2 viewPlaneMin, viewPlaneMax; + m_3DCamera->Get_View_Plane(viewPlaneMin, viewPlaneMax); + Vector2 viewportMin, viewportMax; + m_3DCamera->Get_Viewport(viewportMin, viewportMax); + + const Real viewPlaneScaleX = viewPlaneMax.X - viewPlaneMin.X; + const Real viewPlaneScaleY = viewPlaneMax.Y - viewPlaneMin.Y; + const Real viewPlaneDist = -1.0f; // The view plane is always 1.0 from the camera, looking down -Z. + const Real groundZ = m_pos.z; + + // Bound the projected corners to the map extent so a near horizontal corner ray - which meets the + // ground far away, behind the camera, or not at all - cannot produce a degenerate draw size. + const Int mapExtent = (map->getXExtent() > map->getYExtent()) ? map->getXExtent() : map->getYExtent(); + const Real worldBound = (Real)mapExtent * MAP_XY_FACTOR; + + Real footprintMinX = cameraLocation.X, footprintMaxX = cameraLocation.X; + Real footprintMinY = cameraLocation.Y, footprintMaxY = cameraLocation.Y; + + for (Int i = 0; i < 2; ++i) + { + for (Int j = 0; j < 2; ++j) + { + const Real xMod = (-i + 0.5f + viewportMin.X) * viewPlaneDist * viewPlaneScaleX; + const Real yMod = ( j - 0.5f - viewportMin.Y) * viewPlaneDist * viewPlaneScaleY; + + Vector3 rayDirection( + viewPlaneDist * cameraTransform[0][2] + xMod * cameraTransform[0][0] + yMod * cameraTransform[0][1], + viewPlaneDist * cameraTransform[1][2] + xMod * cameraTransform[1][0] + yMod * cameraTransform[1][1], + viewPlaneDist * cameraTransform[2][2] + xMod * cameraTransform[2][0] + yMod * cameraTransform[2][1]); + rayDirection.Normalize(); + const Vector3 rayPoint = cameraLocation + rayDirection; + + Real groundX = Vector3::Find_X_At_Z(groundZ, cameraLocation, rayPoint); + Real groundY = Vector3::Find_Y_At_Z(groundZ, cameraLocation, rayPoint); + + // Clamp into [camera +/- worldBound]; the !(>=) form also rejects NaN from parallel rays. + if (!(groundX >= cameraLocation.X - worldBound)) groundX = cameraLocation.X - worldBound; + else if (groundX > cameraLocation.X + worldBound) groundX = cameraLocation.X + worldBound; + if (!(groundY >= cameraLocation.Y - worldBound)) groundY = cameraLocation.Y - worldBound; + else if (groundY > cameraLocation.Y + worldBound) groundY = cameraLocation.Y + worldBound; + + if (groundX < footprintMinX) footprintMinX = groundX; + if (groundX > footprintMaxX) footprintMaxX = groundX; + if (groundY < footprintMinY) footprintMinY = groundY; + if (groundY > footprintMaxY) footprintMaxY = groundY; + } + } + + // Use a single square size (the larger footprint dimension) so the window does not reallocate as the + // camera rotates, plus one tile block of margin to match the search padding updateCenter() applies. + const Real footprintSpanX = footprintMaxX - footprintMinX; + const Real footprintSpanY = footprintMaxY - footprintMinY; + const Real footprintSpan = (footprintSpanX > footprintSpanY) ? footprintSpanX : footprintSpanY; + Int footprintTiles = (Int)(footprintSpan / MAP_XY_FACTOR) + 1 + VERTEX_BUFFER_TILE_LENGTH; + if (footprintTiles > mapExtent) + footprintTiles = mapExtent; + + // Snap up to 1 + N * VERTEX_BUFFER_TILE_LENGTH to match the engine's tile-based draw sizes. + const Int zoomDrawSize = ((footprintTiles - 1 + VERTEX_BUFFER_TILE_LENGTH) / VERTEX_BUFFER_TILE_LENGTH) * VERTEX_BUFFER_TILE_LENGTH + 1; + if (zoomDrawSize > drawWidth) drawWidth = zoomDrawSize; + if (zoomDrawSize > drawHeight) drawHeight = zoomDrawSize; + } + TheTerrainRenderObject->setTerrainDrawSize(drawWidth, drawHeight); TheTerrainRenderObject->updateCenter(m_3DCamera, &cameraPivot, it); From 5225da192c4efb92b38cc215589fdad0ee9757ac Mon Sep 17 00:00:00 2001 From: Sebastien Lebreton Date: Fri, 12 Jun 2026 23:27:16 +0200 Subject: [PATCH 2/3] style(terrain): Place if/else bodies on their own line in updateTerrain Split the newly added single-line if/else statements in W3DView::updateTerrain() (the NaN clamping, the footprint min/max update, and the final draw-size update) so each body is on its own line. This follows the project formatting convention and allows precise debugger breakpoint placement. No behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Source/W3DDevice/GameClient/W3DView.cpp | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp index a3c7f4f31f3..80767c8a4ba 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp @@ -3771,15 +3771,23 @@ void W3DView::updateTerrain() Real groundY = Vector3::Find_Y_At_Z(groundZ, cameraLocation, rayPoint); // Clamp into [camera +/- worldBound]; the !(>=) form also rejects NaN from parallel rays. - if (!(groundX >= cameraLocation.X - worldBound)) groundX = cameraLocation.X - worldBound; - else if (groundX > cameraLocation.X + worldBound) groundX = cameraLocation.X + worldBound; - if (!(groundY >= cameraLocation.Y - worldBound)) groundY = cameraLocation.Y - worldBound; - else if (groundY > cameraLocation.Y + worldBound) groundY = cameraLocation.Y + worldBound; - - if (groundX < footprintMinX) footprintMinX = groundX; - if (groundX > footprintMaxX) footprintMaxX = groundX; - if (groundY < footprintMinY) footprintMinY = groundY; - if (groundY > footprintMaxY) footprintMaxY = groundY; + if (!(groundX >= cameraLocation.X - worldBound)) + groundX = cameraLocation.X - worldBound; + else if (groundX > cameraLocation.X + worldBound) + groundX = cameraLocation.X + worldBound; + if (!(groundY >= cameraLocation.Y - worldBound)) + groundY = cameraLocation.Y - worldBound; + else if (groundY > cameraLocation.Y + worldBound) + groundY = cameraLocation.Y + worldBound; + + if (groundX < footprintMinX) + footprintMinX = groundX; + if (groundX > footprintMaxX) + footprintMaxX = groundX; + if (groundY < footprintMinY) + footprintMinY = groundY; + if (groundY > footprintMaxY) + footprintMaxY = groundY; } } @@ -3794,8 +3802,10 @@ void W3DView::updateTerrain() // Snap up to 1 + N * VERTEX_BUFFER_TILE_LENGTH to match the engine's tile-based draw sizes. const Int zoomDrawSize = ((footprintTiles - 1 + VERTEX_BUFFER_TILE_LENGTH) / VERTEX_BUFFER_TILE_LENGTH) * VERTEX_BUFFER_TILE_LENGTH + 1; - if (zoomDrawSize > drawWidth) drawWidth = zoomDrawSize; - if (zoomDrawSize > drawHeight) drawHeight = zoomDrawSize; + if (zoomDrawSize > drawWidth) + drawWidth = zoomDrawSize; + if (zoomDrawSize > drawHeight) + drawHeight = zoomDrawSize; } TheTerrainRenderObject->setTerrainDrawSize(drawWidth, drawHeight); From c2451049de1c1a9e88318224d6acbb4b5e849ca3 Mon Sep 17 00:00:00 2001 From: Sebastien Lebreton Date: Sat, 13 Jun 2026 15:12:49 +0200 Subject: [PATCH 3/3] style(terrain): Make updateTerrain comments concise and self-contained Remove references to other functions and the issue id from the comments added in this PR, and tighten the wording. No code change. --- .../Source/W3DDevice/GameClient/W3DView.cpp | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp index 80767c8a4ba..ef3ac599d2a 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp @@ -3716,21 +3716,10 @@ void W3DView::updateTerrain() } // TheSuperHackers @bugfix sailro 13/06/2026 Grow the terrain draw window to cover the visible ground - // when the camera is zoomed far out, for all maps. - // - // Terrain is only drawn within a window of tiles around the view center (the NORMAL/LOW_ANGLE sizes - // chosen above). When the camera is zoomed far out the visible ground reaches past that window, so the - // terrain there is not drawn and any map water shows through behind it. Many maps that have no real - // water still keep a map-sized "Default Water" polygon, which then makes the whole map look flooded. - // The legacy workaround was DrawEntireTerrain=Yes, which always draws the entire map and is very slow - // even at normal zoom (see TheSuperHackers/GeneralsGameCode#2743). - // - // Instead we size the window to the actual visible footprint: project the four view corners onto the - // ground plane (the same method updateCenter() uses to position the window) and grow the draw size just - // enough to span them. At normal zoom the footprint is smaller than the normal window, so this is a - // no-op and behavior is unchanged; the window only grows - bounded by the map extent - as the camera - // zooms out. The size is snapped to whole vertex-buffer tiles so the terrain reallocates only when a - // zoom threshold is crossed, and a single square size keeps it stable as the camera rotates. + // when zoomed far out, for all maps. Terrain is only drawn in a window around the view center; when the + // visible ground extends past it, the uncovered area lets the map water plane show through (maps with no + // real water still keep a map-sized one, so they look flooded). We size the window to the footprint of + // the four view corners projected onto the ground: a no-op at normal zoom, growing only when zoomed out. if (WorldHeightMap *map = TheTerrainRenderObject->getMap()) { const Matrix3D &cameraTransform = m_3DCamera->Get_Transform(); @@ -3792,7 +3781,7 @@ void W3DView::updateTerrain() } // Use a single square size (the larger footprint dimension) so the window does not reallocate as the - // camera rotates, plus one tile block of margin to match the search padding updateCenter() applies. + // camera rotates, plus one tile block of margin. const Real footprintSpanX = footprintMaxX - footprintMinX; const Real footprintSpanY = footprintMaxY - footprintMinY; const Real footprintSpan = (footprintSpanX > footprintSpanY) ? footprintSpanX : footprintSpanY;