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 5054cd2b8f8..7cc49689903 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( 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 @@ -226,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 @@ -577,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 @@ -661,6 +664,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 +679,14 @@ class ParticleSystem : public MemoryPoolObject, ParticlePriorityType priority, 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 const Coord3D *computeParticlePosition(); ///< compute a position based on emission properties @@ -704,8 +721,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) @@ -735,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 { @@ -751,11 +773,14 @@ 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); + 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 ); @@ -827,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; ///getPlaybackFrameCount() / 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/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp index b516a335000..78f3b95e71e 100644 --- a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp +++ b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp @@ -33,6 +33,7 @@ #include "Common/GameState.h" #include "Common/INI.h" +#include "Common/FramePacer.h" #include "Common/PerfTimer.h" #include "Common/ThingFactory.h" #include "Common/GameLOD.h" @@ -282,7 +283,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; @@ -298,7 +298,7 @@ Particle::Particle( ParticleSystem *system, const ParticleInfo *info ) m_lifetime = info->m_lifetime; m_lifetimeLeft = info->m_lifetime; - m_createTimestamp = TheGameClient->getFrame(); + m_createTimestamp = TheGameLogic->getFrame(); m_personality = 0; m_size = info->m_size; @@ -373,93 +373,56 @@ void Particle::applyForce( const Coord3D *force ) // ------------------------------------------------------------------------------------------------ Bool Particle::update() { - // integrate acceleration into velocity - m_vel.x += m_accel.x; - m_vel.y += m_accel.y; - m_vel.z += m_accel.z; - - m_vel.x *= m_velDamping; - m_vel.y *= m_velDamping; - m_vel.z *= m_velDamping; - - // 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; - - // integrate the wind (if specified) into position - ParticleSystemInfo::WindMotion windMotion = m_system->getWindMotion(); + // monitor lifetime + if (m_lifetimeLeft && --m_lifetimeLeft == 0) + return false; - // see if we should even do anything - if( windMotion != ParticleSystemInfo::WIND_MOTION_NOT_USED ) - doWindMotion(); + DEBUG_ASSERTCRASH( m_lifetimeLeft, ( "A particle has an infinite lifetime..." )); - // update orientation -#if PARTICLE_USE_XY_ROTATION - m_angleX += m_angularRateX; - m_angleY += m_angularRateY; -#endif - m_angleZ += m_angularRateZ; -#if PARTICLE_USE_XY_ROTATION - m_angularRateX *= m_angularDamping; - m_angularRateY *= m_angularDamping; -#endif - m_angularRateZ *= m_angularDamping; + const UnsignedInt frameCount = TheGameLogic->getFrame() - m_createTimestamp; - if (m_particleUpTowardsEmitter) { - // adjust the up position back towards the particle - static const Coord2D upVec = { 0.0f, 1.0f }; - Coord2D emitterDir; - emitterDir.x = m_pos.x - m_emitterPos.x; - emitterDir.y = m_pos.y - m_emitterPos.y; - m_angleZ = (angleBetween(&upVec, &emitterDir) + PI); + 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; } - // update size - m_size += m_sizeRate; - m_sizeRate *= m_sizeRateDamping; +#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) // - 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) + if (frameCount >= m_alphaKey[ m_alphaTargetKey ].frame + KeyFrameDelay) { - 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 // - m_color.red += m_colorRate.red; - 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) + if (frameCount >= m_colorKey[ m_colorTargetKey ].frame + KeyFrameDelay) { - // can't set, because of colorscale - // m_color = m_colorKey[ m_colorTargetKey ].color; m_colorTargetKey++; computeColorRate(); } @@ -471,48 +434,124 @@ Bool Particle::update() m_colorRate.blue = 0.0f; } - /// @todo Rethink this - at least its name - m_color.red += m_colorScale; - m_color.green += m_colorScale; - m_color.blue += m_colorScale; + // 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; - if (m_color.red < 0.0f) - m_color.red = 0.0f; - else if (m_color.red > 1.0f) - m_color.red = 1.0f; + return true; +} - if (m_color.red < 0.0f) - m_color.green = 0.0f; - else if (m_color.green > 1.0f) - m_color.green = 1.0f; +// ------------------------------------------------------------------------------------------------ +// 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) +{ + return pow(damping, timeScale); +} - if (m_color.blue < 0.0f) - m_color.blue = 0.0f; - else if (m_color.blue > 1.0f) - m_color.blue = 1.0f; +// ------------------------------------------------------------------------------------------------ +// 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); + + 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) * 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(timeScale); + + // update orientation +#if PARTICLE_USE_XY_ROTATION + m_angleX += m_angularRateX * timeScale; + m_angleY += m_angularRateY * timeScale; +#endif + m_angleZ += m_angularRateZ * timeScale; + + const Real angularDecay = scaleDamping(m_angularDamping, timeScale); +#if PARTICLE_USE_XY_ROTATION + m_angularRateX *= angularDecay; + m_angularRateY *= angularDecay; +#endif + m_angularRateZ *= angularDecay; + + if (m_particleUpTowardsEmitter) + { + // adjust the up position back towards the particle + static const Coord2D upVec = { 0.0f, 1.0f }; + Coord2D emitterDir; + emitterDir.x = m_pos.x - m_emitterPos.x; + emitterDir.y = m_pos.y - m_emitterPos.y; + m_angleZ = (angleBetween(&upVec, &emitterDir) + PI); + } + // update size + 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 * timeScale; + m_alpha = clamp(0.0f, m_alpha, 1.0f); + } + + // + // Update color + // + m_color += m_colorRate * timeScale; + + /// @todo Rethink this - at least its name + 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); + m_color.blue = clamp(0.0f, m_color.blue, 1.0f); // reset the acceleration for accumulation next frame 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; } // ------------------------------------------------------------------------------------------------ /** 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 @@ -576,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 ) @@ -654,13 +693,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 ); @@ -674,7 +719,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 ); @@ -1073,8 +1122,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; @@ -1136,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; @@ -1741,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() || @@ -1798,17 +1848,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; @@ -1910,123 +1958,22 @@ 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(); - - // 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 + // + updateLastLogicalPos(); + updateTransform(); + updateLogicalPos(); // // Generate new particles if the system hasn't been 'stopped' or 'destroyed' @@ -2034,9 +1981,9 @@ 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 (!isShrouded && m_isStopped == false && m_masterSystem == nullptr) + if (!visibilityState.isShrouded && m_isStopped == false && m_masterSystem == nullptr) { if (m_burstDelayLeft == 0) { @@ -2100,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; @@ -2128,7 +2064,6 @@ Bool ParticleSystem::update( Int localPlayerIndex ) if (m_isDestroyed && !m_systemParticlesHead) return false; - // monitor particle system lifetime if (m_isForever == false) { @@ -2148,10 +2083,201 @@ Bool ParticleSystem::update( Int localPlayerIndex ) return true; } +// ------------------------------------------------------------------------------------------------ +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, otherwise use the local transform. + if (m_attachedToDrawableID) + { + Drawable *attachedTo = TheGameClient->findDrawableByID( m_attachedToDrawableID ); + + if (attachedTo) + { + updateParentTransform( *attachedTo->getTransformMatrix() ); + } + else + { + updateLocalTransform(); + } + } + else if (m_attachedToObjectID) + { + Object *objectAttachedTo = TheGameLogic->findObjectByID( m_attachedToObjectID ); + + if (objectAttachedTo) + { + const Drawable * draw = objectAttachedTo->getDrawable(); + if ( draw ) + { + updateParentTransform( *draw->getTransformMatrix() ); + } + else + { + updateParentTransform( *objectAttachedTo->getTransformMatrix() ); + } + } + else + { + updateLocalTransform(); + } + } + else + { + updateLocalTransform(); + } +} + +// ------------------------------------------------------------------------------------------------ +void ParticleSystem::updateParentTransform(const Matrix3D &parentXfrm) +{ + if (m_skipParentXfrm) + { + //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) + { +#ifdef ALLOW_TEMPORARIES + m_transform = parentXfrm * m_localTransform; +#else + m_transform.mul(parentXfrm, m_localTransform); +#endif + } + else + { + m_transform = parentXfrm; + } + + m_isIdentity = false; + } +} + +// ------------------------------------------------------------------------------------------------ +void ParticleSystem::updateLocalTransform() +{ + if (!m_isLocalIdentity) + { + m_transform = m_localTransform; + m_isIdentity = false; + } + 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 ) +{ + 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; +} + +// ------------------------------------------------------------------------------------------------ +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 ) @@ -2178,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 @@ -2259,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 ) @@ -2526,10 +2652,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 ); @@ -2888,7 +3014,6 @@ ParticleSystemManager::ParticleSystemManager() m_onScreenParticleCount = 0; m_localPlayerIndex = 0; - m_lastLogicFrameUpdate = 0; m_particleCount = 0; m_fieldParticleCount = 0; m_particleSystemCount = 0; @@ -2974,7 +3099,6 @@ void ParticleSystemManager::reset() m_uniqueSystemID = INVALID_PARTICLE_SYSTEM_ID; - m_lastLogicFrameUpdate = -1; // leave templates as-is } @@ -2984,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")); @@ -3043,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/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 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..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 @@ -787,15 +783,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) 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(); 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