diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/BaseHeightMap.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/BaseHeightMap.h index 119acd38e3f..97781438929 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/BaseHeightMap.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/BaseHeightMap.h @@ -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(); diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/FlatHeightMap.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/FlatHeightMap.h index 814865040c8..99dc64e926e 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/FlatHeightMap.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/FlatHeightMap.h @@ -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. diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/HeightMap.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/HeightMap.h index d4dae8a5c84..6033952532e 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/HeightMap.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/HeightMap.h @@ -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; diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h index 6bbe42054ad..0c2c75b471e 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h @@ -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) const; void updateTerrain(); // (gth) C&C3 animation controlled camera feature diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp index 4cb1514c396..a63af2d9f54 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp @@ -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; diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/FlatHeightMap.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/FlatHeightMap.cpp index c2daacdf927..73bea5a2f0a 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/FlatHeightMap.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/FlatHeightMap.cpp @@ -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; diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/HeightMap.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/HeightMap.cpp index 4987a8071c1..13e12c92938 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/HeightMap.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/HeightMap.cpp @@ -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; @@ -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; @@ -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 @@ -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. diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp index 376a5297ed3..cb4b4fdc889 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp @@ -3717,37 +3717,100 @@ 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) const { + const WorldHeightMap *map = TheTerrainRenderObject->getMap(); + if (!map) + return false; + + // A horizon-crossing or invalid projection has no finite footprint. Keep the map-sized fallback. + dimensions.x = map->getXExtent(); + dimensions.y = map->getYExtent(); + drawCenter.Set(0.0f, 0.0f); if (TheGlobalData && TheGlobalData->m_drawEntireTerrain) - { - DEBUG_ASSERTCRASH(TheTerrainRenderObject != nullptr, ("TheTerrainRenderObject is null")); + return true; - if (const WorldHeightMap *heightMap = TheTerrainRenderObject->getMap()) - { - dimensions.x = heightMap->getXExtent(); - dimensions.y = heightMap->getYExtent(); + // 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 groundZ[2] = { + TheTerrainRenderObject->getMinHeight(), + std::min(TheTerrainRenderObject->getMaxHeight(), cameraLocation.Z) + }; + if (!cameraLocation.Is_Valid() || cameraLocation.Z <= groundZ[0]) + return true; + + 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)) 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)); + // 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) + { + 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); + } + } + const Real cameraPitch = asin(fabs(cameraTransform[2][2])); + ICoord2D minimumSize; if (cameraPitch > ViewDefaultLowPitchRadians || !m_isUserControlled) { - // 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; + 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; } - // 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)); + drawCenter.Set(cameraLocation.X + (footprintMin.X + footprintMax.X)*0.5f, + cameraLocation.Y + (footprintMin.Y + footprintMax.Y)*0.5f); return true; } @@ -3756,8 +3819,9 @@ void W3DView::updateTerrain() DEBUG_ASSERTCRASH(TheTerrainRenderObject != nullptr, ("TheTerrainRenderObject is null")); ICoord2D drawSize; - - if (getDesiredTerrainDrawSize(drawSize)) + Vector2 drawCenter; + const bool hasDrawCenter = getDesiredTerrainDrawSize(drawSize, drawCenter); + if (hasDrawCenter) { TheTerrainRenderObject->setTerrainDrawSize(drawSize.x, drawSize.y); } @@ -3765,7 +3829,7 @@ void W3DView::updateTerrain() 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, hasDrawCenter ? &drawCenter : nullptr); if (it) {