Skip to content
122 changes: 117 additions & 5 deletions Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3741,13 +3741,125 @@ bool W3DView::getDesiredTerrainDrawSize(ICoord2D &dimensions) const
// and uses terrain oversize if it needs to enlarge.
dimensions.x = WorldHeightMap::NORMAL_DRAW_WIDTH;
dimensions.y = WorldHeightMap::NORMAL_DRAW_HEIGHT;
return true;
}
else
{
// TheSuperHackers @tweak xezon 31/12/2025 Increases visible terrain area when lowering the camera pitch.
// Note: The default camera pitch in Generals was 37.5, which we prefer to keep the normal draw size for.
dimensions.x = WorldHeightMap::LOW_ANGLE_DRAW_WIDTH;
dimensions.y = 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 avoids swapping the draw dimensions as the
// camera rotates.
//
// This also applies to the scripted camera (for example the main menu shell map), whose view can reach
// past the regular draw window and would otherwise leave the ground uncovered.
const WorldHeightMap *map = TheTerrainRenderObject->getMap();
if (map)
{
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;
Comment thread
sailro marked this conversation as resolved.
Resolved

// 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;
const Real worldMinX = cameraLocation.X - worldBound;
const Real worldMaxX = cameraLocation.X + worldBound;
const Real worldMinY = cameraLocation.Y - worldBound;
const Real worldMaxY = cameraLocation.Y + worldBound;

Real footprintMinX = cameraLocation.X, footprintMaxX = cameraLocation.X;
Real footprintMinY = cameraLocation.Y, footprintMaxY = cameraLocation.Y;
Comment thread
sailro marked this conversation as resolved.
Resolved

for (Int i = 0; i < 2; ++i)
Comment thread
sailro marked this conversation as resolved.
Resolved
{
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;

const 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]);

// A ray's scale does not affect its intersection with the ground plane. Reuse one division
// for both coordinates instead of normalizing the ray and solving X and Y independently.
const Real rayScale = (groundZ - cameraLocation.Z) / rayDirection.Z;
Real groundX = cameraLocation.X + rayDirection.X * rayScale;
Real groundY = cameraLocation.Y + rayDirection.Y * rayScale;
Comment thread
sailro marked this conversation as resolved.
Resolved

// clamp() leaves NaN unchanged, so replace NaN from a parallel ray with the existing
// conservative fallback first. Infinite intersections are handled by clamp().
if (_isnan(groundX))
groundX = worldMinX;
if (_isnan(groundY))
groundY = worldMinY;
groundX = clamp(worldMinX, groundX, worldMaxX);
groundY = clamp(worldMinY, groundY, worldMaxY);

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 covers the footprint at
// any camera yaw without swapping its X and Y extents. Add a small margin for the centering
// tolerance: updateCenter() only recenters the window once it has drifted by more than a couple of
// tiles (CENTER_LIMIT), so the drawn window can trail the footprint by that much.
const Real footprintSpanX = footprintMaxX - footprintMinX;
const Real footprintSpanY = footprintMaxY - footprintMinY;
const Real footprintSpan = (footprintSpanX > footprintSpanY) ? footprintSpanX : footprintSpanY;
Comment thread
sailro marked this conversation as resolved.
Resolved
const Int centeringMarginTiles = 4;
const Int footprintTiles = (Int)(footprintSpan / MAP_XY_FACTOR) + centeringMarginTiles;
Comment thread
sailro marked this conversation as resolved.
Resolved
Comment thread
sailro marked this conversation as resolved.
Resolved

// Snap up to the smallest tile-based draw size (1 + N * VERTEX_BUFFER_TILE_LENGTH) that still covers
// the footprint. Growing only to the required block count - rather than always adding a whole spare
// block - keeps this a no-op at normal zoom and avoids drawing an extra ring of terrain blocks.
Int blocks = (footprintTiles + VERTEX_BUFFER_TILE_LENGTH - 1) / VERTEX_BUFFER_TILE_LENGTH;
if (blocks < 1)
blocks = 1;
Int zoomDrawSize = 1 + blocks * VERTEX_BUFFER_TILE_LENGTH;
if (zoomDrawSize > mapExtent)
zoomDrawSize = mapExtent;
if (zoomDrawSize > dimensions.x)
dimensions.x = zoomDrawSize;
if (zoomDrawSize > dimensions.y)
dimensions.y = zoomDrawSize;
}

// TheSuperHackers @tweak xezon 31/12/2025 Increases visible terrain area when lowering the camera pitch.
// Note: The default camera pitch in Generals was 37.5, which we prefer to keep the normal draw size for.
dimensions.x = WorldHeightMap::LOW_ANGLE_DRAW_WIDTH;
dimensions.y = WorldHeightMap::LOW_ANGLE_DRAW_HEIGHT;
return true;
}

Expand Down
Loading