diff --git a/binaries/data/mods/public/art/particles/particle.rng b/binaries/data/mods/public/art/particles/particle.rng
index 1320604a8d..01e4790949 100644
--- a/binaries/data/mods/public/art/particles/particle.rng
+++ b/binaries/data/mods/public/art/particles/particle.rng
@@ -18,6 +18,17 @@
+
+
+
+
+ closest_in_front
+ youngest_in_front
+ oldest_in_front
+
+
+
+
diff --git a/source/graphics/ParticleEmitter.cpp b/source/graphics/ParticleEmitter.cpp
index 314b27f497..0117c542d3 100644
--- a/source/graphics/ParticleEmitter.cpp
+++ b/source/graphics/ParticleEmitter.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2025 Wildfire Games.
+/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -25,9 +25,11 @@
#include "graphics/ParticleManager.h"
#include "graphics/RenderableObject.h"
#include "graphics/TextureManager.h"
+#include "lib/allocators/STLAllocators.h"
#include "lib/debug.h"
#include "lib/types.h"
#include "maths/Matrix3D.h"
+#include "ps/memory/LinearAllocator.h"
#include "ps/CStrIntern.h"
#include "ps/CStrInternStatic.h"
#include "renderer/Renderer.h"
@@ -42,6 +44,37 @@
#include
#include
+namespace
+{
+
+struct ParticleYoungestInFrontCompare
+{
+ bool operator()(const SParticle& lhs, const SParticle& rhs) const
+ {
+ return lhs.age > rhs.age;
+ }
+};
+
+struct ParticleOldestInFrontCompare
+{
+ bool operator()(const SParticle& lhs, const SParticle& rhs) const
+ {
+ return lhs.age < rhs.age;
+ }
+};
+
+struct ParticleClosestInFrontCompare
+{
+ bool operator()(const SParticle& lhs, const SParticle& rhs) const
+ {
+ return cameraForward.Dot(lhs.pos) > cameraForward.Dot(rhs.pos);
+ }
+
+ CVector3D cameraForward;
+};
+
+} // anonymous namespace
+
CParticleEmitter::CParticleEmitter(const CParticleEmitterTypePtr& type) :
m_Type(type), m_Active(true), m_NextParticleIdx(0), m_EmissionRoundingError(0.f),
m_LastUpdateTime(type->m_Manager.GetCurrentTime()),
@@ -115,6 +148,11 @@ void CParticleEmitter::UpdateArrayData(int frameNumber)
if (m_LastFrameNumber == frameNumber)
return;
+ PS::Memory::ScopedLinearAllocator scopedLinearAllocator{g_Renderer.GetLinearAllocator()};
+
+ using ParticlesVector = std::vector>;
+ ParticlesVector sortedParticles((ParticlesVector::allocator_type(scopedLinearAllocator)));
+
m_LastFrameNumber = frameNumber;
// Update m_Particles
@@ -132,24 +170,48 @@ void CParticleEmitter::UpdateArrayData(int frameNumber)
CBoundingBoxAligned bounds;
- for (size_t i = 0; i < m_Particles.size(); ++i)
+ if (m_Type->m_SortMode != CParticleEmitterType::SortMode::UNSPECIFIED)
+ {
+ sortedParticles.insert(sortedParticles.end(), m_Particles.begin(), m_Particles.end());
+
+ switch (m_Type->m_SortMode)
+ {
+ case CParticleEmitterType::SortMode::YOUNGEST_IN_FRONT:
+ std::sort(sortedParticles.begin(), sortedParticles.end(), ParticleYoungestInFrontCompare{});
+ break;
+ case CParticleEmitterType::SortMode::OLDEST_IN_FRONT:
+ std::sort(sortedParticles.begin(), sortedParticles.end(), ParticleOldestInFrontCompare{});
+ break;
+ case CParticleEmitterType::SortMode::CLOSEST_IN_FRONT:
+ std::sort(sortedParticles.begin(), sortedParticles.end(), ParticleClosestInFrontCompare{
+ g_Renderer.GetSceneRenderer().GetViewCamera().GetOrientation().GetIn()});
+ break;
+ default:
+ break;
+ }
+ }
+
+ const std::span particles{
+ m_Type->m_SortMode == CParticleEmitterType::SortMode::UNSPECIFIED ?
+ std::span{m_Particles} : std::span{sortedParticles}};
+ for (const SParticle& particle : particles)
{
// TODO: for more efficient rendering, maybe we should replace this with
// a degenerate quad if alpha is 0
- bounds += m_Particles[i].pos;
+ bounds += particle.pos;
- *attrPos++ = m_Particles[i].pos;
- *attrPos++ = m_Particles[i].pos;
- *attrPos++ = m_Particles[i].pos;
- *attrPos++ = m_Particles[i].pos;
+ *attrPos++ = particle.pos;
+ *attrPos++ = particle.pos;
+ *attrPos++ = particle.pos;
+ *attrPos++ = particle.pos;
// Compute corner offsets, split into sin/cos components so the vertex
// shader can multiply by the camera-right (or left?) and camera-up vectors
// to get rotating billboards:
- float s = sin(m_Particles[i].angle) * m_Particles[i].size/2.f;
- float c = cos(m_Particles[i].angle) * m_Particles[i].size/2.f;
+ float s = sin(particle.angle) * particle.size * 0.5f;
+ float c = cos(particle.angle) * particle.size * 0.5f;
(*attrAxis)[0] = c;
(*attrAxis)[1] = s;
@@ -177,7 +239,7 @@ void CParticleEmitter::UpdateArrayData(int frameNumber)
(*attrUV)[1] = 1;
++attrUV;
- SColor4ub color = m_Particles[i].color;
+ SColor4ub color{particle.color};
// Special case: If the blending depends on the source color, not the source alpha,
// then pre-multiply by the alpha. (This is kind of a hack.)
diff --git a/source/graphics/ParticleEmitterType.cpp b/source/graphics/ParticleEmitterType.cpp
index c8e2078da6..3936975c05 100644
--- a/source/graphics/ParticleEmitterType.cpp
+++ b/source/graphics/ParticleEmitterType.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2025 Wildfire Games.
+/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -366,6 +366,7 @@ bool CParticleEmitterType::LoadXML(const VfsPath& path)
m_Variables[VAR_COLOR_G] = IParticleVarPtr(new CParticleVarConstant(1.f));
m_Variables[VAR_COLOR_B] = IParticleVarPtr(new CParticleVarConstant(1.f));
m_BlendMode = BlendMode::ADD;
+ m_SortMode = SortMode::UNSPECIFIED;
m_StartFull = false;
m_UseRelativeVelocity = false;
m_Texture = g_Renderer.GetTextureManager().GetErrorTexture();
@@ -383,6 +384,7 @@ bool CParticleEmitterType::LoadXML(const VfsPath& path)
EL(start_full);
EL(use_relative_velocity);
EL(constant);
+ EL(sort);
EL(uniform);
EL(copy);
EL(expr);
@@ -423,6 +425,16 @@ bool CParticleEmitterType::LoadXML(const VfsPath& path)
else if (mode == "multiply")
m_BlendMode = BlendMode::MULTIPLY;
}
+ else if (Child.GetNodeName() == el_sort)
+ {
+ const CStr mode{Child.GetAttributes().GetNamedItem(at_mode)};
+ if (mode == "closest_in_front")
+ m_SortMode = SortMode::CLOSEST_IN_FRONT;
+ else if (mode == "youngest_in_front")
+ m_SortMode = SortMode::YOUNGEST_IN_FRONT;
+ else if (mode == "oldest_in_front")
+ m_SortMode = SortMode::OLDEST_IN_FRONT;
+ }
else if (Child.GetNodeName() == el_start_full)
{
m_StartFull = true;
diff --git a/source/graphics/ParticleEmitterType.h b/source/graphics/ParticleEmitterType.h
index 714b1ead24..bd1e91e67b 100644
--- a/source/graphics/ParticleEmitterType.h
+++ b/source/graphics/ParticleEmitterType.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2025 Wildfire Games.
+/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -87,6 +87,14 @@ private:
MULTIPLY
};
+ enum class SortMode
+ {
+ UNSPECIFIED,
+ YOUNGEST_IN_FRONT,
+ OLDEST_IN_FRONT,
+ CLOSEST_IN_FRONT
+ };
+
int GetVariableID(const std::string& name);
bool LoadXML(const VfsPath& path);
@@ -106,7 +114,8 @@ private:
CTexturePtr m_Texture;
- BlendMode m_BlendMode = BlendMode::ADD;
+ BlendMode m_BlendMode{BlendMode::ADD};
+ SortMode m_SortMode{SortMode::UNSPECIFIED};
bool m_StartFull;
bool m_UseRelativeVelocity;