Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class BaseHeightMapRenderObjClass : public RenderObjClass, public DX8_CleanupHoo
///allocate resources needed to render heightmap
virtual int initHeightData(Int width, Int height, WorldHeightMap *pMap, RefRenderObjListIterator *pLightsIterator, Bool updateExtraPassTiles=TRUE);
virtual Int freeMapResources(); ///< free resources used to render heightmap
virtual void updateCenter(CameraClass *camera, const Vector3 *cameraPivot, RefRenderObjListIterator *pLightsIterator);
virtual void updateCenter(CameraClass *camera, const Vector3 *cameraPivot, RefRenderObjListIterator *pLightsIterator, const Vector2 *drawCenter = nullptr);
virtual void adjustTerrainLOD(Int adj);
virtual void doPartialUpdate(const IRegion2D &partialRange, WorldHeightMap *htMap, RefRenderObjListIterator *pLightsIterator) = 0;
virtual void staticLightingChanged();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class FlatHeightMapRenderObjClass : public BaseHeightMapRenderObjClass
///allocate resources needed to render heightmap
virtual int initHeightData(Int width, Int height, WorldHeightMap *pMap, RefRenderObjListIterator *pLightsIterator,Bool updateExtraPassTiles=TRUE) override;
virtual Int freeMapResources() override; ///< free resources used to render heightmap
virtual void updateCenter(CameraClass *camera, const Vector3 *cameraPivot, RefRenderObjListIterator *pLightsIterator) override;
virtual void updateCenter(CameraClass *camera, const Vector3 *cameraPivot, RefRenderObjListIterator *pLightsIterator, const Vector2 *drawCenter = nullptr) override;
virtual void adjustTerrainLOD(Int adj) override;
virtual void reset() override;
virtual void oversizeTerrain(Int tilesToOversize) override; ///< Oversize the visible terrain area.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class HeightMapRenderObjClass : public BaseHeightMapRenderObjClass
///allocate resources needed to render heightmap
virtual int initHeightData(Int width, Int height, WorldHeightMap *pMap, RefRenderObjListIterator *pLightsIterator, Bool updateExtraPassTiles=TRUE) override;
virtual Int freeMapResources() override; ///< free resources used to render heightmap
virtual void updateCenter(CameraClass *camera, const Vector3 *cameraPivot, RefRenderObjListIterator *pLightsIterator) override;
virtual void updateCenter(CameraClass *camera, const Vector3 *cameraPivot, RefRenderObjListIterator *pLightsIterator, const Vector2 *drawCenter = nullptr) override;

virtual void staticLightingChanged() override;
virtual void adjustTerrainLOD(Int adj) override;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ class W3DView : public View, public SubsystemInterface
void pitchCameraOneFrame(); ///< Do one frame of a pitch camera movement.
void getAxisAlignedViewRegion(Region3D &axisAlignedRegion); ///< Find 3D Region enclosing all possible drawables.
void calcDeltaScroll(Coord2D &screenDelta);
bool getDesiredTerrainDrawSize(ICoord2D &dimensions) const;
bool getDesiredTerrainDrawSize(ICoord2D &dimensions, Vector2 &drawCenter, bool &useDrawCenter) const;
void updateTerrain();

// (gth) C&C3 animation controlled camera feature
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2394,7 +2394,7 @@ rendered portion of the terrain. Only a 96x96 section is rendered at any time,
even though maps can be up to 1024x1024. This function determines which subset
is rendered. */
//=============================================================================
void BaseHeightMapRenderObjClass::updateCenter(CameraClass *camera, const Vector3 *cameraPivot, RefRenderObjListIterator *pLightsIterator)
void BaseHeightMapRenderObjClass::updateCenter(CameraClass *camera, const Vector3 *cameraPivot, RefRenderObjListIterator *pLightsIterator, const Vector2 *drawCenter)
{
if (m_map==nullptr) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,13 +408,13 @@ rendered portion of the terrain. Only a 96x96 section is rendered at any time,
even though maps can be up to 1024x1024. This function determines which subset
is rendered. */
//=============================================================================
void FlatHeightMapRenderObjClass::updateCenter(CameraClass *camera, const Vector3 *cameraPivot, RefRenderObjListIterator *pLightsIterator)
void FlatHeightMapRenderObjClass::updateCenter(CameraClass *camera, const Vector3 *cameraPivot, RefRenderObjListIterator *pLightsIterator, const Vector2 *drawCenter)
{
#ifdef DO_UNIT_TIMINGS
#pragma MESSAGE("*** WARNING *** DOING DO_UNIT_TIMINGS!!!!")
return;
#endif
BaseHeightMapRenderObjClass::updateCenter(camera, cameraPivot, pLightsIterator);
BaseHeightMapRenderObjClass::updateCenter(camera, cameraPivot, pLightsIterator, drawCenter);
m_needFullUpdate = false;
Int i, j;
Int culled = 0;
Expand Down
16 changes: 11 additions & 5 deletions Core/GameEngineDevice/Source/W3DDevice/GameClient/HeightMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1631,7 +1631,7 @@ heightmap. As the view slides around, this determines what is the actually
rendered portion of the terrain. Only a small section is rendered at any time.
*/
//=============================================================================
void HeightMapRenderObjClass::updateCenter(CameraClass *camera, const Vector3 *cameraPivot, RefRenderObjListIterator *pLightsIterator)
void HeightMapRenderObjClass::updateCenter(CameraClass *camera, const Vector3 *cameraPivot, RefRenderObjListIterator *pLightsIterator, const Vector2 *drawCenter)
{
if (m_map==nullptr) {
return;
Expand All @@ -1642,7 +1642,7 @@ void HeightMapRenderObjClass::updateCenter(CameraClass *camera, const Vector3 *c
if (m_vertexBufferTiles ==nullptr)
return; //did not initialize resources yet.

BaseHeightMapRenderObjClass::updateCenter(camera, cameraPivot, pLightsIterator);
BaseHeightMapRenderObjClass::updateCenter(camera, cameraPivot, pLightsIterator, drawCenter);

m_updating = true;

Expand All @@ -1658,11 +1658,17 @@ void HeightMapRenderObjClass::updateCenter(CameraClass *camera, const Vector3 *c
return; // no need to center.
}

const Real cameraPitch = asin(fabs(camera->Get_Forward_Dir().Z));
Int newOrgX;
Int newOrgY;

if (cameraPitch > ViewDefaultLowPitchRadians)
if (drawCenter)
{
// TheSuperHackers @performance sailro 06/09/2026 Reuse the footprint used for sizing instead of
// scanning the heightmap again or applying the low-pitch center approximation.
newOrgX = WWMath::Round(drawCenter->X/MAP_XY_FACTOR) - m_x/2 + m_map->getBorderSizeInline();
newOrgY = WWMath::Round(drawCenter->Y/MAP_XY_FACTOR) - m_y/2 + m_map->getBorderSizeInline();
}
else if (asin(fabs(camera->Get_Forward_Dir().Z)) > ViewDefaultLowPitchRadians)
{
// TheSuperHackers @info This is the original code to determine the center position for the visible terrain area.
// It is relatively expensive and breaks when the frustum planes can no longer intersect with the terrain at low camera
Expand Down Expand Up @@ -1826,7 +1832,7 @@ void HeightMapRenderObjClass::updateCenter(CameraClass *camera, const Vector3 *c
// It is much more efficient to update a couple of columns one frame, and then
// a couple of rows. So if we aren't "jumping" to a new view, and have done X
// recently, return.
if (abs(deltaX) < BIG_JUMP && !m_doXNextTime) {
if (!drawCenter && abs(deltaX) < BIG_JUMP && !m_doXNextTime) {
m_updating = false;
m_doXNextTime = true;
return; // Only do the y this frame. Do x next frame. jba.
Expand Down
113 changes: 92 additions & 21 deletions Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3717,37 +3717,107 @@ void W3DView::Add_Camera_Shake (const Coord3D & position,float radius,float dura
CameraShakerSystem.Add_Camera_Shake(vpos,radius,duration,power);
}

bool W3DView::getDesiredTerrainDrawSize(ICoord2D &dimensions) const
bool W3DView::getDesiredTerrainDrawSize(ICoord2D &dimensions, Vector2 &drawCenter, bool &useDrawCenter) const
{
useDrawCenter = false;
const WorldHeightMap *map = TheTerrainRenderObject->getMap();
if (!map)
return false;

// Keep the map-sized fallback for invalid camera data and footprints larger than the map.
dimensions.x = map->getXExtent();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the top of the view shows sky the corner rays miss the ground, and this falls back to the full map. Cutscenes and low pitch cameras then draw the whole map. Can it use the normal or low angle size instead?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1f6460baf. Horizon-crossing corner rays now use the normal or low-angle minimum draw size and leave centering to the existing calcVis/low-pitch path instead of resizing to the full map.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up after runtime testing: using the normal/low-angle minimum here is not safe, so I reverted that part in 95f3f4d92. On Whiteout at height 1500 and 10° pitch, the minimum fallback selected 257x257 and left large black terrain gaps with floating objects; the conservative map fallback selected 540x540 and rendered correctly. The separate change to use the projected center only after the window expands remains in place.

dimensions.y = map->getYExtent();
if (TheGlobalData && TheGlobalData->m_drawEntireTerrain)
return true;

// TheSuperHackers @bugfix sailro 06/09/2026 Cover the visible terrain without yaw-dependent buffer
// reallocations. The two cached terrain height limits also cover valleys and nearby higher ground.
const Matrix3D &cameraTransform = m_3DCamera->Get_Transform();
const Vector3 cameraLocation = cameraTransform.Get_Translation();
const Real cameraPitch = asin(fabs(cameraTransform[2][2]));
ICoord2D minimumSize;
if (cameraPitch > ViewDefaultLowPitchRadians || !m_isUserControlled)
{
DEBUG_ASSERTCRASH(TheTerrainRenderObject != nullptr, ("TheTerrainRenderObject is null"));
minimumSize.x = WorldHeightMap::NORMAL_DRAW_WIDTH;
minimumSize.y = WorldHeightMap::NORMAL_DRAW_HEIGHT;
}
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.
minimumSize.x = WorldHeightMap::LOW_ANGLE_DRAW_WIDTH;
minimumSize.y = WorldHeightMap::LOW_ANGLE_DRAW_HEIGHT;
}

const Real groundZ[2] = {
TheTerrainRenderObject->getMinHeight(),
std::min(TheTerrainRenderObject->getMaxHeight(), cameraLocation.Z)
};
if (!cameraLocation.Is_Valid() || cameraLocation.Z <= groundZ[0])
return true;

if (const WorldHeightMap *heightMap = TheTerrainRenderObject->getMap())
Vector2 viewPlaneMin, viewPlaneMax;
m_3DCamera->Get_View_Plane(viewPlaneMin, viewPlaneMax);
const Int planeCount = groundZ[0] < groundZ[1] ? 2 : 1;
Vector3 corners[8];
Vector2 footprintMin, footprintMax;
for (Int i = 0; i < 4; ++i)
{
const Vector3 ray((i & 1) ? viewPlaneMax.X : viewPlaneMin.X,
(i & 2) ? viewPlaneMax.Y : viewPlaneMin.Y, -1.0f);
const Real rayZ = cameraTransform[2][0]*ray.X + cameraTransform[2][1]*ray.Y - cameraTransform[2][2];
if (!(rayZ < 0.0f))
{
dimensions.x = heightMap->getXExtent();
dimensions.y = heightMap->getYExtent();
return true;
}

return false;
const Real inverseRayZ = 1.0f/rayZ;
for (Int plane = 0; plane < planeCount; ++plane)
{
Vector3 &corner = corners[plane*4 + i];
corner = ray * ((groundZ[plane] - cameraLocation.Z)*inverseRayZ);
if (!corner.Is_Valid())
return true;
const Vector2 offset(
cameraTransform[0][0]*corner.X + cameraTransform[0][1]*corner.Y + cameraTransform[0][2]*corner.Z,
cameraTransform[1][0]*corner.X + cameraTransform[1][1]*corner.Y + cameraTransform[1][2]*corner.Z);
if (i == 0 && plane == 0)
footprintMin = footprintMax = offset;
footprintMin.Update_Min(offset);
footprintMax.Update_Max(offset);
}
}

const Real cameraPitch = asin(fabs(m_3DCamera->Get_Forward_Dir().Z));

if (cameraPitch > ViewDefaultLowPitchRadians || !m_isUserControlled)
// The maximum pairwise XY distance bounds every rotated span. Compute it in camera space to avoid
// yaw-dependent rounding at allocation thresholds; remove the height difference between planes.
Real diameterSquared = 0.0f;
const Real worldBound = std::max(map->getXExtent(), map->getYExtent())*MAP_XY_FACTOR;
for (Int i = 0; i < planeCount*4; ++i)
{
// TheSuperHackers @info The scripted camera always uses the regular draw sizes
// and uses terrain oversize if it needs to enlarge.
dimensions.x = WorldHeightMap::NORMAL_DRAW_WIDTH;
dimensions.y = WorldHeightMap::NORMAL_DRAW_HEIGHT;
return true;
for (Int j = 0; j < i; ++j)
{
const Vector3 delta = corners[i] - corners[j];
const Real deltaZ = groundZ[i/4] - groundZ[j/4];
const Real distanceSquared = delta.Length2() - deltaZ*deltaZ;
if (!(distanceSquared < worldBound*worldBound))
return true;
diameterSquared = std::max(diameterSquared, distanceSquared);
}
}

// 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;
// CENTER_LIMIT permits two cells of origin drift per axis; nearest-cell centering adds half a cell.
const Int footprintTiles = (Int)ceil(WWMath::Sqrt(diameterSquared)/MAP_XY_FACTOR) + 5;
const Int blocks = (footprintTiles + VERTEX_BUFFER_TILE_LENGTH - 1)/VERTEX_BUFFER_TILE_LENGTH;
const Int drawSize = 1 + blocks*VERTEX_BUFFER_TILE_LENGTH;
dimensions.x = std::min(map->getXExtent(), std::max(minimumSize.x, drawSize));
dimensions.y = std::min(map->getYExtent(), std::max(minimumSize.y, drawSize));
if (dimensions.x > std::min(map->getXExtent(), minimumSize.x)
|| dimensions.y > std::min(map->getYExtent(), minimumSize.y))
{
drawCenter.Set(cameraLocation.X + (footprintMin.X + footprintMax.X)*0.5f,
cameraLocation.Y + (footprintMin.Y + footprintMax.Y)*0.5f);
useDrawCenter = true;
}
return true;
}

Expand All @@ -3756,16 +3826,17 @@ void W3DView::updateTerrain()
DEBUG_ASSERTCRASH(TheTerrainRenderObject != nullptr, ("TheTerrainRenderObject is null"));

ICoord2D drawSize;

if (getDesiredTerrainDrawSize(drawSize))
Vector2 drawCenter;
bool useDrawCenter;
if (getDesiredTerrainDrawSize(drawSize, drawCenter, useDrawCenter))
{
TheTerrainRenderObject->setTerrainDrawSize(drawSize.x, drawSize.y);
}

RefRenderObjListIterator *it = W3DDisplay::m_3DScene->createLightsIterator();

const Vector3 cameraPivot(m_pos.x, m_pos.y, m_pos.z);
TheTerrainRenderObject->updateCenter(m_3DCamera, &cameraPivot, it);
TheTerrainRenderObject->updateCenter(m_3DCamera, &cameraPivot, it, useDrawCenter ? &drawCenter : nullptr);

if (it)
{
Expand Down