feat(BgfxRenderer): Phase 6 - Texture loading with stb_image

- Add TextureLoader class using stb_image for PNG/JPG/etc loading
- Integrate TextureLoader with ResourceCache for cached texture loading
- Add SpritePass::setTexture() for binding textures to sprites
- Add "defaultTexture" config option to load texture at startup
- Create assets/textures folder structure
- Add 1f440.png (eyes emoji) test texture

Visual test confirms textured sprites render correctly.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
StillHammer 2025-11-27 19:44:13 +08:00
parent 262eef377e
commit 2f8e78b247
10 changed files with 165 additions and 28 deletions

BIN
assets/textures/1f440.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -82,8 +82,13 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas
m_renderGraph = std::make_unique<RenderGraph>(); m_renderGraph = std::make_unique<RenderGraph>();
m_renderGraph->addPass(std::make_unique<ClearPass>()); m_renderGraph->addPass(std::make_unique<ClearPass>());
m_logger->info("Added ClearPass"); m_logger->info("Added ClearPass");
m_renderGraph->addPass(std::make_unique<SpritePass>(spriteShader));
// Create SpritePass and keep reference for texture binding
auto spritePass = std::make_unique<SpritePass>(spriteShader);
m_spritePass = spritePass.get(); // Non-owning reference
m_renderGraph->addPass(std::move(spritePass));
m_logger->info("Added SpritePass"); m_logger->info("Added SpritePass");
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);
@ -99,6 +104,18 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas
// Setup resource cache // Setup resource cache
m_resourceCache = std::make_unique<ResourceCache>(); m_resourceCache = std::make_unique<ResourceCache>();
// Load default texture if specified in config
std::string defaultTexturePath = config.getString("defaultTexture", "");
if (!defaultTexturePath.empty()) {
rhi::TextureHandle tex = m_resourceCache->loadTexture(*m_device, defaultTexturePath);
if (tex.isValid()) {
m_spritePass->setTexture(tex);
m_logger->info("Loaded default texture: {}", defaultTexturePath);
} else {
m_logger->warn("Failed to load default texture: {}", defaultTexturePath);
}
}
m_logger->info("BgfxRenderer initialized successfully"); m_logger->info("BgfxRenderer initialized successfully");
} }

View File

@ -16,6 +16,7 @@ class RenderGraph;
class SceneCollector; class SceneCollector;
class ResourceCache; class ResourceCache;
class ShaderManager; class ShaderManager;
class SpritePass;
// ============================================================================ // ============================================================================
// BgfxRenderer Module - 2D rendering via bgfx // BgfxRenderer Module - 2D rendering via bgfx
@ -55,6 +56,9 @@ private:
std::unique_ptr<SceneCollector> m_sceneCollector; std::unique_ptr<SceneCollector> m_sceneCollector;
std::unique_ptr<ResourceCache> m_resourceCache; std::unique_ptr<ResourceCache> m_resourceCache;
// Pass references (non-owning, owned by RenderGraph)
SpritePass* m_spritePass = nullptr;
// IIO (non-owning) // IIO (non-owning)
IIO* m_io = nullptr; IIO* m_io = nullptr;

View File

@ -57,11 +57,13 @@ add_library(BgfxRenderer SHARED
# Resources # Resources
Resources/ResourceCache.cpp Resources/ResourceCache.cpp
Resources/TextureLoader.cpp
) )
target_include_directories(BgfxRenderer PRIVATE target_include_directories(BgfxRenderer PRIVATE
${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/../../include ${CMAKE_CURRENT_SOURCE_DIR}/../../include
${bgfx_SOURCE_DIR}/bimg/3rdparty # stb_image
) )
target_link_libraries(BgfxRenderer PRIVATE target_link_libraries(BgfxRenderer PRIVATE

View File

@ -104,8 +104,9 @@ void SpritePass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi:
cmd.setIndexBuffer(m_quadIB); cmd.setIndexBuffer(m_quadIB);
cmd.setInstanceBuffer(m_instanceBuffer, 0, static_cast<uint32_t>(batchSize)); cmd.setInstanceBuffer(m_instanceBuffer, 0, static_cast<uint32_t>(batchSize));
// Bind default texture (TODO: support per-sprite textures via texture array) // Bind texture (use active texture if set, otherwise default white)
cmd.setTexture(0, m_defaultTexture, m_textureSampler); rhi::TextureHandle texToUse = m_activeTexture.isValid() ? m_activeTexture : m_defaultTexture;
cmd.setTexture(0, texToUse, m_textureSampler);
// Submit draw call // Submit draw call
cmd.drawInstanced(6, static_cast<uint32_t>(batchSize)); // 6 indices per quad cmd.drawInstanced(6, static_cast<uint32_t>(batchSize)); // 6 indices per quad

View File

@ -25,6 +25,14 @@ public:
void shutdown(rhi::IRHIDevice& device) override; void shutdown(rhi::IRHIDevice& device) override;
void execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) override; void execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) override;
/**
* @brief Set a texture to use for all sprites (temporary API)
* @param texture The texture handle to use (must be valid)
*
* TODO: Replace with proper texture array / per-sprite texture support
*/
void setTexture(rhi::TextureHandle texture) { m_activeTexture = texture; }
private: private:
rhi::ShaderHandle m_shader; rhi::ShaderHandle m_shader;
rhi::BufferHandle m_quadVB; rhi::BufferHandle m_quadVB;
@ -32,6 +40,7 @@ private:
rhi::BufferHandle m_instanceBuffer; rhi::BufferHandle m_instanceBuffer;
rhi::UniformHandle m_textureSampler; rhi::UniformHandle m_textureSampler;
rhi::TextureHandle m_defaultTexture; // White 1x1 texture fallback rhi::TextureHandle m_defaultTexture; // White 1x1 texture fallback
rhi::TextureHandle m_activeTexture; // Currently active texture (if set)
static constexpr uint32_t MAX_SPRITES_PER_BATCH = 10000; static constexpr uint32_t MAX_SPRITES_PER_BATCH = 10000;
}; };

View File

@ -1,4 +1,5 @@
#include "ResourceCache.h" #include "ResourceCache.h"
#include "TextureLoader.h"
#include "../RHI/RHIDevice.h" #include "../RHI/RHIDevice.h"
#include <mutex> #include <mutex>
#include <shared_mutex> #include <shared_mutex>
@ -33,29 +34,21 @@ rhi::TextureHandle ResourceCache::loadTexture(rhi::IRHIDevice& device, const std
} }
} }
// Load texture data from file // Load texture from file using TextureLoader (stb_image)
// TODO: Use stb_image or similar to load actual texture files auto result = TextureLoader::loadFromFile(device, path);
// For now, create a placeholder 1x1 white texture
uint32_t whitePixel = 0xFFFFFFFF; if (!result.success) {
// Return invalid handle on failure
rhi::TextureDesc desc; return rhi::TextureHandle{};
desc.width = 1; }
desc.height = 1;
desc.mipLevels = 1;
desc.format = rhi::TextureDesc::RGBA8;
desc.data = &whitePixel;
desc.dataSize = sizeof(whitePixel);
rhi::TextureHandle handle = device.createTexture(desc);
// Store in cache // Store in cache
{ {
std::unique_lock lock(m_mutex); std::unique_lock lock(m_mutex);
m_textures[path] = handle; m_textures[path] = result.handle;
} }
return handle; return result.handle;
} }
rhi::ShaderHandle ResourceCache::loadShader(rhi::IRHIDevice& device, const std::string& name, rhi::ShaderHandle ResourceCache::loadShader(rhi::IRHIDevice& device, const std::string& name,

View File

@ -0,0 +1,70 @@
#include "TextureLoader.h"
#include "../RHI/RHIDevice.h"
#define STB_IMAGE_IMPLEMENTATION
#include <stb/stb_image.h>
#include <fstream>
namespace grove {
TextureLoader::LoadResult TextureLoader::loadFromFile(rhi::IRHIDevice& device, const std::string& path) {
LoadResult result;
// Read file into memory
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
result.error = "Failed to open file: " + path;
return result;
}
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> buffer(static_cast<size_t>(size));
if (!file.read(reinterpret_cast<char*>(buffer.data()), size)) {
result.error = "Failed to read file: " + path;
return result;
}
return loadFromMemory(device, buffer.data(), buffer.size());
}
TextureLoader::LoadResult TextureLoader::loadFromMemory(rhi::IRHIDevice& device, const uint8_t* data, size_t size) {
LoadResult result;
// Decode image with stb_image
int width, height, channels;
// Force RGBA output (4 channels)
stbi_uc* pixels = stbi_load_from_memory(data, static_cast<int>(size), &width, &height, &channels, 4);
if (!pixels) {
result.error = "stb_image failed: ";
result.error += stbi_failure_reason();
return result;
}
// Create texture via RHI
rhi::TextureDesc desc;
desc.width = static_cast<uint16_t>(width);
desc.height = static_cast<uint16_t>(height);
desc.format = rhi::TextureDesc::RGBA8;
desc.data = pixels;
desc.dataSize = static_cast<uint32_t>(width * height * 4);
result.handle = device.createTexture(desc);
result.width = desc.width;
result.height = desc.height;
result.success = result.handle.isValid();
if (!result.success) {
result.error = "Failed to create GPU texture";
}
// Free stb_image memory
stbi_image_free(pixels);
return result;
}
} // namespace grove

View File

@ -0,0 +1,45 @@
#pragma once
#include "../RHI/RHITypes.h"
#include <string>
#include <vector>
#include <cstdint>
namespace grove {
namespace rhi { class IRHIDevice; }
/**
* @brief Loads textures from files (PNG, JPG, etc.)
*
* Uses stb_image for decoding. All textures are loaded as RGBA8.
*/
class TextureLoader {
public:
struct LoadResult {
bool success = false;
rhi::TextureHandle handle;
uint16_t width = 0;
uint16_t height = 0;
std::string error;
};
/**
* @brief Load texture from file
* @param device RHI device for texture creation
* @param path Path to image file (PNG, JPG, BMP, TGA, etc.)
* @return LoadResult with handle and dimensions on success
*/
static LoadResult loadFromFile(rhi::IRHIDevice& device, const std::string& path);
/**
* @brief Load texture from memory
* @param device RHI device for texture creation
* @param data Raw file data (PNG/JPG bytes, not decoded pixels)
* @param size Size of data in bytes
* @return LoadResult with handle and dimensions on success
*/
static LoadResult loadFromMemory(rhi::IRHIDevice& device, const uint8_t* data, size_t size);
};
} // namespace grove

View File

@ -120,6 +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
config.setString("defaultTexture", "../../assets/textures/1f440.png");
module->setConfiguration(config, rendererIO.get(), nullptr); module->setConfiguration(config, rendererIO.get(), nullptr);
std::cout << "Module configured\n"; std::cout << "Module configured\n";
@ -208,15 +211,8 @@ int main(int argc, char* argv[]) {
sprite->setDouble("u1", 1.0); sprite->setDouble("u1", 1.0);
sprite->setDouble("v1", 1.0); sprite->setDouble("v1", 1.0);
// Different colors for each sprite (ABGR format for bgfx) // All sprites white to show texture without tint
uint32_t colors[] = { sprite->setInt("color", static_cast<int>(0xFFFFFFFF));
0xFF0000FF, // Red
0xFF00FF00, // Green
0xFFFF0000, // Blue
0xFF00FFFF, // Yellow
0xFFFF00FF // Magenta
};
sprite->setInt("color", static_cast<int>(colors[i]));
sprite->setInt("textureId", 0); sprite->setInt("textureId", 0);
sprite->setInt("layer", i); sprite->setInt("layer", i);