Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "WW3D2/pointgr.h"
#include "WW3D2/streak.h"
#include "WW3D2/rinfo.h"
#include "WW3D2/textureatlas.h"
#include "WWLib/bittype.h"

class W3DTerrainParticles;
Expand All @@ -51,9 +52,11 @@ class W3DParticleSystemManager : public ParticleSystemManager
///< returns the number of particles shown on screen per frame
virtual Int getOnScreenParticleCount() override { return m_onScreenParticleCount; }

virtual void reset() override;

private:
Bool finishedBatch(const ParticleSystem& system, const RefCountPtr<TextureClass>& texture);
void initializeBatch(const ParticleSystem& system, const RefCountPtr<TextureClass>& texture);
Bool finishedBatch(const ParticleSystem& system, const RefCountPtr<TextureClass>& texture, const ShaderClass& shader);
void initializeBatch(const ParticleSystem& system, const RefCountPtr<TextureClass>& texture, const ShaderClass& shader);
void flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount);

enum { MAX_POINTS_PER_GROUP = 512 };
Expand All @@ -66,9 +69,11 @@ class W3DParticleSystemManager : public ParticleSystemManager
ShareBufferClass<Vector4> *m_RGBABuffer; ///< array of particle color and alpha
ShareBufferClass<float> *m_sizeBuffer; ///< array of particle sizes
ShareBufferClass<uint8> *m_angleBuffer; ///< array of particle orientations
TextureAtlasClass m_atlas; ///< pages shared by particles with different textures
Vector4 m_uvRects[MAX_POINTS_PER_GROUP]; ///< array of particle texture rectangles, for atlas pages
AABoxClass m_currentBoundingBox;

ParticleSystemInfo::ParticleShaderType m_batchShaderType;
ShaderClass m_batchShader; ///< the shader used for batched particle draws
Bool m_readyToRender; ///< if true, it is OK to render
ParticleSystemInfo::ParticleAlignmentType m_batchParticleAlignment;
};
116 changes: 83 additions & 33 deletions Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,57 @@

//-------------------------------------------------------------------------------------------------

static Bool supportsAtlasing(const ParticleSystem& system)
{
return system.isUsingParticles() && system.getParticleAlignment() != ParticleSystemInfo::PARTICLE_ALIGNMENT_CONFORMING;
}

static Bool usesSharedBlend(ParticleSystemInfo::ParticleShaderType shaderType, Bool atlased)
{
return atlased && (shaderType == ParticleSystemInfo::ALPHA || shaderType == ParticleSystemInfo::ADDITIVE);
}

static ShaderClass getParticleShader(ParticleSystemInfo::ParticleShaderType shaderType, Bool sharedBlend)
{
if (sharedBlend)
{
ShaderClass shader = ShaderClass::_PresetAlphaSpriteShader;
shader.Set_Src_Blend_Func(ShaderClass::SRCBLEND_ONE);
return shader;
}

switch (shaderType)
{
case ParticleSystemInfo::ADDITIVE:
return ShaderClass::_PresetAdditiveSpriteShader;
case ParticleSystemInfo::ALPHA:
return ShaderClass::_PresetAlphaSpriteShader;
case ParticleSystemInfo::ALPHA_TEST:
return ShaderClass::_PresetATestSpriteShader;
case ParticleSystemInfo::MULTIPLY:
return ShaderClass::_PresetMultiplicativeSpriteShader;
default:
return ShaderClass();
}
}

// With the shared blend, additive sprites get zero alpha so they add, and alpha sprites a premultiplied color.
// The color is clamped first, as the fixed-function pipeline clamps it before the blend weighs it by alpha.
static Vector4 getVertexColor(const RGBColor& color, Real alpha, ParticleSystemInfo::ParticleShaderType shaderType, Bool sharedBlend)
{
if (!sharedBlend)
return Vector4(color.red, color.green, color.blue, alpha);

if (shaderType == ParticleSystemInfo::ADDITIVE)
return Vector4(color.red, color.green, color.blue, 0.0f);

alpha = clamp(0.0f, alpha, 1.0f);
return Vector4(clamp(0.0f, color.red, 1.0f) * alpha, clamp(0.0f, color.green, 1.0f) * alpha, clamp(0.0f, color.blue, 1.0f) * alpha, alpha);
}

W3DParticleSystemManager::W3DParticleSystemManager()
{
m_batchParticleAlignment = ParticleSystemInfo::PARTICLE_ALIGNMENT_BILLBOARD;
m_batchShaderType = ParticleSystemInfo::INVALID_SHADER;

m_pointGroup = nullptr;
m_streakLine = nullptr;
Expand Down Expand Up @@ -92,6 +138,12 @@ W3DParticleSystemManager::~W3DParticleSystemManager()
REF_PTR_RELEASE(m_angleBuffer);
}

void W3DParticleSystemManager::reset()
{
ParticleSystemManager::reset();
m_atlas.Reset();
}

/**
* Hack because DoParticles is called from Flush(), which is called
* multiple times per frame. We only want to render once.
Expand Down Expand Up @@ -219,8 +271,24 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo)
RefCountPtr<TextureClass> texture;
texture.Assign_No_Add_Ref(W3DDisplay::m_assetManager->Get_Texture(sys->getParticleTypeName().str()));

// TheSuperHackers @performance stephanmeesters 18/10/2026 Draw sprites from a shared atlas page where possible,
// so particles with different textures, and alpha and additive particles, can join the same batch.
const ParticleSystemInfo::ParticleShaderType shaderType = sys->getShaderType();
Vector4 uvRect(0.0f, 0.0f, 1.0f, 1.0f);
Bool atlased = FALSE;
if (supportsAtlasing(*sys))
{
if (TextureClass *page = m_atlas.Find_Or_Insert(texture.Peek(), shaderType == ParticleSystemInfo::ALPHA, uvRect))
{
texture = page;
atlased = TRUE;
}
}
const Bool sharedBlend = usesSharedBlend(shaderType, atlased);
const ShaderClass shader = getParticleShader(shaderType, sharedBlend);

const Bool canBatch = sys->isUsingParticles();
const Bool batchDone = finishedBatch(*sys, texture);
const Bool batchDone = finishedBatch(*sys, texture, shader);
if (!canBatch || batchDone)
{
flushParticleBatch(rinfo, pointCount);
Expand All @@ -229,7 +297,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, shader);
}

UnsignedInt startCount = pointCount;
Expand Down Expand Up @@ -266,11 +334,9 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo)
sizeArray[pointCount] = psize;

color = p->getColor();
RGBAArray[pointCount].X = color->red;
RGBAArray[pointCount].Y = color->green;
RGBAArray[pointCount].Z = color->blue;
RGBAArray[pointCount].W = p->getAlpha();
RGBAArray[pointCount] = getVertexColor(*color, p->getAlpha(), shaderType, sharedBlend);

m_uvRects[pointCount] = uvRect;
angleArray[pointCount] = (uint8)(p->getAngle() * 255.0f / (2.0f * PI));

if (++pointCount == MAX_POINTS_PER_GROUP)
Expand All @@ -284,7 +350,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, shader);
startCount = 0;
}
}
Expand Down Expand Up @@ -345,7 +411,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo)
else if (sys->isUsingStreak() && (pointCount == 1))
{
m_onScreenParticleCount += (pointCount - startCount);
initializeBatch(*sys, texture);
initializeBatch(*sys, texture, shader);
flushParticleBatch(rinfo, pointCount);
startCount = 0;
}
Expand Down Expand Up @@ -427,58 +493,42 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo)
}
}

Bool W3DParticleSystemManager::finishedBatch(const ParticleSystem& system, const RefCountPtr<TextureClass>& texture)
Bool W3DParticleSystemManager::finishedBatch(const ParticleSystem& system, const RefCountPtr<TextureClass>& texture, const ShaderClass& shader)
{
return texture.Peek() != m_batchTexture.Peek() ||
system.getShaderType() != m_batchShaderType ||
shader.Get_Bits() != m_batchShader.Get_Bits() ||
system.getParticleAlignment() != m_batchParticleAlignment;
}

void W3DParticleSystemManager::initializeBatch(const ParticleSystem& system, const RefCountPtr<TextureClass>& texture)
void W3DParticleSystemManager::initializeBatch(const ParticleSystem& system, const RefCountPtr<TextureClass>& texture, const ShaderClass& shader)
{
m_batchTexture = texture;
m_batchShaderType = system.getShaderType();
m_batchShader = shader;
m_batchParticleAlignment = system.getParticleAlignment();
}

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_batchParticleAlignment == ParticleSystemInfo::PARTICLE_ALIGNMENT_CONFORMING)
{
m_terrainParticles->setTexture(m_batchTexture.Peek());
m_terrainParticles->setShader( shader );
m_terrainParticles->setShader( m_batchShader );
m_terrainParticles->setArrays( m_posBuffer, m_RGBABuffer, m_sizeBuffer, m_angleBuffer, pointCount );
m_terrainParticles->setBoundingBox( m_currentBoundingBox );
m_terrainParticles->render();
}
else // draw regular point group
{
m_pointGroup->Set_Texture(m_batchTexture.Peek());
m_pointGroup->Set_Shader(shader);
m_pointGroup->Set_Shader(m_batchShader);
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->Set_Texture_UV_Rects(m_uvRects);
m_pointGroup->Render(rinfo);
}

Expand All @@ -487,5 +537,5 @@ void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, Unsign

m_batchTexture.Clear();
m_batchParticleAlignment = ParticleSystemInfo::PARTICLE_ALIGNMENT_BILLBOARD;
m_batchShaderType = ParticleSystemInfo::INVALID_SHADER;
m_batchShader = ShaderClass();
}
2 changes: 2 additions & 0 deletions Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ set(WW3D2_SRC
textdraw.h
texture.cpp
texture.h
textureatlas.cpp
textureatlas.h
texturefilter.cpp
texturefilter.h
textureloader.cpp
Expand Down
12 changes: 12 additions & 0 deletions Core/Libraries/Source/WWVegas/WW3D2/pointgr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ PointGroupClass::PointGroupClass() :
PointOrientation(nullptr),
PointFrame(nullptr),
PointCount(0),
TextureUVRects(nullptr),
FrameRowColumnCountLog2(0),
Texture(nullptr),
Flags(0),
Expand Down Expand Up @@ -915,6 +916,17 @@ void PointGroupClass::Render(RenderInfoClass &rinfo)
Update_Arrays(current_loc, current_diffuse, current_size, current_orient, current_frame,
PointCount, PointLoc->Get_Count(), vnum, pnum);

// Map each point's frame UVs into its rectangle; Update_Arrays refills VertexUV every render.
if (TextureUVRects) {
const int verts_per_point = (PointMode == QUADS) ? 4 : 3;
for (int point = 0, vert = 0; point < PointCount; ++point) {
const Vector4& rect = TextureUVRects[point];
for (const int end = vert + verts_per_point; vert < end; ++vert) {
VertexUV[vert].Set(rect.X + VertexUV[vert].X * (rect.Z - rect.X), rect.Y + VertexUV[vert].Y * (rect.W - rect.Y));
}
}
}

// the locations are now in view space
// so set world and view matrices to identity and render

Expand Down
2 changes: 2 additions & 0 deletions Core/Libraries/Source/WWVegas/WW3D2/pointgr.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class PointGroupClass
int active_point_count = -1,
float vpxmin = 0.0f, float vpymin = 0.0f,
float vpxmax = 0.0f, float vpymax = 0.0f);
void Set_Texture_UV_Rects(const Vector4* rects) { TextureUVRects = rects; }
void Set_Point_Size(float size);
float Get_Point_Size();
void Set_Point_Color(Vector3 color);
Expand Down Expand Up @@ -158,6 +159,7 @@ class PointGroupClass
ShareBufferClass<unsigned char> * PointFrame; // (null if not used) frame indices
int PointCount; // Active (if APT) or total point count

const Vector4 * TextureUVRects; // (null if not used) uv rectangle per point
// See comments for Get/Set_Frame_Row_Column_Count_Log2 above
unsigned char FrameRowColumnCountLog2; // MUST be equal or lesser than 4

Expand Down
55 changes: 27 additions & 28 deletions Core/Libraries/Source/WWVegas/WW3D2/sortingrenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,19 @@ void SortingRendererClass::Insert_To_Sorting_Pool(SortingNodeStruct* state)
// ----------------------------------------------------------------------------
//static unsigned prevLight = 0xffffffff;

static bool Same_Render_State(const RenderStateStruct& a, const RenderStateStruct& b)
{
if (a.shader.Get_Bits()!=b.shader.Get_Bits() || a.material!=b.material || a.material->Get_Lighting())
return false;

for (int i=0;i<DX8Wrapper::Get_Current_Caps()->Get_Max_Textures_Per_Pass();++i) {
if (a.Textures[i]!=b.Textures[i])
return false;
}

return memcmp(&a.world, &b.world, sizeof(a.world))==0 && memcmp(&a.view, &b.view, sizeof(a.view))==0;
}

static void Apply_Render_State(RenderStateStruct& render_state)
{
DX8Wrapper::Set_Shader(render_state.shader);
Expand Down Expand Up @@ -546,37 +559,23 @@ void SortingRendererClass::Flush_Sorting_Pool()

DX8Wrapper::Apply_Render_State_Changes();

unsigned count_to_render=1;
unsigned start_index=0;
unsigned node_id=tis[chunkOffset].idx;
for (unsigned i=chunkOffset + 1;i<chunkEnd;++i) {
if (node_id!=tis[i].idx) {
SortingNodeStruct* state=overlapping_nodes[node_id];
Apply_Render_State(state->sorting_state);

DX8Wrapper::Draw_Triangles(
start_index*3,
count_to_render,
state->min_vertex_index,
state->vertex_count);

count_to_render=0;
start_index=i - chunkOffset;
node_id=tis[i].idx;
// TheSuperHackers @performance stephanmeesters 18/10/2026 Draw polygons that in the same call when they share the same render state.
for (unsigned start=chunkOffset;start<chunkEnd;) {
SortingNodeStruct* state=overlapping_nodes[tis[start].idx];
unsigned min_vertex=state->min_vertex_index;
unsigned end_vertex=min_vertex + state->vertex_count;
unsigned end=start + 1;
for (;end<chunkEnd;++end) {
SortingNodeStruct* node=overlapping_nodes[tis[end].idx];
if (node!=state && !Same_Render_State(state->sorting_state, node->sorting_state))
break;
min_vertex=std::min<unsigned>(min_vertex, node->min_vertex_index);
end_vertex=std::max<unsigned>(end_vertex, node->min_vertex_index + node->vertex_count);
}
count_to_render++; //keep track of number of polygons of same kind
}

// Render any remaining polygons...
if (count_to_render) {
SortingNodeStruct* state=overlapping_nodes[node_id];
Apply_Render_State(state->sorting_state);

DX8Wrapper::Draw_Triangles(
start_index*3,
count_to_render,
state->min_vertex_index,
state->vertex_count);
DX8Wrapper::Draw_Triangles((start - chunkOffset)*3, end - start, min_vertex, end_vertex - min_vertex);
start=end;
}

chunkOffset += chunkCount;
Expand Down
Loading
Loading