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:
parent
262eef377e
commit
2f8e78b247
BIN
assets/textures/1f440.png
Normal file
BIN
assets/textures/1f440.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
@ -82,8 +82,13 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas
|
||||
m_renderGraph = std::make_unique<RenderGraph>();
|
||||
m_renderGraph->addPass(std::make_unique<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_renderGraph->addPass(std::make_unique<DebugPass>(debugShader));
|
||||
m_logger->info("Added DebugPass");
|
||||
m_renderGraph->setup(*m_device);
|
||||
@ -99,6 +104,18 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas
|
||||
// Setup resource cache
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@ class RenderGraph;
|
||||
class SceneCollector;
|
||||
class ResourceCache;
|
||||
class ShaderManager;
|
||||
class SpritePass;
|
||||
|
||||
// ============================================================================
|
||||
// BgfxRenderer Module - 2D rendering via bgfx
|
||||
@ -55,6 +56,9 @@ private:
|
||||
std::unique_ptr<SceneCollector> m_sceneCollector;
|
||||
std::unique_ptr<ResourceCache> m_resourceCache;
|
||||
|
||||
// Pass references (non-owning, owned by RenderGraph)
|
||||
SpritePass* m_spritePass = nullptr;
|
||||
|
||||
// IIO (non-owning)
|
||||
IIO* m_io = nullptr;
|
||||
|
||||
|
||||
@ -57,11 +57,13 @@ add_library(BgfxRenderer SHARED
|
||||
|
||||
# Resources
|
||||
Resources/ResourceCache.cpp
|
||||
Resources/TextureLoader.cpp
|
||||
)
|
||||
|
||||
target_include_directories(BgfxRenderer PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../include
|
||||
${bgfx_SOURCE_DIR}/bimg/3rdparty # stb_image
|
||||
)
|
||||
|
||||
target_link_libraries(BgfxRenderer PRIVATE
|
||||
|
||||
@ -104,8 +104,9 @@ void SpritePass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi:
|
||||
cmd.setIndexBuffer(m_quadIB);
|
||||
cmd.setInstanceBuffer(m_instanceBuffer, 0, static_cast<uint32_t>(batchSize));
|
||||
|
||||
// Bind default texture (TODO: support per-sprite textures via texture array)
|
||||
cmd.setTexture(0, m_defaultTexture, m_textureSampler);
|
||||
// Bind texture (use active texture if set, otherwise default white)
|
||||
rhi::TextureHandle texToUse = m_activeTexture.isValid() ? m_activeTexture : m_defaultTexture;
|
||||
cmd.setTexture(0, texToUse, m_textureSampler);
|
||||
|
||||
// Submit draw call
|
||||
cmd.drawInstanced(6, static_cast<uint32_t>(batchSize)); // 6 indices per quad
|
||||
|
||||
@ -25,6 +25,14 @@ public:
|
||||
void shutdown(rhi::IRHIDevice& device) 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:
|
||||
rhi::ShaderHandle m_shader;
|
||||
rhi::BufferHandle m_quadVB;
|
||||
@ -32,6 +40,7 @@ private:
|
||||
rhi::BufferHandle m_instanceBuffer;
|
||||
rhi::UniformHandle m_textureSampler;
|
||||
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;
|
||||
};
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#include "ResourceCache.h"
|
||||
#include "TextureLoader.h"
|
||||
#include "../RHI/RHIDevice.h"
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
@ -33,29 +34,21 @@ rhi::TextureHandle ResourceCache::loadTexture(rhi::IRHIDevice& device, const std
|
||||
}
|
||||
}
|
||||
|
||||
// Load texture data from file
|
||||
// TODO: Use stb_image or similar to load actual texture files
|
||||
// For now, create a placeholder 1x1 white texture
|
||||
// Load texture from file using TextureLoader (stb_image)
|
||||
auto result = TextureLoader::loadFromFile(device, path);
|
||||
|
||||
uint32_t whitePixel = 0xFFFFFFFF;
|
||||
|
||||
rhi::TextureDesc desc;
|
||||
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);
|
||||
if (!result.success) {
|
||||
// Return invalid handle on failure
|
||||
return rhi::TextureHandle{};
|
||||
}
|
||||
|
||||
// Store in cache
|
||||
{
|
||||
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,
|
||||
|
||||
70
modules/BgfxRenderer/Resources/TextureLoader.cpp
Normal file
70
modules/BgfxRenderer/Resources/TextureLoader.cpp
Normal 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
|
||||
45
modules/BgfxRenderer/Resources/TextureLoader.h
Normal file
45
modules/BgfxRenderer/Resources/TextureLoader.h
Normal 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
|
||||
@ -120,6 +120,9 @@ int main(int argc, char* argv[]) {
|
||||
config.setDouble("nativeWindowHandle", static_cast<double>(reinterpret_cast<uintptr_t>(nativeWindowHandle)));
|
||||
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);
|
||||
|
||||
std::cout << "Module configured\n";
|
||||
@ -208,15 +211,8 @@ int main(int argc, char* argv[]) {
|
||||
sprite->setDouble("u1", 1.0);
|
||||
sprite->setDouble("v1", 1.0);
|
||||
|
||||
// Different colors for each sprite (ABGR format for bgfx)
|
||||
uint32_t colors[] = {
|
||||
0xFF0000FF, // Red
|
||||
0xFF00FF00, // Green
|
||||
0xFFFF0000, // Blue
|
||||
0xFF00FFFF, // Yellow
|
||||
0xFFFF00FF // Magenta
|
||||
};
|
||||
sprite->setInt("color", static_cast<int>(colors[i]));
|
||||
// All sprites white to show texture without tint
|
||||
sprite->setInt("color", static_cast<int>(0xFFFFFFFF));
|
||||
sprite->setInt("textureId", 0);
|
||||
sprite->setInt("layer", i);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user