GroveEngine/tests/visual/test_renderer_showcase.cpp
StillHammer 5cef0e25b0 fix: UIModule button interaction + JsonDataNode array children support
- Fix JsonDataNode::getChildReadOnly() to handle JSON array access by numeric index
- Fix test_ui_showcase to use JSON array for children (matching test_single_button pattern)
- Add visual test files: test_single_button, test_ui_showcase, test_sprite_debug
- Clean up debug logging from SpritePass, SceneCollector, UIButton, BgfxDevice

The root cause was that UITree couldn't access array children in JSON layouts.
UIButton hover/click now works correctly in both test files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-05 18:23:16 +07:00

661 lines
22 KiB
C++

/**
* BgfxRenderer Complete Showcase
*
* Demonstrates ALL rendering features of BgfxRendererModule:
* - Sprites (static, animated, colored, layered, rotated, scaled)
* - Text (different sizes, colors, multi-line)
* - Particles (with life, velocity, size, additive blending)
* - Tilemap (simple grid with tile indices)
* - Debug primitives (lines, rectangles wireframe and filled)
* - Camera (orthographic projection, panning, zooming)
* - Clear color
*
* Controls:
* - Arrow keys: Move camera
* - +/-: Zoom in/out
* - SPACE: Spawn explosion particles
* - C: Cycle clear color
* - ESC: Exit
*/
#include <SDL.h>
#include <SDL_syswm.h>
#include <iostream>
#include <cmath>
#include <memory>
#include <vector>
#include <random>
#include "BgfxRendererModule.h"
#include <grove/JsonDataNode.h>
#include <grove/IntraIOManager.h>
#include <grove/IntraIO.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
using namespace grove;
// Particle structure for CPU-side simulation
struct Particle {
float x, y;
float vx, vy;
float size;
float life;
float maxLife;
uint32_t color;
bool alive;
};
// Simple tilemap data (10x8 grid)
static const int TILEMAP_WIDTH = 10;
static const int TILEMAP_HEIGHT = 8;
static const int TILE_SIZE = 32;
static uint16_t tilemapData[TILEMAP_WIDTH * TILEMAP_HEIGHT] = {
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 2, 2, 0, 0, 3, 3, 0, 1,
1, 0, 2, 2, 0, 0, 3, 3, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 4, 4, 4, 4, 4, 4, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
};
// Clear colors to cycle through
static const uint32_t clearColors[] = {
0x1a1a2eFF, // Dark blue-gray
0x16213eFF, // Navy blue
0x0f3460FF, // Deep blue
0x1e5128FF, // Forest green
0x2c3333FF, // Dark teal
0x3d0000FF, // Dark red
};
static const int numClearColors = sizeof(clearColors) / sizeof(clearColors[0]);
class RendererShowcase {
public:
RendererShowcase()
: m_rng(std::random_device{}())
{
m_particles.reserve(500);
}
bool init(SDL_Window* window) {
// Get native window handle
SDL_SysWMinfo wmi;
SDL_VERSION(&wmi.version);
SDL_GetWindowWMInfo(window, &wmi);
m_logger = spdlog::stdout_color_mt("Showcase");
spdlog::set_level(spdlog::level::info);
m_logger->info("=== BgfxRenderer Complete Showcase ===");
// Create IIO instances - IMPORTANT: separate for publisher and subscriber
// Keep shared_ptr alive, use IIO* abstract interface
m_rendererIOPtr = IntraIOManager::getInstance().createInstance("renderer");
m_gameIOPtr = IntraIOManager::getInstance().createInstance("game");
m_rendererIO = m_rendererIOPtr.get();
m_gameIO = m_gameIOPtr.get();
// Create and configure renderer
m_renderer = std::make_unique<BgfxRendererModule>();
JsonDataNode config("config");
config.setDouble("nativeWindowHandle", static_cast<double>(reinterpret_cast<uintptr_t>(wmi.info.win.window)));
config.setInt("windowWidth", 1024);
config.setInt("windowHeight", 768);
config.setString("backend", "d3d11");
config.setBool("vsync", true);
// Load textures - try multiple paths for flexibility
// Works from build/ or build/tests/
config.setString("texture1", "../assets/textures/1f440.png"); // Eye emoji
config.setString("texture2", "../assets/textures/5oxaxt1vo2f91.jpg"); // Image
m_renderer->setConfiguration(config, m_rendererIO, nullptr);
m_logger->info("Renderer initialized");
m_logger->info("Controls:");
m_logger->info(" Arrows: Move camera");
m_logger->info(" +/-: Zoom");
m_logger->info(" SPACE: Spawn particles");
m_logger->info(" C: Cycle clear color");
m_logger->info(" ESC: Exit");
return true;
}
void handleInput(SDL_Event& e) {
if (e.type == SDL_KEYDOWN) {
switch (e.key.keysym.sym) {
case SDLK_LEFT: m_cameraVX = -200.0f; break;
case SDLK_RIGHT: m_cameraVX = 200.0f; break;
case SDLK_UP: m_cameraVY = -200.0f; break;
case SDLK_DOWN: m_cameraVY = 200.0f; break;
case SDLK_PLUS:
case SDLK_EQUALS:
m_cameraZoom = std::min(4.0f, m_cameraZoom * 1.1f);
break;
case SDLK_MINUS:
m_cameraZoom = std::max(0.25f, m_cameraZoom / 1.1f);
break;
case SDLK_SPACE:
spawnExplosion(512.0f + m_cameraX, 400.0f + m_cameraY);
break;
case SDLK_c:
m_clearColorIndex = (m_clearColorIndex + 1) % numClearColors;
break;
}
}
else if (e.type == SDL_KEYUP) {
switch (e.key.keysym.sym) {
case SDLK_LEFT:
case SDLK_RIGHT:
m_cameraVX = 0.0f;
break;
case SDLK_UP:
case SDLK_DOWN:
m_cameraVY = 0.0f;
break;
}
}
}
void update(float dt) {
m_time += dt;
m_frameCount++;
// Update camera position
m_cameraX += m_cameraVX * dt;
m_cameraY += m_cameraVY * dt;
// Update particles
updateParticles(dt);
}
void render() {
// 1. Set clear color
sendClearColor();
// 2. Set camera
sendCamera();
// 3. Render tilemap (background layer)
sendTilemap();
// 4. Render sprites (multiple layers)
sendSprites();
// 5. Render particles
sendParticles();
// 6. Render text
sendText();
// 7. Render debug primitives
sendDebugPrimitives();
// Process frame
JsonDataNode input("input");
input.setDouble("deltaTime", 0.016);
input.setInt("frameCount", m_frameCount);
m_renderer->process(input);
}
void shutdown() {
m_renderer->shutdown();
IntraIOManager::getInstance().removeInstance("renderer");
IntraIOManager::getInstance().removeInstance("game");
m_logger->info("Showcase shutdown complete");
}
int getFrameCount() const { return m_frameCount; }
private:
void sendClearColor() {
auto clear = std::make_unique<JsonDataNode>("clear");
clear->setInt("color", clearColors[m_clearColorIndex]);
m_gameIO->publish("render:clear", std::move(clear));
}
void sendCamera() {
auto cam = std::make_unique<JsonDataNode>("camera");
cam->setDouble("x", m_cameraX);
cam->setDouble("y", m_cameraY);
cam->setDouble("zoom", m_cameraZoom);
cam->setInt("viewportX", 0);
cam->setInt("viewportY", 0);
cam->setInt("viewportW", 1024);
cam->setInt("viewportH", 768);
m_gameIO->publish("render:camera", std::move(cam));
}
void sendTilemap() {
auto tilemap = std::make_unique<JsonDataNode>("tilemap");
tilemap->setDouble("x", 50.0);
tilemap->setDouble("y", 400.0);
tilemap->setInt("width", TILEMAP_WIDTH);
tilemap->setInt("height", TILEMAP_HEIGHT);
tilemap->setInt("tileW", TILE_SIZE);
tilemap->setInt("tileH", TILE_SIZE);
tilemap->setInt("textureId", 0);
// Convert tilemap to comma-separated string
std::string tileData;
for (int i = 0; i < TILEMAP_WIDTH * TILEMAP_HEIGHT; ++i) {
if (i > 0) tileData += ",";
tileData += std::to_string(tilemapData[i]);
}
tilemap->setString("tileData", tileData);
m_gameIO->publish("render:tilemap", std::move(tilemap));
}
void sendSprites() {
// Layer 0: Background sprites with TEXTURE 2 (image)
for (int i = 0; i < 5; ++i) {
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setDouble("x", 100 + i * 150);
sprite->setDouble("y", 50);
sprite->setDouble("scaleX", 120.0);
sprite->setDouble("scaleY", 90.0);
sprite->setDouble("rotation", 0.0);
sprite->setInt("color", 0xFFFFFFFF); // White (no tint)
sprite->setInt("layer", 0);
sprite->setInt("textureId", 2); // Image texture
m_gameIO->publish("render:sprite", std::move(sprite));
}
// Layer 5: Bouncing EYE EMOJIS with texture 1
for (int i = 0; i < 5; ++i) {
float offset = std::sin(m_time * 2.0f + i * 1.2f) * 40.0f;
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setDouble("x", 100 + i * 180);
sprite->setDouble("y", 200 + offset);
sprite->setDouble("scaleX", 64.0);
sprite->setDouble("scaleY", 64.0);
sprite->setDouble("rotation", 0.0);
sprite->setInt("color", 0xFFFFFFFF); // No tint
sprite->setInt("layer", 5);
sprite->setInt("textureId", 1); // Eye emoji
m_gameIO->publish("render:sprite", std::move(sprite));
}
// Layer 10: Rotating eye emoji
{
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setDouble("x", 700);
sprite->setDouble("y", 200);
sprite->setDouble("scaleX", 100.0);
sprite->setDouble("scaleY", 100.0);
sprite->setDouble("rotation", m_time); // Radians
sprite->setInt("color", 0xFFFFFFFF);
sprite->setInt("layer", 10);
sprite->setInt("textureId", 1); // Eye emoji
m_gameIO->publish("render:sprite", std::move(sprite));
}
// Layer 15: Scaling/pulsing image
{
float scale = 80.0f + std::sin(m_time * 3.0f) * 30.0f;
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setDouble("x", 850);
sprite->setDouble("y", 200);
sprite->setDouble("scaleX", scale);
sprite->setDouble("scaleY", scale * 0.75f);
sprite->setDouble("rotation", 0.0);
sprite->setInt("color", 0xFFFFFFFF);
sprite->setInt("layer", 15);
sprite->setInt("textureId", 2); // Image
m_gameIO->publish("render:sprite", std::move(sprite));
}
// Layer 20: Tinted sprites (color overlay on texture)
{
uint32_t colors[] = { 0xFF8888FF, 0x88FF88FF, 0x8888FFFF, 0xFFFF88FF };
for (int i = 0; i < 4; ++i) {
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setDouble("x", 100 + i * 100);
sprite->setDouble("y", 320);
sprite->setDouble("scaleX", 80.0);
sprite->setDouble("scaleY", 80.0);
sprite->setDouble("rotation", 0.0);
sprite->setInt("color", colors[i]); // Tinted
sprite->setInt("layer", 20);
sprite->setInt("textureId", 1); // Eye emoji with color tint
m_gameIO->publish("render:sprite", std::move(sprite));
}
}
// Layer 25: Grid of small images
for (int row = 0; row < 2; ++row) {
for (int col = 0; col < 4; ++col) {
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setDouble("x", 550 + col * 70);
sprite->setDouble("y", 320 + row * 55);
sprite->setDouble("scaleX", 60.0);
sprite->setDouble("scaleY", 45.0);
sprite->setDouble("rotation", 0.0);
sprite->setInt("color", 0xFFFFFFFF);
sprite->setInt("layer", 25);
sprite->setInt("textureId", 2); // Image
m_gameIO->publish("render:sprite", std::move(sprite));
}
}
}
void sendParticles() {
for (const auto& p : m_particles) {
if (!p.alive) continue;
auto particle = std::make_unique<JsonDataNode>("particle");
particle->setDouble("x", p.x);
particle->setDouble("y", p.y);
particle->setDouble("vx", p.vx);
particle->setDouble("vy", p.vy);
particle->setDouble("size", p.size);
particle->setDouble("life", p.life / p.maxLife); // Normalized 0-1
particle->setInt("color", p.color);
particle->setInt("textureId", 0);
m_gameIO->publish("render:particle", std::move(particle));
}
}
void sendText() {
// Title (large)
{
auto text = std::make_unique<JsonDataNode>("text");
text->setDouble("x", 10);
text->setDouble("y", 10);
text->setString("text", "BgfxRenderer Showcase");
text->setInt("fontSize", 32);
text->setInt("color", 0xFFFFFFFF);
text->setInt("layer", 100);
m_gameIO->publish("render:text", std::move(text));
}
// Info text
{
auto text = std::make_unique<JsonDataNode>("text");
text->setDouble("x", 10);
text->setDouble("y", 50);
char buf[128];
snprintf(buf, sizeof(buf), "Frame: %d Camera: (%.0f, %.0f) Zoom: %.2fx",
m_frameCount, m_cameraX, m_cameraY, m_cameraZoom);
text->setString("text", buf);
text->setInt("fontSize", 16);
text->setInt("color", 0xAAAAAAFF);
text->setInt("layer", 100);
m_gameIO->publish("render:text", std::move(text));
}
// Particles count
{
auto text = std::make_unique<JsonDataNode>("text");
text->setDouble("x", 10);
text->setDouble("y", 70);
char buf[64];
int aliveCount = 0;
for (const auto& p : m_particles) if (p.alive) aliveCount++;
snprintf(buf, sizeof(buf), "Particles: %d", aliveCount);
text->setString("text", buf);
text->setInt("fontSize", 16);
text->setInt("color", 0xFFCC00FF);
text->setInt("layer", 100);
m_gameIO->publish("render:text", std::move(text));
}
// Controls help
{
auto text = std::make_unique<JsonDataNode>("text");
text->setDouble("x", 750);
text->setDouble("y", 730);
text->setString("text", "SPACE: Particles | C: Color | Arrows: Pan | +/-: Zoom");
text->setInt("fontSize", 14);
text->setInt("color", 0x888888FF);
text->setInt("layer", 100);
m_gameIO->publish("render:text", std::move(text));
}
// Multi-line text demo
{
auto text = std::make_unique<JsonDataNode>("text");
text->setDouble("x", 700);
text->setDouble("y", 300);
text->setString("text", "Multi-line\nText Demo\nWith Colors!");
text->setInt("fontSize", 20);
text->setInt("color", 0x00FF88FF);
text->setInt("layer", 100);
m_gameIO->publish("render:text", std::move(text));
}
}
void sendDebugPrimitives() {
// Grid lines
for (int i = 0; i <= 10; ++i) {
// Vertical lines
auto vline = std::make_unique<JsonDataNode>("line");
vline->setDouble("x1", 50 + i * 32);
vline->setDouble("y1", 400);
vline->setDouble("x2", 50 + i * 32);
vline->setDouble("y2", 400 + TILEMAP_HEIGHT * 32);
vline->setInt("color", 0x444444FF);
m_gameIO->publish("render:debug:line", std::move(vline));
}
for (int i = 0; i <= 8; ++i) {
// Horizontal lines
auto hline = std::make_unique<JsonDataNode>("line");
hline->setDouble("x1", 50);
hline->setDouble("y1", 400 + i * 32);
hline->setDouble("x2", 50 + TILEMAP_WIDTH * 32);
hline->setDouble("y2", 400 + i * 32);
hline->setInt("color", 0x444444FF);
m_gameIO->publish("render:debug:line", std::move(hline));
}
// Diagonal line (animated)
{
auto line = std::make_unique<JsonDataNode>("line");
float offset = std::sin(m_time) * 50.0f;
line->setDouble("x1", 500);
line->setDouble("y1", 450 + offset);
line->setDouble("x2", 700);
line->setDouble("y2", 550 - offset);
line->setInt("color", 0xFFFF00FF); // Yellow
m_gameIO->publish("render:debug:line", std::move(line));
}
// Wireframe rectangle
{
auto rect = std::make_unique<JsonDataNode>("rect");
rect->setDouble("x", 750);
rect->setDouble("y", 400);
rect->setDouble("w", 100);
rect->setDouble("h", 80);
rect->setInt("color", 0x00FFFFFF); // Cyan
rect->setBool("filled", false);
m_gameIO->publish("render:debug:rect", std::move(rect));
}
// Filled rectangle (pulsing)
{
float pulse = (std::sin(m_time * 2.0f) + 1.0f) * 0.5f;
uint8_t alpha = static_cast<uint8_t>(128 + pulse * 127);
uint32_t color = 0xFF4444FF | (alpha);
auto rect = std::make_unique<JsonDataNode>("rect");
rect->setDouble("x", 870);
rect->setDouble("y", 400);
rect->setDouble("w", 80);
rect->setDouble("h", 80);
rect->setInt("color", color);
rect->setBool("filled", true);
m_gameIO->publish("render:debug:rect", std::move(rect));
}
// Crosshair at center
{
float cx = 512.0f, cy = 384.0f;
auto hline = std::make_unique<JsonDataNode>("line");
hline->setDouble("x1", cx - 20);
hline->setDouble("y1", cy);
hline->setDouble("x2", cx + 20);
hline->setDouble("y2", cy);
hline->setInt("color", 0xFF0000FF);
m_gameIO->publish("render:debug:line", std::move(hline));
auto vline = std::make_unique<JsonDataNode>("line");
vline->setDouble("x1", cx);
vline->setDouble("y1", cy - 20);
vline->setDouble("x2", cx);
vline->setDouble("y2", cy + 20);
vline->setInt("color", 0xFF0000FF);
m_gameIO->publish("render:debug:line", std::move(vline));
}
}
void spawnExplosion(float x, float y) {
std::uniform_real_distribution<float> angleDist(0.0f, 2.0f * 3.14159f);
std::uniform_real_distribution<float> speedDist(50.0f, 200.0f);
std::uniform_real_distribution<float> sizeDist(5.0f, 20.0f);
std::uniform_real_distribution<float> lifeDist(0.5f, 2.0f);
uint32_t colors[] = {
0xFF4444FF, 0xFF8844FF, 0xFFCC44FF, 0xFFFF44FF,
0xFF6644FF, 0xFFAA00FF
};
std::uniform_int_distribution<int> colorDist(0, 5);
for (int i = 0; i < 50; ++i) {
Particle p;
float angle = angleDist(m_rng);
float speed = speedDist(m_rng);
p.x = x;
p.y = y;
p.vx = std::cos(angle) * speed;
p.vy = std::sin(angle) * speed;
p.size = sizeDist(m_rng);
p.life = lifeDist(m_rng);
p.maxLife = p.life;
p.color = colors[colorDist(m_rng)];
p.alive = true;
// Find dead particle slot or add new
bool found = false;
for (auto& existing : m_particles) {
if (!existing.alive) {
existing = p;
found = true;
break;
}
}
if (!found) {
m_particles.push_back(p);
}
}
m_logger->info("Spawned explosion at ({:.0f}, {:.0f})", x, y);
}
void updateParticles(float dt) {
for (auto& p : m_particles) {
if (!p.alive) continue;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 100.0f * dt; // Gravity
p.life -= dt;
if (p.life <= 0.0f) {
p.alive = false;
}
}
}
std::shared_ptr<spdlog::logger> m_logger;
std::unique_ptr<BgfxRendererModule> m_renderer;
std::shared_ptr<IntraIO> m_rendererIOPtr; // Keep shared_ptr alive
std::shared_ptr<IntraIO> m_gameIOPtr; // Keep shared_ptr alive
IIO* m_rendererIO = nullptr; // Abstract interface
IIO* m_gameIO = nullptr; // Abstract interface
float m_time = 0.0f;
int m_frameCount = 0;
// Camera
float m_cameraX = 0.0f;
float m_cameraY = 0.0f;
float m_cameraVX = 0.0f;
float m_cameraVY = 0.0f;
float m_cameraZoom = 1.0f;
// Clear color
int m_clearColorIndex = 0;
// Particles
std::vector<Particle> m_particles;
std::mt19937 m_rng;
};
int main(int argc, char* argv[]) {
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl;
return 1;
}
SDL_Window* window = SDL_CreateWindow(
"BgfxRenderer Showcase",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
1024, 768,
SDL_WINDOW_SHOWN
);
if (!window) {
std::cerr << "SDL_CreateWindow failed: " << SDL_GetError() << std::endl;
SDL_Quit();
return 1;
}
RendererShowcase showcase;
if (!showcase.init(window)) {
SDL_DestroyWindow(window);
SDL_Quit();
return 1;
}
bool running = true;
Uint64 lastTime = SDL_GetPerformanceCounter();
while (running) {
SDL_Event e;
while (SDL_PollEvent(&e)) {
if (e.type == SDL_QUIT ||
(e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE)) {
running = false;
}
showcase.handleInput(e);
}
Uint64 now = SDL_GetPerformanceCounter();
float dt = static_cast<float>(now - lastTime) / SDL_GetPerformanceFrequency();
lastTime = now;
showcase.update(dt);
showcase.render();
// Cap to ~60 FPS
SDL_Delay(1);
}
Uint64 endTime = SDL_GetPerformanceCounter();
int frames = showcase.getFrameCount();
showcase.shutdown();
SDL_DestroyWindow(window);
SDL_Quit();
std::cout << "Rendered " << frames << " frames" << std::endl;
return 0;
}