From cbafb26c0da17b1728daaaf1c8c0735bdd39f8ec 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 1/6] 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 2/6] 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 e3b23584b9ceffd485129e84bacc3e767622593e 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 3/6] 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 4bf120e332d..619db49df58 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, UnsignedInt getVolumeParticleDepth() const { return m_volumeParticleDepth; } Bool shouldBillboard() const { return !m_isGroundAligned; } + Bool isTerrainConforming() { return m_template->getIsTerrainConforming(); } ParticleShaderType getShaderType() const { 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 7cf57689c28c8dfb93dfae1de1e15d7a0b717cce 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 4/6] feat(terrainparticle): Add terrain conforming particle renderer --- .../Include/GameClient/ParticleSys.h | 2 +- Core/GameEngineDevice/CMakeLists.txt | 2 + .../W3DDevice/GameClient/W3DParticleSys.h | 8 +- .../W3DDevice/GameClient/W3DTerrainParticle.h | 85 ++++ .../W3DDevice/GameClient/W3DParticleSys.cpp | 78 ++-- .../GameClient/W3DTerrainParticle.cpp | 407 ++++++++++++++++++ 6 files changed, 551 insertions(+), 31 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/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index 619db49df58..b435ed7219b 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -617,7 +617,7 @@ class ParticleSystem : public MemoryPoolObject, UnsignedInt getVolumeParticleDepth() const { return m_volumeParticleDepth; } Bool shouldBillboard() const { return !m_isGroundAligned; } - Bool isTerrainConforming() { return m_template->getIsTerrainConforming(); } + Bool isTerrainConforming() const { return m_template->getIsTerrainConforming(); } ParticleShaderType getShaderType() const { return m_shaderType; } 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 cf17783902e..9df830de461 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,8 +51,8 @@ class W3DParticleSystemManager : public ParticleSystemManager virtual Int getOnScreenParticleCount() override { return m_onScreenParticleCount; } private: - Bool finishedBatch(const ParticleSystem& system, const RefCountPtr& texture); - void initializeBatch(const ParticleSystem& system, const RefCountPtr& texture); + Bool finishedBatch(const ParticleSystem&system, const RefCountPtr&texture); + void initializeBatch(const ParticleSystem& system, const RefCountPtr& texture, const AABoxClass& bbox); void flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount); enum { MAX_POINTS_PER_GROUP = 512 }; @@ -59,6 +60,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 @@ -67,4 +69,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..a0f5fdf53d6 --- /dev/null +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainParticle.h @@ -0,0 +1,85 @@ +/* +** 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 + +#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: + struct ParticleContext; + + void drawRegion(WorldHeightMap& map, const ParticleContext& particle, const IRegion2D& bounds); + void drawQuad(WorldHeightMap& map, const ParticleContext& particle, const IRegion2D& bounds, Bool flipped); + UnsignedShort addVertex(WorldHeightMap& map, const ParticleContext& particle, Int x, Int y); + void addTriangle(UnsignedShort a, UnsignedShort b, UnsignedShort c); + void resetVertexLookup(); + void flushBatch(); + IRegion2D calcTerrainBounds(WorldHeightMap& map, const Vector3& loc, Real projectedRadius) const; + void updateSettings(); + + std::vector m_vertexData; ///< Vertices of the current batch. + std::vector m_indexData; ///< Indices defining the triangles of the current batch. + std::vector m_outcodes; ///< UV outcodes indexed by batch vertex. + std::vector m_vertexLookup; ///< Particle grid vertices mapped to current batch indices. + 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 9ceadaa2894..bd6248b1774 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); @@ -222,7 +227,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); } UnsignedInt startCount = pointCount; @@ -277,7 +282,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; } } @@ -370,7 +375,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); @@ -416,46 +420,64 @@ Bool W3DParticleSystemManager::finishedBatch(const ParticleSystem& system, const { return texture.Peek() != m_batchTexture.Peek() || system.getShaderType() != m_batchShaderType || - system.shouldBillboard() != m_batchBillboard; + system.shouldBillboard() != m_batchBillboard || + system.isTerrainConforming() != m_batchIsConforming; } -void W3DParticleSystemManager::initializeBatch(const ParticleSystem& system, const RefCountPtr& texture) +void W3DParticleSystemManager::initializeBatch(const 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; } m_batchTexture.Clear(); diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp new file mode 100644 index 00000000000..fc6dade27a3 --- /dev/null +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp @@ -0,0 +1,407 @@ +/* +** 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 "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 UnsignedShort INVALID_VERTEX = 0xffff; +constexpr const Real Z_OFFSET = MAP_HEIGHT_SCALE / 10; + +static_assert(MAX_VERTICES < INVALID_VERTEX, "Batch vertex indices must leave room for the lookup sentinel"); + +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 isTerrainFlat(WorldHeightMap& map, const IRegion2D& bounds) +{ + const Int stride = map.getXExtent(); + const Int border = map.getBorderSizeInline(); + const UnsignedByte* firstRow = map.getDataPtr() + (bounds.lo.y + border) * stride + bounds.lo.x + border; + const UnsignedByte height = firstRow[0]; + + for (Int j = 0; j < bounds.height(); j++) + { + const UnsignedByte* row = firstRow + j * stride; + for (Int i = 0; i < bounds.width(); i++) + if (row[i] != height) + return false; + } + + return true; +} + +} // namespace + +struct W3DTerrainParticle::ParticleContext +{ + Vector3 loc; + IRegion2D bounds; + UnsignedInt diffuse; + Real size; + Real cosine; + Real sine; +}; + +W3DTerrainParticle::W3DTerrainParticle() + : m_vertexData(MAX_VERTICES) + , m_indexData(MAX_INDICES) + , m_outcodes(MAX_VERTICES) + , m_vertexLookup(MAX_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() +{ + 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; + + 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)); + + const IRegion2D bounds = calcTerrainBounds(*map, loc, projectedRadius); + if (bounds.width() < 2 || bounds.height() < 2) + continue; + + ParticleContext particle; + particle.loc = loc; + particle.bounds = bounds; + particle.diffuse = diffuse; + particle.size = size; + particle.cosine = cosine; + particle.sine = sine; + + resetVertexLookup(); + drawRegion(*map, particle, bounds); + } + + flushBatch(); + + // Restore the texture state. + if (m_texture) + { + m_texture->Get_Filter().Apply(0); + } +} + +void W3DTerrainParticle::drawRegion(WorldHeightMap& map, const ParticleContext& particle, const IRegion2D& bounds) +{ + if (bounds.width() == 2 && bounds.height() == 2 || isTerrainFlat(map, bounds)) + { + const Bool flipped = map.getQuickFlipState(bounds.lo.x + map.getBorderSizeInline(), + bounds.lo.y + map.getBorderSizeInline()); + drawQuad(map, particle, bounds, flipped); + return; + } + + const Bool splitX = bounds.width() > 2; + const Bool splitY = bounds.height() > 2; + const Int midX = bounds.lo.x + (bounds.width() - 1) / 2; + const Int midY = bounds.lo.y + (bounds.height() - 1) / 2; + for (Int y = 0; y < (splitY ? 2 : 1); y++) + { + for (Int x = 0; x < (splitX ? 2 : 1); x++) + { + IRegion2D child; + child.lo.x = x == 0 ? bounds.lo.x : midX; + child.hi.x = splitX && x == 0 ? midX + 1 : bounds.hi.x; + child.lo.y = y == 0 ? bounds.lo.y : midY; + child.hi.y = splitY && y == 0 ? midY + 1 : bounds.hi.y; + drawRegion(map, particle, child); + } + } +} + +void W3DTerrainParticle::drawQuad(WorldHeightMap& map, const ParticleContext& particle, const IRegion2D& bounds, Bool flipped) +{ + if (m_numVertices + 4 > MAX_VERTICES || m_numIndices + 6 > MAX_INDICES) + { + flushBatch(); + } + + const UnsignedShort a = addVertex(map, particle, bounds.lo.x, bounds.lo.y); + const UnsignedShort b = addVertex(map, particle, bounds.hi.x - 1, bounds.lo.y); + const UnsignedShort c = addVertex(map, particle, bounds.lo.x, bounds.hi.y - 1); + const UnsignedShort d = addVertex(map, particle, bounds.hi.x - 1, bounds.hi.y - 1); + if (flipped) + { + addTriangle(b, c, a); + addTriangle(b, d, c); + } + else + { + addTriangle(a, d, c); + addTriangle(a, b, d); + } +} + +UnsignedShort W3DTerrainParticle::addVertex(WorldHeightMap& map, const ParticleContext& particle, Int x, Int y) +{ + UnsignedShort& index = m_vertexLookup[(y - particle.bounds.lo.y) * particle.bounds.width() + x - particle.bounds.lo.x]; + // Vertex may not exist yet, or got removed in the last flush. + if (index != INVALID_VERTEX) + return index; + + index = m_numVertices++; + VertexFormatXYZNDUV2& vertex = m_vertexData[index]; + vertex.diffuse = particle.diffuse; + vertex.x = x * MAP_XY_FACTOR; + vertex.y = y * MAP_XY_FACTOR; + vertex.z = getMapHeight(map, x, y) + Z_OFFSET; + const Real deltaX = vertex.x - particle.loc.X; + const Real deltaY = vertex.y - particle.loc.Y; + const Real localX = particle.cosine * deltaX - particle.sine * deltaY; + const Real localY = particle.sine * deltaX + particle.cosine * deltaY; + vertex.u1 = 0.5f - localX / (2.0f * particle.size); + vertex.v1 = 0.5f - localY / (2.0f * particle.size); + m_outcodes[index] = getUVOutcode(vertex.u1, vertex.v1); + return index; +} + +inline void W3DTerrainParticle::addTriangle(UnsignedShort a, UnsignedShort b, UnsignedShort c) +{ + // Skip the triangle when all UV's are outside (transparent). + if ((m_outcodes[a] & m_outcodes[b] & m_outcodes[c]) != 0) + return; + + m_indexData[m_numIndices++] = a; + m_indexData[m_numIndices++] = b; + m_indexData[m_numIndices++] = c; +} + +void W3DTerrainParticle::resetVertexLookup() +{ + std::fill(m_vertexLookup.begin(), m_vertexLookup.end(), INVALID_VERTEX); +} + +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[0], + m_numVertices * sizeof(VertexFormatXYZNDUV2)); + } + + DynamicIBAccessClass indexAccess(BUFFER_TYPE_DYNAMIC_DX8, m_numIndices); + { + DynamicIBAccessClass::WriteLockClass indexLock(&indexAccess); + memcpy(indexLock.Get_Index_Array(), + &m_indexData[0], + 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; + resetVertexLookup(); +} + +IRegion2D W3DTerrainParticle::calcTerrainBounds(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 ea7b1ecdcb7127b293e0017a8e1f4bf7aae472f7 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 5/6] feat(terrainparticle): Add terrain conforming particle statistics --- .../GameClient/W3DTerrainParticle.cpp | 2 ++ .../Source/WWVegas/WW3D2/statistics.cpp | 24 +++++++++++++++++++ .../Source/WWVegas/WW3D2/statistics.h | 4 ++++ .../Include/W3DDevice/GameClient/W3DDisplay.h | 1 + .../W3DDevice/GameClient/W3DDisplay.cpp | 6 +++++ 5 files changed, 37 insertions(+) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp index fc6dade27a3..bf8543328e7 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp @@ -27,6 +27,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" @@ -335,6 +336,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..467463730b9 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/statistics.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/statistics.cpp @@ -279,6 +279,10 @@ 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_batches; +static int last_frame_terrain_particle_batches; static int draw_calls; static int last_frame_draw_calls; @@ -344,6 +348,22 @@ int Debug_Statistics::Get_Sorting_Vertices() return last_frame_sorting_vertices; } +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_Batches() +{ + return last_frame_terrain_particle_batches; +} + int Debug_Statistics::Get_Draw_Calls() { return last_frame_draw_calls; @@ -364,6 +384,8 @@ void Debug_Statistics::Begin_Statistics() dx8_skin_renders=0; sorting_polygons=0; sorting_vertices=0; + terrain_particle_triangles=0; + terrain_particle_batches=0; draw_calls=0; Record_Texture_Begin(); DX8Wrapper::Begin_Statistics(); @@ -380,6 +402,8 @@ 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_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..9176bf0cb55 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/statistics.h +++ b/Core/Libraries/Source/WWVegas/WW3D2/statistics.h @@ -51,6 +51,7 @@ 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_Batch(int triangle_count); int Get_DX8_Polygons(); int Get_DX8_Vertices(); int Get_DX8_Skin_Renders(); @@ -58,6 +59,8 @@ 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_Batches(); int Get_Draw_Calls(); void Begin_Statistics(); @@ -71,3 +74,4 @@ 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_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..57a2074699c 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -1472,6 +1472,12 @@ 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 + unibuffer.format( L"Terrain Particles: %d tris, %d draws", + 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 4b2af0aa7cbd0b79268e4097a227e9ee2ae08c1a 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 6/6] 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 b435ed7219b..a0f4e83e997 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