From d648b1b616ba4d79329993ba0a094cc59f092e71 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/7] feat(terrainparticle): Add utility functions to WorldHeightMap --- .../W3DDevice/GameClient/WorldHeightMap.h | 18 ++++++++++++++++++ .../W3DDevice/GameClient/WorldHeightMap.cpp | 15 +++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h index 10176bfa1aa..b3f08058f17 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h @@ -244,6 +244,16 @@ class WorldHeightMap : public RefCountClass, Int getXExtent() const {return m_width;} /// Date: Sun, 13 Sep 2026 13:38:50 +0200 Subject: [PATCH 2/7] refactor(particlesys): Update particle batching to use new alignment enum --- Core/GameEngine/Include/GameClient/ParticleSys.h | 1 + .../Include/W3DDevice/GameClient/W3DParticleSys.h | 2 +- .../Source/W3DDevice/GameClient/W3DParticleSys.cpp | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index d894d8b0ebb..f71e1ee69ee 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -627,6 +627,7 @@ class ParticleSystem : public MemoryPoolObject, Bool isUsingVolumeParticles() const { return m_particleType == VOLUME_PARTICLE; } UnsignedInt getVolumeParticleDepth() const { return m_volumeParticleDepth; } + ParticleAlignmentType getParticleAlignment() const { return m_particleAlignment; } Bool shouldBillboard() const { return m_particleAlignment == PARTICLE_ALIGNMENT_BILLBOARD; } ParticleShaderType getShaderType() const { return m_shaderType; } diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h index cf17783902e..f6343a5b6c3 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h @@ -66,5 +66,5 @@ class W3DParticleSystemManager : public ParticleSystemManager ParticleSystemInfo::ParticleShaderType m_batchShaderType; Bool m_readyToRender; ///< if true, it is OK to render - Bool m_batchBillboard; + ParticleSystemInfo::ParticleAlignmentType m_batchParticleAlignment; }; diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index c890104fe5a..6864d351002 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -46,7 +46,7 @@ W3DParticleSystemManager::W3DParticleSystemManager() { - m_batchBillboard = true; + m_batchParticleAlignment = ParticleSystemInfo::PARTICLE_ALIGNMENT_BILLBOARD; m_batchShaderType = ParticleSystemInfo::INVALID_SHADER; m_pointGroup = nullptr; @@ -416,14 +416,14 @@ Bool W3DParticleSystemManager::finishedBatch(const ParticleSystem& system, const { return texture.Peek() != m_batchTexture.Peek() || system.getShaderType() != m_batchShaderType || - system.shouldBillboard() != m_batchBillboard; + system.getParticleAlignment() != m_batchParticleAlignment; } void W3DParticleSystemManager::initializeBatch(const ParticleSystem& system, const RefCountPtr& texture) { m_batchTexture = texture; m_batchShaderType = system.getShaderType(); - m_batchBillboard = system.shouldBillboard(); + m_batchParticleAlignment = system.getParticleAlignment(); } void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount) @@ -451,7 +451,7 @@ void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, Unsign 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_Billboard(m_batchParticleAlignment == ParticleSystemInfo::PARTICLE_ALIGNMENT_BILLBOARD); m_pointGroup->Set_Point_Frame(0); m_pointGroup->Render(rinfo); @@ -459,6 +459,6 @@ void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, Unsign } m_batchTexture.Clear(); - m_batchBillboard = false; + m_batchParticleAlignment = ParticleSystemInfo::PARTICLE_ALIGNMENT_BILLBOARD; m_batchShaderType = ParticleSystemInfo::INVALID_SHADER; } From ab8c6b72fc8b7daf8bf756f36c2561f55096f9d7 Mon Sep 17 00:00:00 2001 From: stm <14291421+stephanmeesters@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:05:42 +0200 Subject: [PATCH 3/7] feat(terrainparticle): Expand particle alignment enum with Conforming value --- Core/GameEngine/Include/GameClient/ParticleSys.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index f71e1ee69ee..b10ccdfa049 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -436,6 +436,7 @@ class ParticleSystemInfo : public Snapshot { PARTICLE_ALIGNMENT_BILLBOARD = 0, PARTICLE_ALIGNMENT_XYPLANAR, + PARTICLE_ALIGNMENT_CONFORMING, PARTICLE_ALIGNMENT_TYPE_COUNT }; ParticleAlignmentType m_particleAlignment; ///< align particles toward the camera or with the XY plane. @@ -504,7 +505,7 @@ static_assert(ARRAY_SIZE(ParticlePriorityNames) == NUM_PARTICLE_PRIORITIES + 1, static const char *const GroundAlignmentTypeNames[] = { - "No", "Yes", nullptr + "No", "Yes", "Conforming", nullptr }; static_assert(ARRAY_SIZE(GroundAlignmentTypeNames) == ParticleSystemInfo::PARTICLE_ALIGNMENT_TYPE_COUNT + 1, "Incorrect array size"); From 906c57145fc5d958e858bba6a261039fff882628 Mon Sep 17 00:00:00 2001 From: stm <14291421+stephanmeesters@users.noreply.github.com> Date: Sun, 13 Sep 2026 13:38:50 +0200 Subject: [PATCH 4/7] refactor(particlesys): Introduce isFieldParticle --- Core/GameEngine/Include/GameClient/ParticleSys.h | 1 + Core/GameEngine/Source/GameClient/System/ParticleSys.cpp | 3 ++- .../Source/W3DDevice/GameClient/W3DParticleSys.cpp | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index b10ccdfa049..de85b8c87d8 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -629,6 +629,7 @@ class ParticleSystem : public MemoryPoolObject, UnsignedInt getVolumeParticleDepth() const { return m_volumeParticleDepth; } ParticleAlignmentType getParticleAlignment() const { return m_particleAlignment; } + Bool isFieldParticle() const { return m_particleAlignment == PARTICLE_ALIGNMENT_XYPLANAR || m_particleAlignment == PARTICLE_ALIGNMENT_CONFORMING; } Bool shouldBillboard() const { return m_particleAlignment == PARTICLE_ALIGNMENT_BILLBOARD; } 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 9fab714f490..f3b0a73a22c 100644 --- a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp +++ b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp @@ -1766,7 +1766,8 @@ Particle *ParticleSystem::createParticle( const ParticleInfo *info, TheGameLODManager->isParticleSkipped()) ) return nullptr; - if ( getParticleCount() > 0 && priority == AREA_EFFECT && !shouldBillboard() && TheParticleSystemManager->getFieldParticleCount() > (UnsignedInt)TheGlobalData->m_maxFieldParticleCount ) + if ( getParticleCount() > 0 && priority == AREA_EFFECT && isFieldParticle() && + TheParticleSystemManager->getFieldParticleCount() > (UnsignedInt)TheGlobalData->m_maxFieldParticleCount ) return nullptr; // ALWAYS_RENDER particles are exempt from all count limits, and are always created, regardless of LOD issues. diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index 6864d351002..6a63fb148e3 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -247,7 +247,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) pos = p->getPosition(); psize = p->getSize(); - m_fieldParticleCount += ( sys->getPriority() == AREA_EFFECT && !sys->shouldBillboard() ); + m_fieldParticleCount += ( sys->getPriority() == AREA_EFFECT && sys->isFieldParticle() ); //@todo lorenzen sez: use pointer arithmetic for these arrays personalities[pointCount] = p->getPersonality(); From 0e08c494d25d422d9550b286483d481e63ff7c22 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 | 5 +- .../W3DDevice/GameClient/W3DTerrainParticle.h | 99 +++++ .../W3DDevice/GameClient/W3DParticleSys.cpp | 65 +-- .../GameClient/W3DTerrainParticle.cpp | 381 ++++++++++++++++++ 5 files changed, 528 insertions(+), 24 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 99fe2f8c574..4cd266fb028 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 f6343a5b6c3..ed5556930dc 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" @@ -51,7 +52,7 @@ class W3DParticleSystemManager : public ParticleSystemManager private: Bool finishedBatch(const ParticleSystem& system, const RefCountPtr& texture); - void initializeBatch(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-conforming particles renderer 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,5 @@ class W3DParticleSystemManager : public ParticleSystemManager ParticleSystemInfo::ParticleShaderType m_batchShaderType; Bool m_readyToRender; ///< if true, it is OK to render ParticleSystemInfo::ParticleAlignmentType m_batchParticleAlignment; + 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..d4541a7c3ac --- /dev/null +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainParticle.h @@ -0,0 +1,99 @@ +/* +** 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 "Common/MapObject.h" +#include "Lib/BaseType.h" +#include "WW3D2/shader.h" +#include "WWLib/sharebuf.h" +#include "WWMath/vector3.h" +#include "WWMath/vector4.h" + +constexpr const UnsignedShort MAX_VERTICES = 32768; +constexpr const UnsignedShort MAX_INDICES = 65535; +constexpr const UnsignedShort INVALID_VERTEX = MAX_VERTICES + 1; +constexpr const Real Z_OFFSET = MAP_HEIGHT_SCALE / 10; + +static_assert(MAX_VERTICES < INVALID_VERTEX, "Vertex indices must leave room for the INVALID_VERTEX sentinel value."); + +class AABoxClass; +class TextureClass; +class WorldHeightMap; +struct VertexFormatXYZNDUV2; + +/** + * Render particles as terrain conforming overlays. For each particle, form an initial region by calculating the bounds + * of its rotated square and intersecting that with the map bounds and visible-terrain bounds. Recursively subdivide this + * region until each sub-region is either a single terrain cell or is on perfectly flat terrain. Flat regions become one + * large quad, while non-flat regions must match the terrain's topology exactly. + */ +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); + UnsignedShort addVertex(WorldHeightMap& map, const ParticleContext& particle, Int x, Int y); + void addTriangle(UnsignedShort a, UnsignedShort b, UnsignedShort c); + void resetVertexLookup(const ParticleContext& particle); + 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; ///< Map grid location to index in m_vertexData. + 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 6a63fb148e3..34f5a01164a 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -34,6 +34,7 @@ #include "W3DDevice/GameClient/HeightMap.h" #include "W3DDevice/GameClient/W3DSmudge.h" #include "W3DDevice/GameClient/W3DSnow.h" +#include "W3DDevice/GameClient/W3DTerrainParticle.h" #include "WW3D2/camera.h" @@ -51,6 +52,7 @@ W3DParticleSystemManager::W3DParticleSystemManager() m_pointGroup = nullptr; m_streakLine = nullptr; + m_terrainParticles = nullptr; m_posBuffer = nullptr; m_RGBABuffer = nullptr; m_sizeBuffer = nullptr; @@ -62,6 +64,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 +83,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 +228,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 +283,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; } } @@ -419,41 +425,54 @@ Bool W3DParticleSystemManager::finishedBatch(const ParticleSystem& system, const system.getParticleAlignment() != m_batchParticleAlignment; } -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_batchParticleAlignment = system.getParticleAlignment(); + m_batchBoundingBox = bbox; } void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount) { if (pointCount > 0) { - m_pointGroup->Set_Texture(m_batchTexture.Peek()); - + 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_batchParticleAlignment == ParticleSystemInfo::PARTICLE_ALIGNMENT_BILLBOARD); - m_pointGroup->Set_Point_Frame(0); - m_pointGroup->Render(rinfo); + if (m_batchParticleAlignment == ParticleSystemInfo::PARTICLE_ALIGNMENT_CONFORMING) + { + 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_batchParticleAlignment == ParticleSystemInfo::PARTICLE_ALIGNMENT_BILLBOARD); + m_pointGroup->Set_Point_Frame(0); + m_pointGroup->Render(rinfo); + } pointCount = 0; } diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp new file mode 100644 index 00000000000..3432b55af87 --- /dev/null +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp @@ -0,0 +1,381 @@ +/* +** 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" + +enum CPP_11( : UnsignedByte) +{ + 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; +} + +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::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; + + if (size < 0.001f) + continue; + + 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(particle); + 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 || map.isTerrainFlat(bounds)) + { + drawQuad(map, particle, bounds); + return; + } + + // Subdivide the current region into four sub-regions, or two sub-regions if we can't split one of the sides. + 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) +{ + if (m_numVertices + 4 > MAX_VERTICES || m_numIndices + 6 > MAX_INDICES) + { + flushBatch(); + resetVertexLookup(particle); + } + + const UnsignedShort bottomLeft = addVertex(map, particle, bounds.lo.x, bounds.lo.y); + const UnsignedShort bottomRight = addVertex(map, particle, bounds.hi.x - 1, bounds.lo.y); + const UnsignedShort topLeft = addVertex(map, particle, bounds.lo.x, bounds.hi.y - 1); + const UnsignedShort topRight = addVertex(map, particle, bounds.hi.x - 1, bounds.hi.y - 1); + + const Bool flipped = map.getQuickFlipState(bounds.lo.x + map.getBorderSizeInline(), + bounds.lo.y + map.getBorderSizeInline()); + if (flipped) + { + addTriangle(bottomRight, topLeft, bottomLeft); + addTriangle(bottomRight, topRight, topLeft); + } + else + { + addTriangle(bottomLeft, topRight, topLeft); + addTriangle(bottomLeft, bottomRight, topRight); + } +} + +UnsignedShort W3DTerrainParticle::addVertex(WorldHeightMap& map, const ParticleContext& particle, Int x, Int y) +{ + const Int gridLocation = (y - particle.bounds.lo.y) * particle.bounds.width() + (x - particle.bounds.lo.x); + UnsignedShort& index = m_vertexLookup[gridLocation]; + + // 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 = map.getQuickHeight(x + map.getBorderSizeInline(), y + map.getBorderSizeInline()) * MAP_HEIGHT_SCALE + 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::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.data(), + m_numVertices * sizeof(VertexFormatXYZNDUV2)); + } + + DynamicIBAccessClass indexAccess(BUFFER_TYPE_DYNAMIC_DX8, m_numIndices); + { + DynamicIBAccessClass::WriteLockClass indexLock(&indexAccess); + memcpy(indexLock.Get_Index_Array(), + m_indexData.data(), + 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; +} + +void W3DTerrainParticle::resetVertexLookup(const ParticleContext& particle) +{ + const Int count = particle.bounds.width() * particle.bounds.height(); + // For extremely large particles we may need to grow the lookup buffer. + if (m_vertexLookup.size() < count) + m_vertexLookup.resize(count); + + std::fill(m_vertexLookup.begin(), m_vertexLookup.begin() + count, INVALID_VERTEX); +} + +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.intersectWith(map.getLogicalBounds()); + bounds.intersectWith(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); +} + +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_CEIL((worldBoundingBox.Center.X + worldBoundingBox.Extent.X) / MAP_XY_FACTOR) + 1; + m_terrainInViewBounds.lo.y = REAL_TO_INT_FLOOR((worldBoundingBox.Center.Y - worldBoundingBox.Extent.Y) / MAP_XY_FACTOR); + m_terrainInViewBounds.hi.y = REAL_TO_INT_CEIL((worldBoundingBox.Center.Y + worldBoundingBox.Extent.Y) / MAP_XY_FACTOR) + 1; +} From 81867e01c153391acaf6ca19e9f3aecdd284379e 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 | 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 3432b55af87..3f2f34fbce8 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" @@ -260,6 +261,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 4a54116b99c0593363a083c29c69c92047dd4164 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 | 1 + Core/GameEngine/Source/GameClient/System/ParticleSys.cpp | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/Core/GameEngine/Include/Common/GameDefines.h b/Core/GameEngine/Include/Common/GameDefines.h index 0fcd3b1df3c..8ef1907a1ee 100644 --- a/Core/GameEngine/Include/Common/GameDefines.h +++ b/Core/GameEngine/Include/Common/GameDefines.h @@ -167,6 +167,11 @@ #define PRIORITIZE_TEXTURES_BY_SIZE (1) #endif +// Enable drawing particles as a terrain-conforming overlay. +#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 de85b8c87d8..fd7f6e63068 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" diff --git a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp index f3b0a73a22c..d6b3a890e38 100644 --- a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp +++ b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp @@ -2912,6 +2912,11 @@ void ParticleSystemTemplate::validate() m_particleType = ParticleSystemInfo::SMUDGE; } #endif + +#if !ENABLE_TERRAIN_CONFORMING_PARTICLES + if (m_particleAlignment == PARTICLE_ALIGNMENT_CONFORMING) + m_particleAlignment = PARTICLE_ALIGNMENT_XYPLANAR; +#endif } // ------------------------------------------------------------------------------------------------