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 5cd97e06e97..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" @@ -515,7 +516,12 @@ class ParticleSystemTemplate : public MemoryPoolObject, protected ParticleSystem void validate(); - AsciiString getName() const { return m_name; } + 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 @@ -543,6 +549,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 }; @@ -607,6 +614,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; } @@ -614,6 +622,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/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 41481701474..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,13 +51,23 @@ class W3DParticleSystemManager : public ParticleSystemManager virtual Int getOnScreenParticleCount() override { return m_onScreenParticleCount; } private: + void initializeBatch(ParticleSystem* system, const RefCountPtr& texture, const AABoxClass& bbox); + 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 + 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 ShareBufferClass *m_angleBuffer; ///< array of particle orientations + + 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/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;} ///, (MAX_POINTS_PER_GROUP, "W3DParticleSystemManager::m_posBuffer") ); m_RGBABuffer = NEW_REF( ShareBufferClass, (MAX_POINTS_PER_GROUP, "W3DParticleSystemManager::m_RGBABuffer") ); @@ -77,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); @@ -144,6 +152,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 +167,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 +200,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 +212,31 @@ 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 || + m_batchIsConforming != sys->isTerrainConforming(); + 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, bbox); + } + + 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 +250,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)) + if (p->isCulled()) continue; - if (WWMath::Fabs(pos->y - bcY) > (beY + psize)) - 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 (++count == MAX_POINTS_PER_GROUP) - break; + if (++pointCount == MAX_POINTS_PER_GROUP) + { + if (!canBatch) + { + 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, bbox); + 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 +320,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 +338,50 @@ 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; - } - - /// @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()); + 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 Support animated texture particles - /// @todo lorenzen sez: unimplemented code wastes cpu cycles - m_pointGroup->Set_Point_Frame( 0 ); + /// @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()); - //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 ); + /// @todo Support animated texture particles + /// @todo lorenzen sez: unimplemented code wastes cpu cycles + m_pointGroup->Set_Point_Frame( 0 ); - } + 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 +403,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 +419,65 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) ((W3DSmudgeManager *)TheSmudgeManager)->render(rinfo); } } + +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) + { + ShaderClass shader; + switch(m_batchShaderType ) + { + 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; + } + + 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; + } + + if (m_batchTexture != nullptr) + { + 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..f1f35c573a8 --- /dev/null +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainParticle.cpp @@ -0,0 +1,474 @@ +/* +** 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/statistics.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) +{ + DX8_RECORD_TERRAIN_PARTICLE_QUAD(); + + 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) +{ + 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) + { + 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); + DX8_RECORD_TERRAIN_PARTICLE_BATCH(m_numIndices / 3); + 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); +} 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(); 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/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); 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();