From 0b16cf2878749d5cea96683820cce04ecab46192 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Tue, 31 Mar 2026 19:32:53 +0200 Subject: [PATCH 1/3] fix(view): Move camera offset calculation into camera position update path --- Core/GameEngine/Include/Common/GameDefines.h | 4 + .../Include/W3DDevice/GameClient/W3DView.h | 13 +- .../Source/W3DDevice/GameClient/W3DView.cpp | 137 ++++++++++++------ .../GameEngine/Include/Common/GlobalData.h | 2 + .../GameEngine/Source/Common/GlobalData.cpp | 4 + 5 files changed, 111 insertions(+), 49 deletions(-) diff --git a/Core/GameEngine/Include/Common/GameDefines.h b/Core/GameEngine/Include/Common/GameDefines.h index d688cc73ae4..627e5c76eab 100644 --- a/Core/GameEngine/Include/Common/GameDefines.h +++ b/Core/GameEngine/Include/Common/GameDefines.h @@ -27,6 +27,10 @@ #define PRESERVE_RETAIL_BEHAVIOR (1) // Retain behavior present in retail Generals 1.08 and Zero Hour 1.04 #endif +#ifndef PRESERVE_RETAIL_SCRIPTED_CAMERA +#define PRESERVE_RETAIL_SCRIPTED_CAMERA (1) // Retain scripted camera behavior present in retail Generals 1.08 and Zero Hour 1.04 +#endif + #ifndef RETAIL_COMPATIBLE_CRC #define RETAIL_COMPATIBLE_CRC (1) // Game is expected to be CRC compatible with retail Generals 1.08, Zero Hour 1.04 #endif diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h index 6dfd204bd71..7e03f3e3fe4 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h @@ -275,18 +275,27 @@ class W3DView : public View, public SubsystemInterface Bool m_cameraHasMovedSinceRequest; ///< If true, throw out all saved locations VecPosRequests m_locationRequests; ///< These are cached. New requests are added here - Coord3D m_cameraOffset; ///< offset for camera from view center - Coord3D m_previousLookAtPosition; ///< offset for camera from view center + Coord3D m_previousLookAtPosition; Coord2D m_scrollAmount; ///< scroll speed Real m_scrollAmountCutoffSqr; ///< scroll speed at which we do not adjust height Real m_groundLevel; ///< height of ground. +#if PRESERVE_RETAIL_SCRIPTED_CAMERA + // TheSuperHackers @tweak Uses the initial ground level for preserving the original look of the scripted camera, + // because alterations to the ground level do affect the positioning in subtle ways. + Real m_initialGroundLevel; +#endif Region2D m_cameraAreaConstraints; ///< Camera should be constrained to be within this area Bool m_cameraAreaConstraintsValid; ///< If false, recalculates the camera area constraints in the next render update Bool m_recalcCameraConstraintsAfterScrolling; ///< Recalculates the camera area constraints after the user has moved the camera Bool m_recalcCamera; ///< Recalculates the camera transform in the next render update + Real getCameraOffsetZ() const; + Real getDesiredHeight(Real x, Real y) const; + Real getDesiredZoom(Real x, Real y) const; + Real getMaxHeight(Real x, Real y) const; + Real getMaxZoom(Real x, Real y) const; void setCameraTransform(); ///< set the transform matrix of m_3DCamera, based on m_pos & m_angle void buildCameraPosition(Vector3 &sourcePos, Vector3 &targetPos); void buildCameraTransform(Matrix3D *transform, const Vector3 &sourcePos, const Vector3 &targetPos); ///< calculate (but do not set) the transform matrix of m_3DCamera, based on m_pos & m_angle diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp index 1f913d2af82..b6072eb9805 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp @@ -142,9 +142,9 @@ W3DView::W3DView() m_3DCamera = nullptr; m_2DCamera = nullptr; m_groundLevel = 10.0f; - m_cameraOffset.z = TheGlobalData->m_cameraHeight; - m_cameraOffset.y = -(m_cameraOffset.z / tan(TheGlobalData->m_cameraPitch * (PI / 180.0))); - m_cameraOffset.x = -(m_cameraOffset.y * tan(TheGlobalData->m_cameraYaw * (PI / 180.0))); +#if PRESERVE_RETAIL_SCRIPTED_CAMERA + m_initialGroundLevel = m_groundLevel; +#endif m_viewFilterMode = FM_VIEW_DEFAULT; m_viewFilter = FT_VIEW_DEFAULT; @@ -261,9 +261,11 @@ void W3DView::buildCameraPosition( Vector3& sourcePos, Vector3& targetPos ) pos.x += m_shakeOffset.x; pos.y += m_shakeOffset.y; - sourcePos.X = m_cameraOffset.x; - sourcePos.Y = m_cameraOffset.y; - sourcePos.Z = m_cameraOffset.z; + // TheSuperHackers @info The default pitch affects the look-at distance to the target. + // This is strange math which would need special attention when changed. + sourcePos.Z = getCameraOffsetZ(); + sourcePos.Y = -(sourcePos.Z / tan(TheGlobalData->m_cameraPitch * (PI / 180.0))); + sourcePos.X = -(sourcePos.Y * tan(TheGlobalData->m_cameraYaw * (PI / 180.0))); // set position of camera itself if (m_useRealZoomCam) //WST 10/10/2002 Real Zoom using FOV @@ -358,7 +360,7 @@ void W3DView::buildCameraTransform( Matrix3D *transform, const Vector3 &sourcePo { //m_3DCamera->Set_View_Plane(DEG_TO_RADF(50.0f)); //DEBUG_LOG(("zoom %f, SourceZ %f, posZ %f, groundLevel %f CamOffZ %f", - // zoom, sourcePos.Z, pos.z, groundLevel,m_cameraOffset.z)); + // zoom, sourcePos.Z, pos.z, groundLevel, getCameraOffsetZ())); // build new camera transform transform->Make_Identity(); @@ -422,8 +424,7 @@ void W3DView::buildCameraTransform( Matrix3D *transform, const Vector3 &sourcePo // TheSuperHackers @info Original logic responsible for zooming the camera to the desired height. Bool W3DView::zoomCameraToDesiredHeight() { - const Real desiredHeight = (m_terrainHeightAtPivot + m_heightAboveGround); - const Real desiredZoom = desiredHeight / m_cameraOffset.z; + const Real desiredZoom = getDesiredZoom(m_pos.x, m_pos.y); const Real adjustZoom = desiredZoom - m_zoom; if (fabs(adjustZoom) >= 0.001f) { @@ -640,6 +641,71 @@ void W3DView::getPickRay(const ICoord2D *screen, Vector3 *rayStart, Vector3 *ray *rayEnd += *rayStart; //get point on far clip plane along ray from camera. } +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +Real W3DView::getCameraOffsetZ() const +{ +#if PRESERVE_RETAIL_SCRIPTED_CAMERA + // TheSuperHackers @info xezon 04/12/2025 It is necessary to use the initial ground level for the + // scripted camera height to preserve the original look of it. Otherwise the forward distance + // of the camera will slightly change the view pitch. + if (!m_isUserControlled) + { + return m_initialGroundLevel + TheGlobalData->m_cameraHeight; + } +#endif + + return m_groundLevel + TheGlobalData->m_maxCameraHeight; +} + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +Real W3DView::getDesiredHeight(Real x, Real y) const +{ +#if PRESERVE_RETAIL_SCRIPTED_CAMERA + // TheSuperHackers @info xezon 06/12/2025 The height above ground must be relative to the current + // terrain height because the ground level is not updated for it. + if (!m_isUserControlled) + { + return getHeightAroundPos(x, y) + m_heightAboveGround; + } +#endif + + return m_groundLevel + m_heightAboveGround; +} + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +Real W3DView::getMaxHeight(Real x, Real y) const +{ +#if PRESERVE_RETAIL_SCRIPTED_CAMERA + if (!m_isUserControlled) + { + return getHeightAroundPos(x, y) + m_maxHeightAboveGround; + } +#endif + + return m_groundLevel + m_maxHeightAboveGround; +} + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +Real W3DView::getDesiredZoom(Real x, Real y) const +{ + if (!isZoomLimited() || isUserControlLocked()) + { + return m_zoom; + } + return getDesiredHeight(x, y) / getCameraOffsetZ(); +} + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +Real W3DView::getMaxZoom(Real x, Real y) const +{ + return getMaxHeight(x, y) / getCameraOffsetZ(); +} + //------------------------------------------------------------------------------------------------- /** set the transform matrix of m_3DCamera, based on m_pos & m_angle */ //------------------------------------------------------------------------------------------------- @@ -1423,7 +1489,7 @@ void W3DView::update() // ensures that the view can reach and see all areas of the map, and especially the bottom map border. m_terrainHeightAtPivot = getHeightAroundPos(m_pos.x, m_pos.y); - m_currentHeightAboveGround = m_cameraOffset.z * m_zoom - m_terrainHeightAtPivot; + m_currentHeightAboveGround = getCameraOffsetZ() * m_zoom - m_terrainHeightAtPivot; if (m_okToAdjustHeight) { @@ -2068,18 +2134,8 @@ void W3DView::setZoom(Real z) void W3DView::setZoomToDefault() { // default zoom has to be max, otherwise players will just zoom to max always - - // terrain height + desired height offset == cameraOffset * actual zoom - // find best approximation of max terrain height we can see - Real terrainHeightMax = getHeightAroundPos(m_pos.x, m_pos.y); - - Real desiredHeight = (terrainHeightMax + m_maxHeightAboveGround); - Real desiredZoom = desiredHeight / m_cameraOffset.z; - - //DEBUG_LOG(("W3DView::setZoomToDefault() Current zoom: %g Desired zoom: %g", m_zoom, desiredZoom)); - - m_zoom = desiredZoom; m_heightAboveGround = m_maxHeightAboveGround; + m_zoom = getMaxZoom(m_pos.x, m_pos.y); stopDoingScriptedCamera(); m_CameraArrivedAtWaypointOnPathFlag = false; @@ -2448,10 +2504,14 @@ void W3DView::initHeightForMap() { resetPivotToGround(); - m_cameraOffset.z = m_groundLevel+TheGlobalData->m_cameraHeight; - m_cameraOffset.y = -(m_cameraOffset.z / tan(TheGlobalData->m_cameraPitch * (PI / 180.0))); - m_cameraOffset.x = -(m_cameraOffset.y * tan(TheGlobalData->m_cameraYaw * (PI / 180.0))); +#if PRESERVE_RETAIL_SCRIPTED_CAMERA + // jba - starting ground level can't exceed this height. + constexpr const Real MAX_GROUND_LEVEL = 120.0f; + const Real accurateGroundLevel = TheTerrainLogic->getGroundHeight(m_pos.x, m_pos.y); + m_initialGroundLevel = min(MAX_GROUND_LEVEL, accurateGroundLevel); +#endif } + //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- void W3DView::resetPivotToGround( void ) @@ -2638,22 +2698,14 @@ void W3DView::cameraModFinalZoom( Real finalZoom, Real easeIn, Real easeOut ) { if (hasScriptedState(Scripted_Rotate)) { - Real terrainHeightMax = getHeightAroundPos(m_pos.x, m_pos.y); - Real maxHeight = (terrainHeightMax + m_maxHeightAboveGround); - Real maxZoom = maxHeight / m_cameraOffset.z; - Real time = (m_rcInfo.numFrames + m_rcInfo.numHoldFrames - m_rcInfo.curFrame)*TheW3DFrameLengthInMsec; - zoomCamera( finalZoom*maxZoom, time, time*easeIn, time*easeOut ); + zoomCamera( finalZoom*getMaxZoom(m_pos.x, m_pos.y), time, time*easeIn, time*easeOut ); } if (hasScriptedState(Scripted_MoveOnWaypointPath)) { Coord3D pos = m_mcwpInfo.waypoints[m_mcwpInfo.numWaypoints]; - Real terrainHeightMax = getHeightAroundPos(pos.x, pos.y); - Real maxHeight = (terrainHeightMax + m_maxHeightAboveGround); - Real maxZoom = maxHeight / m_cameraOffset.z; - Real time = m_mcwpInfo.totalTimeMilliseconds - m_mcwpInfo.elapsedTimeMilliseconds; - zoomCamera( finalZoom*maxZoom, time, time*easeIn, time*easeOut ); + zoomCamera( finalZoom*getMaxZoom(pos.x, pos.y), time, time*easeIn, time*easeOut ); } } @@ -2888,13 +2940,7 @@ void W3DView::resetCamera(const Coord3D *location, Int milliseconds, Real easeIn // m_mcwpInfo.cameraAngle[2] = m_defaultAngle; View::setAngle(m_mcwpInfo.cameraAngle[0]); - // terrain height + desired height offset == cameraOffset * actual zoom - // find best approximation of max terrain height we can see - Real terrainHeightMax = getHeightAroundPos(location->x, location->y); - Real desiredHeight = (terrainHeightMax + m_maxHeightAboveGround); - Real desiredZoom = desiredHeight / m_cameraOffset.z; - - zoomCamera( desiredZoom, milliseconds, easeIn, easeOut ); // this isn't right... or is it? + zoomCamera( getMaxZoom(location->x, location->y), milliseconds, easeIn, easeOut ); pitchCamera( 1.0f, milliseconds, easeIn, easeOut ); } @@ -3194,6 +3240,9 @@ void W3DView::setUserControlled(Bool value) if (m_isUserControlled != value) { m_isUserControlled = value; +#if PRESERVE_RETAIL_SCRIPTED_CAMERA + m_zoom = getDesiredZoom(m_pos.x, m_pos.y); +#endif } } @@ -3248,9 +3297,6 @@ void W3DView::moveAlongWaypointPath(Real milliseconds) View::setAngle(m_mcwpInfo.cameraAngle[m_mcwpInfo.numWaypoints]); m_groundLevel = m_mcwpInfo.groundHeight[m_mcwpInfo.numWaypoints]; - /////////////////////m_cameraOffset.z = m_groundLevel+TheGlobalData->m_cameraHeight; - m_cameraOffset.y = -(m_cameraOffset.z / tan(TheGlobalData->m_cameraPitch * (PI / 180.0))); - m_cameraOffset.x = -(m_cameraOffset.y * tan(TheGlobalData->m_cameraYaw * (PI / 180.0))); Coord3D pos = m_mcwpInfo.waypoints[m_mcwpInfo.numWaypoints]; pos.z = 0; @@ -3324,9 +3370,6 @@ void W3DView::moveAlongWaypointPath(Real milliseconds) m_groundLevel = m_mcwpInfo.groundHeight[m_mcwpInfo.curSegment]*factor1 + m_mcwpInfo.groundHeight[m_mcwpInfo.curSegment+1]*factor2; - //////////////m_cameraOffset.z = m_groundLevel+TheGlobalData->m_cameraHeight; - m_cameraOffset.y = -(m_cameraOffset.z / tan(TheGlobalData->m_cameraPitch * (PI / 180.0))); - m_cameraOffset.x = -(m_cameraOffset.y * tan(TheGlobalData->m_cameraYaw * (PI / 180.0))); Coord3D start, mid, end; if (factor<0.5) { diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 4f25d868f05..53ae5d7459a 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -185,7 +185,9 @@ class GlobalData : public SubsystemInterface Real m_viewportHeightScale; // The height scale of the tactical view ranging 0..1. Used to hide the world behind the Control Bar. Real m_cameraPitch; Real m_cameraYaw; +#if PRESERVE_RETAIL_SCRIPTED_CAMERA Real m_cameraHeight; +#endif Real m_maxCameraHeight; Real m_minCameraHeight; Real m_terrainHeightAtEdgeOfMap; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index 8b2937cff5e..3d9382d0b60 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -182,7 +182,9 @@ GlobalData* GlobalData::m_theOriginal = nullptr; { "ViewportHeightScale", INI::parseReal, nullptr, offsetof( GlobalData, m_viewportHeightScale ) }, { "CameraPitch", INI::parseReal, nullptr, offsetof( GlobalData, m_cameraPitch ) }, { "CameraYaw", INI::parseReal, nullptr, offsetof( GlobalData, m_cameraYaw ) }, +#if PRESERVE_RETAIL_SCRIPTED_CAMERA { "CameraHeight", INI::parseReal, nullptr, offsetof( GlobalData, m_cameraHeight ) }, +#endif { "MaxCameraHeight", INI::parseReal, nullptr, offsetof( GlobalData, m_maxCameraHeight ) }, { "MinCameraHeight", INI::parseReal, nullptr, offsetof( GlobalData, m_minCameraHeight ) }, { "TerrainHeightAtEdgeOfMap", INI::parseReal, nullptr, offsetof( GlobalData, m_terrainHeightAtEdgeOfMap ) }, @@ -842,7 +844,9 @@ GlobalData::GlobalData() m_cameraPitch = 0.0f; m_cameraYaw = 0.0f; +#if PRESERVE_RETAIL_SCRIPTED_CAMERA m_cameraHeight = 0.0f; +#endif m_minCameraHeight = 100.0f; m_maxCameraHeight = 300.0f; m_terrainHeightAtEdgeOfMap = 0.0f; From 0ac21f09f66e65b10f220eee43aa89848ee7758c Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:32:55 +0200 Subject: [PATCH 2/3] Replicate in Generals --- Generals/Code/GameEngine/Include/Common/GlobalData.h | 2 ++ Generals/Code/GameEngine/Source/Common/GlobalData.cpp | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index ffe8a519dc2..e6d7ed343ad 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -184,7 +184,9 @@ class GlobalData : public SubsystemInterface Real m_viewportHeightScale; // The height scale of the tactical view ranging 0..1. Used to hide the world behind the Control Bar. Real m_cameraPitch; Real m_cameraYaw; +#if PRESERVE_RETAIL_SCRIPTED_CAMERA Real m_cameraHeight; +#endif Real m_maxCameraHeight; Real m_minCameraHeight; Real m_terrainHeightAtEdgeOfMap; diff --git a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp index 04f3b58b25a..1caeaa6ad33 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -182,7 +182,9 @@ GlobalData* GlobalData::m_theOriginal = nullptr; { "ViewportHeightScale", INI::parseReal, nullptr, offsetof( GlobalData, m_viewportHeightScale ) }, { "CameraPitch", INI::parseReal, nullptr, offsetof( GlobalData, m_cameraPitch ) }, { "CameraYaw", INI::parseReal, nullptr, offsetof( GlobalData, m_cameraYaw ) }, +#if PRESERVE_RETAIL_SCRIPTED_CAMERA { "CameraHeight", INI::parseReal, nullptr, offsetof( GlobalData, m_cameraHeight ) }, +#endif { "MaxCameraHeight", INI::parseReal, nullptr, offsetof( GlobalData, m_maxCameraHeight ) }, { "MinCameraHeight", INI::parseReal, nullptr, offsetof( GlobalData, m_minCameraHeight ) }, { "TerrainHeightAtEdgeOfMap", INI::parseReal, nullptr, offsetof( GlobalData, m_terrainHeightAtEdgeOfMap ) }, @@ -838,7 +840,9 @@ GlobalData::GlobalData() m_cameraPitch = 0.0f; m_cameraYaw = 0.0f; +#if PRESERVE_RETAIL_SCRIPTED_CAMERA m_cameraHeight = 0.0f; +#endif m_minCameraHeight = 100.0f; m_maxCameraHeight = 300.0f; m_terrainHeightAtEdgeOfMap = 0.0f; From 20427605758a99185d3171e490cd27c88a79cda7 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:50:26 +0200 Subject: [PATCH 3/3] Fix zoom setup mistakes --- .../Source/W3DDevice/GameClient/W3DView.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp index b6072eb9805..98175b6a75b 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp @@ -692,10 +692,6 @@ Real W3DView::getMaxHeight(Real x, Real y) const //------------------------------------------------------------------------------------------------- Real W3DView::getDesiredZoom(Real x, Real y) const { - if (!isZoomLimited() || isUserControlLocked()) - { - return m_zoom; - } return getDesiredHeight(x, y) / getCameraOffsetZ(); } @@ -2121,7 +2117,7 @@ void W3DView::setHeightAboveGround(Real z) void W3DView::setZoom(Real z) { m_heightAboveGround = m_maxHeightAboveGround * z; - m_zoom = z; + m_zoom = getDesiredZoom(m_pos.x, m_pos.y); stopDoingScriptedCamera(); m_CameraArrivedAtWaypointOnPathFlag = false;