/* 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 * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "ParticleEmitter.h" #include "graphics/LOSTexture.h" #include "graphics/LightEnv.h" #include "graphics/ParticleEmitterType.h" #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" #include "renderer/Scene.h" #include "renderer/SceneRenderer.h" #include "renderer/backend/Format.h" #include "renderer/backend/IBuffer.h" #include "renderer/backend/IDeviceCommandContext.h" #include "renderer/backend/IShaderProgram.h" #include #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()), m_IndexArray(Renderer::Backend::IBuffer::Usage::TRANSFER_DST), m_VertexArray(Renderer::Backend::IBuffer::Type::VERTEX, Renderer::Backend::IBuffer::Usage::DYNAMIC | Renderer::Backend::IBuffer::Usage::TRANSFER_DST), m_LastFrameNumber(-1) { // If we should start with particles fully emitted, pretend that we // were created in the past so the first update will produce lots of // particles. // TODO: instead of this, maybe it would make more sense to do a full // lifetime-length update of all emitters when the game first starts // (so that e.g. buildings constructed later on won't have fully-started // emitters, but those at the start will)? if (m_Type->m_StartFull) m_LastUpdateTime -= m_Type->m_MaxLifetime; m_Particles.reserve(m_Type->m_MaxParticles); m_AttributePos.format = Renderer::Backend::Format::R32G32B32_SFLOAT; m_VertexArray.AddAttribute(&m_AttributePos); m_AttributeAxis.format = Renderer::Backend::Format::R32G32_SFLOAT; m_VertexArray.AddAttribute(&m_AttributeAxis); m_AttributeUV.format = Renderer::Backend::Format::R32G32_SFLOAT; m_VertexArray.AddAttribute(&m_AttributeUV); m_AttributeColor.format = Renderer::Backend::Format::R8G8B8A8_UNORM; m_VertexArray.AddAttribute(&m_AttributeColor); m_AttributeAxisX.format = Renderer::Backend::Format::R32G32B32_SFLOAT; m_VertexArray.AddAttribute(&m_AttributeAxisX); m_AttributeAxisY.format = Renderer::Backend::Format::R32G32B32_SFLOAT; m_VertexArray.AddAttribute(&m_AttributeAxisY); m_VertexArray.SetNumberOfVertices(m_Type->m_MaxParticles * 4); m_VertexArray.Layout(); m_IndexArray.SetNumberOfVertices(m_Type->m_MaxParticles * 6); m_IndexArray.Layout(); VertexArrayIterator index = m_IndexArray.GetIterator(); for (u16 i = 0; i < m_Type->m_MaxParticles; ++i) { *index++ = i*4 + 0; *index++ = i*4 + 1; *index++ = i*4 + 2; *index++ = i*4 + 2; *index++ = i*4 + 3; *index++ = i*4 + 0; } m_IndexArray.Upload(); m_IndexArray.FreeBackingStore(); const uint32_t stride = m_VertexArray.GetStride(); const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, m_AttributePos.format, m_AttributePos.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::COLOR, m_AttributeColor.format, m_AttributeColor.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV0, m_AttributeUV.format, m_AttributeUV.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV1, m_AttributeAxis.format, m_AttributeAxis.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV2, m_AttributeAxisX.format, m_AttributeAxisX.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV3, m_AttributeAxisY.format, m_AttributeAxisY.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, }}; m_VertexInputLayout = g_Renderer.GetVertexInputLayout(attributes); } 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 m_Type->UpdateEmitter(*this, m_Type->m_Manager.GetCurrentTime() - m_LastUpdateTime); m_LastUpdateTime = m_Type->m_Manager.GetCurrentTime(); // Regenerate the vertex array data: VertexArrayIterator attrPos = m_AttributePos.GetIterator(); VertexArrayIterator attrAxis = m_AttributeAxis.GetIterator(); VertexArrayIterator attrUV = m_AttributeUV.GetIterator(); VertexArrayIterator attrColor = m_AttributeColor.GetIterator(); VertexArrayIterator attrAxisX = m_AttributeAxisX.GetIterator(); VertexArrayIterator attrAxisY = m_AttributeAxisY.GetIterator(); ENSURE(m_Particles.size() <= m_Type->m_MaxParticles); CBoundingBoxAligned bounds; 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 += particle.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(particle.angle) * particle.size * 0.5f; float c = cos(particle.angle) * particle.size * 0.5f; (*attrAxis)[0] = c; (*attrAxis)[1] = s; ++attrAxis; (*attrAxis)[0] = s; (*attrAxis)[1] = -c; ++attrAxis; (*attrAxis)[0] = -c; (*attrAxis)[1] = -s; ++attrAxis; (*attrAxis)[0] = -s; (*attrAxis)[1] = c; ++attrAxis; (*attrUV)[0] = 1; (*attrUV)[1] = 0; ++attrUV; (*attrUV)[0] = 0; (*attrUV)[1] = 0; ++attrUV; (*attrUV)[0] = 0; (*attrUV)[1] = 1; ++attrUV; (*attrUV)[0] = 1; (*attrUV)[1] = 1; ++attrUV; 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.) if (m_Type->m_BlendMode == CParticleEmitterType::BlendMode::OVERLAY || m_Type->m_BlendMode == CParticleEmitterType::BlendMode::MULTIPLY) { color.R = (color.R * color.A) / 255; color.G = (color.G * color.A) / 255; color.B = (color.B * color.A) / 255; } *attrColor++ = color; *attrColor++ = color; *attrColor++ = color; *attrColor++ = color; *attrAxisX++ = particle.axisX; *attrAxisX++ = particle.axisX; *attrAxisX++ = particle.axisX; *attrAxisX++ = particle.axisX; *attrAxisY++ = particle.axisY; *attrAxisY++ = particle.axisY; *attrAxisY++ = particle.axisY; *attrAxisY++ = particle.axisY; } m_ParticleBounds = bounds; m_VertexArray.Upload(); } void CParticleEmitter::PrepareForRendering() { m_VertexArray.PrepareForRendering(); } void CParticleEmitter::UploadData( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { m_VertexArray.UploadIfNeeded(deviceCommandContext); } void CParticleEmitter::Bind( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::IShaderProgram* shader) { m_Type->m_Texture->UploadBackendTextureIfNeeded(deviceCommandContext); CLOSTexture& los = g_Renderer.GetSceneRenderer().GetScene().GetLOSTexture(); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_losTex), los.GetTextureSmooth()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_losTransform), los.GetTextureMatrix()[0], los.GetTextureMatrix()[12]); g_Renderer.GetSceneRenderer().GetLightEnv().Bind(deviceCommandContext, shader); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_baseTex), m_Type->m_Texture->GetBackendTexture()); } void CParticleEmitter::RenderArray( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { if (m_Particles.empty()) return; const uint32_t stride = m_VertexArray.GetStride(); const uint32_t firstVertexOffset = m_VertexArray.GetOffset() * stride; deviceCommandContext->SetVertexInputLayout(m_VertexInputLayout); deviceCommandContext->SetVertexBuffer( 0, m_VertexArray.GetBuffer(), firstVertexOffset); deviceCommandContext->SetIndexBuffer(m_IndexArray.GetBuffer()); deviceCommandContext->DrawIndexed(m_IndexArray.GetOffset(), m_Particles.size() * 6, 0); g_Renderer.GetStats().m_DrawCalls++; g_Renderer.GetStats().m_Particles += m_Particles.size(); } void CParticleEmitter::Unattach(const CParticleEmitterPtr& self) { m_Active = false; m_Type->m_Manager.AddUnattachedEmitter(self); } void CParticleEmitter::AddParticle(const SParticle& particle) { if (m_NextParticleIdx >= m_Particles.size()) m_Particles.push_back(particle); else m_Particles[m_NextParticleIdx] = particle; m_NextParticleIdx = (m_NextParticleIdx + 1) % m_Type->m_MaxParticles; } void CParticleEmitter::SetEntityVariable(const std::string& name, float value) { m_EntityVariables[name] = value; } CModelParticleEmitter::CModelParticleEmitter(const CParticleEmitterTypePtr& type) : m_Type(type) { m_Emitter = CParticleEmitterPtr(new CParticleEmitter(m_Type)); } CModelParticleEmitter::~CModelParticleEmitter() { m_Emitter->Unattach(m_Emitter); } void CModelParticleEmitter::SetEntityVariable(const std::string& name, float value) { m_Emitter->SetEntityVariable(name, value); } std::unique_ptr CModelParticleEmitter::Clone() const { return std::make_unique(m_Type); } void CModelParticleEmitter::CalcBounds() { // TODO: we ought to compute sensible bounds here, probably based on the // current computed particle positions plus the emitter type's largest // potential bounding box at the current position m_WorldBounds = m_Type->CalculateBounds(m_Emitter->GetPosition(), m_Emitter->GetParticleBounds()); } void CModelParticleEmitter::ValidatePosition() { // TODO: do we need to do anything here? // This is a convenient (though possibly not particularly appropriate) place // to invalidate bounds so they'll be recomputed from the recent particle data InvalidateBounds(); } void CModelParticleEmitter::InvalidatePosition() { } void CModelParticleEmitter::SetTransform(const CMatrix3D& transform) { if (m_Transform == transform) return; m_Emitter->SetPosition(transform.GetTranslation()); m_Emitter->SetRotation(transform.GetRotation()); // call base class to set transform on this object CRenderableObject::SetTransform(transform); }