diff --git a/Generals/Code/GameEngine/Include/GameClient/Drawable.h b/Generals/Code/GameEngine/Include/GameClient/Drawable.h index bbe206ff429..48377e0c928 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Drawable.h +++ b/Generals/Code/GameEngine/Include/GameClient/Drawable.h @@ -622,7 +622,13 @@ class Drawable : public Thing, Real m_totalYaw; ///< Current total yaw for this frame Real m_totalZ; - PhysicsXformInfo() : m_totalPitch(0), m_totalRoll(0), m_totalYaw(0), m_totalZ(0) { } + Real m_prevTotalPitch; + Real m_prevTotalRoll; + Real m_prevTotalYaw; + Real m_prevTotalZ; + + PhysicsXformInfo() : m_totalPitch(0), m_totalRoll(0), m_totalYaw(0), m_totalZ(0), + m_prevTotalPitch(0), m_prevTotalRoll(0), m_prevTotalYaw(0), m_prevTotalZ(0) { } }; Bool calcPhysicsXform(PhysicsXformInfo& info); @@ -687,7 +693,7 @@ class Drawable : public Thing, FADING_OUT }; FadingMode m_fadeMode; - UnsignedInt m_timeElapsedFade; ///< for how many frames have i been fading + Real m_timeElapsedFade; ///< for how long have i been fading (in 30fps-equivalent frames) UnsignedInt m_timeToFade; ///< how slowly am I fading UnsignedInt m_shroudClearFrame; ///< Last frame the local player saw this drawable "OBJECTSHROUD_CLEAR" diff --git a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp index e482166c024..0755e6b1262 100644 --- a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1147,7 +1147,9 @@ void Drawable::updateDrawable() Real numer = (m_fadeMode == FADING_IN) ? (m_timeElapsedFade) : (m_timeToFade-m_timeElapsedFade); setDrawableOpacity(numer/(Real)m_timeToFade); - ++m_timeElapsedFade; + // TheSuperHackers @tweak Drawable fade is now decoupled from the render update. + const Real fadeTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_timeElapsedFade += fadeTimeScale; if (m_timeElapsedFade > m_timeToFade) m_fadeMode = FADING_NONE; @@ -1165,7 +1167,9 @@ void Drawable::updateDrawable() { //LERP (*dm)->setTerrainDecalOpacity(m_decalOpacity); - m_decalOpacity += m_decalOpacityFadeRate; + // TheSuperHackers @tweak Decal opacity fade is now decoupled from the render update. + const Real decalFadeTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_decalOpacity += m_decalOpacityFadeRate * decalFadeTimeScale; } //--------------- @@ -1349,17 +1353,32 @@ void Drawable::applyPhysicsXform(Matrix3D* mtx) { if (m_physicsXform != nullptr) { - // TheSuperHackers @tweak Update the physics transform on every WW Sync only. - // All calculations are originally catered to a 30 fps logic step. + // TheSuperHackers @tweak Run physics on logic frames only, interpolate for rendering. + // This provides stable physics at any framerate without numerical integration issues. if (WW3D::Get_Sync_Frame_Time() != 0) { + m_physicsXform->m_prevTotalPitch = m_physicsXform->m_totalPitch; + m_physicsXform->m_prevTotalRoll = m_physicsXform->m_totalRoll; + m_physicsXform->m_prevTotalYaw = m_physicsXform->m_totalYaw; + m_physicsXform->m_prevTotalZ = m_physicsXform->m_totalZ; + calcPhysicsXform(*m_physicsXform); } - mtx->Translate(0.0f, 0.0f, m_physicsXform->m_totalZ); - mtx->Rotate_Y( m_physicsXform->m_totalPitch ); - mtx->Rotate_X( -m_physicsXform->m_totalRoll ); - mtx->Rotate_Z( m_physicsXform->m_totalYaw ); + // Interpolate between previous and current state based on fractional sync time. + // The fractional sync time accumulates in logic time, so a full logic step is always MSEC_PER_LOGICFRAME_REAL. + const Real fractionalMs = (Real)WW3D::Get_Fractional_Sync_Milliseconds(); + const Real t = clamp(0.0f, fractionalMs / MSEC_PER_LOGICFRAME_REAL, 1.0f); + + const Real interpPitch = m_physicsXform->m_prevTotalPitch + t * (m_physicsXform->m_totalPitch - m_physicsXform->m_prevTotalPitch); + const Real interpRoll = m_physicsXform->m_prevTotalRoll + t * (m_physicsXform->m_totalRoll - m_physicsXform->m_prevTotalRoll); + const Real interpYaw = m_physicsXform->m_prevTotalYaw + t * (m_physicsXform->m_totalYaw - m_physicsXform->m_prevTotalYaw); + const Real interpZ = m_physicsXform->m_prevTotalZ + t * (m_physicsXform->m_totalZ - m_physicsXform->m_prevTotalZ); + + mtx->Translate(0.0f, 0.0f, interpZ); + mtx->Rotate_Y( interpPitch ); + mtx->Rotate_X( -interpRoll ); + mtx->Rotate_Z( interpYaw ); } } @@ -4845,6 +4864,7 @@ void Drawable::xferDrawableModules( Xfer *xfer ) * 6: Added m_ambientSoundEnabledFromScript flag (Added in Zero Hour) * 7: Save the customize ambient sound info (Added in Zero Hour) * 8: TheSuperHackers @bugfix Removed m_prevTintStatus because loading its value is unnecessary and undesirable + * 9: TheSuperHackers @tweak Changed m_timeElapsedFade from UnsignedInt to Real for frame-rate independent fading */ // ------------------------------------------------------------------------------------------------ void Drawable::xfer( Xfer *xfer ) @@ -4856,7 +4876,7 @@ void Drawable::xfer( Xfer *xfer ) #elif RETAIL_COMPATIBLE_XFER_SAVE const XferVersion currentVersion = 7; #else - const XferVersion currentVersion = 8; + const XferVersion currentVersion = 9; #endif XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -5030,7 +5050,19 @@ void Drawable::xfer( Xfer *xfer ) xfer->xferUser( &m_fadeMode, sizeof( FadingMode ) ); // time elapsed fade - xfer->xferUnsignedInt( &m_timeElapsedFade ); + if (version >= 9) + { + xfer->xferReal( &m_timeElapsedFade ); + } + else + { + UnsignedInt timeElapsedFadeFrames = static_cast(m_timeElapsedFade); + xfer->xferUnsignedInt( &timeElapsedFadeFrames ); + if (xfer->getXferMode() == XFER_LOAD) + { + m_timeElapsedFade = static_cast(timeElapsedFadeFrames); + } + } // time to fade xfer->xferUnsignedInt( &m_timeToFade ); diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h index 7169b8cb51f..8e9a168124d 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h @@ -622,7 +622,13 @@ class Drawable : public Thing, Real m_totalYaw; ///< Current total yaw for this frame Real m_totalZ; - PhysicsXformInfo() : m_totalPitch(0), m_totalRoll(0), m_totalYaw(0), m_totalZ(0) { } + Real m_prevTotalPitch; + Real m_prevTotalRoll; + Real m_prevTotalYaw; + Real m_prevTotalZ; + + PhysicsXformInfo() : m_totalPitch(0), m_totalRoll(0), m_totalYaw(0), m_totalZ(0), + m_prevTotalPitch(0), m_prevTotalRoll(0), m_prevTotalYaw(0), m_prevTotalZ(0) { } }; Bool calcPhysicsXform(PhysicsXformInfo& info); @@ -687,7 +693,7 @@ class Drawable : public Thing, FADING_OUT }; FadingMode m_fadeMode; - UnsignedInt m_timeElapsedFade; ///< for how many frames have i been fading + Real m_timeElapsedFade; ///< for how long have i been fading (in 30fps-equivalent frames) UnsignedInt m_timeToFade; ///< how slowly am I fading UnsignedInt m_shroudClearFrame; ///< Last frame the local player saw this drawable "OBJECTSHROUD_CLEAR" diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index 54dff3325f2..e49cfb858e7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1149,7 +1149,9 @@ void Drawable::updateDrawable() Real numer = (m_fadeMode == FADING_IN) ? (m_timeElapsedFade) : (m_timeToFade-m_timeElapsedFade); setDrawableOpacity(numer/(Real)m_timeToFade); - ++m_timeElapsedFade; + // TheSuperHackers @tweak Drawable fade is now decoupled from the render update. + const Real fadeTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_timeElapsedFade += fadeTimeScale; if (m_timeElapsedFade > m_timeToFade) m_fadeMode = FADING_NONE; @@ -1167,7 +1169,9 @@ void Drawable::updateDrawable() { //LERP (*dm)->setTerrainDecalOpacity(m_decalOpacity); - m_decalOpacity += m_decalOpacityFadeRate; + // TheSuperHackers @tweak Decal opacity fade is now decoupled from the render update. + const Real decalFadeTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + m_decalOpacity += m_decalOpacityFadeRate * decalFadeTimeScale; } //--------------- @@ -1351,17 +1355,32 @@ void Drawable::applyPhysicsXform(Matrix3D* mtx) { if (m_physicsXform != nullptr) { - // TheSuperHackers @tweak Update the physics transform on every WW Sync only. - // All calculations are originally catered to a 30 fps logic step. + // TheSuperHackers @tweak Run physics on logic frames only, interpolate for rendering. + // This provides stable physics at any framerate without numerical integration issues. if (WW3D::Get_Sync_Frame_Time() != 0) { + m_physicsXform->m_prevTotalPitch = m_physicsXform->m_totalPitch; + m_physicsXform->m_prevTotalRoll = m_physicsXform->m_totalRoll; + m_physicsXform->m_prevTotalYaw = m_physicsXform->m_totalYaw; + m_physicsXform->m_prevTotalZ = m_physicsXform->m_totalZ; + calcPhysicsXform(*m_physicsXform); } - mtx->Translate(0.0f, 0.0f, m_physicsXform->m_totalZ); - mtx->Rotate_Y( m_physicsXform->m_totalPitch ); - mtx->Rotate_X( -m_physicsXform->m_totalRoll ); - mtx->Rotate_Z( m_physicsXform->m_totalYaw ); + // Interpolate between previous and current state based on fractional sync time. + // The fractional sync time accumulates in logic time, so a full logic step is always MSEC_PER_LOGICFRAME_REAL. + const Real fractionalMs = (Real)WW3D::Get_Fractional_Sync_Milliseconds(); + const Real t = clamp(0.0f, fractionalMs / MSEC_PER_LOGICFRAME_REAL, 1.0f); + + const Real interpPitch = m_physicsXform->m_prevTotalPitch + t * (m_physicsXform->m_totalPitch - m_physicsXform->m_prevTotalPitch); + const Real interpRoll = m_physicsXform->m_prevTotalRoll + t * (m_physicsXform->m_totalRoll - m_physicsXform->m_prevTotalRoll); + const Real interpYaw = m_physicsXform->m_prevTotalYaw + t * (m_physicsXform->m_totalYaw - m_physicsXform->m_prevTotalYaw); + const Real interpZ = m_physicsXform->m_prevTotalZ + t * (m_physicsXform->m_totalZ - m_physicsXform->m_prevTotalZ); + + mtx->Translate(0.0f, 0.0f, interpZ); + mtx->Rotate_Y( interpPitch ); + mtx->Rotate_X( -interpRoll ); + mtx->Rotate_Z( interpYaw ); } } @@ -4850,6 +4869,7 @@ void Drawable::xferDrawableModules( Xfer *xfer ) * 6: Added m_ambientSoundEnabledFromScript flag (Added in Zero Hour) * 7: Save the customize ambient sound info (Added in Zero Hour) * 8: TheSuperHackers @bugfix Removed m_prevTintStatus because loading its value is unnecessary and undesirable + * 9: TheSuperHackers @tweak Changed m_timeElapsedFade from UnsignedInt to Real for frame-rate independent fading */ // ------------------------------------------------------------------------------------------------ void Drawable::xfer( Xfer *xfer ) @@ -4861,7 +4881,7 @@ void Drawable::xfer( Xfer *xfer ) #elif RETAIL_COMPATIBLE_XFER_SAVE const XferVersion currentVersion = 7; #else - const XferVersion currentVersion = 8; + const XferVersion currentVersion = 9; #endif XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -5035,7 +5055,19 @@ void Drawable::xfer( Xfer *xfer ) xfer->xferUser( &m_fadeMode, sizeof( FadingMode ) ); // time elapsed fade - xfer->xferUnsignedInt( &m_timeElapsedFade ); + if (version >= 9) + { + xfer->xferReal( &m_timeElapsedFade ); + } + else + { + UnsignedInt timeElapsedFadeFrames = static_cast(m_timeElapsedFade); + xfer->xferUnsignedInt( &timeElapsedFadeFrames ); + if (xfer->getXferMode() == XFER_LOAD) + { + m_timeElapsedFade = static_cast(timeElapsedFadeFrames); + } + } // time to fade xfer->xferUnsignedInt( &m_timeToFade );