feat(BgfxRenderer): Fix multi-texture batching and add particle effects
- Fix texture state management in BgfxDevice: defer setTexture until submit() - Add transient instance buffer support for multi-batch rendering - Add ParticlePass with fire, smoke and sparkle particle systems - Load multiple textures from config (texture1..texture10) - Visual test now demonstrates multi-texture sprites and multi-particle effects 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5795bbb37e
commit
9618a647a2
BIN
assets/textures/5oxaxt1vo2f91.jpg
Normal file
BIN
assets/textures/5oxaxt1vo2f91.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
@ -11,6 +11,7 @@
|
|||||||
#include "Passes/TilemapPass.h"
|
#include "Passes/TilemapPass.h"
|
||||||
#include "Passes/SpritePass.h"
|
#include "Passes/SpritePass.h"
|
||||||
#include "Passes/TextPass.h"
|
#include "Passes/TextPass.h"
|
||||||
|
#include "Passes/ParticlePass.h"
|
||||||
#include "Passes/DebugPass.h"
|
#include "Passes/DebugPass.h"
|
||||||
|
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
@ -106,6 +107,12 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas
|
|||||||
m_renderGraph->addPass(std::make_unique<TextPass>(spriteShader));
|
m_renderGraph->addPass(std::make_unique<TextPass>(spriteShader));
|
||||||
m_logger->info("Added TextPass");
|
m_logger->info("Added TextPass");
|
||||||
|
|
||||||
|
// Create ParticlePass (uses sprite shader, renders after sprites with additive blending)
|
||||||
|
auto particlePass = std::make_unique<ParticlePass>(spriteShader);
|
||||||
|
particlePass->setResourceCache(m_resourceCache.get());
|
||||||
|
m_renderGraph->addPass(std::move(particlePass));
|
||||||
|
m_logger->info("Added ParticlePass");
|
||||||
|
|
||||||
m_renderGraph->addPass(std::make_unique<DebugPass>(debugShader));
|
m_renderGraph->addPass(std::make_unique<DebugPass>(debugShader));
|
||||||
m_logger->info("Added DebugPass");
|
m_logger->info("Added DebugPass");
|
||||||
m_renderGraph->setup(*m_device);
|
m_renderGraph->setup(*m_device);
|
||||||
@ -129,15 +136,30 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas
|
|||||||
// Load default texture if specified in config
|
// Load default texture if specified in config
|
||||||
std::string defaultTexturePath = config.getString("defaultTexture", "");
|
std::string defaultTexturePath = config.getString("defaultTexture", "");
|
||||||
if (!defaultTexturePath.empty()) {
|
if (!defaultTexturePath.empty()) {
|
||||||
rhi::TextureHandle tex = m_resourceCache->loadTexture(*m_device, defaultTexturePath);
|
uint16_t texId = m_resourceCache->loadTextureWithId(*m_device, defaultTexturePath);
|
||||||
if (tex.isValid()) {
|
if (texId > 0) {
|
||||||
|
rhi::TextureHandle tex = m_resourceCache->getTextureById(texId);
|
||||||
m_spritePass->setTexture(tex);
|
m_spritePass->setTexture(tex);
|
||||||
m_logger->info("Loaded default texture: {}", defaultTexturePath);
|
m_logger->info("Loaded default texture: {} (id={})", defaultTexturePath, texId);
|
||||||
} else {
|
} else {
|
||||||
m_logger->warn("Failed to load default texture: {}", defaultTexturePath);
|
m_logger->warn("Failed to load default texture: {}", defaultTexturePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load additional textures (texture1, texture2, etc.)
|
||||||
|
for (int i = 1; i <= 10; ++i) {
|
||||||
|
std::string key = "texture" + std::to_string(i);
|
||||||
|
std::string path = config.getString(key, "");
|
||||||
|
if (!path.empty()) {
|
||||||
|
uint16_t texId = m_resourceCache->loadTextureWithId(*m_device, path);
|
||||||
|
if (texId > 0) {
|
||||||
|
m_logger->info("Loaded texture: {} (id={})", path, texId);
|
||||||
|
} else {
|
||||||
|
m_logger->warn("Failed to load texture: {}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m_logger->info("BgfxRenderer initialized successfully");
|
m_logger->info("BgfxRenderer initialized successfully");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -52,6 +52,7 @@ add_library(BgfxRenderer SHARED
|
|||||||
Passes/TilemapPass.cpp
|
Passes/TilemapPass.cpp
|
||||||
Passes/SpritePass.cpp
|
Passes/SpritePass.cpp
|
||||||
Passes/TextPass.cpp
|
Passes/TextPass.cpp
|
||||||
|
Passes/ParticlePass.cpp
|
||||||
Passes/DebugPass.cpp
|
Passes/DebugPass.cpp
|
||||||
|
|
||||||
# Text
|
# Text
|
||||||
|
|||||||
208
modules/BgfxRenderer/Passes/ParticlePass.cpp
Normal file
208
modules/BgfxRenderer/Passes/ParticlePass.cpp
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
#include "ParticlePass.h"
|
||||||
|
#include "../RHI/RHIDevice.h"
|
||||||
|
#include "../Resources/ResourceCache.h"
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
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<uint32_t>(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<float>(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<float>((color >> 24) & 0xFF) / 255.0f;
|
||||||
|
inst.g = static_cast<float>((color >> 16) & 0xFF) / 255.0f;
|
||||||
|
inst.b = static_cast<float>((color >> 8) & 0xFF) / 255.0f;
|
||||||
|
// Alpha fades with life
|
||||||
|
float baseAlpha = static_cast<float>(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<uint32_t>(m_particleInstances.size()));
|
||||||
|
m_particleInstances.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush remaining particles
|
||||||
|
if (!m_particleInstances.empty()) {
|
||||||
|
flushBatch(device, cmd, currentTexture, static_cast<uint32_t>(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
|
||||||
63
modules/BgfxRenderer/Passes/ParticlePass.h
Normal file
63
modules/BgfxRenderer/Passes/ParticlePass.h
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../RenderGraph/RenderPass.h"
|
||||||
|
#include "../RHI/RHITypes.h"
|
||||||
|
#include "../Frame/FramePacket.h"
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace grove {
|
||||||
|
|
||||||
|
class ResourceCache;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Particle Pass - Renders 2D particles with additive blending
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
class ParticlePass : public RenderPass {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Construct ParticlePass with required shader
|
||||||
|
* @param shader The shader program to use for particle rendering
|
||||||
|
*/
|
||||||
|
explicit ParticlePass(rhi::ShaderHandle shader);
|
||||||
|
|
||||||
|
const char* getName() const override { return "Particles"; }
|
||||||
|
uint32_t getSortOrder() const override { return 150; } // After sprites (100)
|
||||||
|
std::vector<const char*> getDependencies() const override { return {"Sprites"}; }
|
||||||
|
|
||||||
|
void setup(rhi::IRHIDevice& device) override;
|
||||||
|
void shutdown(rhi::IRHIDevice& device) override;
|
||||||
|
void execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set resource cache for texture lookup by ID
|
||||||
|
*/
|
||||||
|
void setResourceCache(ResourceCache* cache) { m_resourceCache = cache; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set blend mode for particles
|
||||||
|
* @param additive true for additive blending (fire, sparks), false for alpha (smoke)
|
||||||
|
*/
|
||||||
|
void setAdditiveBlending(bool additive) { m_additiveBlending = additive; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void flushBatch(rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd,
|
||||||
|
rhi::TextureHandle texture, uint32_t count);
|
||||||
|
|
||||||
|
rhi::ShaderHandle m_shader;
|
||||||
|
rhi::BufferHandle m_quadVB;
|
||||||
|
rhi::BufferHandle m_quadIB;
|
||||||
|
rhi::BufferHandle m_instanceBuffer; // Fallback for when transient allocation fails
|
||||||
|
rhi::UniformHandle m_textureSampler;
|
||||||
|
rhi::TextureHandle m_defaultTexture; // White 1x1 texture for untextured particles
|
||||||
|
|
||||||
|
ResourceCache* m_resourceCache = nullptr;
|
||||||
|
bool m_additiveBlending = true; // Default to additive for fire/spark effects
|
||||||
|
|
||||||
|
// GPU-aligned particle instances for batching
|
||||||
|
std::vector<SpriteInstance> m_particleInstances;
|
||||||
|
|
||||||
|
static constexpr uint32_t MAX_PARTICLES_PER_BATCH = 10000;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace grove
|
||||||
@ -3,6 +3,7 @@
|
|||||||
#include "../Frame/FramePacket.h"
|
#include "../Frame/FramePacket.h"
|
||||||
#include "../Resources/ResourceCache.h"
|
#include "../Resources/ResourceCache.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
namespace grove {
|
namespace grove {
|
||||||
|
|
||||||
@ -45,7 +46,8 @@ void SpritePass::setup(rhi::IRHIDevice& device) {
|
|||||||
ibDesc.dynamic = false;
|
ibDesc.dynamic = false;
|
||||||
m_quadIB = device.createBuffer(ibDesc);
|
m_quadIB = device.createBuffer(ibDesc);
|
||||||
|
|
||||||
// Create dynamic instance buffer
|
// Note: We no longer create a persistent instance buffer since we use transient buffers
|
||||||
|
// But keep it for fallback if transient allocation fails
|
||||||
rhi::BufferDesc instDesc;
|
rhi::BufferDesc instDesc;
|
||||||
instDesc.type = rhi::BufferDesc::Instance;
|
instDesc.type = rhi::BufferDesc::Instance;
|
||||||
instDesc.size = MAX_SPRITES_PER_BATCH * sizeof(SpriteInstance);
|
instDesc.size = MAX_SPRITES_PER_BATCH * sizeof(SpriteInstance);
|
||||||
@ -82,7 +84,7 @@ void SpritePass::flushBatch(rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd,
|
|||||||
|
|
||||||
cmd.setVertexBuffer(m_quadVB);
|
cmd.setVertexBuffer(m_quadVB);
|
||||||
cmd.setIndexBuffer(m_quadIB);
|
cmd.setIndexBuffer(m_quadIB);
|
||||||
cmd.setInstanceBuffer(m_instanceBuffer, 0, count);
|
// Note: Instance buffer should be set before calling this
|
||||||
cmd.setTexture(0, texture, m_textureSampler);
|
cmd.setTexture(0, texture, m_textureSampler);
|
||||||
cmd.drawInstanced(6, count);
|
cmd.drawInstanced(6, count);
|
||||||
cmd.submit(0, m_shader, 0);
|
cmd.submit(0, m_shader, 0);
|
||||||
@ -120,61 +122,70 @@ void SpritePass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi:
|
|||||||
return sa.textureId < sb.textureId;
|
return sa.textureId < sb.textureId;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process sprites in batches by layer and texture
|
// Process sprites in batches by texture
|
||||||
// Flush batch when layer OR texture changes to maintain correct draw order
|
// Use transient buffers for proper multi-batch rendering
|
||||||
std::vector<SpriteInstance> batchData;
|
uint32_t batchStart = 0;
|
||||||
batchData.reserve(MAX_SPRITES_PER_BATCH);
|
while (batchStart < frame.spriteCount) {
|
||||||
|
// Find the end of current batch (same texture)
|
||||||
|
uint16_t currentTexId = static_cast<uint16_t>(frame.sprites[m_sortedIndices[batchStart]].textureId);
|
||||||
|
uint32_t batchEnd = batchStart + 1;
|
||||||
|
|
||||||
uint16_t currentTextureId = UINT16_MAX;
|
while (batchEnd < frame.spriteCount) {
|
||||||
float currentLayer = -1e9f; // Use a value that won't match any real layer
|
uint16_t nextTexId = static_cast<uint16_t>(frame.sprites[m_sortedIndices[batchEnd]].textureId);
|
||||||
rhi::TextureHandle currentTexture;
|
if (nextTexId != currentTexId) {
|
||||||
|
break; // Texture changed, flush this batch
|
||||||
for (uint32_t idx : m_sortedIndices) {
|
|
||||||
const SpriteInstance& sprite = frame.sprites[idx];
|
|
||||||
uint16_t texId = static_cast<uint16_t>(sprite.textureId);
|
|
||||||
|
|
||||||
// Check if texture OR layer changed (must flush to maintain layer order)
|
|
||||||
if (texId != currentTextureId || sprite.layer != currentLayer) {
|
|
||||||
// Flush previous batch
|
|
||||||
if (!batchData.empty()) {
|
|
||||||
device.updateBuffer(m_instanceBuffer, batchData.data(),
|
|
||||||
static_cast<uint32_t>(batchData.size() * sizeof(SpriteInstance)));
|
|
||||||
flushBatch(device, cmd, currentTexture, static_cast<uint32_t>(batchData.size()));
|
|
||||||
batchData.clear();
|
|
||||||
}
|
}
|
||||||
|
++batchEnd;
|
||||||
|
}
|
||||||
|
|
||||||
// Update current state
|
uint32_t batchCount = batchEnd - batchStart;
|
||||||
currentTextureId = texId;
|
|
||||||
currentLayer = sprite.layer;
|
// Resolve texture handle for this batch
|
||||||
if (texId == 0 || !m_resourceCache) {
|
rhi::TextureHandle batchTexture;
|
||||||
// Use default/active texture for textureId=0
|
if (currentTexId == 0 || !m_resourceCache) {
|
||||||
currentTexture = m_activeTexture.isValid() ? m_activeTexture : m_defaultTexture;
|
batchTexture = m_activeTexture.isValid() ? m_activeTexture : m_defaultTexture;
|
||||||
} else {
|
} else {
|
||||||
// Look up texture by ID
|
batchTexture = m_resourceCache->getTextureById(currentTexId);
|
||||||
currentTexture = m_resourceCache->getTextureById(texId);
|
if (!batchTexture.isValid()) {
|
||||||
if (!currentTexture.isValid()) {
|
batchTexture = m_activeTexture.isValid() ? m_activeTexture : m_defaultTexture;
|
||||||
currentTexture = m_activeTexture.isValid() ? m_activeTexture : m_defaultTexture;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add sprite to batch
|
// Allocate transient instance buffer for this batch
|
||||||
batchData.push_back(sprite);
|
rhi::TransientInstanceBuffer transientBuffer = device.allocTransientInstanceBuffer(batchCount);
|
||||||
|
|
||||||
// Flush if batch is full
|
if (transientBuffer.isValid()) {
|
||||||
if (batchData.size() >= MAX_SPRITES_PER_BATCH) {
|
// Copy sprite data to transient buffer
|
||||||
|
SpriteInstance* dest = static_cast<SpriteInstance*>(transientBuffer.data);
|
||||||
|
for (uint32_t i = 0; i < batchCount; ++i) {
|
||||||
|
dest[i] = frame.sprites[m_sortedIndices[batchStart + i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-set state for each batch to ensure clean state
|
||||||
|
cmd.setState(state);
|
||||||
|
|
||||||
|
// Set buffers and draw
|
||||||
|
cmd.setVertexBuffer(m_quadVB);
|
||||||
|
cmd.setIndexBuffer(m_quadIB);
|
||||||
|
cmd.setTransientInstanceBuffer(transientBuffer, 0, batchCount);
|
||||||
|
cmd.setTexture(0, batchTexture, m_textureSampler);
|
||||||
|
cmd.drawInstanced(6, batchCount);
|
||||||
|
cmd.submit(0, m_shader, 0);
|
||||||
|
} else {
|
||||||
|
// Fallback: use dynamic buffer (may have issues with multiple batches)
|
||||||
|
// This should only happen if GPU runs out of transient memory
|
||||||
|
std::vector<SpriteInstance> batchData;
|
||||||
|
batchData.reserve(batchCount);
|
||||||
|
for (uint32_t i = 0; i < batchCount; ++i) {
|
||||||
|
batchData.push_back(frame.sprites[m_sortedIndices[batchStart + i]]);
|
||||||
|
}
|
||||||
device.updateBuffer(m_instanceBuffer, batchData.data(),
|
device.updateBuffer(m_instanceBuffer, batchData.data(),
|
||||||
static_cast<uint32_t>(batchData.size() * sizeof(SpriteInstance)));
|
static_cast<uint32_t>(batchData.size() * sizeof(SpriteInstance)));
|
||||||
flushBatch(device, cmd, currentTexture, static_cast<uint32_t>(batchData.size()));
|
cmd.setInstanceBuffer(m_instanceBuffer, 0, batchCount);
|
||||||
batchData.clear();
|
flushBatch(device, cmd, batchTexture, batchCount);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Flush remaining sprites
|
batchStart = batchEnd;
|
||||||
if (!batchData.empty()) {
|
|
||||||
device.updateBuffer(m_instanceBuffer, batchData.data(),
|
|
||||||
static_cast<uint32_t>(batchData.size() * sizeof(SpriteInstance)));
|
|
||||||
flushBatch(device, cmd, currentTexture, static_cast<uint32_t>(batchData.size()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -260,6 +260,38 @@ public:
|
|||||||
bgfx::updateTexture2D(h, 0, 0, 0, 0, m_width, m_height, bgfx::copy(data, size));
|
bgfx::updateTexture2D(h, 0, 0, 0, 0, m_width, m_height, bgfx::copy(data, size));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Transient Instance Buffers
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
TransientInstanceBuffer allocTransientInstanceBuffer(uint32_t count) override {
|
||||||
|
TransientInstanceBuffer result;
|
||||||
|
|
||||||
|
constexpr uint16_t INSTANCE_STRIDE = 80; // 5 x vec4
|
||||||
|
|
||||||
|
// Check if we have space in the pool
|
||||||
|
if (m_transientPoolCount >= MAX_TRANSIENT_BUFFERS) {
|
||||||
|
return result; // Pool full, return invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if bgfx has enough transient memory
|
||||||
|
if (bgfx::getAvailInstanceDataBuffer(count, INSTANCE_STRIDE) < count) {
|
||||||
|
return result; // Not enough memory
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate from bgfx
|
||||||
|
uint16_t poolIndex = m_transientPoolCount++;
|
||||||
|
bgfx::allocInstanceDataBuffer(&m_transientPool[poolIndex], count, INSTANCE_STRIDE);
|
||||||
|
|
||||||
|
result.data = m_transientPool[poolIndex].data;
|
||||||
|
result.size = count * INSTANCE_STRIDE;
|
||||||
|
result.count = count;
|
||||||
|
result.stride = INSTANCE_STRIDE;
|
||||||
|
result.poolIndex = poolIndex;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// View Setup
|
// View Setup
|
||||||
// ========================================
|
// ========================================
|
||||||
@ -282,6 +314,8 @@ public:
|
|||||||
|
|
||||||
void frame() override {
|
void frame() override {
|
||||||
bgfx::frame();
|
bgfx::frame();
|
||||||
|
// Reset transient pool for next frame
|
||||||
|
m_transientPoolCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void executeCommandBuffer(const RHICommandBuffer& cmdBuffer) override {
|
void executeCommandBuffer(const RHICommandBuffer& cmdBuffer) override {
|
||||||
@ -293,6 +327,12 @@ public:
|
|||||||
uint32_t instStart = 0;
|
uint32_t instStart = 0;
|
||||||
uint32_t instCount = 0;
|
uint32_t instCount = 0;
|
||||||
|
|
||||||
|
// Store texture state to apply at draw time (not immediately)
|
||||||
|
TextureHandle pendingTexture;
|
||||||
|
UniformHandle pendingSampler;
|
||||||
|
uint8_t pendingTextureSlot = 0;
|
||||||
|
bool hasTexture = false;
|
||||||
|
|
||||||
for (const Command& cmd : cmdBuffer.getCommands()) {
|
for (const Command& cmd : cmdBuffer.getCommands()) {
|
||||||
switch (cmd.type) {
|
switch (cmd.type) {
|
||||||
case CommandType::SetState: {
|
case CommandType::SetState: {
|
||||||
@ -339,9 +379,12 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
case CommandType::SetTexture: {
|
case CommandType::SetTexture: {
|
||||||
bgfx::TextureHandle tex = { cmd.setTexture.texture.id };
|
// Store texture state - apply at draw time, not immediately
|
||||||
bgfx::UniformHandle sampler = { cmd.setTexture.sampler.id };
|
// This ensures texture is set after all other state is configured
|
||||||
bgfx::setTexture(cmd.setTexture.slot, sampler, tex);
|
pendingTexture = cmd.setTexture.texture;
|
||||||
|
pendingSampler = cmd.setTexture.sampler;
|
||||||
|
pendingTextureSlot = cmd.setTexture.slot;
|
||||||
|
hasTexture = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,6 +408,15 @@ public:
|
|||||||
currentInstBuffer = cmd.setInstanceBuffer.buffer;
|
currentInstBuffer = cmd.setInstanceBuffer.buffer;
|
||||||
instStart = cmd.setInstanceBuffer.start;
|
instStart = cmd.setInstanceBuffer.start;
|
||||||
instCount = cmd.setInstanceBuffer.count;
|
instCount = cmd.setInstanceBuffer.count;
|
||||||
|
m_useTransientInstance = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CommandType::SetTransientInstanceBuffer: {
|
||||||
|
m_currentTransientIndex = cmd.setTransientInstanceBuffer.poolIndex;
|
||||||
|
instStart = cmd.setTransientInstanceBuffer.start;
|
||||||
|
instCount = cmd.setTransientInstanceBuffer.count;
|
||||||
|
m_useTransientInstance = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,7 +493,11 @@ public:
|
|||||||
bgfx::setIndexBuffer(h, 0, cmd.drawInstanced.indexCount);
|
bgfx::setIndexBuffer(h, 0, cmd.drawInstanced.indexCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentInstBuffer.isValid()) {
|
// Set instance buffer (either dynamic or transient)
|
||||||
|
if (m_useTransientInstance && m_currentTransientIndex < m_transientPoolCount) {
|
||||||
|
// Transient instance buffer from pool
|
||||||
|
bgfx::setInstanceDataBuffer(&m_transientPool[m_currentTransientIndex], instStart, instCount);
|
||||||
|
} else if (currentInstBuffer.isValid()) {
|
||||||
bool isDynamic = (currentInstBuffer.id & 0x8000) != 0;
|
bool isDynamic = (currentInstBuffer.id & 0x8000) != 0;
|
||||||
uint16_t idx = currentInstBuffer.id & 0x7FFF;
|
uint16_t idx = currentInstBuffer.id & 0x7FFF;
|
||||||
if (isDynamic) {
|
if (isDynamic) {
|
||||||
@ -453,8 +509,16 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
case CommandType::Submit: {
|
case CommandType::Submit: {
|
||||||
|
// Apply pending texture right before submit
|
||||||
|
if (hasTexture) {
|
||||||
|
bgfx::TextureHandle tex = { pendingTexture.id };
|
||||||
|
bgfx::UniformHandle sampler = { pendingSampler.id };
|
||||||
|
bgfx::setTexture(pendingTextureSlot, sampler, tex);
|
||||||
|
}
|
||||||
bgfx::ProgramHandle program = { cmd.submit.shader.id };
|
bgfx::ProgramHandle program = { cmd.submit.shader.id };
|
||||||
bgfx::submit(cmd.submit.view, program, cmd.submit.depth);
|
bgfx::submit(cmd.submit.view, program, cmd.submit.depth);
|
||||||
|
// Reset texture state after submit (consumed)
|
||||||
|
hasTexture = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -466,6 +530,15 @@ private:
|
|||||||
uint16_t m_height = 0;
|
uint16_t m_height = 0;
|
||||||
bool m_initialized = false;
|
bool m_initialized = false;
|
||||||
|
|
||||||
|
// Transient instance buffer pool (reset each frame)
|
||||||
|
static constexpr uint16_t MAX_TRANSIENT_BUFFERS = 256;
|
||||||
|
bgfx::InstanceDataBuffer m_transientPool[MAX_TRANSIENT_BUFFERS];
|
||||||
|
uint16_t m_transientPoolCount = 0;
|
||||||
|
|
||||||
|
// Transient buffer state for command execution
|
||||||
|
bool m_useTransientInstance = false;
|
||||||
|
uint16_t m_currentTransientIndex = UINT16_MAX;
|
||||||
|
|
||||||
// Empty buffer for null data fallback in buffer creation
|
// Empty buffer for null data fallback in buffer creation
|
||||||
inline static const uint8_t s_emptyBuffer[1] = {0};
|
inline static const uint8_t s_emptyBuffer[1] = {0};
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,15 @@ void RHICommandBuffer::setInstanceBuffer(BufferHandle buffer, uint32_t start, ui
|
|||||||
m_commands.push_back(cmd);
|
m_commands.push_back(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RHICommandBuffer::setTransientInstanceBuffer(const TransientInstanceBuffer& buffer, uint32_t start, uint32_t count) {
|
||||||
|
Command cmd;
|
||||||
|
cmd.type = CommandType::SetTransientInstanceBuffer;
|
||||||
|
cmd.setTransientInstanceBuffer.poolIndex = buffer.poolIndex;
|
||||||
|
cmd.setTransientInstanceBuffer.start = start;
|
||||||
|
cmd.setTransientInstanceBuffer.count = count;
|
||||||
|
m_commands.push_back(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
void RHICommandBuffer::setScissor(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
|
void RHICommandBuffer::setScissor(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
|
||||||
Command cmd;
|
Command cmd;
|
||||||
cmd.type = CommandType::SetScissor;
|
cmd.type = CommandType::SetScissor;
|
||||||
|
|||||||
@ -17,6 +17,7 @@ enum class CommandType : uint8_t {
|
|||||||
SetVertexBuffer,
|
SetVertexBuffer,
|
||||||
SetIndexBuffer,
|
SetIndexBuffer,
|
||||||
SetInstanceBuffer,
|
SetInstanceBuffer,
|
||||||
|
SetTransientInstanceBuffer, // For frame-local multi-batch rendering
|
||||||
SetScissor,
|
SetScissor,
|
||||||
Draw,
|
Draw,
|
||||||
DrawIndexed,
|
DrawIndexed,
|
||||||
@ -33,6 +34,7 @@ struct Command {
|
|||||||
struct { BufferHandle buffer; uint32_t offset; } setVertexBuffer;
|
struct { BufferHandle buffer; uint32_t offset; } setVertexBuffer;
|
||||||
struct { BufferHandle buffer; uint32_t offset; bool is32Bit; } setIndexBuffer;
|
struct { BufferHandle buffer; uint32_t offset; bool is32Bit; } setIndexBuffer;
|
||||||
struct { BufferHandle buffer; uint32_t start; uint32_t count; } setInstanceBuffer;
|
struct { BufferHandle buffer; uint32_t start; uint32_t count; } setInstanceBuffer;
|
||||||
|
struct { uint16_t poolIndex; uint32_t start; uint32_t count; } setTransientInstanceBuffer;
|
||||||
struct { uint16_t x, y, w, h; } setScissor;
|
struct { uint16_t x, y, w, h; } setScissor;
|
||||||
struct { uint32_t vertexCount; uint32_t startVertex; } draw;
|
struct { uint32_t vertexCount; uint32_t startVertex; } draw;
|
||||||
struct { uint32_t indexCount; uint32_t startIndex; } drawIndexed;
|
struct { uint32_t indexCount; uint32_t startIndex; } drawIndexed;
|
||||||
@ -68,6 +70,7 @@ public:
|
|||||||
void setVertexBuffer(BufferHandle buffer, uint32_t offset = 0);
|
void setVertexBuffer(BufferHandle buffer, uint32_t offset = 0);
|
||||||
void setIndexBuffer(BufferHandle buffer, uint32_t offset = 0, bool is32Bit = false);
|
void setIndexBuffer(BufferHandle buffer, uint32_t offset = 0, bool is32Bit = false);
|
||||||
void setInstanceBuffer(BufferHandle buffer, uint32_t start, uint32_t count);
|
void setInstanceBuffer(BufferHandle buffer, uint32_t start, uint32_t count);
|
||||||
|
void setTransientInstanceBuffer(const TransientInstanceBuffer& buffer, uint32_t start, uint32_t count);
|
||||||
void setScissor(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
|
void setScissor(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
|
||||||
void draw(uint32_t vertexCount, uint32_t startVertex = 0);
|
void draw(uint32_t vertexCount, uint32_t startVertex = 0);
|
||||||
void drawIndexed(uint32_t indexCount, uint32_t startIndex = 0);
|
void drawIndexed(uint32_t indexCount, uint32_t startIndex = 0);
|
||||||
|
|||||||
@ -54,6 +54,11 @@ public:
|
|||||||
virtual void updateBuffer(BufferHandle handle, const void* data, uint32_t size) = 0;
|
virtual void updateBuffer(BufferHandle handle, const void* data, uint32_t size) = 0;
|
||||||
virtual void updateTexture(TextureHandle handle, const void* data, uint32_t size) = 0;
|
virtual void updateTexture(TextureHandle handle, const void* data, uint32_t size) = 0;
|
||||||
|
|
||||||
|
// Transient instance buffers (frame-local, for multi-batch rendering)
|
||||||
|
// These are automatically freed at end of frame - no manual cleanup needed
|
||||||
|
// Returns buffer with data pointer for CPU-side writing
|
||||||
|
virtual TransientInstanceBuffer allocTransientInstanceBuffer(uint32_t count) = 0;
|
||||||
|
|
||||||
// View setup
|
// View setup
|
||||||
virtual void setViewClear(ViewId id, uint32_t rgba, float depth) = 0;
|
virtual void setViewClear(ViewId id, uint32_t rgba, float depth) = 0;
|
||||||
virtual void setViewRect(ViewId id, uint16_t x, uint16_t y, uint16_t w, uint16_t h) = 0;
|
virtual void setViewRect(ViewId id, uint16_t x, uint16_t y, uint16_t w, uint16_t h) = 0;
|
||||||
|
|||||||
@ -35,6 +35,19 @@ struct FramebufferHandle {
|
|||||||
|
|
||||||
using ViewId = uint16_t;
|
using ViewId = uint16_t;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Transient Instance Buffer - Frame-local allocation for multi-batch rendering
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
struct TransientInstanceBuffer {
|
||||||
|
void* data = nullptr; // CPU-side pointer for writing data
|
||||||
|
uint32_t size = 0; // Size in bytes
|
||||||
|
uint32_t count = 0; // Number of instances
|
||||||
|
uint16_t stride = 0; // Bytes per instance
|
||||||
|
uint16_t poolIndex = UINT16_MAX; // Index in device's transient pool
|
||||||
|
bool isValid() const { return data != nullptr && poolIndex != UINT16_MAX; }
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Render States
|
// Render States
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@ -120,8 +120,9 @@ int main(int argc, char* argv[]) {
|
|||||||
config.setDouble("nativeWindowHandle", static_cast<double>(reinterpret_cast<uintptr_t>(nativeWindowHandle)));
|
config.setDouble("nativeWindowHandle", static_cast<double>(reinterpret_cast<uintptr_t>(nativeWindowHandle)));
|
||||||
config.setDouble("nativeDisplayHandle", static_cast<double>(reinterpret_cast<uintptr_t>(nativeDisplayHandle)));
|
config.setDouble("nativeDisplayHandle", static_cast<double>(reinterpret_cast<uintptr_t>(nativeDisplayHandle)));
|
||||||
|
|
||||||
// Load texture from assets folder
|
// Load textures from assets folder
|
||||||
config.setString("defaultTexture", "../../assets/textures/1f440.png");
|
config.setString("defaultTexture", "../../assets/textures/1f440.png");
|
||||||
|
config.setString("texture1", "../../assets/textures/5oxaxt1vo2f91.jpg"); // Second texture (Multipla)
|
||||||
|
|
||||||
// Enable debug overlay
|
// Enable debug overlay
|
||||||
config.setBool("debugOverlay", true);
|
config.setBool("debugOverlay", true);
|
||||||
@ -228,7 +229,7 @@ int main(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send animated sprites in a circle
|
// Send animated sprites in a circle
|
||||||
// Using different textureIds to test batching (all will use default texture fallback)
|
// Using different textureIds to test multi-texture batching
|
||||||
for (int i = 0; i < 5; ++i) {
|
for (int i = 0; i < 5; ++i) {
|
||||||
auto sprite = std::make_unique<grove::JsonDataNode>("sprite");
|
auto sprite = std::make_unique<grove::JsonDataNode>("sprite");
|
||||||
|
|
||||||
@ -252,29 +253,157 @@ int main(int argc, char* argv[]) {
|
|||||||
uint32_t colors[] = {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0xFF00FFFF};
|
uint32_t colors[] = {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0xFF00FFFF};
|
||||||
sprite->setInt("color", static_cast<int>(colors[i]));
|
sprite->setInt("color", static_cast<int>(colors[i]));
|
||||||
|
|
||||||
// Use textureId=0 for all (default texture)
|
// Alternate between textures: even = eye (id=1), odd = multipla (id=2)
|
||||||
sprite->setInt("textureId", 0);
|
sprite->setInt("textureId", (i % 2 == 0) ? 1 : 2);
|
||||||
sprite->setInt("layer", i);
|
sprite->setInt("layer", i);
|
||||||
|
|
||||||
std::unique_ptr<grove::IDataNode> data = std::move(sprite);
|
std::unique_ptr<grove::IDataNode> data = std::move(sprite);
|
||||||
gameIO->publish("render:sprite", std::move(data));
|
gameIO->publish("render:sprite", std::move(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a center sprite
|
// Add a center sprite (Multipla!)
|
||||||
{
|
{
|
||||||
auto sprite = std::make_unique<grove::JsonDataNode>("sprite");
|
auto sprite = std::make_unique<grove::JsonDataNode>("sprite");
|
||||||
sprite->setDouble("x", width / 2.0f);
|
sprite->setDouble("x", width / 2.0f);
|
||||||
sprite->setDouble("y", height / 2.0f);
|
sprite->setDouble("y", height / 2.0f);
|
||||||
sprite->setDouble("scaleX", 80.0);
|
sprite->setDouble("scaleX", 120.0);
|
||||||
sprite->setDouble("scaleY", 80.0);
|
sprite->setDouble("scaleY", 80.0); // Wider than tall (car aspect ratio)
|
||||||
sprite->setDouble("rotation", -time);
|
sprite->setDouble("rotation", -time * 0.5f);
|
||||||
sprite->setInt("color", 0xFFFFFFFF); // White
|
sprite->setInt("color", 0xFFFFFFFF); // White (no tint)
|
||||||
|
sprite->setInt("textureId", 2); // Multipla texture
|
||||||
sprite->setInt("layer", 10);
|
sprite->setInt("layer", 10);
|
||||||
|
|
||||||
std::unique_ptr<grove::IDataNode> data = std::move(sprite);
|
std::unique_ptr<grove::IDataNode> data = std::move(sprite);
|
||||||
gameIO->publish("render:sprite", std::move(data));
|
gameIO->publish("render:sprite", std::move(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Test particle rendering (fire-like effect)
|
||||||
|
// ========================================
|
||||||
|
for (int i = 0; i < 20; ++i) {
|
||||||
|
auto particle = std::make_unique<grove::JsonDataNode>("particle");
|
||||||
|
|
||||||
|
// Spawn particles rising from bottom center
|
||||||
|
float spawnX = width / 2.0f + (std::sin(time * 5.0f + i * 0.5f) * 50.0f);
|
||||||
|
float spawnY = height - 50.0f;
|
||||||
|
|
||||||
|
// Particles rise up with some horizontal drift
|
||||||
|
float particleAge = std::fmod(time * 2.0f + i * 0.2f, 2.0f); // 0-2 second cycle
|
||||||
|
float life = 1.0f - (particleAge / 2.0f); // 1.0 -> 0.0
|
||||||
|
|
||||||
|
float px = spawnX + std::sin(particleAge * 3.0f + i) * 30.0f;
|
||||||
|
float py = spawnY - particleAge * 100.0f; // Rise up
|
||||||
|
|
||||||
|
particle->setDouble("x", px);
|
||||||
|
particle->setDouble("y", py);
|
||||||
|
particle->setDouble("vx", 0.0);
|
||||||
|
particle->setDouble("vy", -50.0);
|
||||||
|
particle->setDouble("size", 15.0 + life * 20.0); // Shrink as they age
|
||||||
|
particle->setDouble("life", life);
|
||||||
|
|
||||||
|
// Fire colors: white -> yellow -> orange -> red based on life
|
||||||
|
uint8_t r, g, b;
|
||||||
|
if (life > 0.7f) {
|
||||||
|
// White/yellow core
|
||||||
|
r = 255; g = 255; b = static_cast<uint8_t>(200 * (life - 0.7f) / 0.3f);
|
||||||
|
} else if (life > 0.3f) {
|
||||||
|
// Orange
|
||||||
|
r = 255; g = static_cast<uint8_t>(100 + 155 * (life - 0.3f) / 0.4f); b = 0;
|
||||||
|
} else {
|
||||||
|
// Red fading out
|
||||||
|
r = static_cast<uint8_t>(200 + 55 * life / 0.3f); g = static_cast<uint8_t>(50 * life / 0.3f); b = 0;
|
||||||
|
}
|
||||||
|
uint32_t color = (r << 24) | (g << 16) | (b << 8) | 0xFF;
|
||||||
|
particle->setInt("color", static_cast<int>(color));
|
||||||
|
particle->setInt("textureId", 0);
|
||||||
|
|
||||||
|
std::unique_ptr<grove::IDataNode> data = std::move(particle);
|
||||||
|
gameIO->publish("render:particle", std::move(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Smoke particles (second particle system - alpha blending, gray colors)
|
||||||
|
// ========================================
|
||||||
|
float smokeSpawnX = width * 0.7f; // Right side
|
||||||
|
float smokeSpawnY = height * 0.8f;
|
||||||
|
|
||||||
|
for (int i = 0; i < 15; ++i) {
|
||||||
|
auto smoke = std::make_unique<grove::JsonDataNode>("particle");
|
||||||
|
|
||||||
|
// Smoke rises slower, drifts more
|
||||||
|
float smokeAge = std::fmod(time * 1.0f + i * 0.15f, 3.0f); // 3 second cycle
|
||||||
|
float life = 1.0f - (smokeAge / 3.0f);
|
||||||
|
|
||||||
|
float sx = smokeSpawnX + std::sin(smokeAge * 2.0f + i * 0.5f) * 50.0f;
|
||||||
|
float sy = smokeSpawnY - smokeAge * 60.0f; // Rise slower
|
||||||
|
|
||||||
|
smoke->setDouble("x", sx);
|
||||||
|
smoke->setDouble("y", sy);
|
||||||
|
smoke->setDouble("vx", std::sin(time + i) * 20.0f);
|
||||||
|
smoke->setDouble("vy", -30.0);
|
||||||
|
smoke->setDouble("size", 20.0 + (1.0f - life) * 40.0); // Grow as they age
|
||||||
|
smoke->setDouble("life", life);
|
||||||
|
|
||||||
|
// Smoke colors: dark gray -> light gray, fading alpha
|
||||||
|
uint8_t gray = static_cast<uint8_t>(80 + 80 * (1.0f - life)); // Gets lighter
|
||||||
|
uint8_t alpha = static_cast<uint8_t>(200 * life); // Fades out
|
||||||
|
uint32_t smokeColor = (gray << 24) | (gray << 16) | (gray << 8) | alpha;
|
||||||
|
smoke->setInt("color", static_cast<int>(smokeColor));
|
||||||
|
smoke->setInt("textureId", 0);
|
||||||
|
smoke->setInt("blendMode", 0); // Alpha blend (not additive)
|
||||||
|
|
||||||
|
std::unique_ptr<grove::IDataNode> smokeData = std::move(smoke);
|
||||||
|
gameIO->publish("render:particle", std::move(smokeData));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Sparkle particles (third particle system - small, fast, bright)
|
||||||
|
// ========================================
|
||||||
|
float sparkleX = width * 0.5f;
|
||||||
|
float sparkleY = height * 0.3f;
|
||||||
|
|
||||||
|
for (int i = 0; i < 8; ++i) {
|
||||||
|
auto sparkle = std::make_unique<grove::JsonDataNode>("particle");
|
||||||
|
|
||||||
|
float angle = (time * 3.0f + i * (3.14159f * 2.0f / 8.0f));
|
||||||
|
float radius = 50.0f + std::sin(time * 5.0f + i) * 20.0f;
|
||||||
|
float sparkleAge = std::fmod(time * 4.0f + i * 0.1f, 1.0f);
|
||||||
|
float life = 1.0f - sparkleAge;
|
||||||
|
|
||||||
|
float spx = sparkleX + std::cos(angle) * radius;
|
||||||
|
float spy = sparkleY + std::sin(angle) * radius * 0.5f;
|
||||||
|
|
||||||
|
sparkle->setDouble("x", spx);
|
||||||
|
sparkle->setDouble("y", spy);
|
||||||
|
sparkle->setDouble("vx", 0.0);
|
||||||
|
sparkle->setDouble("vy", 0.0);
|
||||||
|
sparkle->setDouble("size", 5.0 + life * 10.0);
|
||||||
|
sparkle->setDouble("life", life);
|
||||||
|
|
||||||
|
// Sparkle colors: cyan -> white -> magenta cycle
|
||||||
|
float hue = std::fmod(time * 0.5f + i * 0.1f, 1.0f);
|
||||||
|
uint8_t r, g, b;
|
||||||
|
if (hue < 0.33f) {
|
||||||
|
r = static_cast<uint8_t>(255 * (1.0f - hue * 3.0f));
|
||||||
|
g = 255;
|
||||||
|
b = static_cast<uint8_t>(255 * hue * 3.0f);
|
||||||
|
} else if (hue < 0.66f) {
|
||||||
|
r = static_cast<uint8_t>(255 * (hue - 0.33f) * 3.0f);
|
||||||
|
g = static_cast<uint8_t>(255 * (1.0f - (hue - 0.33f) * 3.0f));
|
||||||
|
b = 255;
|
||||||
|
} else {
|
||||||
|
r = 255;
|
||||||
|
g = static_cast<uint8_t>(255 * (hue - 0.66f) * 3.0f);
|
||||||
|
b = static_cast<uint8_t>(255 * (1.0f - (hue - 0.66f) * 3.0f));
|
||||||
|
}
|
||||||
|
uint32_t sparkleColor = (r << 24) | (g << 16) | (b << 8) | 0xFF;
|
||||||
|
sparkle->setInt("color", static_cast<int>(sparkleColor));
|
||||||
|
sparkle->setInt("textureId", 0);
|
||||||
|
|
||||||
|
std::unique_ptr<grove::IDataNode> sparkleData = std::move(sparkle);
|
||||||
|
gameIO->publish("render:particle", std::move(sparkleData));
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Test text rendering
|
// Test text rendering
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user