From 2e0c34d479833820c8dd31bdffb3f1bc594a1aa5 Mon Sep 17 00:00:00 2001 From: Vladislav Belov Date: Mon, 9 Feb 2026 21:29:42 +0100 Subject: [PATCH] Adds sort mode to particles --- .../mods/public/art/particles/particle.rng | 11 +++ source/graphics/ParticleEmitter.cpp | 82 ++++++++++++++++--- source/graphics/ParticleEmitterType.cpp | 14 +++- source/graphics/ParticleEmitterType.h | 13 ++- 4 files changed, 107 insertions(+), 13 deletions(-) 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;