From ba697f70447ec381f631e614bf267de2208840ca Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:37:40 +0200 Subject: [PATCH 1/7] perf(particlesys): Implement batched rendering for particles Squash merge of Mauller/perf-batch-particle-draws. --- .../Include/GameClient/ParticleSys.h | 3 +- .../W3DDevice/GameClient/W3DParticleSys.h | 7 + .../W3DDevice/GameClient/W3DParticleSys.cpp | 258 ++++++++++++------ 3 files changed, 180 insertions(+), 88 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index 5cd97e06e97..da460f1765e 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -515,7 +515,7 @@ class ParticleSystemTemplate : public MemoryPoolObject, protected ParticleSystem void validate(); - AsciiString getName() const { return m_name; } + const AsciiString& getName() const { return m_name; } // This function was made const because of update modules' module data being all const. ParticleSystem *createSlaveSystem( Bool createSlaves = TRUE ) const ; ///< if returns non-null, it is a slave system for use @@ -607,6 +607,7 @@ class ParticleSystem : public MemoryPoolObject, void setInitialDelay( UnsignedInt delay ) { m_delayLeft = delay; } const AsciiString& getParticleTypeName() const { return m_particleTypeName; } ///< return the name of the particles + const Bool isUsingParticles() const { return m_particleType == PARTICLE; } const Bool isUsingDrawables() const { return m_particleType == DRAWABLE; } const Bool isUsingStreak() const { return m_particleType == STREAK; } const Bool isUsingSmudge() const { return m_particleType == SMUDGE; } diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h index 41481701474..424814dd291 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h @@ -50,13 +50,20 @@ class W3DParticleSystemManager : public ParticleSystemManager virtual Int getOnScreenParticleCount() override { return m_onScreenParticleCount; } private: + void initializeBatch(ParticleSystem* system, const RefCountPtr& texture); + void flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount); + enum { MAX_POINTS_PER_GROUP = 512 }; + RefCountPtr m_batchTexture; ///< the texture used as the drawing surface for batched particle draws PointGroupClass *m_pointGroup; ///< the point group that contains all of the particles StreakLineClass *m_streakLine; ///< the streak class that contains all of the streaks ShareBufferClass *m_posBuffer; ///< array of particle positions ShareBufferClass *m_RGBABuffer; ///< array of particle color and alpha ShareBufferClass *m_sizeBuffer; ///< array of particle sizes ShareBufferClass *m_angleBuffer; ///< array of particle orientations + + ParticleSystemInfo::ParticleShaderType m_batchShaderType; Bool m_readyToRender; ///< if true, it is OK to render + Bool m_batchBillboard; }; diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index c0a901ec7e6..49f098163b9 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -46,6 +46,9 @@ W3DParticleSystemManager::W3DParticleSystemManager() { + m_batchBillboard = true; + m_batchShaderType = ParticleSystemInfo::INVALID_SHADER; + m_pointGroup = nullptr; m_streakLine = nullptr; m_posBuffer = nullptr; @@ -144,6 +147,9 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) TheSmudgeManager->resetDraw(); } + // Number of particles/points being rendered. + UnsignedInt pointCount = 0; + ParticleSystemManager::ParticleSystemList &particleSysList = TheParticleSystemManager->getAllParticleSystems(); for( ParticleSystemManager::ParticleSystemListIt it = particleSysList.begin(); it != particleSysList.end(); ++it) { @@ -156,6 +162,31 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) if (sys->isUsingDrawables()) continue; + // TheSuperHackers @performance Mauller 16/08/2026 Test if the particle system has any visible particles that can be drawn. + // Earlier visibility testing prevents the particle texture lookup which can cause a batch flush. + int particleCount = 0; + for (Particle* vp = sys->getFirstParticle(); vp; vp = vp->m_systemNext) + { + const Coord3D* pos = vp->getPosition(); + Real psize = vp->getSize(); + + //Test if particle is at the screen or terrain edges. + if (WWMath::Fabs(pos->x - bcX) > (beX + psize) || + WWMath::Fabs(pos->y - bcY) > (beY + psize) || + WWMath::Fabs(pos->z - bcZ) > (beZ + psize)) + { + vp->setIsCulled(true); + continue; + } + + vp->setIsCulled(false); + particleCount++; + } + + // Particle system has no particles on screen + if (particleCount == 0) + continue; + // Handle smudge type particles if (sys->isUsingSmudge()) { @@ -164,17 +195,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) for (Particle *p = sys->getFirstParticle(); p; p = p->m_systemNext) { - const Coord3D *pos = p->getPosition(); - Real psize = p->getSize(); - - //Cull particle to edges of screen and terrain. - if (WWMath::Fabs( pos->x - bcX ) > ( beX + psize ) ) - continue; - - if (WWMath::Fabs( pos->y - bcY ) > ( beY + psize ) ) - continue; - - if (WWMath::Fabs( pos->z - bcZ ) > ( beZ + psize ) ) + if (p->isCulled()) continue; if (Smudge *smudge = TheSmudgeManager->findSmudge(p)) @@ -186,10 +207,28 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) continue; } - /// @todo lorenzen sez: declare these outside the sys loop, and put some in registers - // initialize them here still, of course + // TheSuperHackers @performance Ronin/Mauller 09/08/2026 Implement batched rendering for similar particles. + // Particles with the same properties will now be batched onto a single texture surface before being drawn. + // If a different particle type appears before the batch is filled, the previous batch will be drawn first. + RefCountPtr texture; + texture.Assign_No_Add_Ref(W3DDisplay::m_assetManager->Get_Texture(sys->getParticleTypeName().str())); + + const Bool canBatch = sys->isUsingParticles(); + const Bool batchDone = texture.Peek() != m_batchTexture.Peek() || sys->getShaderType() != m_batchShaderType || sys->shouldBillboard() != m_batchBillboard; + if (!canBatch || batchDone) + { + flushParticleBatch(rinfo, pointCount); + } + + // setup a new particle batch texture if prior batch was flushed. + if (canBatch && m_batchTexture == nullptr) + { + initializeBatch(sys, texture); + } + + Int startCount = pointCount; + // build W3D particle buffer - Int count = 0; Vector3 *posArray = m_posBuffer->Get_Array(); Real *sizeArray = m_sizeBuffer->Get_Array(); Vector4 *RGBAArray = m_RGBABuffer->Get_Array(); @@ -203,53 +242,58 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) //set-up all the per-particle for (Particle *p = sys->getFirstParticle(); p; p = p->m_systemNext) { - pos = p->getPosition(); - psize = p->getSize(); - - //Cull particle to edges of screen and terrain. - if (WWMath::Fabs(pos->x - bcX) > (beX + psize)) - continue; - - if (WWMath::Fabs(pos->y - bcY) > (beY + psize)) + if (p->isCulled()) continue; - if (WWMath::Fabs(pos->z - bcZ) > (beZ + psize)) - continue; + pos = p->getPosition(); + psize = p->getSize(); m_fieldParticleCount += ( sys->getPriority() == AREA_EFFECT && sys->m_isGroundAligned != FALSE ); //@todo lorenzen sez: use pointer arithmetic for these arrays - personalities[count] = p->getPersonality(); + personalities[pointCount] = p->getPersonality(); - posArray[count].X = pos->x; - posArray[count].Y = pos->y; - posArray[count].Z = pos->z; + posArray[pointCount].X = pos->x; + posArray[pointCount].Y = pos->y; + posArray[pointCount].Z = pos->z; - sizeArray[count] = psize; + sizeArray[pointCount] = psize; color = p->getColor(); - RGBAArray[count].X = color->red; - RGBAArray[count].Y = color->green; - RGBAArray[count].Z = color->blue; - RGBAArray[count].W = p->getAlpha(); + RGBAArray[pointCount].X = color->red; + RGBAArray[pointCount].Y = color->green; + RGBAArray[pointCount].Z = color->blue; + RGBAArray[pointCount].W = p->getAlpha(); - angleArray[count] = (uint8)(p->getAngle() * 255.0f / (2.0f * PI)); + angleArray[pointCount] = (uint8)(p->getAngle() * 255.0f / (2.0f * PI)); + + if (++pointCount == MAX_POINTS_PER_GROUP) + { + if (!canBatch) + { + break; + } - if (++count == MAX_POINTS_PER_GROUP) - break; + // TheSuperHackers @info The Buffer is full mid-system so draw what we have and carry on with the SAME system. + // This prevents particles being dropped. Bank the stats first as the flush resets count to 0. + m_onScreenParticleCount += (pointCount - startCount); + flushParticleBatch(rinfo, pointCount); + initializeBatch(sys, texture); + startCount = 0; + } } - if ( count == 0 ) + if (pointCount == startCount) + { continue; //this system has no particles to render + } - TextureClass *texture = W3DDisplay::m_assetManager->Get_Texture( sys->getParticleTypeName().str() ); - - if ( m_streakLine && sys->isUsingStreak() && (count >= 2) ) + // Handle drawing streak type particles. + if ( sys->isUsingStreak() && (pointCount >= 2) ) { m_streakLine->Reset_Line(); - m_streakLine->Set_Texture( texture ); - texture->Release_Ref();//release reference since it's held by streakline + m_streakLine->Set_Texture( texture.Peek() ); switch( sys->getShaderType() ) { case ParticleSystemInfo::ADDITIVE: @@ -268,14 +312,14 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) //UPDATE THE STREAK'S ARRAYS m_streakLine->Set_LocsWidthsColors( - count, + pointCount, m_posBuffer->Get_Array(), m_sizeBuffer->Get_Array(), m_RGBABuffer->Get_Array(), &personalities[0] ); - //WWASSERT( m_streakLine->Get_Num_Points() == count ); + //WWASSERT( m_streakLine->Get_Num_Points() == pointCount ); // This is the happy place for this! RGBAArray[0].X = 0;//eliminates the scissor edge on the trailing edge of the streak @@ -286,61 +330,51 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) //RENDER STREAK! m_streakLine->Render( rinfo ); - + m_onScreenParticleCount += (pointCount - startCount); + pointCount = startCount; } - else - { - WWASSERT( m_pointGroup ); + // Handle volumetric type particle systems. + const UnsignedInt volumeParticleDepth = sys->getVolumeParticleDepth(); + if( sys->isUsingVolumeParticles() && volumeParticleDepth > DEFAULT_VOLUME_PARTICLE_DEPTH ) + { + m_pointGroup->Set_Texture( texture.Peek() ); + m_pointGroup->Set_Flag( PointGroupClass::TRANSFORM, true ); // transform to screen space - if ( m_pointGroup ) // this catches the particle and volumeparticle cases + switch( sys->getShaderType() ) { - // render all the systems' particles - m_pointGroup->Set_Texture( texture ); - texture->Release_Ref();//release reference since it's held by pointGroup - m_pointGroup->Set_Flag( PointGroupClass::TRANSFORM, true ); // transform to screen space - - switch( sys->getShaderType() ) - { - case ParticleSystemInfo::ADDITIVE: - m_pointGroup->Set_Shader( ShaderClass::_PresetAdditiveSpriteShader ); - break; - case ParticleSystemInfo::ALPHA: - m_pointGroup->Set_Shader( ShaderClass::_PresetAlphaSpriteShader ); - break; - case ParticleSystemInfo::ALPHA_TEST: - m_pointGroup->Set_Shader( ShaderClass::_PresetATestSpriteShader ); - break; - case ParticleSystemInfo::MULTIPLY: - m_pointGroup->Set_Shader( ShaderClass::_PresetMultiplicativeSpriteShader ); - break; - } + case ParticleSystemInfo::ADDITIVE: + m_pointGroup->Set_Shader( ShaderClass::_PresetAdditiveSpriteShader ); + break; + case ParticleSystemInfo::ALPHA: + m_pointGroup->Set_Shader( ShaderClass::_PresetAlphaSpriteShader ); + break; + case ParticleSystemInfo::ALPHA_TEST: + m_pointGroup->Set_Shader( ShaderClass::_PresetATestSpriteShader ); + break; + case ParticleSystemInfo::MULTIPLY: + m_pointGroup->Set_Shader( ShaderClass::_PresetMultiplicativeSpriteShader ); + break; + } - /// @todo Use both QUADS and TRIS for particles - m_pointGroup->Set_Point_Mode( PointGroupClass::QUADS ); - m_pointGroup->Set_Arrays( m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, count ); - m_pointGroup->Set_Billboard(sys->shouldBillboard()); + /// @todo Use both QUADS and TRIS for particles + m_pointGroup->Set_Point_Mode( PointGroupClass::QUADS ); + m_pointGroup->Set_Arrays( m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, pointCount ); + m_pointGroup->Set_Billboard(sys->shouldBillboard()); - /// @todo Support animated texture particles - /// @todo lorenzen sez: unimplemented code wastes cpu cycles - m_pointGroup->Set_Point_Frame( 0 ); + /// @todo Support animated texture particles + /// @todo lorenzen sez: unimplemented code wastes cpu cycles + m_pointGroup->Set_Point_Frame( 0 ); - //RENDER IT! - const UnsignedInt volumeParticleDepth = sys->getVolumeParticleDepth(); - if( sys->isUsingVolumeParticles() && volumeParticleDepth > DEFAULT_VOLUME_PARTICLE_DEPTH ) - { - m_pointGroup->RenderVolumeParticle( rinfo, volumeParticleDepth); - } - else - m_pointGroup->Render( rinfo ); - - } + m_pointGroup->RenderVolumeParticle( rinfo, volumeParticleDepth); + m_onScreenParticleCount += (pointCount - startCount); + pointCount = startCount; } /// @todo lorenzen sez: this should be debug only: //add particle count to total - m_onScreenParticleCount += count; + m_onScreenParticleCount += (pointCount - startCount); /* // draw the wind vector for this particle system on the screen @@ -362,6 +396,9 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) } + // TheSuperHackers @info Flush the last batch if one is pending. + flushParticleBatch(rinfo, pointCount); + /// @todo lorenzen sez: this should be debug only: TheParticleSystemManager->setOnScreenParticleCount(m_onScreenParticleCount); @@ -375,3 +412,50 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) ((W3DSmudgeManager *)TheSmudgeManager)->render(rinfo); } } + +void W3DParticleSystemManager::initializeBatch(ParticleSystem* system, const RefCountPtr& texture) +{ + m_batchTexture = texture; + m_batchShaderType = system->getShaderType(); + m_batchBillboard = system->shouldBillboard(); +} + +void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount) +{ + if (pointCount > 0) + { + m_pointGroup->Set_Texture(m_batchTexture.Peek()); + + switch (m_batchShaderType) + { + case ParticleSystemInfo::ADDITIVE: + m_pointGroup->Set_Shader(ShaderClass::_PresetAdditiveSpriteShader); + break; + case ParticleSystemInfo::ALPHA: + m_pointGroup->Set_Shader(ShaderClass::_PresetAlphaSpriteShader); + break; + case ParticleSystemInfo::ALPHA_TEST: + m_pointGroup->Set_Shader(ShaderClass::_PresetATestSpriteShader); + break; + case ParticleSystemInfo::MULTIPLY: + m_pointGroup->Set_Shader(ShaderClass::_PresetMultiplicativeSpriteShader); + break; + } + + m_pointGroup->Set_Flag(PointGroupClass::TRANSFORM, true); + m_pointGroup->Set_Point_Mode(PointGroupClass::QUADS); + m_pointGroup->Set_Arrays(m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, pointCount); + m_pointGroup->Set_Billboard(m_batchBillboard); + m_pointGroup->Set_Point_Frame(0); + m_pointGroup->Render(rinfo); + + m_batchBillboard = false; + m_batchShaderType = ParticleSystemInfo::INVALID_SHADER; + pointCount = 0; + } + + if (m_batchTexture != nullptr) + { + m_batchTexture.Clear(); + } +} From 4f00ff05b62103f7a61985d930ec053e790efcf0 Mon Sep 17 00:00:00 2001 From: stm <14291421+stephanmeesters@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:15:33 +0200 Subject: [PATCH 2/7] refactor(worldheightmap): Add logical bounds function --- .../Include/W3DDevice/GameClient/WorldHeightMap.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h index 10176bfa1aa..9d2a24bf34b 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h @@ -243,6 +243,15 @@ class WorldHeightMap : public RefCountClass, Int getXExtent() const {return m_width;} /// Date: Tue, 1 Sep 2026 13:16:06 +0200 Subject: [PATCH 3/7] refactor(icoord2d): Add intersect function --- Core/Libraries/Include/Lib/BaseType.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Core/Libraries/Include/Lib/BaseType.h b/Core/Libraries/Include/Lib/BaseType.h index 8b5e760aff4..61b9d1c122f 100644 --- a/Core/Libraries/Include/Lib/BaseType.h +++ b/Core/Libraries/Include/Lib/BaseType.h @@ -498,6 +498,18 @@ struct IRegion2D { ICoord2D lo, hi; // bounds of 2D rectangular region + void intersect(const IRegion2D &other) + { + if (lo.x < other.lo.x) + lo.x = other.lo.x; + if (lo.y < other.lo.y) + lo.y = other.lo.y; + if (hi.x > other.hi.x) + hi.x = other.hi.x; + if (hi.y > other.hi.y) + hi.y = other.hi.y; + } + void zero() { lo.zero(); From f1a30cb00943b354beafd1911815086b3b91cee0 Mon Sep 17 00:00:00 2001 From: stm <14291421+stephanmeesters@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:55:49 +0200 Subject: [PATCH 4/7] feat(terrainparticle): Add ConformToTerrain INI option --- Core/GameEngine/Include/GameClient/ParticleSys.h | 3 +++ Core/GameEngine/Source/GameClient/System/ParticleSys.cpp | 2 ++ .../GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp | 2 ++ 3 files changed, 7 insertions(+) diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index da460f1765e..c154c4034ae 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -516,6 +516,7 @@ class ParticleSystemTemplate : public MemoryPoolObject, protected ParticleSystem void validate(); const AsciiString& getName() const { return m_name; } + Bool getIsTerrainConforming() const { return m_isTerrainConforming; } // This function was made const because of update modules' module data being all const. ParticleSystem *createSlaveSystem( Bool createSlaves = TRUE ) const ; ///< if returns non-null, it is a slave system for use @@ -543,6 +544,7 @@ class ParticleSystemTemplate : public MemoryPoolObject, protected ParticleSystem // This has to be mutable because of the delayed initialization thing in createSlaveSystem mutable const ParticleSystemTemplate *m_slaveTemplate; ///< if non-null, use this to create a slave system + Bool m_isTerrainConforming; ///< render ground-aligned particles conforming to the terrain // template attribute data inherited from ParticleSystemInfo class }; @@ -615,6 +617,7 @@ class ParticleSystem : public MemoryPoolObject, const UnsignedInt getVolumeParticleDepth() const { return m_volumeParticleDepth; } Bool shouldBillboard() { return !m_isGroundAligned; } + Bool isTerrainConforming() { return m_template->getIsTerrainConforming(); } ParticleShaderType getShaderType() { return m_shaderType; } diff --git a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp index 0d697e89fe7..2c4bc3daff5 100644 --- a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp +++ b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp @@ -2757,6 +2757,7 @@ const FieldParse ParticleSystemTemplate::m_fieldParseTable[] = { "IsHollow", INI::parseBool, nullptr, offsetof( ParticleSystemTemplate, m_isEmissionVolumeHollow ) }, { "IsGroundAligned", INI::parseBool, nullptr, offsetof( ParticleSystemTemplate, m_isGroundAligned ) }, + { "IsTerrainConforming", INI::parseBool, nullptr, offsetof( ParticleSystemTemplate, m_isTerrainConforming ) }, { "IsEmitAboveGroundOnly", INI::parseBool, nullptr, offsetof( ParticleSystemTemplate, m_isEmitAboveGroundOnly) }, { "IsParticleUpTowardsEmitter", INI::parseBool, nullptr, offsetof( ParticleSystemTemplate, m_isParticleUpTowardsEmitter) }, @@ -2862,6 +2863,7 @@ ParticleSystemTemplate::ParticleSystemTemplate( const AsciiString &name ) : m_name(name) { m_slaveTemplate = nullptr; + m_isTerrainConforming = FALSE; } // ------------------------------------------------------------------------------------------------ diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp index 9ee0ee728f4..edc60daff7f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp @@ -9777,6 +9777,7 @@ static const std::string F_VOLCYLRAD = "VolCylinderRadius"; static const std::string F_VOLCYLLEN = "VolCylinderLength"; static const std::string F_ISHOLLOW = "IsHollow"; static const std::string F_ISXYPLANAR = "IsGroundAligned"; +static const std::string F_ISTERRAINCONFORMING = "IsTerrainConforming"; static const std::string F_ISEMITABOVEGROUNDONLY = "IsEmitAboveGroundOnly"; static const std::string F_ISPARTICLEUPTOWARDSEMITTER @@ -10079,6 +10080,7 @@ void _writeSingleParticleSystem( File *out, ParticleSystemTemplate *templ ) thisEntry.append(SEP_HEAD).append(F_ISHOLLOW).append(EQ_WITH_SPACES).append((templ->m_isEmissionVolumeHollow ? STR_TRUE : STR_FALSE)).append(SEP_EOL); thisEntry.append(SEP_HEAD).append(F_ISXYPLANAR).append(EQ_WITH_SPACES).append((templ->m_isGroundAligned ? STR_TRUE : STR_FALSE)).append(SEP_EOL); + thisEntry.append(SEP_HEAD).append(F_ISTERRAINCONFORMING).append(EQ_WITH_SPACES).append((templ->getIsTerrainConforming() ? STR_TRUE : STR_FALSE)).append(SEP_EOL); thisEntry.append(SEP_HEAD).append(F_ISEMITABOVEGROUNDONLY).append(EQ_WITH_SPACES).append((templ->m_isEmitAboveGroundOnly ? STR_TRUE : STR_FALSE)).append(SEP_EOL); thisEntry.append(SEP_HEAD).append(F_ISPARTICLEUPTOWARDSEMITTER).append(EQ_WITH_SPACES).append((templ->m_isParticleUpTowardsEmitter ? STR_TRUE : STR_FALSE)).append(SEP_EOL); From ca2b51b9bbeb2d1482d5a5c77b1e44e34df78783 Mon Sep 17 00:00:00 2001 From: stm <14291421+stephanmeesters@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:56:12 +0200 Subject: [PATCH 5/7] feat(terrainparticle): Add terrain conforming particle renderer --- Core/GameEngineDevice/CMakeLists.txt | 2 + .../W3DDevice/GameClient/W3DParticleSys.h | 6 +- .../W3DDevice/GameClient/W3DTerrainParticle.h | 80 +++ .../W3DDevice/GameClient/W3DParticleSys.cpp | 80 +-- .../GameClient/W3DTerrainParticle.cpp | 468 ++++++++++++++++++ 5 files changed, 606 insertions(+), 30 deletions(-) create mode 100644 Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainParticle.h create mode 100644 Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index 876148f42d0..a1ec5e32774 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -68,6 +68,7 @@ set(GAMEENGINEDEVICE_SRC Include/W3DDevice/GameClient/W3DSnow.h # Include/W3DDevice/GameClient/W3DStatusCircle.h Include/W3DDevice/GameClient/W3DTerrainBackground.h + Include/W3DDevice/GameClient/W3DTerrainParticle.h Include/W3DDevice/GameClient/W3DTerrainTracks.h Include/W3DDevice/GameClient/W3DTerrainVisual.h Include/W3DDevice/GameClient/W3DTreeBuffer.h @@ -171,6 +172,7 @@ set(GAMEENGINEDEVICE_SRC Source/W3DDevice/GameClient/W3DSnow.cpp # Source/W3DDevice/GameClient/W3DStatusCircle.cpp Source/W3DDevice/GameClient/W3DTerrainBackground.cpp + Source/W3DDevice/GameClient/W3DTerrainParticle.cpp Source/W3DDevice/GameClient/W3DTerrainTracks.cpp Source/W3DDevice/GameClient/W3DTerrainVisual.cpp Source/W3DDevice/GameClient/W3DTreeBuffer.cpp diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h index 424814dd291..006d00cc97f 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h @@ -28,6 +28,7 @@ #pragma once #include "GameClient/ParticleSys.h" +#include "W3DDevice/GameClient/W3DTerrainParticle.h" #include "WW3D2/pointgr.h" #include "WW3D2/streak.h" #include "WW3D2/rinfo.h" @@ -50,7 +51,7 @@ class W3DParticleSystemManager : public ParticleSystemManager virtual Int getOnScreenParticleCount() override { return m_onScreenParticleCount; } private: - void initializeBatch(ParticleSystem* system, const RefCountPtr& texture); + void initializeBatch(ParticleSystem* system, const RefCountPtr& texture, const AABoxClass& bbox); void flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount); enum { MAX_POINTS_PER_GROUP = 512 }; @@ -58,6 +59,7 @@ class W3DParticleSystemManager : public ParticleSystemManager RefCountPtr m_batchTexture; ///< the texture used as the drawing surface for batched particle draws PointGroupClass *m_pointGroup; ///< the point group that contains all of the particles StreakLineClass *m_streakLine; ///< the streak class that contains all of the streaks + W3DTerrainParticle *m_terrainParticles; ///< the terrain particles renderer that contains all of the terrain conforming particles ShareBufferClass *m_posBuffer; ///< array of particle positions ShareBufferClass *m_RGBABuffer; ///< array of particle color and alpha ShareBufferClass *m_sizeBuffer; ///< array of particle sizes @@ -66,4 +68,6 @@ class W3DParticleSystemManager : public ParticleSystemManager ParticleSystemInfo::ParticleShaderType m_batchShaderType; Bool m_readyToRender; ///< if true, it is OK to render Bool m_batchBillboard; + Bool m_batchIsConforming; + AABoxClass m_batchBoundingBox; }; diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainParticle.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainParticle.h new file mode 100644 index 00000000000..d884b688761 --- /dev/null +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainParticle.h @@ -0,0 +1,80 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +#include "Lib/BaseType.h" +#include "WW3D2/shader.h" +#include "WWLib/sharebuf.h" +#include "WWMath/vector3.h" +#include "WWMath/vector4.h" + +class AABoxClass; +class TextureClass; +class WorldHeightMap; +struct VertexFormatXYZNDUV2; + +// Renders particles as meshes that follow the shape of the terrain below them. +class W3DTerrainParticle +{ +public: + W3DTerrainParticle(); + ~W3DTerrainParticle(); + + void setTexture(TextureClass* texture); + void setShader(ShaderClass shader); + void setArrays(ShareBufferClass* locs, + ShareBufferClass* diffuse = nullptr, + ShareBufferClass* sizes = nullptr, + ShareBufferClass* orientations = nullptr, + Int activePointCount = -1); + void setBoundingBox(const AABoxClass& worldBoundingBox); + void render(); + +private: + + void processParticle(WorldHeightMap& map, const Vector3& loc, UnsignedInt diffuse, Real size, UnsignedByte orientation); + void drawQuad(const Vector3& loc, UnsignedInt diffuse, Real size, Real cosine, Real sine, Real height, const Vector3& normal); + void drawTerrainConformingMesh(WorldHeightMap& map, const Vector3& loc, const IRegion2D& bounds, UnsignedInt diffuse, Real size, Real cosine, Real sine); + void addTriangle(UnsignedShort baseVertex, UnsignedShort offsetA, UnsignedShort offsetB, UnsignedShort offsetC); + void flushBatch(); + IRegion2D calcBounds(WorldHeightMap& map, const Vector3& loc, Real projectedRadius) const; + void updateSettings(); + + VertexFormatXYZNDUV2* m_vertexData; ///< Vertices of the current batch. + UnsignedShort* m_indexData; ///< Indices defining the triangles of the current batch. + UnsignedByte* m_outcodes; ///< UV outcodes to keep track which triangles are fully transparent. + UnsignedShort m_numVertices; ///< Number of vertices used in m_vertexData. + UnsignedShort m_numIndices; ///< Number of indices used in m_indexData. + + TextureClass* m_texture; + ShaderClass m_shader; + + ShareBufferClass* m_pointLoc; ///< World space point locations. + ShareBufferClass* m_pointDiffuse; ///< RGBA values (nullptr if not used). + ShareBufferClass* m_pointSize; ///< Size override table (nullptr if not used). + ShareBufferClass* m_pointOrientation; ///< Orientation indices (nullptr if not used). + Int m_pointCount; ///< Total point count. + IRegion2D m_terrainInViewBounds; ///< Bounding box of which terrain cell indices are on the screen. + + Real m_defaultPointSize; ///< Point size (size array overrides if present). + Vector3 m_defaultPointColor; ///< Point color (color array overrides if present). + Real m_defaultPointAlpha; ///< Point alpha (alpha array overrides if present). + UnsignedByte m_defaultPointOrientation; ///< Point orientation (orientation array overrides if present). + UnsignedInt m_defaultDiffuse; ///< Diffuse built from m_defaultPointColor and m_defaultPointAlpha. +}; diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index 49f098163b9..d348517be4e 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -34,9 +34,9 @@ #include "W3DDevice/GameClient/HeightMap.h" #include "W3DDevice/GameClient/W3DSmudge.h" #include "W3DDevice/GameClient/W3DSnow.h" +#include "W3DDevice/GameClient/W3DTerrainParticle.h" #include "WW3D2/camera.h" - //------------------------------------------------------------------------------ Performance Timers //#include "Common/PerfMetrics.h" //#include "Common/PerfTimer.h" @@ -51,6 +51,7 @@ W3DParticleSystemManager::W3DParticleSystemManager() m_pointGroup = nullptr; m_streakLine = nullptr; + m_terrainParticles = nullptr; m_posBuffer = nullptr; m_RGBABuffer = nullptr; m_sizeBuffer = nullptr; @@ -62,6 +63,7 @@ W3DParticleSystemManager::W3DParticleSystemManager() m_pointGroup = NEW PointGroupClass(); //m_streakLine = nullptr; m_streakLine = NEW StreakLineClass(); + m_terrainParticles = NEW W3DTerrainParticle(); m_posBuffer = NEW_REF( ShareBufferClass, (MAX_POINTS_PER_GROUP, "W3DParticleSystemManager::m_posBuffer") ); m_RGBABuffer = NEW_REF( ShareBufferClass, (MAX_POINTS_PER_GROUP, "W3DParticleSystemManager::m_RGBABuffer") ); @@ -80,6 +82,9 @@ W3DParticleSystemManager::~W3DParticleSystemManager() REF_PTR_RELEASE(m_streakLine); } + delete m_terrainParticles; + m_terrainParticles = nullptr; + REF_PTR_RELEASE(m_posBuffer); REF_PTR_RELEASE(m_RGBABuffer); REF_PTR_RELEASE(m_sizeBuffer); @@ -214,7 +219,10 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) texture.Assign_No_Add_Ref(W3DDisplay::m_assetManager->Get_Texture(sys->getParticleTypeName().str())); const Bool canBatch = sys->isUsingParticles(); - const Bool batchDone = texture.Peek() != m_batchTexture.Peek() || sys->getShaderType() != m_batchShaderType || sys->shouldBillboard() != m_batchBillboard; + const Bool batchDone = texture.Peek() != m_batchTexture.Peek() || + sys->getShaderType() != m_batchShaderType || + sys->shouldBillboard() != m_batchBillboard || + m_batchIsConforming != sys->isTerrainConforming(); if (!canBatch || batchDone) { flushParticleBatch(rinfo, pointCount); @@ -223,7 +231,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) // setup a new particle batch texture if prior batch was flushed. if (canBatch && m_batchTexture == nullptr) { - initializeBatch(sys, texture); + initializeBatch(sys, texture, bbox); } Int startCount = pointCount; @@ -278,7 +286,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) // This prevents particles being dropped. Bank the stats first as the flush resets count to 0. m_onScreenParticleCount += (pointCount - startCount); flushParticleBatch(rinfo, pointCount); - initializeBatch(sys, texture); + initializeBatch(sys, texture, bbox); startCount = 0; } } @@ -371,7 +379,6 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) pointCount = startCount; } - /// @todo lorenzen sez: this should be debug only: //add particle count to total m_onScreenParticleCount += (pointCount - startCount); @@ -413,45 +420,60 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) } } -void W3DParticleSystemManager::initializeBatch(ParticleSystem* system, const RefCountPtr& texture) +void W3DParticleSystemManager::initializeBatch(ParticleSystem* system, const RefCountPtr& texture, const AABoxClass& bbox) { m_batchTexture = texture; m_batchShaderType = system->getShaderType(); m_batchBillboard = system->shouldBillboard(); + m_batchIsConforming = !system->shouldBillboard() && !system->isUsingVolumeParticles() && system->isTerrainConforming(); + m_batchBoundingBox = bbox; } void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount) { - if (pointCount > 0) + if(pointCount > 0) { - m_pointGroup->Set_Texture(m_batchTexture.Peek()); - - switch (m_batchShaderType) + ShaderClass shader; + switch(m_batchShaderType ) { - case ParticleSystemInfo::ADDITIVE: - m_pointGroup->Set_Shader(ShaderClass::_PresetAdditiveSpriteShader); - break; - case ParticleSystemInfo::ALPHA: - m_pointGroup->Set_Shader(ShaderClass::_PresetAlphaSpriteShader); - break; - case ParticleSystemInfo::ALPHA_TEST: - m_pointGroup->Set_Shader(ShaderClass::_PresetATestSpriteShader); - break; - case ParticleSystemInfo::MULTIPLY: - m_pointGroup->Set_Shader(ShaderClass::_PresetMultiplicativeSpriteShader); - break; + case ParticleSystemInfo::ADDITIVE: + shader = ShaderClass::_PresetAdditiveSpriteShader; + break; + case ParticleSystemInfo::ALPHA: + shader = ShaderClass::_PresetAlphaSpriteShader; + break; + case ParticleSystemInfo::ALPHA_TEST: + shader = ShaderClass::_PresetATestSpriteShader; + break; + case ParticleSystemInfo::MULTIPLY: + shader = ShaderClass::_PresetMultiplicativeSpriteShader; + break; } - m_pointGroup->Set_Flag(PointGroupClass::TRANSFORM, true); - m_pointGroup->Set_Point_Mode(PointGroupClass::QUADS); - m_pointGroup->Set_Arrays(m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, pointCount); - m_pointGroup->Set_Billboard(m_batchBillboard); - m_pointGroup->Set_Point_Frame(0); - m_pointGroup->Render(rinfo); + if (m_batchIsConforming) + { + m_terrainParticles->setTexture(m_batchTexture.Peek()); + m_terrainParticles->setShader( shader ); + m_terrainParticles->setArrays( m_posBuffer, m_RGBABuffer, m_sizeBuffer, m_angleBuffer, pointCount ); + m_terrainParticles->setBoundingBox( m_batchBoundingBox ); + m_terrainParticles->render(); + } + else // draw regular point group + { + m_pointGroup->Set_Texture(m_batchTexture.Peek()); + m_pointGroup->Set_Shader(shader); + m_pointGroup->Set_Flag(PointGroupClass::TRANSFORM, true); + m_pointGroup->Set_Point_Mode(PointGroupClass::QUADS); + m_pointGroup->Set_Arrays(m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, pointCount); + m_pointGroup->Set_Billboard(m_batchBillboard); + m_pointGroup->Set_Point_Frame(0); + m_pointGroup->Render(rinfo); + } + pointCount = 0; m_batchBillboard = false; + m_batchIsConforming = false; m_batchShaderType = ParticleSystemInfo::INVALID_SHADER; - pointCount = 0; } if (m_batchTexture != nullptr) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp new file mode 100644 index 00000000000..a8ea5de91df --- /dev/null +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp @@ -0,0 +1,468 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "W3DDevice/GameClient/W3DTerrainParticle.h" + +#include + +#include "GameClient/ParticleSys.h" +#include "GameLogic/TerrainLogic.h" +#include "Lib/BaseType.h" +#include "W3DDevice/GameClient/BaseHeightMap.h" +#include "WW3D2/dx8indexbuffer.h" +#include "WW3D2/dx8vertexbuffer.h" +#include "WW3D2/dx8wrapper.h" +#include "WW3D2/rinfo.h" +#include "WW3D2/texture.h" +#include "WW3D2/vertmaterial.h" +#include "WWLib/refcount.h" +#include "WWMath/vector3.h" +#include "WWMath/vector4.h" +#include "WWMath/wwmath.h" + +constexpr const Int MAX_VERTICES = 32768; +constexpr const Int MAX_INDICES = 65535; +constexpr const Int MAX_TILES_IN_BATCH = 32; +constexpr const Int MAX_BATCH_VERTICES = (MAX_TILES_IN_BATCH + 1) * (MAX_TILES_IN_BATCH + 1); +constexpr const Real Z_OFFSET = MAP_HEIGHT_SCALE / 10; +constexpr const Real Z_OFFSET_BRIDGE = MAP_HEIGHT_SCALE; + +static_assert(MAX_BATCH_VERTICES <= MAX_VERTICES, "Tile block exceeds the batch vertex buffer"); +static_assert(6 * MAX_TILES_IN_BATCH * MAX_TILES_IN_BATCH <= MAX_INDICES, "Tile block exceeds the batch index buffer"); + +namespace +{ + +enum CPP_11(: Int) +{ + U_MIN = 1 << 0, + U_MAX = 1 << 1, + V_MIN = 1 << 2, + V_MAX = 1 << 3, +}; + +UnsignedByte getUVOutcode(const Real u, const Real v) +{ + UnsignedByte outcode = 0; + if (u < 0.0f) + outcode |= U_MIN; + else if (u > 1.0f) + outcode |= U_MAX; + if (v < 0.0f) + outcode |= V_MIN; + else if (v > 1.0f) + outcode |= V_MAX; + return outcode; +} + +Real getMapHeight(WorldHeightMap& map, Int x, Int y) +{ + x += map.getBorderSizeInline(); + y += map.getBorderSizeInline(); + return map.getDataPtr()[x + y * map.getXExtent()] * MAP_HEIGHT_SCALE; +} + +Bool isOnBridge(const Vector3& loc, Real& height, Vector3& normal) +{ + if(!TheTerrainLogic->getFirstBridge()) + return false; + + Coord3D center; + center.x = loc.X; + center.y = loc.Y; + center.z = loc.Z; + + PathfindLayerEnum layer = TheTerrainLogic->getLayerForDestination(¢er); + Bridge* bridge = TheTerrainLogic->findBridgeLayerAt(¢er, layer); + if (bridge) + { + Coord3D bridgeNormal; + height = bridge->getBridgeHeight(¢er, &bridgeNormal) + Z_OFFSET_BRIDGE; + normal.Set(bridgeNormal.x, bridgeNormal.y, bridgeNormal.z); + return true; + } + + return false; +} + +Bool isTerrainFlat(WorldHeightMap& map, const IRegion2D& bounds, Real& height) +{ + height = getMapHeight(map, bounds.lo.x, bounds.lo.y); + for (Int j = bounds.lo.y; j < bounds.hi.y; j++) + for (Int i = bounds.lo.x; i < bounds.hi.x; i++) + if (fabsf(getMapHeight(map, i, j) - height) > Z_OFFSET) + return false; + height += Z_OFFSET; + return true; +} + +} + +W3DTerrainParticle::W3DTerrainParticle() + : m_vertexData(W3DNEWARRAY VertexFormatXYZNDUV2[MAX_VERTICES]) + , m_indexData(W3DNEWARRAY UnsignedShort[MAX_INDICES]) + , m_outcodes(W3DNEWARRAY UnsignedByte[MAX_BATCH_VERTICES]) + , m_numVertices(0) + , m_numIndices(0) + , m_texture(nullptr) + , m_pointLoc(nullptr) + , m_pointDiffuse(nullptr) + , m_pointSize(nullptr) + , m_pointOrientation(nullptr) + , m_pointCount(0) + , m_terrainInViewBounds() + , m_defaultPointSize(0.0f) + , m_defaultPointColor(1.0f, 1.0f, 1.0f) + , m_defaultPointAlpha(1.0f) + , m_defaultPointOrientation(0) +{ + m_defaultDiffuse = DX8Wrapper::Convert_Color_Clamp(Vector4(m_defaultPointColor.X, m_defaultPointColor.Y, m_defaultPointColor.Z, m_defaultPointAlpha)); +} + +W3DTerrainParticle::~W3DTerrainParticle() +{ + delete[] m_vertexData; + delete[] m_indexData; + delete[] m_outcodes; + REF_PTR_RELEASE(m_texture); + REF_PTR_RELEASE(m_pointLoc); + REF_PTR_RELEASE(m_pointDiffuse); + REF_PTR_RELEASE(m_pointSize); + REF_PTR_RELEASE(m_pointOrientation); +} + +void W3DTerrainParticle::setTexture(TextureClass* texture) +{ + REF_PTR_SET(m_texture, texture); +} + +void W3DTerrainParticle::setShader(ShaderClass shader) +{ + m_shader = shader; +} + +void W3DTerrainParticle::setArrays( + ShareBufferClass* locs, + ShareBufferClass* diffuse, + ShareBufferClass* sizes, + ShareBufferClass* orientations, + Int activePointCount) +{ + WWASSERT(locs); + WWASSERT(activePointCount <= locs->Get_Count()); + + // Ensure lengths of all arrays are the same + WWASSERT(!diffuse || locs->Get_Count() == diffuse->Get_Count()); + WWASSERT(!sizes || locs->Get_Count() == sizes->Get_Count()); + WWASSERT(!orientations || locs->Get_Count() == orientations->Get_Count()); + + REF_PTR_SET(m_pointLoc, locs); + REF_PTR_SET(m_pointDiffuse, diffuse); + REF_PTR_SET(m_pointSize, sizes); + REF_PTR_SET(m_pointOrientation, orientations); + + m_pointCount = activePointCount >= 0 ? activePointCount : locs->Get_Count(); +} + +void W3DTerrainParticle::setBoundingBox(const AABoxClass& worldBoundingBox) +{ + m_terrainInViewBounds.lo.x = REAL_TO_INT_FLOOR((worldBoundingBox.Center.X - worldBoundingBox.Extent.X) / MAP_XY_FACTOR); + m_terrainInViewBounds.hi.x = REAL_TO_INT_FLOOR((worldBoundingBox.Center.X + worldBoundingBox.Extent.X) / MAP_XY_FACTOR); + m_terrainInViewBounds.lo.y = REAL_TO_INT_FLOOR((worldBoundingBox.Center.Y - worldBoundingBox.Extent.Y) / MAP_XY_FACTOR); + m_terrainInViewBounds.hi.y = REAL_TO_INT_FLOOR((worldBoundingBox.Center.Y + worldBoundingBox.Extent.Y) / MAP_XY_FACTOR); +} + +void W3DTerrainParticle::render() +{ + if (m_pointCount <= 0 || !m_pointLoc || !TheTerrainRenderObject) + return; + + WorldHeightMap* map = TheTerrainRenderObject->getMap(); + if (!map) + return; + + updateSettings(); + + for (Int p = 0; p < m_pointCount; p++) + { + Vector3 loc = m_pointLoc->Get_Array()[p]; + UnsignedInt diffuse = m_pointDiffuse ? DX8Wrapper::Convert_Color_Clamp(m_pointDiffuse->Get_Array()[p]) : m_defaultDiffuse; + Real size = m_pointSize ? m_pointSize->Get_Array()[p] : m_defaultPointSize; + UnsignedByte orientation = m_pointOrientation ? m_pointOrientation->Get_Array()[p] : m_defaultPointOrientation; + + processParticle(*map, loc, diffuse, size, orientation); + } + + flushBatch(); + + // Restore the texture state. + if (m_texture) + { + m_texture->Get_Filter().Apply(0); + } +} + +void W3DTerrainParticle::processParticle(WorldHeightMap& map, const Vector3& loc, UnsignedInt diffuse, Real size, UnsignedByte orientation) +{ + const Real angle = orientation / 255.0f * 2.0f * WWMATH_PI; + const Real cosine = WWMath::Fast_Cos(angle); + const Real sine = WWMath::Fast_Sin(angle); + const Real projectedRadius = size * (fabsf(cosine) + fabsf(sine)); + + IRegion2D bounds = calcBounds(map, loc, projectedRadius); + if (bounds.width() < 2 || bounds.height() < 2) + return; + + Real z; + Vector3 normal = Vector3(0.0f, 0.0f, 1.0f); + if (isOnBridge(loc, z, normal) || isTerrainFlat(map, bounds, z)) + { + drawQuad(loc, diffuse, size, cosine, sine, z, normal); + } + else + { + drawTerrainConformingMesh(map, loc, bounds, diffuse, size, cosine, sine); + } +} + +void W3DTerrainParticle::drawQuad(const Vector3& loc, + UnsignedInt diffuse, + Real size, + Real cosine, + Real sine, + Real height, + const Vector3& normal) +{ + if (m_numVertices + 4 > MAX_VERTICES || m_numIndices + 6 > MAX_INDICES) + { + flushBatch(); + } + + static constexpr const Real cornerU[4] = { 0.0f, 1.0f, 1.0f, 0.0f }; + static constexpr const Real cornerV[4] = { 0.0f, 0.0f, 1.0f, 1.0f }; + + const UnsignedShort baseVertex = m_numVertices; + for (Int index = 0; index < 4; index++) + { + const Real localX = size * (1.0f - 2.0f * cornerU[index]); + const Real localY = size * (1.0f - 2.0f * cornerV[index]); + VertexFormatXYZNDUV2 vertex; + vertex.diffuse = diffuse; + vertex.x = loc.X + cosine * localX + sine * localY; + vertex.y = loc.Y - sine * localX + cosine * localY; + vertex.z = height; + if (fabsf(normal.Z) > WWMATH_EPSILON) + { + const Real deltaX = vertex.x - loc.X; + const Real deltaY = vertex.y - loc.Y; + vertex.z -= (normal.X * deltaX + normal.Y * deltaY) / normal.Z; + } + vertex.nx = normal.X; + vertex.ny = normal.Y; + vertex.nz = normal.Z; + vertex.u1 = cornerU[index]; + vertex.v1 = cornerV[index]; + vertex.u2 = 0.0f; + vertex.v2 = 0.0f; + m_outcodes[index] = 0; + m_vertexData[m_numVertices++] = vertex; + } + + addTriangle(baseVertex, 0, 1, 2); + addTriangle(baseVertex, 0, 2, 3); +} + +void W3DTerrainParticle::drawTerrainConformingMesh(WorldHeightMap& map, const Vector3& loc, const IRegion2D& bounds, UnsignedInt diffuse, Real size, Real cosine, Real sine) +{ + // Split the drawing into blocks of at most MAX_TILES_IN_BATCH cells per axis. + // This lets large particles that exceed the batch buffers be drawn over multiple flushes. + for (Int batchMinY = bounds.lo.y; batchMinY < bounds.hi.y - 1; batchMinY += MAX_TILES_IN_BATCH) + { + const Int batchMaxY = std::min(batchMinY + MAX_TILES_IN_BATCH + 1, bounds.hi.y); + for (Int batchMinX = bounds.lo.x; batchMinX < bounds.hi.x - 1; batchMinX += MAX_TILES_IN_BATCH) + { + const Int batchMaxX = std::min(batchMinX + MAX_TILES_IN_BATCH + 1, bounds.hi.x); + const Int batchWidth = batchMaxX - batchMinX; + const Int batchHeight = batchMaxY - batchMinY; + + // The buffer is full and a draw to screen is needed. + // Note that this estimate is conservative as it does not account for triangles filtered by outcodes. + if (m_numVertices + batchWidth * batchHeight > MAX_VERTICES || + m_numIndices + 6 * (batchWidth - 1) * (batchHeight - 1) > MAX_INDICES) + { + flushBatch(); + } + + Int i, j; + const UnsignedShort baseVertex = m_numVertices; + for (j = batchMinY; j < batchMaxY; j++) + { + for (i = batchMinX; i < batchMaxX; i++) + { + VertexFormatXYZNDUV2 vertex; + vertex.diffuse = diffuse; + vertex.x = i * MAP_XY_FACTOR; + vertex.y = j * MAP_XY_FACTOR; + vertex.z = getMapHeight(map, i, j) + Z_OFFSET; + // The vertex normal is not used by the renderer and does not align with the terrain. + vertex.nx = 0.0f; + vertex.ny = 0.0f; + vertex.nz = 1.0f; + const Real deltaX = vertex.x - loc.X; + const Real deltaY = vertex.y - loc.Y; + const Real localX = cosine * deltaX - sine * deltaY; + const Real localY = sine * deltaX + cosine * deltaY; + vertex.u1 = 0.5f - localX / (2.0f * size); + vertex.v1 = 0.5f - localY / (2.0f * size); + vertex.u2 = 0.0f; + vertex.v2 = 0.0f; + m_outcodes[(j - batchMinY) * batchWidth + i - batchMinX] = getUVOutcode(vertex.u1, vertex.v1); + m_vertexData[m_numVertices++] = vertex; + } + } + + for (j = 0; j < batchHeight - 1; j++) + { + for (i = 0; i < batchWidth - 1; i++) + { + const UnsignedShort topLeftOffset = j * batchWidth + i; + const UnsignedShort topRightOffset = topLeftOffset + 1; + const UnsignedShort bottomLeftOffset = topLeftOffset + batchWidth; + const UnsignedShort bottomRightOffset = bottomLeftOffset + 1; + const Int mapCellX = batchMinX + i + map.getBorderSizeInline(); + const Int mapCellY = batchMinY + j + map.getBorderSizeInline(); + if (map.getQuickFlipState(mapCellX, mapCellY)) + { + addTriangle(baseVertex, topRightOffset, bottomLeftOffset, topLeftOffset); + addTriangle(baseVertex, topRightOffset, bottomRightOffset, bottomLeftOffset); + } + else + { + addTriangle(baseVertex, topLeftOffset, bottomRightOffset, bottomLeftOffset); + addTriangle(baseVertex, topLeftOffset, topRightOffset, bottomRightOffset); + } + } + } + } + } +} + +inline void W3DTerrainParticle::addTriangle(UnsignedShort baseVertex, + UnsignedShort offsetA, + UnsignedShort offsetB, + UnsignedShort offsetC) +{ + if ((m_outcodes[offsetA] & m_outcodes[offsetB] & m_outcodes[offsetC]) != 0) + return; + + m_indexData[m_numIndices++] = baseVertex + offsetA; + m_indexData[m_numIndices++] = baseVertex + offsetB; + m_indexData[m_numIndices++] = baseVertex + offsetC; +} + +void W3DTerrainParticle::flushBatch() +{ + if (m_numIndices > 0 && m_numVertices > 0) + { + DynamicVBAccessClass vertexAccess(BUFFER_TYPE_DYNAMIC_DX8, dynamic_fvf_type, m_numVertices); + { + DynamicVBAccessClass::WriteLockClass vertexLock(&vertexAccess); + memcpy(vertexLock.Get_Formatted_Vertex_Array(), + m_vertexData, + m_numVertices * sizeof(VertexFormatXYZNDUV2)); + } + + DynamicIBAccessClass indexAccess(BUFFER_TYPE_DYNAMIC_DX8, m_numIndices); + { + DynamicIBAccessClass::WriteLockClass indexLock(&indexAccess); + memcpy(indexLock.Get_Index_Array(), + m_indexData, + m_numIndices * sizeof(UnsignedShort)); + } + + DX8Wrapper::Set_Index_Buffer(indexAccess, 0); + DX8Wrapper::Set_Vertex_Buffer(vertexAccess); + DX8Wrapper::Draw_Triangles(0, + m_numIndices / 3, + 0, + m_numVertices); + } + + m_numVertices = 0; + m_numIndices = 0; +} + +IRegion2D W3DTerrainParticle::calcBounds(WorldHeightMap& map, const Vector3& loc, Real projectedRadius) const +{ + IRegion2D bounds; + + bounds.lo.x = REAL_TO_INT_FLOOR((loc.X - projectedRadius) / MAP_XY_FACTOR); + bounds.lo.y = REAL_TO_INT_FLOOR((loc.Y - projectedRadius) / MAP_XY_FACTOR); + bounds.hi.x = REAL_TO_INT_CEIL((loc.X + projectedRadius) / MAP_XY_FACTOR) + 1; + bounds.hi.y = REAL_TO_INT_CEIL((loc.Y + projectedRadius) / MAP_XY_FACTOR) + 1; + + bounds.intersect(map.getLogicalBounds()); + bounds.intersect(m_terrainInViewBounds); + + return bounds; +} + +void W3DTerrainParticle::updateSettings() +{ + // If there is a color or alpha array enable gradient in shader - otherwise disable. + const Real value255 = 0.9961f; // 254 / 255 + const Bool defaultWhiteOpaque = m_defaultPointColor.X > value255 && + m_defaultPointColor.Y > value255 && + m_defaultPointColor.Z > value255 && + m_defaultPointAlpha > value255; + + // The reason we check for lack of texture here is that SR seems to render black triangles + // rather than white triangles as would be expected) when there is no texture AND no gradient. + if (m_pointDiffuse || !defaultWhiteOpaque || !m_texture) + { + m_shader.Set_Primary_Gradient(ShaderClass::GRADIENT_MODULATE); + } + else + { + m_shader.Set_Primary_Gradient(ShaderClass::GRADIENT_DISABLE); + } + + // If m_texture is non-null enable texturing in shader - otherwise disable. + if (m_texture) + { + m_shader.Set_Texturing(ShaderClass::TEXTURING_ENABLE); + } + else + { + m_shader.Set_Texturing(ShaderClass::TEXTURING_DISABLE); + } + m_shader.Set_Cull_Mode(ShaderClass::CULL_MODE_ENABLE); + + DX8Wrapper::Set_World_Identity(); + VertexMaterialClass* material = VertexMaterialClass::Get_Preset(VertexMaterialClass::PRELIT_DIFFUSE); + DX8Wrapper::Set_Material(material); + REF_PTR_RELEASE(material); + DX8Wrapper::Set_Shader(m_shader); + DX8Wrapper::Set_Texture(0, m_texture); + + // To prevent visual glitches on overdraw we clamp the texture to a transparent black pixel. + DX8Wrapper::Apply_Render_State_Changes(); + DX8Wrapper::Set_DX8_Texture_Stage_State(0, D3DTSS_ADDRESSU, D3DTADDRESS_BORDER); + DX8Wrapper::Set_DX8_Texture_Stage_State(0, D3DTSS_ADDRESSV, D3DTADDRESS_BORDER); + DX8Wrapper::Set_DX8_Texture_Stage_State(0, D3DTSS_BORDERCOLOR, 0x00000000); +} From deeba04e2fc9f54d6177bd90afb7057025fc5479 Mon Sep 17 00:00:00 2001 From: stm <14291421+stephanmeesters@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:56:43 +0200 Subject: [PATCH 6/7] feat(terrainparticle): Add terrain conforming particle statistics --- .../GameClient/W3DTerrainParticle.cpp | 6 +++ .../Source/WWVegas/WW3D2/statistics.cpp | 52 +++++++++++++++++++ .../Source/WWVegas/WW3D2/statistics.h | 10 ++++ .../Include/W3DDevice/GameClient/W3DDisplay.h | 1 + .../W3DDevice/GameClient/W3DDisplay.cpp | 8 +++ 5 files changed, 77 insertions(+) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp index a8ea5de91df..f1f35c573a8 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp @@ -28,6 +28,7 @@ #include "WW3D2/dx8vertexbuffer.h" #include "WW3D2/dx8wrapper.h" #include "WW3D2/rinfo.h" +#include "WW3D2/statistics.h" #include "WW3D2/texture.h" #include "WW3D2/vertmaterial.h" #include "WWLib/refcount.h" @@ -248,6 +249,8 @@ void W3DTerrainParticle::drawQuad(const Vector3& loc, Real height, const Vector3& normal) { + DX8_RECORD_TERRAIN_PARTICLE_QUAD(); + if (m_numVertices + 4 > MAX_VERTICES || m_numIndices + 6 > MAX_INDICES) { flushBatch(); @@ -289,6 +292,8 @@ void W3DTerrainParticle::drawQuad(const Vector3& loc, void W3DTerrainParticle::drawTerrainConformingMesh(WorldHeightMap& map, const Vector3& loc, const IRegion2D& bounds, UnsignedInt diffuse, Real size, Real cosine, Real sine) { + DX8_RECORD_TERRAIN_PARTICLE_MESH(); + // Split the drawing into blocks of at most MAX_TILES_IN_BATCH cells per axis. // This lets large particles that exceed the batch buffers be drawn over multiple flushes. for (Int batchMinY = bounds.lo.y; batchMinY < bounds.hi.y - 1; batchMinY += MAX_TILES_IN_BATCH) @@ -397,6 +402,7 @@ void W3DTerrainParticle::flushBatch() DX8Wrapper::Set_Index_Buffer(indexAccess, 0); DX8Wrapper::Set_Vertex_Buffer(vertexAccess); + DX8_RECORD_TERRAIN_PARTICLE_BATCH(m_numIndices / 3); DX8Wrapper::Draw_Triangles(0, m_numIndices / 3, 0, diff --git a/Core/Libraries/Source/WWVegas/WW3D2/statistics.cpp b/Core/Libraries/Source/WWVegas/WW3D2/statistics.cpp index 0c6d2311e1e..c39dfd2a4c5 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/statistics.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/statistics.cpp @@ -279,6 +279,14 @@ static int sorting_polygons; static int last_frame_sorting_polygons; static int sorting_vertices; static int last_frame_sorting_vertices; +static int terrain_particle_triangles; +static int last_frame_terrain_particle_triangles; +static int terrain_particle_quads; +static int last_frame_terrain_particle_quads; +static int terrain_particle_meshes; +static int last_frame_terrain_particle_meshes; +static int terrain_particle_batches; +static int last_frame_terrain_particle_batches; static int draw_calls; static int last_frame_draw_calls; @@ -344,6 +352,42 @@ int Debug_Statistics::Get_Sorting_Vertices() return last_frame_sorting_vertices; } +void Debug_Statistics::Record_Terrain_Particle_Quad() +{ + terrain_particle_quads++; +} + +void Debug_Statistics::Record_Terrain_Particle_Mesh() +{ + terrain_particle_meshes++; +} + +void Debug_Statistics::Record_Terrain_Particle_Batch(int triangle_count) +{ + terrain_particle_triangles += triangle_count; + terrain_particle_batches++; +} + +int Debug_Statistics::Get_Terrain_Particle_Triangles() +{ + return last_frame_terrain_particle_triangles; +} + +int Debug_Statistics::Get_Terrain_Particle_Quads() +{ + return last_frame_terrain_particle_quads; +} + +int Debug_Statistics::Get_Terrain_Particle_Meshes() +{ + return last_frame_terrain_particle_meshes; +} + +int Debug_Statistics::Get_Terrain_Particle_Batches() +{ + return last_frame_terrain_particle_batches; +} + int Debug_Statistics::Get_Draw_Calls() { return last_frame_draw_calls; @@ -364,6 +408,10 @@ void Debug_Statistics::Begin_Statistics() dx8_skin_renders=0; sorting_polygons=0; sorting_vertices=0; + terrain_particle_triangles=0; + terrain_particle_quads=0; + terrain_particle_meshes=0; + terrain_particle_batches=0; draw_calls=0; Record_Texture_Begin(); DX8Wrapper::Begin_Statistics(); @@ -380,6 +428,10 @@ void Debug_Statistics::End_Statistics() last_frame_dx8_vertices=dx8_vertices; last_frame_sorting_polygons=sorting_polygons; last_frame_sorting_vertices=sorting_vertices; + last_frame_terrain_particle_triangles=terrain_particle_triangles; + last_frame_terrain_particle_quads=terrain_particle_quads; + last_frame_terrain_particle_meshes=terrain_particle_meshes; + last_frame_terrain_particle_batches=terrain_particle_batches; last_frame_draw_calls=draw_calls; // DX8MeshRendererClass::End_Statistics(); DX8Wrapper::End_Statistics(); diff --git a/Core/Libraries/Source/WWVegas/WW3D2/statistics.h b/Core/Libraries/Source/WWVegas/WW3D2/statistics.h index c7d1f26a00a..2ba3ee23029 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/statistics.h +++ b/Core/Libraries/Source/WWVegas/WW3D2/statistics.h @@ -51,6 +51,9 @@ namespace Debug_Statistics void Record_DX8_Skin_Polys_And_Vertices(int pcount,int vcount); void Record_DX8_Polys_And_Vertices(int pcount,int vcount,const ShaderClass& shader); void Record_Sorting_Polys_And_Vertices(int pcount,int vcount); + void Record_Terrain_Particle_Quad(); + void Record_Terrain_Particle_Mesh(); + void Record_Terrain_Particle_Batch(int triangle_count); int Get_DX8_Polygons(); int Get_DX8_Vertices(); int Get_DX8_Skin_Renders(); @@ -58,6 +61,10 @@ namespace Debug_Statistics int Get_DX8_Skin_Vertices(); int Get_Sorting_Polygons(); int Get_Sorting_Vertices(); + int Get_Terrain_Particle_Triangles(); + int Get_Terrain_Particle_Quads(); + int Get_Terrain_Particle_Meshes(); + int Get_Terrain_Particle_Batches(); int Get_Draw_Calls(); void Begin_Statistics(); @@ -71,3 +78,6 @@ namespace Debug_Statistics #define DX8_RECORD_RENDER(polys,verts,shader) Debug_Statistics::Record_DX8_Polys_And_Vertices(polys,verts,shader) #define DX8_RECORD_SORTING_RENDER(polys,verts) Debug_Statistics::Record_Sorting_Polys_And_Vertices(polys,verts) #define DX8_RECORD_SKIN_RENDER(polys,verts) Debug_Statistics::Record_DX8_Skin_Polys_And_Vertices(polys,verts) +#define DX8_RECORD_TERRAIN_PARTICLE_QUAD() Debug_Statistics::Record_Terrain_Particle_Quad() +#define DX8_RECORD_TERRAIN_PARTICLE_MESH() Debug_Statistics::Record_Terrain_Particle_Mesh() +#define DX8_RECORD_TERRAIN_PARTICLE_BATCH(triangles) Debug_Statistics::Record_Terrain_Particle_Batch(triangles) diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index ade19e0ef83..3a67a056ca9 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -199,6 +199,7 @@ class W3DDisplay : public Display KEY_MOUSE_STATES, ///< keyboard modifier and mouse button states. MousePosition, ///< debug display mouse position Particles, ///< debug display particles + TerrainParticles, ///< debug display for terrain-conforming particles Objects, ///< debug display total number of objects NetIncoming, ///< debug display network incoming stats NetOutgoing, ///< debug display network outgoing stats diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 155e8ac43ec..e730e1b461b 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -1472,6 +1472,14 @@ void W3DDisplay::gatherDebugStats() unibuffer.format( L"Particles: %d in world, %d being displayed", totalParticles, onScreenParticleCount ); m_displayStrings[Particles]->setText( unibuffer ); + //display the terrain-conforming particle load, split by render path + unibuffer.format( L"Terrain Particles: %d quad, %d mesh, %d tris, %d draws", + Debug_Statistics::Get_Terrain_Particle_Quads(), + Debug_Statistics::Get_Terrain_Particle_Meshes(), + Debug_Statistics::Get_Terrain_Particle_Triangles(), + Debug_Statistics::Get_Terrain_Particle_Batches() ); + m_displayStrings[TerrainParticles]->setText( unibuffer ); + //display the number of objects in the world UnsignedInt objCount = TheGameLogic->getObjectCount(); UnsignedInt objScreenCount = TheGameClient->getRenderedObjectCount(); From b5352279077c13f7069d7aa84994128c289bdcae Mon Sep 17 00:00:00 2001 From: stm <14291421+stephanmeesters@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:12:40 +0200 Subject: [PATCH 7/7] feat(terrainparticle): Add define guard for terrain conforming particles --- Core/GameEngine/Include/Common/GameDefines.h | 5 +++++ Core/GameEngine/Include/GameClient/ParticleSys.h | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/Core/GameEngine/Include/Common/GameDefines.h b/Core/GameEngine/Include/Common/GameDefines.h index be20d29c292..1b2ad204926 100644 --- a/Core/GameEngine/Include/Common/GameDefines.h +++ b/Core/GameEngine/Include/Common/GameDefines.h @@ -159,6 +159,11 @@ #define PRIORITIZE_TEXTURES_BY_SIZE (1) #endif +// Enable drawing particles as a terrain-conforming mesh. +#ifndef ENABLE_TERRAIN_CONFORMING_PARTICLES +#define ENABLE_TERRAIN_CONFORMING_PARTICLES (1) +#endif + // Enable obsolete code. This mainly refers to code that existed in Generals but was removed in GeneralsMD. // Disable and remove this when Generals and GeneralsMD are merged. #if RTS_GENERALS diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index c154c4034ae..0757f03823c 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -30,6 +30,7 @@ #pragma once #include "Common/AsciiString.h" +#include "Common/GameDefines.h" #include "Common/GameMemory.h" #include "Common/GameType.h" #include "Common/Snapshot.h" @@ -516,7 +517,11 @@ class ParticleSystemTemplate : public MemoryPoolObject, protected ParticleSystem void validate(); const AsciiString& getName() const { return m_name; } +#if ENABLE_TERRAIN_CONFORMING_PARTICLES Bool getIsTerrainConforming() const { return m_isTerrainConforming; } +#else + Bool getIsTerrainConforming() const { return FALSE; } +#endif // This function was made const because of update modules' module data being all const. ParticleSystem *createSlaveSystem( Bool createSlaves = TRUE ) const ; ///< if returns non-null, it is a slave system for use