#include "ParticlePass.h" #include "../RHI/RHIDevice.h" #include "../Resources/ResourceCache.h" #include namespace grove { ParticlePass::ParticlePass(rhi::ShaderHandle shader) : m_shader(shader) { m_particleInstances.reserve(MAX_PARTICLES_PER_BATCH); } void ParticlePass::setup(rhi::IRHIDevice& device) { // Create quad vertex buffer (unit quad centered at origin for particles) float quadVertices[] = { // pos.x, pos.y, pos.z, r, g, b, a -0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // bottom-left 0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // bottom-right 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // top-right -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // top-left }; rhi::BufferDesc vbDesc; vbDesc.type = rhi::BufferDesc::Vertex; vbDesc.size = sizeof(quadVertices); vbDesc.data = quadVertices; vbDesc.dynamic = false; vbDesc.layout = rhi::BufferDesc::PosColor; m_quadVB = device.createBuffer(vbDesc); // Create index buffer uint16_t quadIndices[] = { 0, 1, 2, 0, 2, 3 }; rhi::BufferDesc ibDesc; ibDesc.type = rhi::BufferDesc::Index; ibDesc.size = sizeof(quadIndices); ibDesc.data = quadIndices; ibDesc.dynamic = false; m_quadIB = device.createBuffer(ibDesc); // Fallback dynamic instance buffer (only used if transient allocation fails) rhi::BufferDesc instDesc; instDesc.type = rhi::BufferDesc::Instance; instDesc.size = MAX_PARTICLES_PER_BATCH * sizeof(SpriteInstance); instDesc.data = nullptr; instDesc.dynamic = true; m_instanceBuffer = device.createBuffer(instDesc); // Create texture sampler uniform m_textureSampler = device.createUniform("s_texColor", 1); // Create default white texture for untextured particles uint32_t whitePixel = 0xFFFFFFFF; rhi::TextureDesc texDesc; texDesc.width = 1; texDesc.height = 1; texDesc.format = rhi::TextureDesc::RGBA8; texDesc.data = &whitePixel; texDesc.dataSize = sizeof(whitePixel); m_defaultTexture = device.createTexture(texDesc); } void ParticlePass::shutdown(rhi::IRHIDevice& device) { device.destroy(m_quadVB); device.destroy(m_quadIB); device.destroy(m_instanceBuffer); device.destroy(m_textureSampler); device.destroy(m_defaultTexture); } void ParticlePass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) { if (frame.particleCount == 0) { return; } // Set render state for particles rhi::RenderState state; state.blend = m_additiveBlending ? rhi::BlendMode::Additive : rhi::BlendMode::Alpha; state.cull = rhi::CullMode::None; state.depthTest = false; state.depthWrite = false; cmd.setState(state); m_particleInstances.clear(); // Current texture for batching uint16_t currentTextureId = UINT16_MAX; rhi::TextureHandle currentTexture = m_defaultTexture; for (size_t i = 0; i < frame.particleCount; ++i) { const ParticleInstance& particle = frame.particles[i]; // Skip dead particles if (particle.life <= 0.0f) { continue; } // Check if texture changed - flush batch if (particle.textureId != currentTextureId && !m_particleInstances.empty()) { flushBatch(device, cmd, currentTexture, static_cast(m_particleInstances.size())); m_particleInstances.clear(); } // Update current texture if needed if (particle.textureId != currentTextureId) { currentTextureId = particle.textureId; if (particle.textureId > 0 && m_resourceCache) { currentTexture = m_resourceCache->getTextureById(particle.textureId); } if (!currentTexture.isValid()) { currentTexture = m_defaultTexture; } } // Convert ParticleInstance to GPU-aligned SpriteInstance SpriteInstance inst; // Position (particle position is center) inst.x = particle.x; inst.y = particle.y; // Scale by particle size inst.scaleX = particle.size; inst.scaleY = particle.size; // No rotation (could add spin later) inst.rotation = 0.0f; // Full UV (use entire texture) inst.u0 = 0.0f; inst.v0 = 0.0f; inst.u1 = 1.0f; inst.v1 = 1.0f; // Texture ID inst.textureId = static_cast(particle.textureId); // Layer (particles render at high layer by default) inst.layer = 200.0f; // Padding/reserved inst.padding0 = 0.0f; inst.reserved[0] = 0.0f; inst.reserved[1] = 0.0f; inst.reserved[2] = 0.0f; inst.reserved[3] = 0.0f; // Color with life-based alpha fade uint32_t color = particle.color; inst.r = static_cast((color >> 24) & 0xFF) / 255.0f; inst.g = static_cast((color >> 16) & 0xFF) / 255.0f; inst.b = static_cast((color >> 8) & 0xFF) / 255.0f; // Alpha fades with life float baseAlpha = static_cast(color & 0xFF) / 255.0f; inst.a = baseAlpha * particle.life; m_particleInstances.push_back(inst); // Flush if batch is full if (m_particleInstances.size() >= MAX_PARTICLES_PER_BATCH) { flushBatch(device, cmd, currentTexture, static_cast(m_particleInstances.size())); m_particleInstances.clear(); } } // Flush remaining particles if (!m_particleInstances.empty()) { flushBatch(device, cmd, currentTexture, static_cast(m_particleInstances.size())); } } void ParticlePass::flushBatch(rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd, rhi::TextureHandle texture, uint32_t count) { if (count == 0) return; // Try to use transient buffer for multi-batch support rhi::TransientInstanceBuffer transientBuffer = device.allocTransientInstanceBuffer(count); if (transientBuffer.isValid()) { // Copy particle data to transient buffer std::memcpy(transientBuffer.data, m_particleInstances.data(), count * sizeof(SpriteInstance)); // Set buffers cmd.setVertexBuffer(m_quadVB); cmd.setIndexBuffer(m_quadIB); cmd.setTransientInstanceBuffer(transientBuffer, 0, count); cmd.setTexture(0, texture, m_textureSampler); cmd.drawInstanced(6, count); cmd.submit(0, m_shader, 0); } else { // Fallback to dynamic buffer (single batch per frame limitation) device.updateBuffer(m_instanceBuffer, m_particleInstances.data(), count * sizeof(SpriteInstance)); cmd.setVertexBuffer(m_quadVB); cmd.setIndexBuffer(m_quadIB); cmd.setInstanceBuffer(m_instanceBuffer, 0, count); cmd.setTexture(0, texture, m_textureSampler); cmd.drawInstanced(6, count); cmd.submit(0, m_shader, 0); } } } // namespace grove