From 74f5c3639291d599552c9b758590ff3157b7eaa8 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Thu, 14 May 2026 17:56:19 +0200 Subject: [PATCH 1/7] refactor(particlesys): Split parts of ParticleSystem::update() and Particle::update() into additional functions (#2709) --- .../Include/GameClient/ParticleSys.h | 14 +- .../Source/GameClient/System/ParticleSys.cpp | 350 ++++++++++-------- .../GameLogic/Object/Update/BoneFXUpdate.cpp | 7 +- 3 files changed, 215 insertions(+), 156 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index 5054cd2b8f8..585dc786e4a 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -178,8 +178,10 @@ class Particle : public MemoryPoolObject, Particle( ParticleSystem *system, const ParticleInfo *data ); - Bool update(); ///< update this particle's behavior - return false if dead - void doWindMotion(); ///< do wind motion (if present) from particle system + Bool update(); ///< update this particle's behavior - return false if dead + + void draw(); ///< render update + void doWindMotion(); ///< do wind motion (if present) from particle system void applyForce( const Coord3D *force ); ///< add the given acceleration @@ -661,6 +663,12 @@ class ParticleSystem : public MemoryPoolObject, protected: + struct VisibilityState + { + VisibilityState() : isShrouded(false) {} + Bool isShrouded; + }; + // snapshot methods virtual void crc( Xfer *xfer ) override; virtual void xfer( Xfer *xfer ) override; @@ -670,6 +678,8 @@ class ParticleSystem : public MemoryPoolObject, ParticlePriorityType priority, Bool forceCreate = FALSE ); ///< factory method for particles + void updateTransform(); + VisibilityState updateVisibility( Int localPlayerIndex ); const ParticleInfo *generateParticleInfo( Int particleNum, Int particleCount ); ///< generate a new, random set of ParticleInfo const Coord3D *computeParticlePosition(); ///< compute a position based on emission properties diff --git a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp index b516a335000..5cfc623db87 100644 --- a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp +++ b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp @@ -372,6 +372,66 @@ void Particle::applyForce( const Coord3D *force ) /** Update the behavior of an individual particle */ // ------------------------------------------------------------------------------------------------ Bool Particle::update() +{ + draw(); + + // + // Update alpha (if used) + // + if (m_system->getShaderType() != ParticleSystemInfo::ADDITIVE) + { + if (m_alphaTargetKey < MAX_KEYFRAMES && m_alphaKey[ m_alphaTargetKey ].frame) + { + if (TheGameClient->getFrame() - m_createTimestamp >= m_alphaKey[ m_alphaTargetKey ].frame) + { + m_alpha = m_alphaKey[ m_alphaTargetKey ].value; + m_alphaTargetKey++; + computeAlphaRate(); + } + } + else + m_alphaRate = 0.0f; + + if (m_alpha < 0.0f) + m_alpha = 0.0f; + else if (m_alpha > 1.0f) + m_alpha = 1.0f; + } + + // + // Update color + // + if (m_colorTargetKey < MAX_KEYFRAMES && m_colorKey[ m_colorTargetKey ].frame) + { + if (TheGameClient->getFrame() - m_createTimestamp >= m_colorKey[ m_colorTargetKey ].frame) + { + // can't set, because of colorscale + // m_color = m_colorKey[ m_colorTargetKey ].color; + m_colorTargetKey++; + computeColorRate(); + } + } + else + { + m_colorRate.red = 0.0f; + m_colorRate.green = 0.0f; + m_colorRate.blue = 0.0f; + } + + // monitor lifetime + if (m_lifetimeLeft && --m_lifetimeLeft == 0) + return false; + + DEBUG_ASSERTCRASH( m_lifetimeLeft, ( "A particle has an infinite lifetime..." )); + + // if we've gone totally invisible, destroy ourselves + if (isInvisible()) + return false; + return true; +} + +// ------------------------------------------------------------------------------------------------ +void Particle::draw() { // integrate acceleration into velocity m_vel.x += m_accel.x; @@ -423,30 +483,16 @@ Bool Particle::update() // // Update alpha (if used) // - if (m_system->getShaderType() != ParticleSystemInfo::ADDITIVE) { m_alpha += m_alphaRate; - if (m_alphaTargetKey < MAX_KEYFRAMES && m_alphaKey[ m_alphaTargetKey ].frame) - { - if (TheGameClient->getFrame() - m_createTimestamp >= m_alphaKey[ m_alphaTargetKey ].frame) - { - m_alpha = m_alphaKey[ m_alphaTargetKey ].value; - m_alphaTargetKey++; - computeAlphaRate(); - } - } - else - m_alphaRate = 0.0f; - if (m_alpha < 0.0f) m_alpha = 0.0f; else if (m_alpha > 1.0f) m_alpha = 1.0f; } - // // Update color // @@ -454,23 +500,6 @@ Bool Particle::update() m_color.green += m_colorRate.green; m_color.blue += m_colorRate.blue; - if (m_colorTargetKey < MAX_KEYFRAMES && m_colorKey[ m_colorTargetKey ].frame) - { - if (TheGameClient->getFrame() - m_createTimestamp >= m_colorKey[ m_colorTargetKey ].frame) - { - // can't set, because of colorscale - // m_color = m_colorKey[ m_colorTargetKey ].color; - m_colorTargetKey++; - computeColorRate(); - } - } - else - { - m_colorRate.red = 0.0f; - m_colorRate.green = 0.0f; - m_colorRate.blue = 0.0f; - } - /// @todo Rethink this - at least its name m_color.red += m_colorScale; m_color.green += m_colorScale; @@ -496,17 +525,6 @@ Bool Particle::update() m_accel.x = 0.0f; m_accel.y = 0.0f; m_accel.z = 0.0f; - - // monitor lifetime - if (m_lifetimeLeft && --m_lifetimeLeft == 0) - return false; - - DEBUG_ASSERTCRASH( m_lifetimeLeft, ( "A particle has an infinite lifetime..." )); - - // if we've gone totally invisible, destroy ourselves - if (isInvisible()) - return false; - return true; } // ------------------------------------------------------------------------------------------------ @@ -1919,114 +1937,15 @@ Bool ParticleSystem::update( Int localPlayerIndex ) if (m_windMotion != ParticleSystemInfo::WIND_MOTION_NOT_USED ) updateWindMotion(); - // if this system is attached to a Drawable/Object, update the current transform - // matrix so generated particles' are relative to the parent Drawable's - // position and orientation - Bool transformSet = false; - const Matrix3D *parentXfrm = nullptr; - Bool isShrouded = false; - - if (m_attachedToDrawableID) - { - Drawable *attachedTo = TheGameClient->findDrawableByID( m_attachedToDrawableID ); - - if (attachedTo) - { - if (attachedTo->getFullyObscuredByShroud()) - isShrouded = true; - - parentXfrm = attachedTo->getTransformMatrix(); - m_lastPos = m_pos; - m_pos = *attachedTo->getPosition(); - } - else - { - // Drawable has been destroyed - lose our attachment to it - m_attachedToDrawableID = INVALID_DRAWABLE_ID; - - // destroy ourselves - destroy(); - } - } - else if (m_attachedToObjectID) - { - Object *objectAttachedTo = TheGameLogic->findObjectByID( m_attachedToObjectID ); - - if (objectAttachedTo) - { - if (!isShrouded) - isShrouded = (objectAttachedTo->getShroudedStatus(localPlayerIndex) >= OBJECTSHROUD_FOGGED); - - const Drawable * draw = objectAttachedTo->getDrawable(); - if ( draw ) - parentXfrm = draw->getTransformMatrix(); - else - parentXfrm = objectAttachedTo->getTransformMatrix(); - - m_lastPos = m_pos; - m_pos = *objectAttachedTo->getPosition(); - } - else - { - // Drawable has been destroyed - lose our attachment to it - m_attachedToObjectID = INVALID_ID; - - // destroy ourselves - destroy(); - } - } - - if (parentXfrm) - { - if (m_skipParentXfrm) - { - //this particle system is already in world space so no need to apply parent xform. - m_transform = m_localTransform; - } - else - { - // if system has its own local transform, concatenate them - if (m_isLocalIdentity == false) - #ifdef ALLOW_TEMPORARIES - m_transform = (*parentXfrm) * m_localTransform; - #else - m_transform.mul(*parentXfrm, m_localTransform); - #endif - else - m_transform = *parentXfrm; - } - - m_isIdentity = false; - transformSet = true; - } - - - if (transformSet == false) - { - if (m_isLocalIdentity == false) - { - m_transform = m_localTransform; - m_isIdentity = false; - } - else - { - m_isIdentity = true; - } - } - - // if we are controlled by a particle, its position is local origin - if (m_controlParticle) - { - const Coord3D *controlPos = m_controlParticle->getPosition(); - /// @todo Concatenate this, instead of overriding (MSB) - m_transform.Set_X_Translation( controlPos->x ); - m_transform.Set_Y_Translation( controlPos->y ); - m_transform.Set_Z_Translation( controlPos->z ); - m_isIdentity = false; - m_lastPos = m_pos; - m_pos = *controlPos; - } + // + // Update shrouding and drawable/object lifetime for the particle system + // + VisibilityState visibilityState = updateVisibility(localPlayerIndex); + // + // Update position and rotation of the particle system + // + updateTransform(); // // Generate new particles if the system hasn't been 'stopped' or 'destroyed' @@ -2036,7 +1955,7 @@ Bool ParticleSystem::update( Int localPlayerIndex ) { if (m_isForever || (m_isForever == false && m_systemLifetimeLeft > 0)) { - if (!isShrouded && m_isStopped == false && m_masterSystem == nullptr) + if (!visibilityState.isShrouded && m_isStopped == false && m_masterSystem == nullptr) { if (m_burstDelayLeft == 0) { @@ -2148,6 +2067,137 @@ Bool ParticleSystem::update( Int localPlayerIndex ) return true; } +// ------------------------------------------------------------------------------------------------ +void ParticleSystem::updateTransform() +{ + // if this system is attached to a Drawable/Object, update the current transform + // matrix so generated particles' are relative to the parent Drawable's + // position and orientation + Bool transformSet = false; + const Matrix3D *parentXfrm = nullptr; + + if (m_attachedToDrawableID) + { + Drawable *attachedTo = TheGameClient->findDrawableByID( m_attachedToDrawableID ); + + if (attachedTo) + { + parentXfrm = attachedTo->getTransformMatrix(); + m_lastPos = m_pos; + m_pos = *attachedTo->getPosition(); + } + } + else if (m_attachedToObjectID) + { + Object *objectAttachedTo = TheGameLogic->findObjectByID( m_attachedToObjectID ); + + if (objectAttachedTo) + { + const Drawable * draw = objectAttachedTo->getDrawable(); + if ( draw ) + parentXfrm = draw->getTransformMatrix(); + else + parentXfrm = objectAttachedTo->getTransformMatrix(); + + m_lastPos = m_pos; + m_pos = *objectAttachedTo->getPosition(); + } + } + + if (parentXfrm) + { + if (m_skipParentXfrm) + { + //this particle system is already in world space so no need to apply parent xform. + m_transform = m_localTransform; + } + else + { + // if system has its own local transform, concatenate them + if (m_isLocalIdentity == false) + #ifdef ALLOW_TEMPORARIES + m_transform = (*parentXfrm) * m_localTransform; + #else + m_transform.mul(*parentXfrm, m_localTransform); + #endif + else + m_transform = *parentXfrm; + } + + m_isIdentity = false; + transformSet = true; + } + + if (transformSet == false) + { + if (m_isLocalIdentity == false) + { + m_transform = m_localTransform; + m_isIdentity = false; + } + else + { + m_isIdentity = true; + } + } + + // if we are controlled by a particle, its position is local origin + if (m_controlParticle) + { + const Coord3D *controlPos = m_controlParticle->getPosition(); + /// @todo Concatenate this, instead of overriding (MSB) + m_transform.Set_X_Translation( controlPos->x ); + m_transform.Set_Y_Translation( controlPos->y ); + m_transform.Set_Z_Translation( controlPos->z ); + m_isIdentity = false; + m_lastPos = m_pos; + m_pos = *controlPos; + } +} + +// ------------------------------------------------------------------------------------------------ +ParticleSystem::VisibilityState ParticleSystem::updateVisibility( Int localPlayerIndex ) +{ + VisibilityState visibilityState; + + if (m_attachedToDrawableID) + { + Drawable *attachedTo = TheGameClient->findDrawableByID( m_attachedToDrawableID ); + + if (attachedTo) + { + visibilityState.isShrouded = attachedTo->getFullyObscuredByShroud(); + } + else + { + // Drawable has been destroyed - lose our attachment to it + m_attachedToDrawableID = INVALID_DRAWABLE_ID; + + // destroy ourselves + destroy(); + } + } + else if (m_attachedToObjectID) + { + Object *objectAttachedTo = TheGameLogic->findObjectByID( m_attachedToObjectID ); + + if (objectAttachedTo) + { + visibilityState.isShrouded = objectAttachedTo->getShroudedStatus(localPlayerIndex) >= OBJECTSHROUD_FOGGED; + } + else + { + // Drawable has been destroyed - lose our attachment to it + m_attachedToObjectID = INVALID_ID; + + // destroy ourselves + destroy(); + } + } + + return visibilityState; +} + // ------------------------------------------------------------------------------------------------ /** Update the wind motion */ // ------------------------------------------------------------------------------------------------ diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/BoneFXUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/BoneFXUpdate.cpp index 0d18b06592f..fa2334b0b44 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/BoneFXUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/BoneFXUpdate.cpp @@ -441,15 +441,14 @@ void BoneFXUpdate::doParticleSystemAtBone(const ParticleSystemTemplate *particle if( lastDamageInfo && getDamageTypeFlag( d->m_damageParticleTypes, lastDamageInfo->in.m_damageType ) == FALSE ) return; - Object *building = getObject(); - ParticleSystem *psys = TheParticleSystemManager->createParticleSystem(particleSystemTemplate); if (psys != nullptr) { + Object *object = getObject(); m_particleSystemIDs.push_back(psys->getSystemID()); psys->setPosition(bonePosition); - psys->attachToObject(building); - Drawable *drawable = building->getDrawable(); + psys->attachToObject(object); + Drawable *drawable = object->getDrawable(); if (drawable && drawable->isDrawableEffectivelyHidden()) { psys->stop(); From 4e0d78a7208bf0d4de3a1add2af7a72511488acf Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Thu, 14 May 2026 18:41:27 +0200 Subject: [PATCH 2/7] fix(particlesys): Fix wrong color clamp in Particle::draw() (#2709) --- Core/GameEngine/Source/GameClient/System/ParticleSys.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp index 5cfc623db87..052696a50da 100644 --- a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp +++ b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp @@ -510,7 +510,7 @@ void Particle::draw() else if (m_color.red > 1.0f) m_color.red = 1.0f; - if (m_color.red < 0.0f) + if (m_color.green < 0.0f) m_color.green = 0.0f; else if (m_color.green > 1.0f) m_color.green = 1.0f; From 366a691daa7c63145154b30780f1d22fd9e86df7 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Thu, 14 May 2026 18:48:14 +0200 Subject: [PATCH 3/7] refactor(particlesys): Simplify color arithmetic and clamping in Particle::draw() (#2709) --- .../Source/GameClient/System/ParticleSys.cpp | 43 ++----- Core/Libraries/Include/Lib/BaseType.h | 120 ++++++++++++++++++ 2 files changed, 132 insertions(+), 31 deletions(-) diff --git a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp index 052696a50da..d0b9ed39367 100644 --- a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp +++ b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp @@ -390,12 +390,11 @@ Bool Particle::update() } } else + { m_alphaRate = 0.0f; + } - if (m_alpha < 0.0f) - m_alpha = 0.0f; - else if (m_alpha > 1.0f) - m_alpha = 1.0f; + m_alpha = clamp(0.0f, m_alpha, 1.0f); } // @@ -427,6 +426,7 @@ Bool Particle::update() // if we've gone totally invisible, destroy ourselves if (isInvisible()) return false; + return true; } @@ -467,7 +467,8 @@ void Particle::draw() #endif m_angularRateZ *= m_angularDamping; - if (m_particleUpTowardsEmitter) { + if (m_particleUpTowardsEmitter) + { // adjust the up position back towards the particle static const Coord2D upVec = { 0.0f, 1.0f }; Coord2D emitterDir; @@ -486,40 +487,20 @@ void Particle::draw() if (m_system->getShaderType() != ParticleSystemInfo::ADDITIVE) { m_alpha += m_alphaRate; - - if (m_alpha < 0.0f) - m_alpha = 0.0f; - else if (m_alpha > 1.0f) - m_alpha = 1.0f; + m_alpha = clamp(0.0f, m_alpha, 1.0f); } // // Update color // - m_color.red += m_colorRate.red; - m_color.green += m_colorRate.green; - m_color.blue += m_colorRate.blue; + m_color += m_colorRate; /// @todo Rethink this - at least its name - m_color.red += m_colorScale; - m_color.green += m_colorScale; - m_color.blue += m_colorScale; - - if (m_color.red < 0.0f) - m_color.red = 0.0f; - else if (m_color.red > 1.0f) - m_color.red = 1.0f; - - if (m_color.green < 0.0f) - m_color.green = 0.0f; - else if (m_color.green > 1.0f) - m_color.green = 1.0f; - - if (m_color.blue < 0.0f) - m_color.blue = 0.0f; - else if (m_color.blue > 1.0f) - m_color.blue = 1.0f; + m_color += m_colorScale; + m_color.red = clamp(0.0f, m_color.red, 1.0f); + m_color.green = clamp(0.0f, m_color.green, 1.0f); + m_color.blue = clamp(0.0f, m_color.blue, 1.0f); // reset the acceleration for accumulation next frame m_accel.x = 0.0f; diff --git a/Core/Libraries/Include/Lib/BaseType.h b/Core/Libraries/Include/Lib/BaseType.h index cbe51e3e995..0b4d7063da3 100644 --- a/Core/Libraries/Include/Lib/BaseType.h +++ b/Core/Libraries/Include/Lib/BaseType.h @@ -635,6 +635,126 @@ struct RGBColor blue = ((c >> 0) & 0xff) / 255.0f; } + RGBColor& operator+=(const RGBColor& c) + { + red += c.red; + green += c.green; + blue += c.blue; + return *this; + } + + RGBColor& operator-=(const RGBColor& c) + { + red -= c.red; + green -= c.green; + blue -= c.blue; + return *this; + } + + RGBColor& operator*=(const RGBColor& c) + { + red *= c.red; + green *= c.green; + blue *= c.blue; + return *this; + } + + RGBColor& operator/=(const RGBColor& c) + { + red /= c.red; + green /= c.green; + blue /= c.blue; + return *this; + } + + RGBColor operator+(const RGBColor& c) const + { + RGBColor res = *this; + res += c; + return res; + } + + RGBColor operator-(const RGBColor& c) const + { + RGBColor res = *this; + res -= c; + return res; + } + + RGBColor operator*(const RGBColor& c) const + { + RGBColor res = *this; + res *= c; + return res; + } + + RGBColor operator/(const RGBColor& c) const + { + RGBColor res = *this; + res /= c; + return res; + } + + RGBColor& operator+=(Real s) + { + red += s; + green += s; + blue += s; + return *this; + } + + RGBColor& operator-=(Real s) + { + red -= s; + green -= s; + blue -= s; + return *this; + } + + RGBColor& operator*=(Real s) + { + red *= s; + green *= s; + blue *= s; + return *this; + } + + RGBColor& operator/=(Real s) + { + red /= s; + green /= s; + blue /= s; + return *this; + } + + RGBColor operator+(Real s) const + { + RGBColor res = *this; + res += s; + return res; + } + + RGBColor operator-(Real s) const + { + RGBColor res = *this; + res -= s; + return res; + } + + RGBColor operator*(Real s) const + { + RGBColor res = *this; + res *= s; + return res; + } + + RGBColor operator/(Real s) const + { + RGBColor res = *this; + res /= s; + return res; + } + }; struct RGBAColorReal From 05e3574cf6a43a261639d3e2f95814e0b735adfa Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Fri, 15 May 2026 11:53:59 +0200 Subject: [PATCH 4/7] fix(particlesys): Simplify ParticleSystemManagerDummy setup (#2709) --- Core/GameEngine/Include/GameClient/ParticleSys.h | 10 +++++++++- Core/GameEngine/Source/Common/ReplaySimulation.cpp | 2 -- .../GameClient/Drawable/Update/BeaconClientUpdate.cpp | 6 ++---- Core/GameEngine/Source/GameClient/FXList.cpp | 2 +- .../Code/GameEngine/Include/GameClient/GameClient.h | 2 -- .../Code/GameEngine/Source/GameClient/GameClient.cpp | 9 --------- 6 files changed, 12 insertions(+), 19 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index 585dc786e4a..6fbddc3bcad 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -766,6 +766,8 @@ class ParticleSystemManager : public SubsystemInterface, virtual Int getOnScreenParticleCount() = 0; ///< returns the number of particles on screen virtual void setOnScreenParticleCount(int count); + virtual Bool isDummy() const { return false; } + ParticleSystemTemplate *findTemplate( const AsciiString &name ) const; ParticleSystemTemplate *findParentTemplate( const AsciiString &name, int parentNum ) const; ParticleSystemTemplate *newTemplate( const AsciiString &name ); @@ -846,14 +848,20 @@ class ParticleSystemManager : public SubsystemInterface, }; // TheSuperHackers @feature bobtista 31/01/2026 -// ParticleSystemManager that does nothing. Used for Headless Mode. +// ParticleSystemManager that does nothing. Cannot create particle systems and templates. Used for Headless Mode. class ParticleSystemManagerDummy : public ParticleSystemManager { public: + virtual void init() override {} + virtual void reset() override {} + virtual void update() override {} + virtual Int getOnScreenParticleCount() override { return 0; } virtual void doParticles(RenderInfoClass &rinfo) override {} virtual void queueParticleRender() override {} + virtual Bool isDummy() const override { return true; } + protected: virtual void crc( Xfer *xfer ) override {} virtual void xfer( Xfer *xfer ) override {} diff --git a/Core/GameEngine/Source/Common/ReplaySimulation.cpp b/Core/GameEngine/Source/Common/ReplaySimulation.cpp index 7d18b5cb58f..e6871769a94 100644 --- a/Core/GameEngine/Source/Common/ReplaySimulation.cpp +++ b/Core/GameEngine/Source/Common/ReplaySimulation.cpp @@ -86,8 +86,6 @@ int ReplaySimulation::simulateReplaysInThisProcess(const std::vectorgetPlaybackFrameCount() / LOGICFRAMES_PER_SECOND; while (TheRecorder->isPlaybackInProgress()) { - TheGameClient->updateHeadless(); - const int progressFrameInterval = 10*60*LOGICFRAMES_PER_SECOND; if (TheGameLogic->getFrame() != 0 && TheGameLogic->getFrame() % progressFrameInterval == 0) { diff --git a/Core/GameEngine/Source/GameClient/Drawable/Update/BeaconClientUpdate.cpp b/Core/GameEngine/Source/GameClient/Drawable/Update/BeaconClientUpdate.cpp index c76e3dc3528..70b53f3b822 100644 --- a/Core/GameEngine/Source/GameClient/Drawable/Update/BeaconClientUpdate.cpp +++ b/Core/GameEngine/Source/GameClient/Drawable/Update/BeaconClientUpdate.cpp @@ -94,9 +94,7 @@ static ParticleSystem* createParticleSystem( Drawable *draw ) AsciiString templateName; templateName.format("BeaconSmoke%6.6X", (0xffffff & obj->getIndicatorColor())); const ParticleSystemTemplate *particleTemplate = TheParticleSystemManager->findTemplate( templateName ); - - DEBUG_ASSERTCRASH(particleTemplate, ("Could not find particle system %s", templateName.str())); - + DEBUG_ASSERTCRASH(TheParticleSystemManager->isDummy() || particleTemplate, ("Could not find particle system %s", templateName.str())); if (particleTemplate) { system = TheParticleSystemManager->createParticleSystem( particleTemplate ); @@ -107,7 +105,7 @@ static ParticleSystem* createParticleSystem( Drawable *draw ) {// THis this will whip up a new particle system to match the house color provided templateName.format("BeaconSmokeFFFFFF"); const ParticleSystemTemplate *failsafeTemplate = TheParticleSystemManager->findTemplate( templateName ); - DEBUG_ASSERTCRASH(failsafeTemplate, ("Doh, this is bad \n I Could not even find the white particle system to make a failsafe system out of.")); + DEBUG_ASSERTCRASH(TheParticleSystemManager->isDummy() || failsafeTemplate, ("Doh, this is bad \n I Could not even find the white particle system to make a failsafe system out of.")); if (failsafeTemplate) { system = TheParticleSystemManager->createParticleSystem( failsafeTemplate ); diff --git a/Core/GameEngine/Source/GameClient/FXList.cpp b/Core/GameEngine/Source/GameClient/FXList.cpp index 50ab4135ae0..a9f630638fd 100644 --- a/Core/GameEngine/Source/GameClient/FXList.cpp +++ b/Core/GameEngine/Source/GameClient/FXList.cpp @@ -600,7 +600,7 @@ class ParticleSystemFXNugget : public FXNugget } const ParticleSystemTemplate *tmp = TheParticleSystemManager->findTemplate(m_name); - DEBUG_ASSERTCRASH(tmp, ("ParticleSystem %s not found",m_name.str())); + DEBUG_ASSERTCRASH(TheParticleSystemManager->isDummy() || tmp, ("ParticleSystem %s not found",m_name.str())); if (tmp) { for (Int i = 0; i < m_count; i++ ) diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h b/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h index ea8e3a59c5d..3650be5ee24 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h @@ -98,8 +98,6 @@ class GameClient : public SubsystemInterface, void step(); ///< Do one fixed time step - void updateHeadless(); - void addDrawableToLookupTable( Drawable *draw ); ///< add drawable ID to hash lookup table void removeDrawableFromLookupTable( Drawable *draw ); ///< remove drawable ID from hash lookup table diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp index cb91389555b..d1c956e533a 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp @@ -787,15 +787,6 @@ void GameClient::step() TheDisplay->step(); } -void GameClient::updateHeadless() -{ - // TheSuperHackers @info helmutbuhler 03/05/2025 bobtista 02/02/2026 - // Update particles to prevent accumulation in headless mode. Particles are generated - // during GameLogic and only cleaned up during rendering. update() lets particles finish - // their lifecycle naturally instead of abruptly removing them with reset(). - TheParticleSystemManager->update(); -} - Bool GameClient::isMovieAbortRequested() { if (TheGameEngine) From 0196604d1e662991c1dd772d05fec443ddc8cbab Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sun, 17 May 2026 13:19:09 +0200 Subject: [PATCH 5/7] tweak(particlesys): Remove unused class member Particle::m_lastPos (#2709) --- Core/GameEngine/Include/GameClient/ParticleSys.h | 1 - .../Source/GameClient/System/ParticleSys.cpp | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index 6fbddc3bcad..14272b55055 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -228,7 +228,6 @@ class Particle : public MemoryPoolObject, // most of the particle data is derived from ParticleInfo Coord3D m_accel; ///< current acceleration - Coord3D m_lastPos; ///< previous position UnsignedInt m_lifetimeLeft; ///< lifetime remaining, if zero -> destroy UnsignedInt m_createTimestamp; ///< frame this particle was created diff --git a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp index d0b9ed39367..568597a9146 100644 --- a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp +++ b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp @@ -282,7 +282,6 @@ Particle::Particle( ParticleSystem *system, const ParticleInfo *info ) #endif m_angleZ = info->m_angleZ; - m_lastPos.zero(); m_windRandomness = info->m_windRandomness; m_particleUpTowardsEmitter = info->m_particleUpTowardsEmitter; m_emitterPos = info->m_emitterPos; @@ -653,13 +652,19 @@ void Particle::crc( Xfer *xfer ) // ------------------------------------------------------------------------------------------------ /** Xfer method * Version Info: - * 1: Initial version */ + * 1: Initial version + * 2: TheSuperHackers @tweak Removed unused m_lastPos + */ // ------------------------------------------------------------------------------------------------ void Particle::xfer( Xfer *xfer ) { // version +#if RETAIL_COMPATIBLE_XFER_SAVE XferVersion currentVersion = 1; +#else + XferVersion currentVersion = 2; +#endif XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -673,7 +678,11 @@ void Particle::xfer( Xfer *xfer ) xfer->xferCoord3D( &m_accel ); // last position - xfer->xferCoord3D( &m_lastPos ); + if (version <= 1) + { + Coord3D m_lastPos; + xfer->xferCoord3D( &m_lastPos ); + } // lifetime left xfer->xferUnsignedInt( &m_lifetimeLeft ); From 9d7d62e14642701f53b37e5175ebac6d37900a03 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sun, 17 May 2026 13:26:31 +0200 Subject: [PATCH 6/7] tweak(particlesys): Simplify and improve transform update of particle systems (#2709) --- .../Include/GameClient/ParticleSys.h | 12 +- .../Source/GameClient/System/ParticleSys.cpp | 154 +++++++++++------- 2 files changed, 103 insertions(+), 63 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index 14272b55055..6b46c3d480c 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -678,6 +678,12 @@ class ParticleSystem : public MemoryPoolObject, Bool forceCreate = FALSE ); ///< factory method for particles void updateTransform(); + void updateParentTransform(const Matrix3D &parentXfrm); + void updateLocalTransform(); + + void updateLogicalPos(); + void updateLastLogicalPos(); + VisibilityState updateVisibility( Int localPlayerIndex ); const ParticleInfo *generateParticleInfo( Int particleNum, Int particleCount ); ///< generate a new, random set of ParticleInfo @@ -713,8 +719,10 @@ class ParticleSystem : public MemoryPoolObject, Real m_delayCoeff; ///< scalar value multiplied by burst delay Real m_sizeCoeff; ///< scalar value multiplied by initial size - Coord3D m_pos; ///< this is the position to emit at. - Coord3D m_lastPos; ///< this is the previous position we emitted at. + Coord3D m_logicalPos; ///< this is the current logic position to emit at. + ///< Can be different from the actual emitter transform + ///< if the render update is faster than the logic step. + Coord3D m_lastLogicalPos; ///< this is the previous logic position we emitted at. ParticleSystem * m_slaveSystem; ///< if non-null, another system this one has control of ParticleSystemID m_slaveSystemID; ///< id of slave system (if present) diff --git a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp index 568597a9146..056045fb51d 100644 --- a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp +++ b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp @@ -1081,8 +1081,9 @@ ParticleSystem::ParticleSystem( const ParticleSystemTemplate *sysTemplate, m_template = sysTemplate; m_systemID = id; - m_lastPos.zero(); - m_pos.zero(); + m_logicalPos.zero(); + m_lastLogicalPos.zero(); + m_velCoeff.zero(); m_velCoeff.zero(); m_attachedToDrawableID = INVALID_DRAWABLE_ID; @@ -1806,17 +1807,15 @@ const ParticleInfo *ParticleSystem::generateParticleInfo( Int particleNum, Int p // transform particle position to world coordinates Vector3 p, pr; - Coord3D emissionAdjustment; // this is the adjustment for inter-frame emission - // @todo : This should work, if m_lastPos = m_pos is removed from here but it doesn't. - // @todo : Investigate why. jkmcd - if (m_isFirstPos) { - m_lastPos = m_pos; - m_isFirstPos = false; - } + Coord3D frameDeltaPos; + frameDeltaPos.x = m_logicalPos.x - m_lastLogicalPos.x; + frameDeltaPos.y = m_logicalPos.y - m_lastLogicalPos.y; + frameDeltaPos.z = m_logicalPos.z - m_lastLogicalPos.z; - emissionAdjustment.x = (1 - (INT_TO_REAL(particleNum) / particleCount)) * (m_pos.x - m_lastPos.x); - emissionAdjustment.y = (1 - (INT_TO_REAL(particleNum) / particleCount)) * (m_pos.y - m_lastPos.y); - emissionAdjustment.z = (1 - (INT_TO_REAL(particleNum) / particleCount)) * (m_pos.z - m_lastPos.z); + Coord3D emissionAdjustment; // this is the adjustment for inter-frame emission + emissionAdjustment.x = (1 - (INT_TO_REAL(particleNum) / particleCount)) * frameDeltaPos.x; + emissionAdjustment.y = (1 - (INT_TO_REAL(particleNum) / particleCount)) * frameDeltaPos.y; + emissionAdjustment.z = (1 - (INT_TO_REAL(particleNum) / particleCount)) * frameDeltaPos.z; p.X = info.m_pos.x; p.Y = info.m_pos.y; @@ -1935,7 +1934,9 @@ Bool ParticleSystem::update( Int localPlayerIndex ) // // Update position and rotation of the particle system // + updateLastLogicalPos(); updateTransform(); + updateLogicalPos(); // // Generate new particles if the system hasn't been 'stopped' or 'destroyed' @@ -2060,21 +2061,31 @@ Bool ParticleSystem::update( Int localPlayerIndex ) // ------------------------------------------------------------------------------------------------ void ParticleSystem::updateTransform() { + if (m_controlParticle) + { + // if we are controlled by a particle, its position is local origin + const Coord3D *controlPos = m_controlParticle->getPosition(); + m_transform.Set_X_Translation( controlPos->x ); + m_transform.Set_Y_Translation( controlPos->y ); + m_transform.Set_Z_Translation( controlPos->z ); + m_isIdentity = false; + return; + } + // if this system is attached to a Drawable/Object, update the current transform // matrix so generated particles' are relative to the parent Drawable's - // position and orientation - Bool transformSet = false; - const Matrix3D *parentXfrm = nullptr; - + // position and orientation, otherwise use the local transform. if (m_attachedToDrawableID) { Drawable *attachedTo = TheGameClient->findDrawableByID( m_attachedToDrawableID ); if (attachedTo) { - parentXfrm = attachedTo->getTransformMatrix(); - m_lastPos = m_pos; - m_pos = *attachedTo->getPosition(); + updateParentTransform( *attachedTo->getTransformMatrix() ); + } + else + { + updateLocalTransform(); } } else if (m_attachedToObjectID) @@ -2085,66 +2096,87 @@ void ParticleSystem::updateTransform() { const Drawable * draw = objectAttachedTo->getDrawable(); if ( draw ) - parentXfrm = draw->getTransformMatrix(); + { + updateParentTransform( *draw->getTransformMatrix() ); + } else - parentXfrm = objectAttachedTo->getTransformMatrix(); - - m_lastPos = m_pos; - m_pos = *objectAttachedTo->getPosition(); - } - } - - if (parentXfrm) - { - if (m_skipParentXfrm) - { - //this particle system is already in world space so no need to apply parent xform. - m_transform = m_localTransform; + { + updateParentTransform( *objectAttachedTo->getTransformMatrix() ); + } } else { - // if system has its own local transform, concatenate them - if (m_isLocalIdentity == false) - #ifdef ALLOW_TEMPORARIES - m_transform = (*parentXfrm) * m_localTransform; - #else - m_transform.mul(*parentXfrm, m_localTransform); - #endif - else - m_transform = *parentXfrm; + updateLocalTransform(); } - - m_isIdentity = false; - transformSet = true; } + else + { + updateLocalTransform(); + } +} - if (transformSet == false) +// ------------------------------------------------------------------------------------------------ +void ParticleSystem::updateParentTransform(const Matrix3D &parentXfrm) +{ + if (m_skipParentXfrm) { - if (m_isLocalIdentity == false) + //this particle system is already in world space so no need to apply parent xform. + updateLocalTransform(); + } + else + { + // if system has its own local transform, concatenate them + if (!m_isLocalIdentity) { - m_transform = m_localTransform; - m_isIdentity = false; +#ifdef ALLOW_TEMPORARIES + m_transform = parentXfrm * m_localTransform; +#else + m_transform.mul(parentXfrm, m_localTransform); +#endif } else { - m_isIdentity = true; + m_transform = parentXfrm; } + + m_isIdentity = false; } +} - // if we are controlled by a particle, its position is local origin - if (m_controlParticle) +// ------------------------------------------------------------------------------------------------ +void ParticleSystem::updateLocalTransform() +{ + if (!m_isLocalIdentity) { - const Coord3D *controlPos = m_controlParticle->getPosition(); - /// @todo Concatenate this, instead of overriding (MSB) - m_transform.Set_X_Translation( controlPos->x ); - m_transform.Set_Y_Translation( controlPos->y ); - m_transform.Set_Z_Translation( controlPos->z ); + m_transform = m_localTransform; m_isIdentity = false; - m_lastPos = m_pos; - m_pos = *controlPos; + } + else + { + m_isIdentity = true; } } +// ------------------------------------------------------------------------------------------------ +void ParticleSystem::updateLogicalPos() +{ + m_logicalPos.x = m_transform.Get_X_Translation(); + m_logicalPos.y = m_transform.Get_Y_Translation(); + m_logicalPos.z = m_transform.Get_Z_Translation(); + + if (m_isFirstPos) { + // On the very first update, initialize last pos to the first position. + m_lastLogicalPos = m_logicalPos; + m_isFirstPos = false; + } +} + +// ------------------------------------------------------------------------------------------------ +void ParticleSystem::updateLastLogicalPos() +{ + m_lastLogicalPos = m_logicalPos; +} + // ------------------------------------------------------------------------------------------------ ParticleSystem::VisibilityState ParticleSystem::updateVisibility( Int localPlayerIndex ) { @@ -2566,10 +2598,10 @@ void ParticleSystem::xfer( Xfer *xfer ) xfer->xferReal( &m_sizeCoeff ); // position - xfer->xferCoord3D( &m_pos ); + xfer->xferCoord3D( &m_logicalPos ); // last position - xfer->xferCoord3D( &m_lastPos ); + xfer->xferCoord3D( &m_lastLogicalPos ); // is first pos xfer->xferBool( &m_isFirstPos ); From b86a504e6321d2a7bc5893558694877e43f760ed Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sun, 17 May 2026 13:37:47 +0200 Subject: [PATCH 7/7] tweak(particlesys): Decouple Particles render update from logic step (#2709) --- Core/GameEngine/Include/Common/GameDefines.h | 4 + .../Include/GameClient/ParticleSys.h | 16 +- .../Source/GameClient/System/ParticleSys.cpp | 207 +++++++++++------- .../Source/GameClient/GameClient.cpp | 6 +- .../Source/GameLogic/System/GameLogic.cpp | 6 +- .../W3DDevice/GameClient/W3DDisplay.cpp | 3 +- 6 files changed, 153 insertions(+), 89 deletions(-) diff --git a/Core/GameEngine/Include/Common/GameDefines.h b/Core/GameEngine/Include/Common/GameDefines.h index 1bf9f096fcf..73f5c22f541 100644 --- a/Core/GameEngine/Include/Common/GameDefines.h +++ b/Core/GameEngine/Include/Common/GameDefines.h @@ -83,6 +83,10 @@ #define PRESERVE_RETAIL_SCRIPTED_CAMERA (1) // Retain scripted camera behavior present in retail Generals 1.08 and Zero Hour 1.04 #endif +#ifndef PRESERVE_RETAIL_PARTICLES +#define PRESERVE_RETAIL_PARTICLES (1) // Preserve original look of particles 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/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index 6b46c3d480c..7cc49689903 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -180,8 +180,8 @@ class Particle : public MemoryPoolObject, Bool update(); ///< update this particle's behavior - return false if dead - void draw(); ///< render update - void doWindMotion(); ///< do wind motion (if present) from particle system + void draw( Real timeScale ); ///< render update + void doWindMotion( Real timeScale ); ///< do wind motion (if present) from particle system void applyForce( const Coord3D *force ); ///< add the given acceleration @@ -578,7 +578,9 @@ class ParticleSystem : public MemoryPoolObject, void attachToObject( const Object *obj ); ///< attach this particle system to an Object virtual Bool update( Int localPlayerIndex ); ///< update this particle system, return false if dead - void updateWindMotion(); ///< update wind motion + + void draw( Real timeScale ); ///< render update + void updateWindMotion( Real timeScale ); ///< update wind motion void setControlParticle( Particle *p ); ///< set control particle @@ -752,6 +754,9 @@ class ParticleSystem : public MemoryPoolObject, /** * The particle system manager, responsible for maintaining all ParticleSystems */ +// TheSuperHacker @tweak The particle render update is now decoupled from the logic step. +// The lifetime management remains coupled to the logic step. +// class ParticleSystemManager : public SubsystemInterface, public Snapshot { @@ -768,7 +773,8 @@ class ParticleSystemManager : public SubsystemInterface, virtual void init() override; ///< initialize the manager virtual void reset() override; ///< reset the manager and all particle systems - virtual void update() override; ///< update all particle systems + virtual void update() override; ///< logic update for all particle systems + virtual void draw() override; ///< render update for all particle systems virtual Int getOnScreenParticleCount() = 0; ///< returns the number of particles on screen virtual void setOnScreenParticleCount(int count); @@ -846,7 +852,6 @@ class ParticleSystemManager : public SubsystemInterface, UnsignedInt m_fieldParticleCount; ///< this does not need to be xfered, since it is evaluated every frame UnsignedInt m_particleSystemCount; Int m_onScreenParticleCount; ///< number of particles displayed on screen per frame - UnsignedInt m_lastLogicFrameUpdate; Int m_localPlayerIndex; ///m_lifetime; m_lifetimeLeft = info->m_lifetime; - m_createTimestamp = TheGameClient->getFrame(); + m_createTimestamp = TheGameLogic->getFrame(); m_personality = 0; m_size = info->m_size; @@ -372,7 +373,29 @@ void Particle::applyForce( const Coord3D *force ) // ------------------------------------------------------------------------------------------------ Bool Particle::update() { - draw(); + // monitor lifetime + if (m_lifetimeLeft && --m_lifetimeLeft == 0) + return false; + + DEBUG_ASSERTCRASH( m_lifetimeLeft, ( "A particle has an infinite lifetime..." )); + + const UnsignedInt frameCount = TheGameLogic->getFrame() - m_createTimestamp; + + if (frameCount == 0) + { + // TheSuperHackers @info Pass one full logic frame before trying to update and delete this potentially now + // invisible particle, because the later render update may fade it in and make it visible. + return true; + } + +#if PRESERVE_RETAIL_PARTICLES + // This delay is required to preserve the look of the original particle color and alpha key frames, + // because originally the color and alpha rates were accumulated before the key frames advanced. + // This setup can cause visual glitches, such as greenish flames with Dragon Tanks and Inferno Cannons. + constexpr const UnsignedInt KeyFrameDelay = 1; +#else + constexpr const UnsignedInt KeyFrameDelay = 0; +#endif // // Update alpha (if used) @@ -381,9 +404,8 @@ Bool Particle::update() { if (m_alphaTargetKey < MAX_KEYFRAMES && m_alphaKey[ m_alphaTargetKey ].frame) { - if (TheGameClient->getFrame() - m_createTimestamp >= m_alphaKey[ m_alphaTargetKey ].frame) + if (frameCount >= m_alphaKey[ m_alphaTargetKey ].frame + KeyFrameDelay) { - m_alpha = m_alphaKey[ m_alphaTargetKey ].value; m_alphaTargetKey++; computeAlphaRate(); } @@ -392,8 +414,6 @@ Bool Particle::update() { m_alphaRate = 0.0f; } - - m_alpha = clamp(0.0f, m_alpha, 1.0f); } // @@ -401,10 +421,8 @@ Bool Particle::update() // if (m_colorTargetKey < MAX_KEYFRAMES && m_colorKey[ m_colorTargetKey ].frame) { - if (TheGameClient->getFrame() - m_createTimestamp >= m_colorKey[ m_colorTargetKey ].frame) + if (frameCount >= m_colorKey[ m_colorTargetKey ].frame + KeyFrameDelay) { - // can't set, because of colorscale - // m_color = m_colorKey[ m_colorTargetKey ].color; m_colorTargetKey++; computeColorRate(); } @@ -416,13 +434,8 @@ Bool Particle::update() m_colorRate.blue = 0.0f; } - // monitor lifetime - if (m_lifetimeLeft && --m_lifetimeLeft == 0) - return false; - - DEBUG_ASSERTCRASH( m_lifetimeLeft, ( "A particle has an infinite lifetime..." )); - // if we've gone totally invisible, destroy ourselves + // TheSuperHackers @todo This check is shady for particles that fade in first. A more robust logic would be good. if (isInvisible()) return false; @@ -430,41 +443,68 @@ Bool Particle::update() } // ------------------------------------------------------------------------------------------------ -void Particle::draw() +// Get frame-rate independent damping from fixed-time-step damping. +// Example: +// damping = 0.95 +// timeScale = 0.5 -> sqrt(0.95) +// timeScale = 2.0 -> 0.95^2 +// +static inline Real scaleDamping(Real damping, Real timeScale) { - // integrate acceleration into velocity - m_vel.x += m_accel.x; - m_vel.y += m_accel.y; - m_vel.z += m_accel.z; + return pow(damping, timeScale); +} + +// ------------------------------------------------------------------------------------------------ +// Get frame-rate independent equivalent of: +// vel += accel; +// vel *= damping; +// +static inline Real scaleAccelDamping(Real damping, Real timeScale) +{ + if (fabs(damping - 1.0f) < 0.00001f) + return timeScale; + + const Real decay = pow(damping, timeScale); - m_vel.x *= m_velDamping; - m_vel.y *= m_velDamping; - m_vel.z *= m_velDamping; + return damping * (1.0f - decay) / (1.0f - damping); +} + +// ------------------------------------------------------------------------------------------------ +void Particle::draw(Real timeScale) +{ + // integrate acceleration into velocity + const Real velDecay = scaleDamping(m_velDamping, timeScale); + const Real accelDecay = scaleAccelDamping(m_velDamping, timeScale); + m_vel.x = m_vel.x * velDecay + m_accel.x * accelDecay; + m_vel.y = m_vel.y * velDecay + m_accel.y * accelDecay; + m_vel.z = m_vel.z * velDecay + m_accel.z * accelDecay; // integrate velocity into position const Coord3D *driftVel = m_system->getDriftVelocity(); - m_pos.x += m_vel.x + driftVel->x; - m_pos.y += m_vel.y + driftVel->y; - m_pos.z += m_vel.z + driftVel->z; + m_pos.x += (m_vel.x + driftVel->x) * timeScale; + m_pos.y += (m_vel.y + driftVel->y) * timeScale; + m_pos.z += (m_vel.z + driftVel->z) * timeScale; // integrate the wind (if specified) into position ParticleSystemInfo::WindMotion windMotion = m_system->getWindMotion(); // see if we should even do anything if( windMotion != ParticleSystemInfo::WIND_MOTION_NOT_USED ) - doWindMotion(); + doWindMotion(timeScale); // update orientation #if PARTICLE_USE_XY_ROTATION - m_angleX += m_angularRateX; - m_angleY += m_angularRateY; + m_angleX += m_angularRateX * timeScale; + m_angleY += m_angularRateY * timeScale; #endif - m_angleZ += m_angularRateZ; + m_angleZ += m_angularRateZ * timeScale; + + const Real angularDecay = scaleDamping(m_angularDamping, timeScale); #if PARTICLE_USE_XY_ROTATION - m_angularRateX *= m_angularDamping; - m_angularRateY *= m_angularDamping; + m_angularRateX *= angularDecay; + m_angularRateY *= angularDecay; #endif - m_angularRateZ *= m_angularDamping; + m_angularRateZ *= angularDecay; if (m_particleUpTowardsEmitter) { @@ -477,25 +517,26 @@ void Particle::draw() } // update size - m_size += m_sizeRate; - m_sizeRate *= m_sizeRateDamping; + m_size += m_sizeRate * timeScale; + const Real sizeDecay = scaleDamping(m_sizeRateDamping, timeScale); + m_sizeRate *= sizeDecay; // // Update alpha (if used) // if (m_system->getShaderType() != ParticleSystemInfo::ADDITIVE) { - m_alpha += m_alphaRate; + m_alpha += m_alphaRate * timeScale; m_alpha = clamp(0.0f, m_alpha, 1.0f); } // // Update color // - m_color += m_colorRate; + m_color += m_colorRate * timeScale; /// @todo Rethink this - at least its name - m_color += m_colorScale; + m_color += m_colorScale * timeScale; m_color.red = clamp(0.0f, m_color.red, 1.0f); m_color.green = clamp(0.0f, m_color.green, 1.0f); @@ -510,7 +551,7 @@ void Particle::draw() // ------------------------------------------------------------------------------------------------ /** Do wind motion as specified by the particle system template, if present */ // ------------------------------------------------------------------------------------------------ -void Particle::doWindMotion() +void Particle::doWindMotion(Real timeScale) { // get the angle of the wind @@ -574,7 +615,7 @@ void Particle::doWindMotion() Real distFromWind = v.length(); if( distFromWind < noForceDistance ) { - Real windForceStrength = 2.0f * m_windRandomness; + Real windForceStrength = 2.0f * m_windRandomness * timeScale; // only apply force if still within the circle of influence if( distFromWind > fullForceDistance ) @@ -1145,14 +1186,14 @@ ParticleSystem::ParticleSystem( const ParticleSystemTemplate *sysTemplate, m_delayLeft = (UnsignedInt)sysTemplate->m_initialDelay.getValue(); - m_startTimestamp = TheGameClient->getFrame(); + m_startTimestamp = TheGameLogic->getFrame(); m_systemLifetimeLeft = sysTemplate->m_systemLifetime; if (sysTemplate->m_systemLifetime) m_isForever = false; else m_isForever = true; - m_accumulatedSizeBonus = 0; + m_accumulatedSizeBonus = 0.0f; m_velDamping = sysTemplate->m_velDamping; @@ -1750,7 +1791,7 @@ Particle *ParticleSystem::createParticle( const ParticleInfo *info, // // Check if particle is below priorities we allow for this FPS or if it being skipped because - // all particesl are being skipped (excluding special fps independent particles at + // all particles are being skipped (excluding special fps independent particles at // getMinDynamicParticleSkipPriority()) // if( priority < TheGameLODManager->getMinDynamicParticlePriority() || @@ -1917,15 +1958,11 @@ Bool ParticleSystem::update( Int localPlayerIndex ) // system actually "starts" once initial delay is over /// @todo reset start time when system is stopped/started if (m_delayLeft == 0) - m_startTimestamp = TheGameClient->getFrame(); + m_startTimestamp = TheGameLogic->getFrame(); return true; } - // update the wind motion - if (m_windMotion != ParticleSystemInfo::WIND_MOTION_NOT_USED ) - updateWindMotion(); - // // Update shrouding and drawable/object lifetime for the particle system // @@ -1944,7 +1981,7 @@ Bool ParticleSystem::update( Int localPlayerIndex ) // if (m_isDestroyed == false) { - if (m_isForever || (m_isForever == false && m_systemLifetimeLeft > 0)) + if (m_isForever || m_systemLifetimeLeft > 0) { if (!visibilityState.isShrouded && m_isStopped == false && m_masterSystem == nullptr) { @@ -2010,17 +2047,6 @@ Bool ParticleSystem::update( Int localPlayerIndex ) Particle *oldParticle; while (p) { - - // apply 'gravity' force - if (m_gravity != 0.0f) - { - Coord3D force; - force.x = 0.0f; - force.y = 0.0f; - force.z = m_gravity; - p->applyForce( &force ); - } - if (p->update() == false) { oldParticle = p; @@ -2038,7 +2064,6 @@ Bool ParticleSystem::update( Int localPlayerIndex ) if (m_isDestroyed && !m_systemParticlesHead) return false; - // monitor particle system lifetime if (m_isForever == false) { @@ -2220,10 +2245,39 @@ ParticleSystem::VisibilityState ParticleSystem::updateVisibility( Int localPlaye return visibilityState; } +// ------------------------------------------------------------------------------------------------ +void ParticleSystem::draw(Real timeScale) +{ + if (TheGlobalData->m_useFX == FALSE) + return; + + if (m_windMotion != ParticleSystemInfo::WIND_MOTION_NOT_USED ) + updateWindMotion(timeScale); + + updateTransform(); + + Particle *p = m_systemParticlesHead; + while (p) + { + // apply 'gravity' force + if (m_gravity != 0.0f) + { + Coord3D force; + force.x = 0.0f; + force.y = 0.0f; + force.z = m_gravity; + p->applyForce( &force ); + } + + p->draw(timeScale); + p = p->m_systemNext; + } +} + // ------------------------------------------------------------------------------------------------ /** Update the wind motion */ // ------------------------------------------------------------------------------------------------ -void ParticleSystem::updateWindMotion() +void ParticleSystem::updateWindMotion(Real timeScale) { switch( m_windMotion ) @@ -2250,7 +2304,7 @@ void ParticleSystem::updateWindMotion() // the angle. When we are closer to the center we change it faster (more), and when // we are near the edges we change is slower (less) // - Real change = (1.0f - (diffFromCenter / halfSpan)) * m_windAngleChange; + Real change = (1.0f - (diffFromCenter / halfSpan)) * m_windAngleChange * timeScale; // we will always change a little bit #define MINIMUM_CHANGE 0.005f // lower #'s have softer swings at the edge angles @@ -2331,7 +2385,7 @@ void ParticleSystem::updateWindMotion() m_windAngleChange = GameClientRandomValueReal( m_windAngleChangeMin, m_windAngleChangeMax ); // add to our wind angle - m_windAngle += m_windAngleChange; + m_windAngle += m_windAngleChange * timeScale; // keep in 0 to 2PI range just to keep the numbers safe and sane if( m_windAngle > TWO_PI ) @@ -2960,7 +3014,6 @@ ParticleSystemManager::ParticleSystemManager() m_onScreenParticleCount = 0; m_localPlayerIndex = 0; - m_lastLogicFrameUpdate = 0; m_particleCount = 0; m_fieldParticleCount = 0; m_particleSystemCount = 0; @@ -3046,7 +3099,6 @@ void ParticleSystemManager::reset() m_uniqueSystemID = INVALID_PARTICLE_SYSTEM_ID; - m_lastLogicFrameUpdate = -1; // leave templates as-is } @@ -3056,18 +3108,10 @@ void ParticleSystemManager::reset() //DECLARE_PERF_TIMER(ParticleSystemManager) void ParticleSystemManager::update() { - if (m_lastLogicFrameUpdate == TheGameLogic->getFrame()) { - return; - } - - // update the last logic frame. - m_lastLogicFrameUpdate = TheGameLogic->getFrame(); - //USE_PERF_TIMER(ParticleSystemManager) ParticleSystemListIt it = m_allParticleSystemList.begin(); while( it != m_allParticleSystemList.end() ) { - // TheSuperHackers @info Must increment the list iterator before potential element erasure from the list. ParticleSystem* sys = *it++; DEBUG_ASSERTCRASH(sys != nullptr, ("ParticleSystemManager::update: ParticleSystem is null")); @@ -3115,6 +3159,21 @@ void ParticleSystemManager::update() } } +// ------------------------------------------------------------------------------------------------ +void ParticleSystemManager::draw() +{ + const Real timeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio(); + + ParticleSystemListIt it = m_allParticleSystemList.begin(); + while( it != m_allParticleSystemList.end() ) + { + ParticleSystem* sys = *it++; + DEBUG_ASSERTCRASH(sys != nullptr, ("ParticleSystemManager::draw: ParticleSystem is null")); + + sys->draw(timeScale); + } +} + // ------------------------------------------------------------------------------------------------ /** sets the count of the particles on screen after each frame */ // ------------------------------------------------------------------------------------------------ diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp index d1c956e533a..f2a49e12795 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp @@ -739,12 +739,8 @@ void GameClient::update() #endif // update all particle systems - if( !freezeTime ) { - // update particle systems - TheParticleSystemManager->setLocalPlayerIndex(localPlayerIndex); -// TheParticleSystemManager->update(); - + //TheParticleSystemManager->draw(); //LORENZEN AND WILCZYNSKI MOVED THIS TO W3DDisplay } // update the terrain visuals diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 622aeb5da10..3f7444f0dbf 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -3912,9 +3912,9 @@ void GameLogic::update() } } - - - + const Int localPlayerIndex = rts::getObservedOrLocalPlayer()->getPlayerIndex(); + TheParticleSystemManager->setLocalPlayerIndex(localPlayerIndex); + TheParticleSystemManager->update(); // increment world time if (!m_startNewGame) diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 7d083f3393e..ee4e44e9a60 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -1934,7 +1934,7 @@ void W3DDisplay::draw() //trying to refresh the visible terrain geometry. // if(TheGlobalData->m_loadScreenRender != TRUE) updateViews(); - TheParticleSystemManager->update();//LORENZEN AND WILCZYNSKI MOVED THIS FROM ITS NATIVE POSITION, ABOVE + TheParticleSystemManager->draw();//LORENZEN AND WILCZYNSKI MOVED THIS FROM ITS NATIVE POSITION, ABOVE //FOR THE PURPOSE OF LETTING THE PARTICLE SYSTEM LOOK UP THE RENDER OBJECT"S //TRANSFORM MATRIX, WHILE IT IS STILL VALID (HAVING DONE ITS CLIENT TRANSFORMS //BUT NOT YET RESETTING TOT HE LOGICAL TRANSFORM) @@ -1943,7 +1943,6 @@ void W3DDisplay::draw() //REVOLUTIONARY! //-LORENZEN - if (TheWaterRenderObj && TheGlobalData->m_waterType == 2) TheWaterRenderObj->updateRenderTargetTextures(primaryW3DView->get3DCamera()); //do a render into each texture