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 = 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
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("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);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user