feat(resource): 实现资源管理系统及示例程序
新增资源管理系统,支持纹理、字体、着色器、音频等资源的加载和管理: - 添加 ResourceManager 核心模块 - 实现 Texture、Font、Shader、Audio 等资源类 - 添加 FontAtlas 动态字符图集管理 - 实现 Material 材质系统 - 添加 Text 文本渲染支持 - 新增资源加载示例程序 - 更新构建系统以支持资源加载示例 - 完善文档和 README
This commit is contained in:
parent
6717015e28
commit
bdf78f5eca
|
|
@ -0,0 +1,100 @@
|
||||||
|
# 资源加载示例
|
||||||
|
|
||||||
|
演示如何使用 Extra2D 引擎的资源管理器加载各种资源。
|
||||||
|
|
||||||
|
## 功能演示
|
||||||
|
|
||||||
|
1. **纹理加载** - 从文件加载 PNG/JPG 纹理
|
||||||
|
2. **字体加载** - 使用 FreeType 加载 TTF 字体,支持中英文
|
||||||
|
3. **文本创建** - 创建可渲染的文本对象
|
||||||
|
4. **着色器加载** - 从文件或源码加载 GLSL 着色器
|
||||||
|
5. **材质创建** - 组合纹理和着色器创建材质
|
||||||
|
6. **音频加载** - 加载音效和音乐
|
||||||
|
7. **资源统计** - 查看已加载资源的统计信息
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
resource_loading/
|
||||||
|
├── main.cpp # 示例主文件
|
||||||
|
├── xmake.lua # 构建脚本
|
||||||
|
├── README.md # 本文件
|
||||||
|
└── romfs/
|
||||||
|
└── assets/ # 资源目录
|
||||||
|
├── textures/ # 纹理文件
|
||||||
|
├── fonts/ # 字体文件
|
||||||
|
├── audio/ # 音频文件
|
||||||
|
└── shaders/ # 着色器文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
### MinGW (Windows)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xmake f -p mingw -a x86_64
|
||||||
|
xmake resource_loading
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nintendo Switch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xmake f -p switch -a aarch64
|
||||||
|
xmake resource_loading
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build/examples/resource_loading/resource_loading.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Switch
|
||||||
|
|
||||||
|
将生成的 `resource_loading.nro` 复制到 Switch SD 卡运行。
|
||||||
|
|
||||||
|
## 资源管理 API 示例
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 获取资源管理器
|
||||||
|
auto& resources = app->context().resources();
|
||||||
|
|
||||||
|
// 加载纹理
|
||||||
|
auto texture = resources.getTexture("assets/textures/image.png");
|
||||||
|
|
||||||
|
// 加载字体
|
||||||
|
FontConfig config;
|
||||||
|
config.fontSize = 24;
|
||||||
|
config.preloadCommon = true;
|
||||||
|
auto font = resources.getFont("assets/fonts/font.ttf", config);
|
||||||
|
|
||||||
|
// 创建文本
|
||||||
|
auto text = resources.createText(font, "Hello 世界!");
|
||||||
|
text->setFontSize(32);
|
||||||
|
text->rebuild();
|
||||||
|
|
||||||
|
// 加载着色器
|
||||||
|
auto shader = resources.getShader("vs.glsl", "fs.glsl");
|
||||||
|
|
||||||
|
// 创建材质
|
||||||
|
auto material = resources.createMaterial(shader);
|
||||||
|
material->setTexture("uTexture", texture);
|
||||||
|
material->setColor("uColor", Color::Red);
|
||||||
|
|
||||||
|
// 加载音频
|
||||||
|
auto sound = resources.getAudio("jump.wav", AudioType::Sound);
|
||||||
|
auto music = resources.getAudio("bgm.mp3", AudioType::Music);
|
||||||
|
|
||||||
|
// 获取资源统计
|
||||||
|
auto stats = resources.getStats();
|
||||||
|
printf("Textures: %zu\n", stats.textureCount);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 字体文件需要是 TTF 格式
|
||||||
|
- 纹理支持 PNG、JPG、BMP 等格式(通过 stb_image)
|
||||||
|
- 音频支持 WAV、MP3、OGG 等格式(通过 SDL2_mixer)
|
||||||
|
- 着色器使用 OpenGL ES 3.2 语法
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
#include <extra2d.h>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
using namespace extra2d;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 资源加载示例
|
||||||
|
*
|
||||||
|
* 演示如何使用资源管理器加载各种资源:
|
||||||
|
* - 纹理 (Texture)
|
||||||
|
* - 字体 (Font)
|
||||||
|
* - 着色器 (Shader)
|
||||||
|
* - 音频 (Audio)
|
||||||
|
*/
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
// 创建应用(自动创建窗口)
|
||||||
|
auto app = Application::create();
|
||||||
|
|
||||||
|
AppConfig config;
|
||||||
|
config.title = "Resource Loading Example";
|
||||||
|
config.width = 1280;
|
||||||
|
config.height = 720;
|
||||||
|
|
||||||
|
if (!app->init(config)) {
|
||||||
|
printf("Failed to initialize!\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取资源管理器
|
||||||
|
auto& resources = app->getContext()->resources();
|
||||||
|
|
||||||
|
printf("=== Resource Loading Example ===\n\n");
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 1. 加载纹理
|
||||||
|
// ============================================
|
||||||
|
printf("1. Loading Textures...\n");
|
||||||
|
|
||||||
|
// 从文件加载纹理
|
||||||
|
auto texture = resources.getTexture("assets/textures/test.png");
|
||||||
|
if (texture) {
|
||||||
|
printf(" ✓ Loaded texture: %dx%d\n", texture->getWidth(), texture->getHeight());
|
||||||
|
} else {
|
||||||
|
printf(" ✗ Failed to load texture (file not found)\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用默认纹理
|
||||||
|
auto defaultTex = resources.getDefaultTexture();
|
||||||
|
if (defaultTex) {
|
||||||
|
printf(" ✓ Default texture available: %dx%d\n",
|
||||||
|
defaultTex->getWidth(), defaultTex->getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 2. 加载字体
|
||||||
|
// ============================================
|
||||||
|
printf("\n2. Loading Fonts...\n");
|
||||||
|
|
||||||
|
FontConfig fontConfig;
|
||||||
|
fontConfig.fontSize = 24;
|
||||||
|
fontConfig.preloadCommon = true; // 预加载常用字符
|
||||||
|
|
||||||
|
auto font = resources.getFont("assets/fonts/font.ttf", fontConfig);
|
||||||
|
if (font) {
|
||||||
|
printf(" ✓ Loaded font with size %d\n", fontConfig.fontSize);
|
||||||
|
|
||||||
|
// 测试字体测量
|
||||||
|
Vec2 textSize = font->measureText("Hello 世界!", 24.0f);
|
||||||
|
printf(" ✓ Text 'Hello 世界!' size: %.1f x %.1f\n", textSize.x, textSize.y);
|
||||||
|
|
||||||
|
// 获取字符信息(会自动渲染并缓存)
|
||||||
|
const GlyphInfo* glyph = font->getGlyph('A');
|
||||||
|
if (glyph && glyph->valid) {
|
||||||
|
printf(" ✓ Glyph 'A' cached in atlas page %d\n", glyph->pageIndex);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printf(" ✗ Failed to load font (file not found)\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 3. 创建文本对象
|
||||||
|
// ============================================
|
||||||
|
printf("\n3. Creating Text Objects...\n");
|
||||||
|
|
||||||
|
if (font) {
|
||||||
|
auto text = resources.createText(font, "Resource Loading Example\n你好,世界!");
|
||||||
|
text->setFontSize(32);
|
||||||
|
text->setColor(Color::White);
|
||||||
|
text->setAlign(TextAlign::Center);
|
||||||
|
text->rebuild();
|
||||||
|
|
||||||
|
Vec2 size = text->getSize();
|
||||||
|
printf(" ✓ Created text object: %.1f x %.1f\n", size.x, size.y);
|
||||||
|
printf(" ✓ Text has %zu lines\n", text->getLines().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 4. 加载着色器
|
||||||
|
// ============================================
|
||||||
|
printf("\n4. Loading Shaders...\n");
|
||||||
|
|
||||||
|
// 从文件加载着色器
|
||||||
|
auto shader = resources.getShader("assets/shaders/sprite.vs",
|
||||||
|
"assets/shaders/sprite.fs");
|
||||||
|
if (shader) {
|
||||||
|
printf(" ✓ Loaded shader from files\n");
|
||||||
|
} else {
|
||||||
|
printf(" ✗ Failed to load shader (files not found)\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用默认着色器
|
||||||
|
auto defaultShader = resources.getDefaultShader();
|
||||||
|
if (defaultShader) {
|
||||||
|
printf(" ✓ Default shader available\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从源码创建着色器
|
||||||
|
const char* vsSource = R"(
|
||||||
|
#version 320 es
|
||||||
|
layout(location = 0) in vec2 aPosition;
|
||||||
|
layout(location = 1) in vec2 aTexCoord;
|
||||||
|
out vec2 vTexCoord;
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(aPosition, 0.0, 1.0);
|
||||||
|
vTexCoord = aTexCoord;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* fsSource = R"(
|
||||||
|
#version 320 es
|
||||||
|
precision mediump float;
|
||||||
|
in vec2 vTexCoord;
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main() {
|
||||||
|
fragColor = vec4(vTexCoord, 0.5, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
auto runtimeShader = resources.getShaderFromSource("runtime_shader", vsSource, fsSource);
|
||||||
|
if (runtimeShader) {
|
||||||
|
printf(" ✓ Created shader from source\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 5. 创建材质
|
||||||
|
// ============================================
|
||||||
|
printf("\n5. Creating Materials...\n");
|
||||||
|
|
||||||
|
auto material = resources.createMaterial(defaultShader);
|
||||||
|
if (material) {
|
||||||
|
material->setTexture("uTexture", defaultTex);
|
||||||
|
material->setColor("uColor", Color(1.0f, 0.5f, 0.2f, 1.0f));
|
||||||
|
material->setFloat("uTime", 0.0f);
|
||||||
|
printf(" ✓ Created material with texture and uniforms\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册命名材质以便后续获取
|
||||||
|
resources.registerMaterial("sprite_mat", material);
|
||||||
|
auto retrievedMat = resources.getMaterial("sprite_mat");
|
||||||
|
if (retrievedMat) {
|
||||||
|
printf(" ✓ Retrieved material by name\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 6. 加载音频
|
||||||
|
// ============================================
|
||||||
|
printf("\n6. Loading Audio...\n");
|
||||||
|
|
||||||
|
// 加载音效
|
||||||
|
auto sound = resources.getAudio("assets/audio/jump.wav", AudioType::Sound);
|
||||||
|
if (sound) {
|
||||||
|
printf(" ✓ Loaded sound effect\n");
|
||||||
|
// sound->play(); // 播放一次
|
||||||
|
} else {
|
||||||
|
printf(" ✗ Failed to load sound (file not found)\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载音乐
|
||||||
|
auto music = resources.getAudio("assets/audio/bgm.mp3", AudioType::Music);
|
||||||
|
if (music) {
|
||||||
|
printf(" ✓ Loaded music\n");
|
||||||
|
// music->play(-1); // 循环播放
|
||||||
|
} else {
|
||||||
|
printf(" ✗ Failed to load music (file not found)\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 7. 资源统计
|
||||||
|
// ============================================
|
||||||
|
printf("\n7. Resource Statistics...\n");
|
||||||
|
|
||||||
|
auto stats = resources.getStats();
|
||||||
|
printf(" Textures: %zu\n", stats.textureCount);
|
||||||
|
printf(" Shaders: %zu\n", stats.shaderCount);
|
||||||
|
printf(" Materials: %zu\n", stats.materialCount);
|
||||||
|
printf(" Fonts: %zu\n", stats.fontCount);
|
||||||
|
printf(" Texts: %zu\n", stats.textCount);
|
||||||
|
printf(" Audio: %zu\n", stats.audioCount);
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 8. 默认资源
|
||||||
|
// ============================================
|
||||||
|
printf("\n8. Default Resources...\n");
|
||||||
|
|
||||||
|
auto defTex = resources.getDefaultTexture();
|
||||||
|
auto defShader = resources.getDefaultShader();
|
||||||
|
auto defMaterial = resources.getDefaultMaterial();
|
||||||
|
|
||||||
|
printf(" ✓ Default Texture: %s\n", defTex ? "available" : "null");
|
||||||
|
printf(" ✓ Default Shader: %s\n", defShader ? "available" : "null");
|
||||||
|
printf(" ✓ Default Material: %s\n", defMaterial ? "available" : "null");
|
||||||
|
|
||||||
|
printf("\n=== Resource Loading Complete ===\n");
|
||||||
|
|
||||||
|
// 运行应用(主循环)
|
||||||
|
printf("\nRunning main loop...\n");
|
||||||
|
app->run();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
-- ==============================================
|
||||||
|
-- 资源加载示例 - Xmake 构建脚本
|
||||||
|
-- 支持平台: MinGW (Windows), Nintendo Switch
|
||||||
|
-- ==============================================
|
||||||
|
|
||||||
|
-- 获取当前脚本所在目录(示例根目录)
|
||||||
|
local example_dir = os.scriptdir()
|
||||||
|
|
||||||
|
-- 可执行文件目标
|
||||||
|
target("resource_loading")
|
||||||
|
set_kind("binary")
|
||||||
|
add_files("main.cpp")
|
||||||
|
add_includedirs("../../include")
|
||||||
|
add_deps("extra2d")
|
||||||
|
|
||||||
|
-- 使用与主项目相同的平台配置
|
||||||
|
if is_plat("switch") then
|
||||||
|
set_targetdir("../../build/examples/resource_loading")
|
||||||
|
|
||||||
|
-- 构建后生成 NRO 文件
|
||||||
|
after_build(function (target)
|
||||||
|
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
|
||||||
|
local elf_file = target:targetfile()
|
||||||
|
local output_dir = path.directory(elf_file)
|
||||||
|
local nacp_file = path.join(output_dir, "resource_loading.nacp")
|
||||||
|
local nro_file = path.join(output_dir, "resource_loading.nro")
|
||||||
|
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
|
||||||
|
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
|
||||||
|
|
||||||
|
if os.isfile(nacptool) and os.isfile(elf2nro) then
|
||||||
|
os.vrunv(nacptool, {"--create", "Resource Loading Example", "Extra2D Team", "1.0.0", nacp_file})
|
||||||
|
local romfs = path.join(example_dir, "romfs")
|
||||||
|
if os.isdir(romfs) then
|
||||||
|
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs})
|
||||||
|
else
|
||||||
|
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file})
|
||||||
|
end
|
||||||
|
print("Generated NRO: " .. nro_file)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- 打包时将 NRO 文件复制到 package 目录
|
||||||
|
after_package(function (target)
|
||||||
|
local nro_file = path.join(target:targetdir(), "resource_loading.nro")
|
||||||
|
local package_dir = target:packagedir()
|
||||||
|
if os.isfile(nro_file) and package_dir then
|
||||||
|
os.cp(nro_file, package_dir)
|
||||||
|
print("Copied NRO to package: " .. package_dir)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
elseif is_plat("mingw") then
|
||||||
|
set_targetdir("../../build/examples/resource_loading")
|
||||||
|
|
||||||
|
-- 复制资源到输出目录
|
||||||
|
after_build(function (target)
|
||||||
|
local romfs = path.join(example_dir, "romfs")
|
||||||
|
if os.isdir(romfs) then
|
||||||
|
local target_dir = path.directory(target:targetfile())
|
||||||
|
local assets_dir = path.join(target_dir, "assets")
|
||||||
|
|
||||||
|
-- 创建 assets 目录
|
||||||
|
if not os.isdir(assets_dir) then
|
||||||
|
os.mkdir(assets_dir)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 复制所有资源文件(包括子目录)
|
||||||
|
os.cp(path.join(romfs, "assets/**"), assets_dir)
|
||||||
|
print("Copied assets from " .. romfs .. " to " .. assets_dir)
|
||||||
|
else
|
||||||
|
print("Warning: romfs directory not found at " .. romfs)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
target_end()
|
||||||
|
|
@ -9,6 +9,7 @@ namespace extra2d {
|
||||||
class ModuleRegistry;
|
class ModuleRegistry;
|
||||||
class PluginLoader;
|
class PluginLoader;
|
||||||
class TimerModule;
|
class TimerModule;
|
||||||
|
class ResourceManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 引擎上下文
|
* @brief 引擎上下文
|
||||||
|
|
@ -66,6 +67,12 @@ public:
|
||||||
*/
|
*/
|
||||||
TimerModule& timer();
|
TimerModule& timer();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取资源管理器
|
||||||
|
*/
|
||||||
|
ResourceManager& resources();
|
||||||
|
|
||||||
|
private:
|
||||||
/**
|
/**
|
||||||
* @brief 获取总运行时间
|
* @brief 获取总运行时间
|
||||||
*/
|
*/
|
||||||
|
|
@ -89,6 +96,7 @@ public:
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<PluginLoader> pluginLoader_;
|
std::unique_ptr<PluginLoader> pluginLoader_;
|
||||||
std::unique_ptr<TimerModule> timerModule_;
|
std::unique_ptr<TimerModule> timerModule_;
|
||||||
|
std::unique_ptr<ResourceManager> resourceManager_;
|
||||||
|
|
||||||
float totalTime_ = 0.0f;
|
float totalTime_ = 0.0f;
|
||||||
uint64 frameCount_ = 0;
|
uint64 frameCount_ = 0;
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,18 @@
|
||||||
#include <config/app_config.h>
|
#include <config/app_config.h>
|
||||||
#include <config/window_config.h>
|
#include <config/window_config.h>
|
||||||
|
|
||||||
|
// Resource System
|
||||||
|
#include <resource/resource.h>
|
||||||
|
#include <resource/resource_cache.h>
|
||||||
|
#include <resource/resource_manager.h>
|
||||||
|
#include <resource/texture.h>
|
||||||
|
#include <resource/shader.h>
|
||||||
|
#include <resource/material.h>
|
||||||
|
#include <resource/font.h>
|
||||||
|
#include <resource/font_atlas.h>
|
||||||
|
#include <resource/text.h>
|
||||||
|
#include <resource/audio.h>
|
||||||
|
|
||||||
// Application
|
// Application
|
||||||
#include <app/application.h>
|
#include <app/application.h>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <resource/resource.h>
|
||||||
|
#include <SDL2/SDL_mixer.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 音频类型枚举
|
||||||
|
*/
|
||||||
|
enum class AudioType : uint8 {
|
||||||
|
Sound, // 音效(短音频,完全加载到内存)
|
||||||
|
Music // 音乐(流式播放)
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 音频资源类
|
||||||
|
*
|
||||||
|
* 封装 SDL2_mixer 音频
|
||||||
|
*/
|
||||||
|
class Audio : public Resource {
|
||||||
|
public:
|
||||||
|
Audio();
|
||||||
|
~Audio() override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取资源类型
|
||||||
|
*/
|
||||||
|
ResourceType getType() const override { return ResourceType::Audio; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 从文件加载音频
|
||||||
|
* @param path 文件路径
|
||||||
|
* @param type 音频类型
|
||||||
|
* @return 是否加载成功
|
||||||
|
*/
|
||||||
|
bool loadFromFile(const std::string& path, AudioType type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 播放音频
|
||||||
|
* @param loops 循环次数,-1 表示无限循环,0 表示播放一次
|
||||||
|
*/
|
||||||
|
void play(int loops = 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 暂停播放
|
||||||
|
*/
|
||||||
|
void pause();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 恢复播放
|
||||||
|
*/
|
||||||
|
void resume();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 停止播放
|
||||||
|
*/
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置音量
|
||||||
|
* @param volume 音量(0.0 - 1.0)
|
||||||
|
*/
|
||||||
|
void setVolume(float volume);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取音量
|
||||||
|
* @return 音量(0.0 - 1.0)
|
||||||
|
*/
|
||||||
|
float getVolume() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 检查是否正在播放
|
||||||
|
*/
|
||||||
|
bool isPlaying() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 检查是否已暂停
|
||||||
|
*/
|
||||||
|
bool isPaused() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取音频类型
|
||||||
|
*/
|
||||||
|
AudioType getAudioType() const { return type_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
AudioType type_ = AudioType::Sound; // 音频类型
|
||||||
|
Mix_Chunk* chunk_ = nullptr; // 音效数据
|
||||||
|
Mix_Music* music_ = nullptr; // 音乐数据
|
||||||
|
int channel_ = -1; // 音效通道
|
||||||
|
float volume_ = 1.0f; // 音量
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <resource/resource.h>
|
||||||
|
#include <resource/font_atlas.h>
|
||||||
|
#include <types/math/vec2.h>
|
||||||
|
#include <types/base/types.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// FreeType 头文件
|
||||||
|
#include <ft2build.h>
|
||||||
|
#include FT_FREETYPE_H
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 常用字符集
|
||||||
|
*/
|
||||||
|
namespace CommonCharset {
|
||||||
|
// ASCII 可打印字符
|
||||||
|
constexpr const char* ASCII =
|
||||||
|
" !\"#$%&'()*+,-./0123456789:;<=>?"
|
||||||
|
"@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
|
||||||
|
"`abcdefghijklmnopqrstuvwxyz{|}~";
|
||||||
|
|
||||||
|
// 常用中文(前100个高频字)
|
||||||
|
constexpr const char* CHINESE_COMMON =
|
||||||
|
"的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成"
|
||||||
|
"会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着"
|
||||||
|
"等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把";
|
||||||
|
|
||||||
|
// 英文标点
|
||||||
|
constexpr const char* PUNCTUATION_EN = ".,;:!?-'\"()[]{}/|\\@#$%&*+-_=<>~`";
|
||||||
|
|
||||||
|
// 中文标点
|
||||||
|
constexpr const char* PUNCTUATION_CN = ",。、;:!?\"\"''()【】《》";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 字体配置结构
|
||||||
|
*/
|
||||||
|
struct FontConfig {
|
||||||
|
uint32 fontSize = 16; // 字体大小(像素)
|
||||||
|
bool useSDF = false; // 是否使用 SDF(有向距离场)
|
||||||
|
uint32 sdfPadding = 4; // SDF 边距
|
||||||
|
bool preloadCommon = true; // 是否预加载常用字符
|
||||||
|
bool hinting = true; // 是否使用字体 hinting
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 字体资源类
|
||||||
|
*
|
||||||
|
* 使用 FreeType 加载字体,动态生成字符图集
|
||||||
|
*/
|
||||||
|
class Font : public Resource {
|
||||||
|
public:
|
||||||
|
Font();
|
||||||
|
~Font() override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取资源类型
|
||||||
|
*/
|
||||||
|
ResourceType getType() const override { return ResourceType::Font; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 从文件加载字体
|
||||||
|
* @param path 字体文件路径
|
||||||
|
* @param config 字体配置
|
||||||
|
* @return 是否加载成功
|
||||||
|
*/
|
||||||
|
bool loadFromFile(const std::string& path, const FontConfig& config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 从内存加载字体
|
||||||
|
* @param data 字体数据
|
||||||
|
* @param config 字体配置
|
||||||
|
* @return 是否加载成功
|
||||||
|
*/
|
||||||
|
bool loadFromMemory(const std::vector<uint8>& data, const FontConfig& config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取或创建字符
|
||||||
|
* @param codepoint Unicode 码点
|
||||||
|
* @return 字符信息指针,如果失败返回 nullptr
|
||||||
|
*/
|
||||||
|
const GlyphInfo* getGlyph(uint32 codepoint);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 测量文本尺寸
|
||||||
|
* @param text 文本内容
|
||||||
|
* @param fontSize 字体大小(0 表示使用默认大小)
|
||||||
|
* @return 文本尺寸(宽度和高度)
|
||||||
|
*/
|
||||||
|
Vec2 measureText(const std::string& text, float fontSize = 0.0f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取两个字之间的字距
|
||||||
|
* @param codepoint1 第一个字符
|
||||||
|
* @param codepoint2 第二个字符
|
||||||
|
* @return 字距调整值
|
||||||
|
*/
|
||||||
|
float getKerning(uint32 codepoint1, uint32 codepoint2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取行高
|
||||||
|
* @param fontSize 字体大小(0 表示使用默认大小)
|
||||||
|
* @return 行高
|
||||||
|
*/
|
||||||
|
float getLineHeight(float fontSize = 0.0f) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取字体图集
|
||||||
|
*/
|
||||||
|
FontAtlas* getAtlas() const { return atlas_.get(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 预加载字符集
|
||||||
|
* @param charset 字符集字符串
|
||||||
|
*/
|
||||||
|
void preloadCharset(const std::string& charset);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取字体配置
|
||||||
|
*/
|
||||||
|
const FontConfig& getConfig() const { return config_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取字体度量信息
|
||||||
|
* @param outAscender 输出上升高度
|
||||||
|
* @param outDescender 输出下降高度
|
||||||
|
* @param outLineHeight 输出行高
|
||||||
|
*/
|
||||||
|
void getMetrics(float& outAscender, float& outDescender, float& outLineHeight) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
FT_Face face_ = nullptr; // FreeType 字体面
|
||||||
|
FontConfig config_; // 字体配置
|
||||||
|
Ptr<FontAtlas> atlas_; // 字体图集
|
||||||
|
float baseScale_ = 1.0f; // 基础缩放比例
|
||||||
|
std::vector<uint8> fontData_; // 字体数据(保持内存中)
|
||||||
|
|
||||||
|
// FreeType 库静态实例
|
||||||
|
static FT_Library ftLibrary_;
|
||||||
|
static bool ftInitialized_;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化 FreeType 库
|
||||||
|
*/
|
||||||
|
static bool initFreeType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 渲染字符到位图
|
||||||
|
* @param codepoint Unicode 码点
|
||||||
|
* @param outWidth 输出宽度
|
||||||
|
* @param outHeight 输出高度
|
||||||
|
* @param outBearingX 输出水平基线偏移
|
||||||
|
* @param outBearingY 输出垂直基线偏移
|
||||||
|
* @param outAdvance 输出水平步进
|
||||||
|
* @param outBitmap 输出位图数据
|
||||||
|
* @return 是否渲染成功
|
||||||
|
*/
|
||||||
|
bool renderGlyph(uint32 codepoint, uint32& outWidth, uint32& outHeight,
|
||||||
|
float& outBearingX, float& outBearingY, float& outAdvance,
|
||||||
|
std::vector<uint8>& outBitmap);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将 UTF-8 字符串转换为 Unicode 码点数组
|
||||||
|
* @param text UTF-8 字符串
|
||||||
|
* @return Unicode 码点数组
|
||||||
|
*/
|
||||||
|
static std::vector<uint32> utf8ToCodepoints(const std::string& text);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <resource/resource.h>
|
||||||
|
#include <resource/texture.h>
|
||||||
|
#include <types/math/vec2.h>
|
||||||
|
#include <types/base/types.h>
|
||||||
|
#include <types/ptr/intrusive_ptr.h>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
// STB 矩形打包
|
||||||
|
#include <stb/stb_rect_pack.h>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 字符信息结构
|
||||||
|
*/
|
||||||
|
struct GlyphInfo {
|
||||||
|
Vec2 uv0; // 左下角 UV 坐标
|
||||||
|
Vec2 uv1; // 右上角 UV 坐标
|
||||||
|
Vec2 size; // 像素尺寸
|
||||||
|
Vec2 bearing; // 基线偏移(左下角相对原点的偏移)
|
||||||
|
float advance; // 水平步进(到下一个字符的距离)
|
||||||
|
uint32 pageIndex; // 所在图集页索引
|
||||||
|
bool valid = false; // 是否有效
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 图集页结构
|
||||||
|
*
|
||||||
|
* 使用 stb_rect_pack 进行矩形打包
|
||||||
|
*/
|
||||||
|
struct AtlasPage {
|
||||||
|
static constexpr uint32 DEFAULT_SIZE = 1024; // 默认图集尺寸
|
||||||
|
|
||||||
|
Ptr<Texture> texture; // 纹理
|
||||||
|
stbrp_context packContext; // 打包上下文
|
||||||
|
std::vector<stbrp_node> packNodes; // 打包节点数组
|
||||||
|
std::vector<uint8> pixelData; // CPU 端像素数据
|
||||||
|
uint32 width = DEFAULT_SIZE; // 图集宽度
|
||||||
|
uint32 height = DEFAULT_SIZE; // 图集高度
|
||||||
|
bool dirty = false; // 是否需要更新 GPU 纹理
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 构造函数
|
||||||
|
*/
|
||||||
|
AtlasPage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化图集页
|
||||||
|
* @param size 图集尺寸
|
||||||
|
* @return 是否初始化成功
|
||||||
|
*/
|
||||||
|
bool init(uint32 size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 打包矩形到图集
|
||||||
|
* @param width 矩形宽度
|
||||||
|
* @param height 矩形高度
|
||||||
|
* @param outX 输出 X 坐标
|
||||||
|
* @param outY 输出 Y 坐标
|
||||||
|
* @return 是否打包成功
|
||||||
|
*/
|
||||||
|
bool packRect(uint32 width, uint32 height, uint32& outX, uint32& outY);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 写入像素数据到图集
|
||||||
|
* @param x 起始 X 坐标
|
||||||
|
* @param y 起始 Y 坐标
|
||||||
|
* @param width 宽度
|
||||||
|
* @param height 高度
|
||||||
|
* @param data 像素数据(单通道)
|
||||||
|
*/
|
||||||
|
void writePixels(uint32 x, uint32 y, uint32 width, uint32 height, const uint8* data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 更新 GPU 纹理
|
||||||
|
*/
|
||||||
|
void updateTexture();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 清空图集
|
||||||
|
*/
|
||||||
|
void clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 字体图集类
|
||||||
|
*
|
||||||
|
* 管理动态字符图集,支持多页扩展
|
||||||
|
*/
|
||||||
|
class FontAtlas : public Resource {
|
||||||
|
public:
|
||||||
|
FontAtlas();
|
||||||
|
~FontAtlas() override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取资源类型
|
||||||
|
*/
|
||||||
|
ResourceType getType() const override { return ResourceType::FontAtlas; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化字体图集
|
||||||
|
* @param pageSize 图集页尺寸
|
||||||
|
* @return 是否初始化成功
|
||||||
|
*/
|
||||||
|
bool init(uint32 pageSize = AtlasPage::DEFAULT_SIZE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 添加字符到图集
|
||||||
|
* @param codepoint Unicode 码点
|
||||||
|
* @param bitmap 字符位图数据(单通道)
|
||||||
|
* @param width 位图宽度
|
||||||
|
* @param height 位图高度
|
||||||
|
* @param bearingX 水平基线偏移
|
||||||
|
* @param bearingY 垂直基线偏移
|
||||||
|
* @param advance 水平步进
|
||||||
|
* @return 是否添加成功
|
||||||
|
*/
|
||||||
|
bool addGlyph(uint32 codepoint, const uint8* bitmap, uint32 width, uint32 height,
|
||||||
|
float bearingX, float bearingY, float advance);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取字符信息
|
||||||
|
* @param codepoint Unicode 码点
|
||||||
|
* @return 字符信息指针,如果不存在返回 nullptr
|
||||||
|
*/
|
||||||
|
const GlyphInfo* getGlyph(uint32 codepoint) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 检查字符是否已缓存
|
||||||
|
* @param codepoint Unicode 码点
|
||||||
|
* @return 是否已缓存
|
||||||
|
*/
|
||||||
|
bool hasGlyph(uint32 codepoint) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取图集页数量
|
||||||
|
*/
|
||||||
|
uint32 getPageCount() const { return static_cast<uint32>(pages_.size()); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取指定页的纹理
|
||||||
|
* @param pageIndex 页索引
|
||||||
|
* @return 纹理指针
|
||||||
|
*/
|
||||||
|
Texture* getPageTexture(uint32 pageIndex) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 更新所有需要更新的图集页纹理
|
||||||
|
*/
|
||||||
|
void updateTextures();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 清空所有图集
|
||||||
|
*/
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取图集页尺寸
|
||||||
|
*/
|
||||||
|
uint32 getPageSize() const { return pageSize_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<std::unique_ptr<AtlasPage>> pages_; // 图集页数组
|
||||||
|
std::unordered_map<uint32, GlyphInfo> glyphs_; // 字符信息映射表
|
||||||
|
uint32 pageSize_ = AtlasPage::DEFAULT_SIZE; // 图集页尺寸
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取或创建可用的图集页
|
||||||
|
* @return 图集页指针
|
||||||
|
*/
|
||||||
|
AtlasPage* getOrCreatePage();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <resource/resource.h>
|
||||||
|
#include <resource/shader.h>
|
||||||
|
#include <resource/texture.h>
|
||||||
|
#include <types/math/color.h>
|
||||||
|
#include <types/math/vec2.h>
|
||||||
|
#include <types/math/vec3.h>
|
||||||
|
#include <types/ptr/intrusive_ptr.h>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
// 材质属性类型定义
|
||||||
|
using MaterialProperty = std::variant<
|
||||||
|
bool,
|
||||||
|
int,
|
||||||
|
float,
|
||||||
|
Vec2,
|
||||||
|
Vec3,
|
||||||
|
Color,
|
||||||
|
Ptr<Texture>
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 材质资源类
|
||||||
|
*
|
||||||
|
* 封装着色器和材质属性
|
||||||
|
*/
|
||||||
|
class Material : public Resource {
|
||||||
|
public:
|
||||||
|
Material();
|
||||||
|
~Material() override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取资源类型
|
||||||
|
*/
|
||||||
|
ResourceType getType() const override { return ResourceType::Material; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置着色器
|
||||||
|
* @param shader 着色器指针
|
||||||
|
*/
|
||||||
|
void setShader(Ptr<Shader> shader);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取着色器
|
||||||
|
*/
|
||||||
|
Shader* getShader() const { return shader_.get(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置布尔属性
|
||||||
|
*/
|
||||||
|
void setBool(const std::string& name, bool value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置整数属性
|
||||||
|
*/
|
||||||
|
void setInt(const std::string& name, int value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置浮点数属性
|
||||||
|
*/
|
||||||
|
void setFloat(const std::string& name, float value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置 Vec2 属性
|
||||||
|
*/
|
||||||
|
void setVec2(const std::string& name, const Vec2& value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置 Vec3 属性
|
||||||
|
*/
|
||||||
|
void setVec3(const std::string& name, const Vec3& value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置颜色属性
|
||||||
|
*/
|
||||||
|
void setColor(const std::string& name, const Color& value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置纹理属性
|
||||||
|
*/
|
||||||
|
void setTexture(const std::string& name, Ptr<Texture> texture);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 应用材质(绑定着色器并设置属性)
|
||||||
|
*/
|
||||||
|
void apply();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 克隆材质
|
||||||
|
* @return 新材质指针
|
||||||
|
*/
|
||||||
|
Ptr<Material> clone() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Ptr<Shader> shader_; // 着色器
|
||||||
|
std::unordered_map<std::string, MaterialProperty> properties_; // 属性表
|
||||||
|
mutable uint32 textureUnit_ = 0; // 当前纹理单元
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 应用所有属性到着色器
|
||||||
|
*/
|
||||||
|
void applyProperties();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <types/ptr/ref_counted.h>
|
||||||
|
#include <types/base/types.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 资源类型枚举
|
||||||
|
*/
|
||||||
|
enum class ResourceType : uint8 {
|
||||||
|
Texture, // 纹理
|
||||||
|
Shader, // 着色器
|
||||||
|
Material, // 材质
|
||||||
|
Font, // 字体
|
||||||
|
FontAtlas, // 字体图集
|
||||||
|
Text, // 文本
|
||||||
|
Audio, // 音频
|
||||||
|
Mesh // 网格
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 资源基类
|
||||||
|
*
|
||||||
|
* 所有引擎资源都继承此类,支持引用计数生命周期管理
|
||||||
|
*/
|
||||||
|
class Resource : public RefCounted {
|
||||||
|
public:
|
||||||
|
virtual ~Resource() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取资源类型
|
||||||
|
* @return 资源类型枚举
|
||||||
|
*/
|
||||||
|
virtual ResourceType getType() const = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取资源名称
|
||||||
|
* @return 资源名称
|
||||||
|
*/
|
||||||
|
virtual const char* getName() const { return name_.c_str(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取资源ID
|
||||||
|
* @return 资源唯一ID
|
||||||
|
*/
|
||||||
|
uint32 getId() const { return id_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 检查资源是否已加载
|
||||||
|
* @return 是否已加载
|
||||||
|
*/
|
||||||
|
bool isLoaded() const { return loaded_; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::string name_; // 资源名称
|
||||||
|
uint32 id_ = 0; // 资源唯一ID
|
||||||
|
bool loaded_ = false; // 是否已加载
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <resource/resource.h>
|
||||||
|
#include <types/ptr/intrusive_ptr.h>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <string>
|
||||||
|
#include <mutex>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 资源缓存模板类
|
||||||
|
*
|
||||||
|
* 管理资源的加载和缓存,支持线程安全
|
||||||
|
* T 必须是 Resource 的子类
|
||||||
|
*/
|
||||||
|
template<typename T, typename... LoadArgs>
|
||||||
|
class ResourceCache {
|
||||||
|
public:
|
||||||
|
using Loader = std::function<Ptr<T>(const std::string&, LoadArgs...)>;
|
||||||
|
|
||||||
|
ResourceCache() = default;
|
||||||
|
~ResourceCache() = default;
|
||||||
|
|
||||||
|
// 禁止拷贝
|
||||||
|
ResourceCache(const ResourceCache&) = delete;
|
||||||
|
ResourceCache& operator=(const ResourceCache&) = delete;
|
||||||
|
|
||||||
|
// 允许移动
|
||||||
|
ResourceCache(ResourceCache&&) = default;
|
||||||
|
ResourceCache& operator=(ResourceCache&&) = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取资源(如果不存在则加载)
|
||||||
|
* @param path 资源路径
|
||||||
|
* @param args 加载参数
|
||||||
|
* @return 资源指针
|
||||||
|
*/
|
||||||
|
Ptr<T> get(const std::string& path, LoadArgs... args) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
auto it = cache_.find(path);
|
||||||
|
if (it != cache_.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ptr<T> resource;
|
||||||
|
if (customLoader_) {
|
||||||
|
resource = customLoader_(path, args...);
|
||||||
|
} else {
|
||||||
|
resource = defaultLoad(path, args...);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resource && resource->isLoaded()) {
|
||||||
|
cache_[path] = resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 预加载资源
|
||||||
|
* @param path 资源路径
|
||||||
|
* @param args 加载参数
|
||||||
|
* @return 资源指针
|
||||||
|
*/
|
||||||
|
Ptr<T> preload(const std::string& path, LoadArgs... args) {
|
||||||
|
return get(path, args...);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 检查资源是否已缓存
|
||||||
|
* @param path 资源路径
|
||||||
|
* @return 是否已缓存
|
||||||
|
*/
|
||||||
|
bool has(const std::string& path) const {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
return cache_.find(path) != cache_.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 移除资源
|
||||||
|
* @param path 资源路径
|
||||||
|
*/
|
||||||
|
void remove(const std::string& path) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
cache_.erase(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 清空缓存
|
||||||
|
*/
|
||||||
|
void clear() {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
cache_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置自定义加载器
|
||||||
|
* @param loader 自定义加载函数
|
||||||
|
*/
|
||||||
|
void setLoader(Loader loader) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
customLoader_ = loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取缓存大小
|
||||||
|
* @return 缓存中的资源数量
|
||||||
|
*/
|
||||||
|
size_t size() const {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
return cache_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable std::mutex mutex_;
|
||||||
|
std::unordered_map<std::string, Ptr<T>> cache_;
|
||||||
|
Loader customLoader_;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 默认加载函数
|
||||||
|
*/
|
||||||
|
Ptr<T> defaultLoad(const std::string& path, LoadArgs... args) {
|
||||||
|
Ptr<T> resource = makePtr<T>();
|
||||||
|
if (resource->loadFromFile(path, args...)) {
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <resource/resource_cache.h>
|
||||||
|
#include <resource/texture.h>
|
||||||
|
#include <resource/shader.h>
|
||||||
|
#include <resource/material.h>
|
||||||
|
#include <resource/font.h>
|
||||||
|
#include <resource/text.h>
|
||||||
|
#include <resource/audio.h>
|
||||||
|
#include <module/imodule.h>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 资源管理器类
|
||||||
|
*
|
||||||
|
* 统一管理所有资源的加载和缓存
|
||||||
|
*/
|
||||||
|
class ResourceManager : public IModule {
|
||||||
|
public:
|
||||||
|
ResourceManager();
|
||||||
|
~ResourceManager() override;
|
||||||
|
|
||||||
|
// IModule 接口
|
||||||
|
const char* name() const override { return "ResourceManager"; }
|
||||||
|
ModuleType type() const override { return ModuleType::Core; }
|
||||||
|
int priority() const override { return 10; }
|
||||||
|
bool init() override;
|
||||||
|
void shutdown() override;
|
||||||
|
|
||||||
|
// 纹理管理
|
||||||
|
Ptr<Texture> getTexture(const std::string& path);
|
||||||
|
Ptr<Texture> createTexture(uint32 width, uint32 height, TextureFormat format);
|
||||||
|
|
||||||
|
// 着色器管理
|
||||||
|
Ptr<Shader> getShader(const std::string& vsPath, const std::string& fsPath);
|
||||||
|
Ptr<Shader> getShaderFromSource(const std::string& name,
|
||||||
|
const std::string& vsSource,
|
||||||
|
const std::string& fsSource);
|
||||||
|
|
||||||
|
// 材质管理
|
||||||
|
Ptr<Material> createMaterial(Ptr<Shader> shader);
|
||||||
|
Ptr<Material> getMaterial(const std::string& name);
|
||||||
|
void registerMaterial(const std::string& name, Ptr<Material> material);
|
||||||
|
|
||||||
|
// 字体管理
|
||||||
|
Ptr<Font> getFont(const std::string& path, const FontConfig& config = {});
|
||||||
|
|
||||||
|
// 文本管理
|
||||||
|
Ptr<Text> createText(Ptr<Font> font, const std::string& content = "");
|
||||||
|
|
||||||
|
// 音频管理
|
||||||
|
Ptr<Audio> getAudio(const std::string& path, AudioType type);
|
||||||
|
|
||||||
|
// 默认资源(延迟初始化,确保 OpenGL 上下文已创建)
|
||||||
|
Ptr<Texture> getDefaultTexture();
|
||||||
|
Ptr<Shader> getDefaultShader();
|
||||||
|
Ptr<Material> getDefaultMaterial();
|
||||||
|
Ptr<Font> getDefaultFont();
|
||||||
|
|
||||||
|
// 检查默认资源是否已创建
|
||||||
|
bool areDefaultResourcesCreated() const { return defaultResourcesCreated_; }
|
||||||
|
|
||||||
|
// 垃圾回收
|
||||||
|
void collectGarbage();
|
||||||
|
|
||||||
|
// 统计信息
|
||||||
|
struct Stats {
|
||||||
|
size_t textureCount;
|
||||||
|
size_t shaderCount;
|
||||||
|
size_t materialCount;
|
||||||
|
size_t fontCount;
|
||||||
|
size_t textCount;
|
||||||
|
size_t audioCount;
|
||||||
|
};
|
||||||
|
Stats getStats() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ResourceCache<Texture> textureCache_;
|
||||||
|
std::unordered_map<std::string, Ptr<Shader>> shaderCache_;
|
||||||
|
std::unordered_map<std::string, Ptr<Material>> materials_;
|
||||||
|
std::unordered_map<std::string, Ptr<Font>> fontCache_;
|
||||||
|
std::vector<Ptr<Text>> texts_;
|
||||||
|
std::unordered_map<std::string, Ptr<Audio>> audioCache_;
|
||||||
|
|
||||||
|
// 默认资源
|
||||||
|
Ptr<Texture> defaultTexture_;
|
||||||
|
Ptr<Shader> defaultShader_;
|
||||||
|
Ptr<Material> defaultMaterial_;
|
||||||
|
Ptr<Font> defaultFont_;
|
||||||
|
|
||||||
|
bool defaultResourcesCreated_ = false;
|
||||||
|
|
||||||
|
bool createDefaultResources();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <resource/resource.h>
|
||||||
|
#include <glad/glad.h>
|
||||||
|
#include <types/math/vec2.h>
|
||||||
|
#include <types/math/vec3.h>
|
||||||
|
#include <types/math/color.h>
|
||||||
|
#include <types/base/types.h>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 着色器类型枚举
|
||||||
|
*/
|
||||||
|
enum class ShaderType : uint8 {
|
||||||
|
Vertex, // 顶点着色器
|
||||||
|
Fragment, // 片段着色器
|
||||||
|
Geometry, // 几何着色器
|
||||||
|
Compute // 计算着色器
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 着色器资源类
|
||||||
|
*
|
||||||
|
* 封装 OpenGL ES 3.2 着色器程序
|
||||||
|
*/
|
||||||
|
class Shader : public Resource {
|
||||||
|
public:
|
||||||
|
Shader();
|
||||||
|
~Shader() override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取资源类型
|
||||||
|
*/
|
||||||
|
ResourceType getType() const override { return ResourceType::Shader; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 从文件加载着色器
|
||||||
|
* @param vsPath 顶点着色器文件路径
|
||||||
|
* @param fsPath 片段着色器文件路径
|
||||||
|
* @return 是否加载成功
|
||||||
|
*/
|
||||||
|
bool loadFromFile(const std::string& vsPath, const std::string& fsPath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 从源码加载着色器
|
||||||
|
* @param vsSource 顶点着色器源码
|
||||||
|
* @param fsSource 片段着色器源码
|
||||||
|
* @return 是否加载成功
|
||||||
|
*/
|
||||||
|
bool loadFromSource(const std::string& vsSource, const std::string& fsSource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 使用着色器程序
|
||||||
|
*/
|
||||||
|
void use() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 停止使用着色器程序
|
||||||
|
*/
|
||||||
|
void unuse() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置布尔 Uniform
|
||||||
|
*/
|
||||||
|
void setBool(const std::string& name, bool value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置整数 Uniform
|
||||||
|
*/
|
||||||
|
void setInt(const std::string& name, int value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置浮点数 Uniform
|
||||||
|
*/
|
||||||
|
void setFloat(const std::string& name, float value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置 Vec2 Uniform
|
||||||
|
*/
|
||||||
|
void setVec2(const std::string& name, const Vec2& value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置 Vec3 Uniform
|
||||||
|
*/
|
||||||
|
void setVec3(const std::string& name, const Vec3& value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置颜色 Uniform
|
||||||
|
*/
|
||||||
|
void setColor(const std::string& name, const Color& value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置 4x4 矩阵 Uniform
|
||||||
|
*/
|
||||||
|
void setMat4(const std::string& name, const float* value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取 OpenGL 程序句柄
|
||||||
|
*/
|
||||||
|
GLuint getProgram() const { return program_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
GLuint program_ = 0; // OpenGL 程序句柄
|
||||||
|
std::unordered_map<std::string, GLint> uniformCache_; // Uniform 位置缓存
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 编译着色器
|
||||||
|
* @param type 着色器类型
|
||||||
|
* @param source 源码
|
||||||
|
* @return 着色器句柄,失败返回 0
|
||||||
|
*/
|
||||||
|
GLuint compileShader(GLenum type, const std::string& source);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取 Uniform 位置
|
||||||
|
* @param name Uniform 名称
|
||||||
|
* @return Uniform 位置
|
||||||
|
*/
|
||||||
|
GLint getUniformLocation(const std::string& name);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <resource/resource.h>
|
||||||
|
#include <resource/font.h>
|
||||||
|
#include <types/math/color.h>
|
||||||
|
#include <types/math/vec2.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 文本对齐方式
|
||||||
|
*/
|
||||||
|
enum class TextAlign : uint8 {
|
||||||
|
Left, // 左对齐
|
||||||
|
Center, // 居中对齐
|
||||||
|
Right // 右对齐
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 文本顶点结构
|
||||||
|
*/
|
||||||
|
struct TextVertex {
|
||||||
|
Vec2 position; // 位置
|
||||||
|
Vec2 uv; // UV 坐标
|
||||||
|
Color color; // 颜色
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 文本行信息
|
||||||
|
*/
|
||||||
|
struct TextLine {
|
||||||
|
std::vector<TextVertex> vertices; // 顶点数组
|
||||||
|
std::vector<uint16> indices; // 索引数组
|
||||||
|
float width = 0.0f; // 行宽度
|
||||||
|
float yOffset = 0.0f; // Y 偏移
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 文本资源类
|
||||||
|
*
|
||||||
|
* 管理文本渲染数据
|
||||||
|
*/
|
||||||
|
class Text : public Resource {
|
||||||
|
public:
|
||||||
|
Text();
|
||||||
|
~Text() override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取资源类型
|
||||||
|
*/
|
||||||
|
ResourceType getType() const override { return ResourceType::Text; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置字体
|
||||||
|
* @param font 字体指针
|
||||||
|
*/
|
||||||
|
void setFont(Ptr<Font> font);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取字体
|
||||||
|
*/
|
||||||
|
Font* getFont() const { return font_.get(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置文本内容
|
||||||
|
* @param text 文本字符串
|
||||||
|
*/
|
||||||
|
void setText(const std::string& text);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取文本内容
|
||||||
|
*/
|
||||||
|
const std::string& getText() const { return text_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置字体大小
|
||||||
|
* @param size 字体大小
|
||||||
|
*/
|
||||||
|
void setFontSize(float size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取字体大小
|
||||||
|
*/
|
||||||
|
float getFontSize() const { return fontSize_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置颜色
|
||||||
|
* @param color 颜色
|
||||||
|
*/
|
||||||
|
void setColor(const Color& color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取颜色
|
||||||
|
*/
|
||||||
|
const Color& getColor() const { return color_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置对齐方式
|
||||||
|
* @param align 对齐方式
|
||||||
|
*/
|
||||||
|
void setAlign(TextAlign align);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取对齐方式
|
||||||
|
*/
|
||||||
|
TextAlign getAlign() const { return align_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置最大宽度(自动换行)
|
||||||
|
* @param width 最大宽度,0 表示不限制
|
||||||
|
*/
|
||||||
|
void setMaxWidth(float width);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取最大宽度
|
||||||
|
*/
|
||||||
|
float getMaxWidth() const { return maxWidth_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置行间距
|
||||||
|
* @param spacing 行间距倍数
|
||||||
|
*/
|
||||||
|
void setLineSpacing(float spacing);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取行间距
|
||||||
|
*/
|
||||||
|
float getLineSpacing() const { return lineSpacing_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取文本尺寸
|
||||||
|
*/
|
||||||
|
Vec2 getSize() const { return size_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取文本行数组
|
||||||
|
*/
|
||||||
|
const std::vector<TextLine>& getLines() const { return lines_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重新生成顶点数据
|
||||||
|
*/
|
||||||
|
void rebuild();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 检查是否需要重建
|
||||||
|
*/
|
||||||
|
bool isDirty() const { return dirty_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
Ptr<Font> font_; // 字体
|
||||||
|
std::string text_; // 文本内容
|
||||||
|
float fontSize_ = 16.0f; // 字体大小
|
||||||
|
Color color_ = Color::White; // 颜色
|
||||||
|
TextAlign align_ = TextAlign::Left; // 对齐方式
|
||||||
|
float maxWidth_ = 0.0f; // 最大宽度
|
||||||
|
float lineSpacing_ = 1.2f; // 行间距
|
||||||
|
|
||||||
|
Vec2 size_; // 文本尺寸
|
||||||
|
std::vector<TextLine> lines_; // 文本行数组
|
||||||
|
bool dirty_ = true; // 是否需要重建
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 构建顶点数据
|
||||||
|
*/
|
||||||
|
void buildVertices();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <resource/resource.h>
|
||||||
|
#include <glad/glad.h>
|
||||||
|
#include <types/base/types.h>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 纹理格式枚举
|
||||||
|
*/
|
||||||
|
enum class TextureFormat : uint8 {
|
||||||
|
RGB8, // RGB 8位每通道
|
||||||
|
RGBA8, // RGBA 8位每通道
|
||||||
|
R8, // 单通道(字体用)
|
||||||
|
Depth, // 深度缓冲
|
||||||
|
DepthStencil // 深度模板缓冲
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 纹理过滤模式
|
||||||
|
*/
|
||||||
|
enum class TextureFilter : uint8 {
|
||||||
|
Nearest, // 最近邻
|
||||||
|
Linear, // 线性
|
||||||
|
MipmapNearest, // 最近邻 Mipmap
|
||||||
|
MipmapLinear // 线性 Mipmap
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 纹理环绕模式
|
||||||
|
*/
|
||||||
|
enum class TextureWrap : uint8 {
|
||||||
|
Repeat, // 重复
|
||||||
|
Clamp, // 边缘钳制
|
||||||
|
Mirror // 镜像重复
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 纹理资源类
|
||||||
|
*
|
||||||
|
* 封装 OpenGL ES 3.2 纹理对象
|
||||||
|
*/
|
||||||
|
class Texture : public Resource {
|
||||||
|
public:
|
||||||
|
Texture();
|
||||||
|
~Texture() override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取资源类型
|
||||||
|
*/
|
||||||
|
ResourceType getType() const override { return ResourceType::Texture; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 创建空纹理
|
||||||
|
* @param width 纹理宽度
|
||||||
|
* @param height 纹理高度
|
||||||
|
* @param format 纹理格式
|
||||||
|
* @return 是否创建成功
|
||||||
|
*/
|
||||||
|
bool create(uint32 width, uint32 height, TextureFormat format);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 从文件加载纹理
|
||||||
|
* @param path 文件路径
|
||||||
|
* @return 是否加载成功
|
||||||
|
*/
|
||||||
|
bool loadFromFile(const std::string& path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 从内存加载纹理
|
||||||
|
* @param data 像素数据
|
||||||
|
* @param width 纹理宽度
|
||||||
|
* @param height 纹理高度
|
||||||
|
* @param format 纹理格式
|
||||||
|
* @return 是否加载成功
|
||||||
|
*/
|
||||||
|
bool loadFromMemory(const uint8* data, uint32 width, uint32 height, TextureFormat format);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 更新纹理部分区域
|
||||||
|
* @param x 起始X坐标
|
||||||
|
* @param y 起始Y坐标
|
||||||
|
* @param width 区域宽度
|
||||||
|
* @param height 区域高度
|
||||||
|
* @param data 像素数据
|
||||||
|
*/
|
||||||
|
void updateRegion(uint32 x, uint32 y, uint32 width, uint32 height, const uint8* data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 绑定纹理到指定槽位
|
||||||
|
* @param slot 纹理槽位(0-31)
|
||||||
|
*/
|
||||||
|
void bind(uint32 slot = 0) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 解绑纹理
|
||||||
|
*/
|
||||||
|
void unbind() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置纹理过滤模式
|
||||||
|
* @param minFilter 缩小过滤模式
|
||||||
|
* @param magFilter 放大过滤模式
|
||||||
|
*/
|
||||||
|
void setFilter(TextureFilter minFilter, TextureFilter magFilter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置纹理环绕模式
|
||||||
|
* @param wrapS S轴环绕模式
|
||||||
|
* @param wrapT T轴环绕模式
|
||||||
|
*/
|
||||||
|
void setWrap(TextureWrap wrapS, TextureWrap wrapT);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 生成 Mipmap
|
||||||
|
*/
|
||||||
|
void generateMipmap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取纹理宽度
|
||||||
|
*/
|
||||||
|
uint32 getWidth() const { return width_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取纹理高度
|
||||||
|
*/
|
||||||
|
uint32 getHeight() const { return height_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取纹理格式
|
||||||
|
*/
|
||||||
|
TextureFormat getFormat() const { return format_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取 OpenGL 纹理句柄
|
||||||
|
*/
|
||||||
|
GLuint getHandle() const { return handle_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
GLuint handle_ = 0; // OpenGL 纹理句柄
|
||||||
|
uint32 width_ = 0; // 纹理宽度
|
||||||
|
uint32 height_ = 0; // 纹理高度
|
||||||
|
TextureFormat format_ = TextureFormat::RGBA8; // 纹理格式
|
||||||
|
|
||||||
|
// 将 TextureFormat 转换为 OpenGL 格式
|
||||||
|
static GLint getGLInternalFormat(TextureFormat format);
|
||||||
|
static GLenum getGLFormat(TextureFormat format);
|
||||||
|
static GLenum getGLType(TextureFormat format);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -36,6 +36,8 @@ public:
|
||||||
T* release() { T* ret = p_; p_ = nullptr; return ret; }
|
T* release() { T* ret = p_; p_ = nullptr; return ret; }
|
||||||
void swap(Ptr<T>& r) noexcept { T* tmp = p_; p_ = r.p_; r.p_ = tmp; }
|
void swap(Ptr<T>& r) noexcept { T* tmp = p_; p_ = r.p_; r.p_ = tmp; }
|
||||||
|
|
||||||
|
uint32 refCount() const { return p_ ? p_->refCount() : 0; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
T* p_;
|
T* p_;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <types/base/types.h>
|
||||||
|
|
||||||
namespace extra2d {
|
namespace extra2d {
|
||||||
|
|
||||||
|
|
@ -25,7 +26,7 @@ protected:
|
||||||
RefCounted& operator=(const RefCounted&) { return *this; }
|
RefCounted& operator=(const RefCounted&) { return *this; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
mutable std::atomic<uint32> refCount_;
|
mutable std::atomic<uint32_t> refCount_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace extra2d
|
} // namespace extra2d
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,14 @@
|
||||||
#include <module/module_registry.h>
|
#include <module/module_registry.h>
|
||||||
#include <plugin/plugin_loader.h>
|
#include <plugin/plugin_loader.h>
|
||||||
#include <utils/timer_module.h>
|
#include <utils/timer_module.h>
|
||||||
|
#include <resource/resource_manager.h>
|
||||||
|
|
||||||
namespace extra2d {
|
namespace extra2d {
|
||||||
|
|
||||||
Context::Context()
|
Context::Context()
|
||||||
: pluginLoader_(std::make_unique<PluginLoader>()),
|
: pluginLoader_(std::make_unique<PluginLoader>()),
|
||||||
timerModule_(std::make_unique<TimerModule>()) {}
|
timerModule_(std::make_unique<TimerModule>()),
|
||||||
|
resourceManager_(std::make_unique<ResourceManager>()) {}
|
||||||
|
|
||||||
Context::~Context() {
|
Context::~Context() {
|
||||||
if (inited_) {
|
if (inited_) {
|
||||||
|
|
@ -31,6 +33,11 @@ bool Context::init() {
|
||||||
// 发送引擎初始化事件
|
// 发送引擎初始化事件
|
||||||
events::OnInit::emit();
|
events::OnInit::emit();
|
||||||
|
|
||||||
|
// 初始化资源管理器
|
||||||
|
if (!resourceManager_->init()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化定时器模块
|
// 初始化定时器模块
|
||||||
if (!timerModule_->init()) {
|
if (!timerModule_->init()) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -62,6 +69,9 @@ void Context::shutdown() {
|
||||||
// 发送引擎关闭事件
|
// 发送引擎关闭事件
|
||||||
events::OnShutdown::emit();
|
events::OnShutdown::emit();
|
||||||
|
|
||||||
|
// 关闭资源管理器
|
||||||
|
resourceManager_->shutdown();
|
||||||
|
|
||||||
inited_ = false;
|
inited_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,4 +100,6 @@ PluginLoader &Context::plugins() { return *pluginLoader_; }
|
||||||
|
|
||||||
TimerModule &Context::timer() { return *timerModule_; }
|
TimerModule &Context::timer() { return *timerModule_; }
|
||||||
|
|
||||||
|
ResourceManager &Context::resources() { return *resourceManager_; }
|
||||||
|
|
||||||
} // namespace extra2d
|
} // namespace extra2d
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,11 @@ void WindowModule::onModuleConfig(const AppConfig &config) {
|
||||||
cfg.resizable = config.resizable;
|
cfg.resizable = config.resizable;
|
||||||
|
|
||||||
if (create(cfg)) {
|
if (create(cfg)) {
|
||||||
|
// 创建 OpenGL 上下文
|
||||||
|
if (!createGLContext()) {
|
||||||
|
E2D_LOG_ERROR("Failed to create OpenGL context");
|
||||||
|
return;
|
||||||
|
}
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
#include <resource/audio.h>
|
||||||
|
#include <utils/logger.h>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
Audio::Audio() = default;
|
||||||
|
|
||||||
|
Audio::~Audio() {
|
||||||
|
stop();
|
||||||
|
if (chunk_) {
|
||||||
|
Mix_FreeChunk(chunk_);
|
||||||
|
chunk_ = nullptr;
|
||||||
|
}
|
||||||
|
if (music_) {
|
||||||
|
Mix_FreeMusic(music_);
|
||||||
|
music_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Audio::loadFromFile(const std::string& path, AudioType type) {
|
||||||
|
type_ = type;
|
||||||
|
|
||||||
|
if (type_ == AudioType::Sound) {
|
||||||
|
chunk_ = Mix_LoadWAV(path.c_str());
|
||||||
|
if (!chunk_) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to load sound: {} - {}", path.c_str(), Mix_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
music_ = Mix_LoadMUS(path.c_str());
|
||||||
|
if (!music_) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to load music: {} - {}", path.c_str(), Mix_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
name_ = path;
|
||||||
|
loaded_ = true;
|
||||||
|
Logger::log(LogLevel::Info, "Loaded audio: {}", path.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::play(int loops) {
|
||||||
|
if (type_ == AudioType::Sound && chunk_) {
|
||||||
|
channel_ = Mix_PlayChannel(-1, chunk_, loops);
|
||||||
|
if (channel_ == -1) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to play sound: {}", Mix_GetError());
|
||||||
|
}
|
||||||
|
} else if (type_ == AudioType::Music && music_) {
|
||||||
|
if (Mix_PlayMusic(music_, loops) == -1) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to play music: {}", Mix_GetError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::pause() {
|
||||||
|
if (type_ == AudioType::Sound && channel_ != -1) {
|
||||||
|
Mix_Pause(channel_);
|
||||||
|
} else if (type_ == AudioType::Music) {
|
||||||
|
Mix_PauseMusic();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::resume() {
|
||||||
|
if (type_ == AudioType::Sound && channel_ != -1) {
|
||||||
|
Mix_Resume(channel_);
|
||||||
|
} else if (type_ == AudioType::Music) {
|
||||||
|
Mix_ResumeMusic();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::stop() {
|
||||||
|
if (type_ == AudioType::Sound && channel_ != -1) {
|
||||||
|
Mix_HaltChannel(channel_);
|
||||||
|
channel_ = -1;
|
||||||
|
} else if (type_ == AudioType::Music) {
|
||||||
|
Mix_HaltMusic();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::setVolume(float volume) {
|
||||||
|
volume_ = std::max(0.0f, std::min(1.0f, volume));
|
||||||
|
int mixVolume = static_cast<int>(volume_ * MIX_MAX_VOLUME);
|
||||||
|
|
||||||
|
if (type_ == AudioType::Sound && chunk_) {
|
||||||
|
Mix_VolumeChunk(chunk_, mixVolume);
|
||||||
|
} else if (type_ == AudioType::Music) {
|
||||||
|
Mix_VolumeMusic(mixVolume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float Audio::getVolume() const {
|
||||||
|
return volume_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Audio::isPlaying() const {
|
||||||
|
if (type_ == AudioType::Sound && channel_ != -1) {
|
||||||
|
return Mix_Playing(channel_) != 0;
|
||||||
|
} else if (type_ == AudioType::Music) {
|
||||||
|
return Mix_PlayingMusic() != 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Audio::isPaused() const {
|
||||||
|
if (type_ == AudioType::Sound && channel_ != -1) {
|
||||||
|
return Mix_Paused(channel_) != 0;
|
||||||
|
} else if (type_ == AudioType::Music) {
|
||||||
|
return Mix_PausedMusic() != 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,340 @@
|
||||||
|
#include <resource/font.h>
|
||||||
|
#include <utils/logger.h>
|
||||||
|
#include <fstream>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
// 静态成员初始化
|
||||||
|
FT_Library Font::ftLibrary_ = nullptr;
|
||||||
|
bool Font::ftInitialized_ = false;
|
||||||
|
|
||||||
|
Font::Font() = default;
|
||||||
|
|
||||||
|
Font::~Font() {
|
||||||
|
if (face_) {
|
||||||
|
FT_Done_Face(face_);
|
||||||
|
face_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Font::initFreeType() {
|
||||||
|
if (ftInitialized_) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_Error error = FT_Init_FreeType(&ftLibrary_);
|
||||||
|
if (error) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to initialize FreeType library");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ftInitialized_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Font::loadFromFile(const std::string& path, const FontConfig& config) {
|
||||||
|
// 读取字体文件到内存
|
||||||
|
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to open font file: {}", path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::streamsize size = file.tellg();
|
||||||
|
file.seekg(0, std::ios::beg);
|
||||||
|
|
||||||
|
std::vector<uint8> data(size);
|
||||||
|
if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to read font file: {}", path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadFromMemory(data, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Font::loadFromMemory(const std::vector<uint8>& data, const FontConfig& config) {
|
||||||
|
if (!initFreeType()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
config_ = config;
|
||||||
|
fontData_ = data; // 保存字体数据
|
||||||
|
|
||||||
|
// 从内存加载字体
|
||||||
|
FT_Error error = FT_New_Memory_Face(ftLibrary_,
|
||||||
|
fontData_.data(),
|
||||||
|
static_cast<FT_Long>(fontData_.size()),
|
||||||
|
0, &face_);
|
||||||
|
if (error) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to load font from memory");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置字体大小
|
||||||
|
error = FT_Set_Pixel_Sizes(face_, 0, config_.fontSize);
|
||||||
|
if (error) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to set font size");
|
||||||
|
FT_Done_Face(face_);
|
||||||
|
face_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建字体图集
|
||||||
|
atlas_ = makePtr<FontAtlas>();
|
||||||
|
if (!atlas_->init()) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to create font atlas");
|
||||||
|
FT_Done_Face(face_);
|
||||||
|
face_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预加载常用字符
|
||||||
|
if (config_.preloadCommon) {
|
||||||
|
preloadCharset(CommonCharset::ASCII);
|
||||||
|
preloadCharset(CommonCharset::PUNCTUATION_EN);
|
||||||
|
preloadCharset(CommonCharset::PUNCTUATION_CN);
|
||||||
|
preloadCharset(CommonCharset::CHINESE_COMMON);
|
||||||
|
}
|
||||||
|
|
||||||
|
name_ = "Font";
|
||||||
|
loaded_ = true;
|
||||||
|
Logger::log(LogLevel::Info, "Loaded font with size {}", config_.fontSize);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GlyphInfo* Font::getGlyph(uint32 codepoint) {
|
||||||
|
if (!atlas_) return nullptr;
|
||||||
|
|
||||||
|
// 检查是否已缓存
|
||||||
|
const GlyphInfo* cached = atlas_->getGlyph(codepoint);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染字符
|
||||||
|
uint32 width, height;
|
||||||
|
float bearingX, bearingY, advance;
|
||||||
|
std::vector<uint8> bitmap;
|
||||||
|
|
||||||
|
if (!renderGlyph(codepoint, width, height, bearingX, bearingY, advance, bitmap)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到图集
|
||||||
|
if (!atlas_->addGlyph(codepoint, bitmap.data(), width, height,
|
||||||
|
bearingX, bearingY, advance)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新纹理
|
||||||
|
atlas_->updateTextures();
|
||||||
|
|
||||||
|
return atlas_->getGlyph(codepoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Font::renderGlyph(uint32 codepoint, uint32& outWidth, uint32& outHeight,
|
||||||
|
float& outBearingX, float& outBearingY, float& outAdvance,
|
||||||
|
std::vector<uint8>& outBitmap) {
|
||||||
|
if (!face_) return false;
|
||||||
|
|
||||||
|
// 加载字符
|
||||||
|
FT_UInt glyphIndex = FT_Get_Char_Index(face_, codepoint);
|
||||||
|
if (glyphIndex == 0) {
|
||||||
|
// 字符不存在于字体中
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_Int32 loadFlags = FT_LOAD_RENDER;
|
||||||
|
if (!config_.hinting) {
|
||||||
|
loadFlags |= FT_LOAD_NO_HINTING;
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_Error error = FT_Load_Glyph(face_, glyphIndex, loadFlags);
|
||||||
|
if (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_GlyphSlot slot = face_->glyph;
|
||||||
|
|
||||||
|
// 获取位图信息
|
||||||
|
outWidth = slot->bitmap.width;
|
||||||
|
outHeight = slot->bitmap.rows;
|
||||||
|
outBearingX = static_cast<float>(slot->bitmap_left);
|
||||||
|
outBearingY = static_cast<float>(slot->bitmap_top);
|
||||||
|
outAdvance = static_cast<float>(slot->advance.x >> 6); // 转换为像素
|
||||||
|
|
||||||
|
// 复制位图数据
|
||||||
|
if (outWidth > 0 && outHeight > 0) {
|
||||||
|
outBitmap.resize(outWidth * outHeight);
|
||||||
|
|
||||||
|
// FreeType 位图可能是灰度或单色
|
||||||
|
if (slot->bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) {
|
||||||
|
for (uint32 y = 0; y < outHeight; ++y) {
|
||||||
|
std::memcpy(&outBitmap[y * outWidth],
|
||||||
|
&slot->bitmap.buffer[y * slot->bitmap.pitch],
|
||||||
|
outWidth);
|
||||||
|
}
|
||||||
|
} else if (slot->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) {
|
||||||
|
// 单色位图需要展开
|
||||||
|
for (uint32 y = 0; y < outHeight; ++y) {
|
||||||
|
for (uint32 x = 0; x < outWidth; ++x) {
|
||||||
|
uint32 byteIndex = x / 8;
|
||||||
|
uint32 bitIndex = 7 - (x % 8);
|
||||||
|
uint8 byte = slot->bitmap.buffer[y * slot->bitmap.pitch + byteIndex];
|
||||||
|
outBitmap[y * outWidth + x] = (byte & (1 << bitIndex)) ? 255 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec2 Font::measureText(const std::string& text, float fontSize) {
|
||||||
|
if (!atlas_) return Vec2(0, 0);
|
||||||
|
|
||||||
|
float scale = (fontSize > 0) ? (fontSize / config_.fontSize) : 1.0f;
|
||||||
|
float lineHeight = getLineHeight(fontSize);
|
||||||
|
|
||||||
|
std::vector<uint32> codepoints = utf8ToCodepoints(text);
|
||||||
|
|
||||||
|
float width = 0.0f;
|
||||||
|
float maxWidth = 0.0f;
|
||||||
|
float height = lineHeight;
|
||||||
|
uint32 prevCodepoint = 0;
|
||||||
|
|
||||||
|
for (uint32 codepoint : codepoints) {
|
||||||
|
if (codepoint == '\n') {
|
||||||
|
maxWidth = std::max(maxWidth, width);
|
||||||
|
width = 0.0f;
|
||||||
|
height += lineHeight;
|
||||||
|
prevCodepoint = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GlyphInfo* glyph = getGlyph(codepoint);
|
||||||
|
if (!glyph) {
|
||||||
|
prevCodepoint = codepoint;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加字距调整
|
||||||
|
if (prevCodepoint != 0) {
|
||||||
|
width += getKerning(prevCodepoint, codepoint) * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
width += glyph->advance * scale;
|
||||||
|
prevCodepoint = codepoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
maxWidth = std::max(maxWidth, width);
|
||||||
|
return Vec2(maxWidth, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
float Font::getKerning(uint32 codepoint1, uint32 codepoint2) {
|
||||||
|
if (!face_) return 0.0f;
|
||||||
|
|
||||||
|
FT_UInt index1 = FT_Get_Char_Index(face_, codepoint1);
|
||||||
|
FT_UInt index2 = FT_Get_Char_Index(face_, codepoint2);
|
||||||
|
|
||||||
|
FT_Vector kerning;
|
||||||
|
FT_Error error = FT_Get_Kerning(face_, index1, index2, FT_KERNING_DEFAULT, &kerning);
|
||||||
|
if (error) {
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<float>(kerning.x >> 6); // 转换为像素
|
||||||
|
}
|
||||||
|
|
||||||
|
float Font::getLineHeight(float fontSize) const {
|
||||||
|
if (!face_) return 0.0f;
|
||||||
|
|
||||||
|
float scale = (fontSize > 0) ? (fontSize / config_.fontSize) : 1.0f;
|
||||||
|
return static_cast<float>(face_->size->metrics.height >> 6) * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Font::preloadCharset(const std::string& charset) {
|
||||||
|
std::vector<uint32> codepoints = utf8ToCodepoints(charset);
|
||||||
|
|
||||||
|
for (uint32 codepoint : codepoints) {
|
||||||
|
getGlyph(codepoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量更新纹理
|
||||||
|
if (atlas_) {
|
||||||
|
atlas_->updateTextures();
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::log(LogLevel::Info, "Preloaded {} glyphs", codepoints.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Font::getMetrics(float& outAscender, float& outDescender, float& outLineHeight) const {
|
||||||
|
if (!face_) {
|
||||||
|
outAscender = outDescender = outLineHeight = 0.0f;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
outAscender = static_cast<float>(face_->size->metrics.ascender >> 6);
|
||||||
|
outDescender = static_cast<float>(face_->size->metrics.descender >> 6);
|
||||||
|
outLineHeight = static_cast<float>(face_->size->metrics.height >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint32> Font::utf8ToCodepoints(const std::string& text) {
|
||||||
|
std::vector<uint32> codepoints;
|
||||||
|
codepoints.reserve(text.length());
|
||||||
|
|
||||||
|
const uint8* data = reinterpret_cast<const uint8*>(text.data());
|
||||||
|
size_t length = text.length();
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
while (i < length) {
|
||||||
|
uint32 codepoint = 0;
|
||||||
|
uint8 byte = data[i];
|
||||||
|
|
||||||
|
if ((byte & 0x80) == 0) {
|
||||||
|
// 单字节 ASCII
|
||||||
|
codepoint = byte;
|
||||||
|
i += 1;
|
||||||
|
} else if ((byte & 0xE0) == 0xC0) {
|
||||||
|
// 2 字节序列
|
||||||
|
if (i + 1 < length) {
|
||||||
|
codepoint = ((byte & 0x1F) << 6) | (data[i + 1] & 0x3F);
|
||||||
|
i += 2;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if ((byte & 0xF0) == 0xE0) {
|
||||||
|
// 3 字节序列
|
||||||
|
if (i + 2 < length) {
|
||||||
|
codepoint = ((byte & 0x0F) << 12) |
|
||||||
|
((data[i + 1] & 0x3F) << 6) |
|
||||||
|
(data[i + 2] & 0x3F);
|
||||||
|
i += 3;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if ((byte & 0xF8) == 0xF0) {
|
||||||
|
// 4 字节序列
|
||||||
|
if (i + 3 < length) {
|
||||||
|
codepoint = ((byte & 0x07) << 18) |
|
||||||
|
((data[i + 1] & 0x3F) << 12) |
|
||||||
|
((data[i + 2] & 0x3F) << 6) |
|
||||||
|
(data[i + 3] & 0x3F);
|
||||||
|
i += 4;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 无效的 UTF-8 序列,跳过
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
codepoints.push_back(codepoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return codepoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
#include <resource/font_atlas.h>
|
||||||
|
|
||||||
|
#define STB_RECT_PACK_IMPLEMENTATION
|
||||||
|
#include <stb/stb_rect_pack.h>
|
||||||
|
#include <utils/logger.h>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
// AtlasPage 实现
|
||||||
|
|
||||||
|
AtlasPage::AtlasPage() = default;
|
||||||
|
|
||||||
|
bool AtlasPage::init(uint32 size) {
|
||||||
|
width = size;
|
||||||
|
height = size;
|
||||||
|
|
||||||
|
// 创建纹理
|
||||||
|
texture = makePtr<Texture>();
|
||||||
|
if (!texture->create(width, height, TextureFormat::R8)) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to create font atlas texture");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置纹理过滤为最近邻,避免字体模糊
|
||||||
|
texture->setFilter(TextureFilter::Nearest, TextureFilter::Nearest);
|
||||||
|
|
||||||
|
// 初始化像素数据(清零)
|
||||||
|
pixelData.resize(width * height, 0);
|
||||||
|
|
||||||
|
// 初始化矩形打包上下文
|
||||||
|
// 节点数量至少为宽度,以保证最佳打包效果
|
||||||
|
packNodes.resize(width);
|
||||||
|
stbrp_init_target(&packContext, width, height, packNodes.data(), width);
|
||||||
|
|
||||||
|
dirty = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AtlasPage::packRect(uint32 w, uint32 h, uint32& outX, uint32& outY) {
|
||||||
|
stbrp_rect rect;
|
||||||
|
rect.id = 0;
|
||||||
|
rect.w = static_cast<stbrp_coord>(w);
|
||||||
|
rect.h = static_cast<stbrp_coord>(h);
|
||||||
|
rect.x = 0;
|
||||||
|
rect.y = 0;
|
||||||
|
rect.was_packed = 0;
|
||||||
|
|
||||||
|
int result = stbrp_pack_rects(&packContext, &rect, 1);
|
||||||
|
if (!rect.was_packed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
outX = static_cast<uint32>(rect.x);
|
||||||
|
outY = static_cast<uint32>(rect.y);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AtlasPage::writePixels(uint32 x, uint32 y, uint32 w, uint32 h, const uint8* data) {
|
||||||
|
if (!data || x + w > width || y + h > height) return;
|
||||||
|
|
||||||
|
// 逐行复制像素数据
|
||||||
|
for (uint32 row = 0; row < h; ++row) {
|
||||||
|
uint32 dstY = y + row;
|
||||||
|
uint32 srcY = row;
|
||||||
|
std::memcpy(&pixelData[dstY * width + x], &data[srcY * w], w);
|
||||||
|
}
|
||||||
|
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AtlasPage::updateTexture() {
|
||||||
|
if (!dirty || !texture) return;
|
||||||
|
|
||||||
|
texture->updateRegion(0, 0, width, height, pixelData.data());
|
||||||
|
dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AtlasPage::clear() {
|
||||||
|
if (!pixelData.empty()) {
|
||||||
|
std::fill(pixelData.begin(), pixelData.end(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新初始化打包上下文
|
||||||
|
if (!packNodes.empty()) {
|
||||||
|
stbrp_init_target(&packContext, width, height, packNodes.data(), width);
|
||||||
|
}
|
||||||
|
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FontAtlas 实现
|
||||||
|
|
||||||
|
FontAtlas::FontAtlas() = default;
|
||||||
|
|
||||||
|
FontAtlas::~FontAtlas() = default;
|
||||||
|
|
||||||
|
bool FontAtlas::init(uint32 pageSize) {
|
||||||
|
pageSize_ = pageSize;
|
||||||
|
|
||||||
|
// 创建第一页
|
||||||
|
auto page = std::make_unique<AtlasPage>();
|
||||||
|
if (!page->init(pageSize_)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pages_.push_back(std::move(page));
|
||||||
|
loaded_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FontAtlas::addGlyph(uint32 codepoint, const uint8* bitmap, uint32 width, uint32 height,
|
||||||
|
float bearingX, float bearingY, float advance) {
|
||||||
|
if (!bitmap || width == 0 || height == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已存在
|
||||||
|
if (hasGlyph(codepoint)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试在所有现有页面中打包
|
||||||
|
uint32 pageIndex = 0;
|
||||||
|
uint32 x = 0, y = 0;
|
||||||
|
bool packed = false;
|
||||||
|
|
||||||
|
for (uint32 i = 0; i < pages_.size(); ++i) {
|
||||||
|
if (pages_[i]->packRect(width, height, x, y)) {
|
||||||
|
pageIndex = i;
|
||||||
|
packed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果所有页面都满了,创建新页面
|
||||||
|
if (!packed) {
|
||||||
|
auto newPage = std::make_unique<AtlasPage>();
|
||||||
|
if (!newPage->init(pageSize_)) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to create new atlas page");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newPage->packRect(width, height, x, y)) {
|
||||||
|
Logger::log(LogLevel::Error, "Glyph too large for atlas page: {}x{}", width, height);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pageIndex = static_cast<uint32>(pages_.size());
|
||||||
|
pages_.push_back(std::move(newPage));
|
||||||
|
packed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入像素数据
|
||||||
|
pages_[pageIndex]->writePixels(x, y, width, height, bitmap);
|
||||||
|
|
||||||
|
// 计算 UV 坐标
|
||||||
|
float invWidth = 1.0f / pages_[pageIndex]->width;
|
||||||
|
float invHeight = 1.0f / pages_[pageIndex]->height;
|
||||||
|
|
||||||
|
GlyphInfo info;
|
||||||
|
info.uv0 = Vec2(x * invWidth, y * invHeight);
|
||||||
|
info.uv1 = Vec2((x + width) * invWidth, (y + height) * invHeight);
|
||||||
|
info.size = Vec2(static_cast<float>(width), static_cast<float>(height));
|
||||||
|
info.bearing = Vec2(bearingX, bearingY);
|
||||||
|
info.advance = advance;
|
||||||
|
info.pageIndex = pageIndex;
|
||||||
|
info.valid = true;
|
||||||
|
|
||||||
|
glyphs_[codepoint] = info;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GlyphInfo* FontAtlas::getGlyph(uint32 codepoint) const {
|
||||||
|
auto it = glyphs_.find(codepoint);
|
||||||
|
if (it != glyphs_.end()) {
|
||||||
|
return &it->second;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FontAtlas::hasGlyph(uint32 codepoint) const {
|
||||||
|
return glyphs_.find(codepoint) != glyphs_.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
Texture* FontAtlas::getPageTexture(uint32 pageIndex) const {
|
||||||
|
if (pageIndex < pages_.size()) {
|
||||||
|
return pages_[pageIndex]->texture.get();
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FontAtlas::updateTextures() {
|
||||||
|
for (auto& page : pages_) {
|
||||||
|
page->updateTexture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FontAtlas::clear() {
|
||||||
|
glyphs_.clear();
|
||||||
|
for (auto& page : pages_) {
|
||||||
|
page->clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AtlasPage* FontAtlas::getOrCreatePage() {
|
||||||
|
// 尝试找到一个有空间的页面
|
||||||
|
for (auto& page : pages_) {
|
||||||
|
// 这里简化处理,实际应该检查剩余空间
|
||||||
|
return page.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新页面
|
||||||
|
auto newPage = std::make_unique<AtlasPage>();
|
||||||
|
if (newPage->init(pageSize_)) {
|
||||||
|
AtlasPage* ptr = newPage.get();
|
||||||
|
pages_.push_back(std::move(newPage));
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
#include <resource/material.h>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
Material::Material() = default;
|
||||||
|
|
||||||
|
Material::~Material() = default;
|
||||||
|
|
||||||
|
void Material::setShader(Ptr<Shader> shader) {
|
||||||
|
shader_ = shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Material::setBool(const std::string& name, bool value) {
|
||||||
|
properties_[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Material::setInt(const std::string& name, int value) {
|
||||||
|
properties_[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Material::setFloat(const std::string& name, float value) {
|
||||||
|
properties_[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Material::setVec2(const std::string& name, const Vec2& value) {
|
||||||
|
properties_[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Material::setVec3(const std::string& name, const Vec3& value) {
|
||||||
|
properties_[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Material::setColor(const std::string& name, const Color& value) {
|
||||||
|
properties_[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Material::setTexture(const std::string& name, Ptr<Texture> texture) {
|
||||||
|
properties_[name] = texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Material::apply() {
|
||||||
|
if (!shader_) return;
|
||||||
|
|
||||||
|
shader_->use();
|
||||||
|
applyProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Material::applyProperties() {
|
||||||
|
if (!shader_) return;
|
||||||
|
|
||||||
|
textureUnit_ = 0;
|
||||||
|
|
||||||
|
for (const auto& [name, prop] : properties_) {
|
||||||
|
std::visit([this, &name](auto&& value) {
|
||||||
|
using T = std::decay_t<decltype(value)>;
|
||||||
|
|
||||||
|
if constexpr (std::is_same_v<T, bool>) {
|
||||||
|
shader_->setBool(name, value);
|
||||||
|
} else if constexpr (std::is_same_v<T, int>) {
|
||||||
|
shader_->setInt(name, value);
|
||||||
|
} else if constexpr (std::is_same_v<T, float>) {
|
||||||
|
shader_->setFloat(name, value);
|
||||||
|
} else if constexpr (std::is_same_v<T, Vec2>) {
|
||||||
|
shader_->setVec2(name, value);
|
||||||
|
} else if constexpr (std::is_same_v<T, Vec3>) {
|
||||||
|
shader_->setVec3(name, value);
|
||||||
|
} else if constexpr (std::is_same_v<T, Color>) {
|
||||||
|
shader_->setColor(name, value);
|
||||||
|
} else if constexpr (std::is_same_v<T, Ptr<Texture>>) {
|
||||||
|
if (value) {
|
||||||
|
value->bind(textureUnit_);
|
||||||
|
shader_->setInt(name, textureUnit_);
|
||||||
|
textureUnit_++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ptr<Material> Material::clone() const {
|
||||||
|
Ptr<Material> newMat = makePtr<Material>();
|
||||||
|
newMat->setShader(shader_);
|
||||||
|
newMat->properties_ = properties_;
|
||||||
|
return newMat;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
#include <resource/resource_manager.h>
|
||||||
|
#include <utils/logger.h>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
ResourceManager::ResourceManager() = default;
|
||||||
|
|
||||||
|
ResourceManager::~ResourceManager() = default;
|
||||||
|
|
||||||
|
bool ResourceManager::init() {
|
||||||
|
Logger::log(LogLevel::Info, "Initializing ResourceManager...");
|
||||||
|
|
||||||
|
// 注意:默认资源延迟到首次访问时创建,以确保 OpenGL 上下文已就绪
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResourceManager::shutdown() {
|
||||||
|
Logger::log(LogLevel::Info, "Shutting down ResourceManager...");
|
||||||
|
|
||||||
|
// 清理所有资源
|
||||||
|
texts_.clear();
|
||||||
|
materials_.clear();
|
||||||
|
audioCache_.clear();
|
||||||
|
fontCache_.clear();
|
||||||
|
shaderCache_.clear();
|
||||||
|
textureCache_.clear();
|
||||||
|
|
||||||
|
// 清理默认资源
|
||||||
|
defaultFont_.reset();
|
||||||
|
defaultMaterial_.reset();
|
||||||
|
defaultShader_.reset();
|
||||||
|
defaultTexture_.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ptr<Texture> ResourceManager::getTexture(const std::string& path) {
|
||||||
|
return textureCache_.get(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ptr<Texture> ResourceManager::createTexture(uint32 width, uint32 height, TextureFormat format) {
|
||||||
|
Ptr<Texture> texture = makePtr<Texture>();
|
||||||
|
if (texture->create(width, height, format)) {
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ptr<Shader> ResourceManager::getShader(const std::string& vsPath, const std::string& fsPath) {
|
||||||
|
std::string key = vsPath + "|" + fsPath;
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
auto it = shaderCache_.find(key);
|
||||||
|
if (it != shaderCache_.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接加载
|
||||||
|
Ptr<Shader> shader = makePtr<Shader>();
|
||||||
|
if (shader->loadFromFile(vsPath, fsPath)) {
|
||||||
|
shaderCache_[key] = shader;
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ptr<Shader> ResourceManager::getShaderFromSource(const std::string& name,
|
||||||
|
const std::string& vsSource,
|
||||||
|
const std::string& fsSource) {
|
||||||
|
Ptr<Shader> shader = makePtr<Shader>();
|
||||||
|
if (shader->loadFromSource(vsSource, fsSource)) {
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ptr<Material> ResourceManager::createMaterial(Ptr<Shader> shader) {
|
||||||
|
Ptr<Material> material = makePtr<Material>();
|
||||||
|
material->setShader(shader);
|
||||||
|
return material;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ptr<Material> ResourceManager::getMaterial(const std::string& name) {
|
||||||
|
auto it = materials_.find(name);
|
||||||
|
if (it != materials_.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResourceManager::registerMaterial(const std::string& name, Ptr<Material> material) {
|
||||||
|
materials_[name] = material;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ptr<Font> ResourceManager::getFont(const std::string& path, const FontConfig& config) {
|
||||||
|
// 检查缓存
|
||||||
|
auto it = fontCache_.find(path);
|
||||||
|
if (it != fontCache_.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接加载
|
||||||
|
Ptr<Font> font = makePtr<Font>();
|
||||||
|
if (font->loadFromFile(path, config)) {
|
||||||
|
fontCache_[path] = font;
|
||||||
|
return font;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ptr<Text> ResourceManager::createText(Ptr<Font> font, const std::string& content) {
|
||||||
|
Ptr<Text> text = makePtr<Text>();
|
||||||
|
text->setFont(font);
|
||||||
|
text->setText(content);
|
||||||
|
texts_.push_back(text);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ptr<Audio> ResourceManager::getAudio(const std::string& path, AudioType type) {
|
||||||
|
// 检查缓存
|
||||||
|
auto it = audioCache_.find(path);
|
||||||
|
if (it != audioCache_.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接加载
|
||||||
|
Ptr<Audio> audio = makePtr<Audio>();
|
||||||
|
if (audio->loadFromFile(path, type)) {
|
||||||
|
audioCache_[path] = audio;
|
||||||
|
return audio;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ptr<Texture> ResourceManager::getDefaultTexture() {
|
||||||
|
if (!defaultResourcesCreated_) {
|
||||||
|
createDefaultResources();
|
||||||
|
}
|
||||||
|
return defaultTexture_;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ptr<Shader> ResourceManager::getDefaultShader() {
|
||||||
|
if (!defaultResourcesCreated_) {
|
||||||
|
createDefaultResources();
|
||||||
|
}
|
||||||
|
return defaultShader_;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ptr<Material> ResourceManager::getDefaultMaterial() {
|
||||||
|
if (!defaultResourcesCreated_) {
|
||||||
|
createDefaultResources();
|
||||||
|
}
|
||||||
|
return defaultMaterial_;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ptr<Font> ResourceManager::getDefaultFont() {
|
||||||
|
if (!defaultResourcesCreated_) {
|
||||||
|
createDefaultResources();
|
||||||
|
}
|
||||||
|
return defaultFont_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResourceManager::collectGarbage() {
|
||||||
|
// 清理未引用的文本对象
|
||||||
|
texts_.erase(
|
||||||
|
std::remove_if(texts_.begin(), texts_.end(),
|
||||||
|
[](const Ptr<Text>& text) { return text.refCount() <= 1; }),
|
||||||
|
texts_.end()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceManager::Stats ResourceManager::getStats() const {
|
||||||
|
Stats stats;
|
||||||
|
stats.textureCount = textureCache_.size();
|
||||||
|
stats.shaderCount = shaderCache_.size();
|
||||||
|
stats.materialCount = materials_.size();
|
||||||
|
stats.fontCount = fontCache_.size();
|
||||||
|
stats.textCount = texts_.size();
|
||||||
|
stats.audioCount = audioCache_.size();
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResourceManager::createDefaultResources() {
|
||||||
|
if (defaultResourcesCreated_) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建默认纹理(1x1 白色像素)
|
||||||
|
defaultTexture_ = makePtr<Texture>();
|
||||||
|
uint8 whitePixel[4] = {255, 255, 255, 255};
|
||||||
|
if (!defaultTexture_->loadFromMemory(whitePixel, 1, 1, TextureFormat::RGBA8)) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to create default texture");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建默认着色器
|
||||||
|
defaultShader_ = makePtr<Shader>();
|
||||||
|
const char* vsSource = R"(
|
||||||
|
layout(location = 0) in vec2 aPosition;
|
||||||
|
layout(location = 1) in vec2 aTexCoord;
|
||||||
|
layout(location = 2) in vec4 aColor;
|
||||||
|
uniform mat4 uMVP;
|
||||||
|
out vec2 vTexCoord;
|
||||||
|
out vec4 vColor;
|
||||||
|
void main() {
|
||||||
|
gl_Position = uMVP * vec4(aPosition, 0.0, 1.0);
|
||||||
|
vTexCoord = aTexCoord;
|
||||||
|
vColor = aColor;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* fsSource = R"(
|
||||||
|
in vec2 vTexCoord;
|
||||||
|
in vec4 vColor;
|
||||||
|
uniform sampler2D uTexture;
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main() {
|
||||||
|
fragColor = vColor * texture(uTexture, vTexCoord);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
if (!defaultShader_->loadFromSource(vsSource, fsSource)) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to create default shader");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建默认材质
|
||||||
|
defaultMaterial_ = makePtr<Material>();
|
||||||
|
defaultMaterial_->setShader(defaultShader_);
|
||||||
|
defaultMaterial_->setTexture("uTexture", defaultTexture_);
|
||||||
|
defaultMaterial_->setColor("uColor", Color::White);
|
||||||
|
|
||||||
|
defaultResourcesCreated_ = true;
|
||||||
|
Logger::log(LogLevel::Info, "Default resources created successfully");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
#include <resource/shader.h>
|
||||||
|
#include <utils/logger.h>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
Shader::Shader() = default;
|
||||||
|
|
||||||
|
Shader::~Shader() {
|
||||||
|
if (program_ != 0) {
|
||||||
|
glDeleteProgram(program_);
|
||||||
|
program_ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Shader::loadFromFile(const std::string& vsPath, const std::string& fsPath) {
|
||||||
|
// 读取顶点着色器
|
||||||
|
std::ifstream vsFile(vsPath);
|
||||||
|
if (!vsFile.is_open()) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to open vertex shader: {}", vsPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::stringstream vsStream;
|
||||||
|
vsStream << vsFile.rdbuf();
|
||||||
|
std::string vsSource = vsStream.str();
|
||||||
|
|
||||||
|
// 读取片段着色器
|
||||||
|
std::ifstream fsFile(fsPath);
|
||||||
|
if (!fsFile.is_open()) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to open fragment shader: {}", fsPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::stringstream fsStream;
|
||||||
|
fsStream << fsFile.rdbuf();
|
||||||
|
std::string fsSource = fsStream.str();
|
||||||
|
|
||||||
|
return loadFromSource(vsSource, fsSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Shader::loadFromSource(const std::string& vsSource, const std::string& fsSource) {
|
||||||
|
// 删除旧程序
|
||||||
|
if (program_ != 0) {
|
||||||
|
glDeleteProgram(program_);
|
||||||
|
program_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编译顶点着色器
|
||||||
|
GLuint vs = compileShader(GL_VERTEX_SHADER, vsSource);
|
||||||
|
if (vs == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编译片段着色器
|
||||||
|
GLuint fs = compileShader(GL_FRAGMENT_SHADER, fsSource);
|
||||||
|
if (fs == 0) {
|
||||||
|
glDeleteShader(vs);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 链接着色器程序
|
||||||
|
program_ = glCreateProgram();
|
||||||
|
glAttachShader(program_, vs);
|
||||||
|
glAttachShader(program_, fs);
|
||||||
|
glLinkProgram(program_);
|
||||||
|
|
||||||
|
// 检查链接状态
|
||||||
|
GLint success;
|
||||||
|
glGetProgramiv(program_, GL_LINK_STATUS, &success);
|
||||||
|
if (!success) {
|
||||||
|
char infoLog[512];
|
||||||
|
glGetProgramInfoLog(program_, 512, nullptr, infoLog);
|
||||||
|
Logger::log(LogLevel::Error, "Shader program linking failed: {}", infoLog);
|
||||||
|
glDeleteProgram(program_);
|
||||||
|
program_ = 0;
|
||||||
|
glDeleteShader(vs);
|
||||||
|
glDeleteShader(fs);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理着色器对象
|
||||||
|
glDeleteShader(vs);
|
||||||
|
glDeleteShader(fs);
|
||||||
|
|
||||||
|
// 清空 Uniform 缓存
|
||||||
|
uniformCache_.clear();
|
||||||
|
|
||||||
|
loaded_ = true;
|
||||||
|
Logger::log(LogLevel::Info, "Shader program created successfully");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader::use() const {
|
||||||
|
if (program_ != 0) {
|
||||||
|
glUseProgram(program_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader::unuse() const {
|
||||||
|
glUseProgram(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader::setBool(const std::string& name, bool value) {
|
||||||
|
glUniform1i(getUniformLocation(name), value ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader::setInt(const std::string& name, int value) {
|
||||||
|
glUniform1i(getUniformLocation(name), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader::setFloat(const std::string& name, float value) {
|
||||||
|
glUniform1f(getUniformLocation(name), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader::setVec2(const std::string& name, const Vec2& value) {
|
||||||
|
glUniform2f(getUniformLocation(name), value.x, value.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader::setVec3(const std::string& name, const Vec3& value) {
|
||||||
|
glUniform3f(getUniformLocation(name), value.x, value.y, value.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader::setColor(const std::string& name, const Color& value) {
|
||||||
|
glUniform4f(getUniformLocation(name), value.r, value.g, value.b, value.a);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader::setMat4(const std::string& name, const float* value) {
|
||||||
|
glUniformMatrix4fv(getUniformLocation(name), 1, GL_FALSE, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint Shader::compileShader(GLenum type, const std::string& source) {
|
||||||
|
GLuint shader = glCreateShader(type);
|
||||||
|
|
||||||
|
// 添加 OpenGL ES 3.2 版本声明
|
||||||
|
std::string fullSource;
|
||||||
|
if (source.find("#version") == std::string::npos) {
|
||||||
|
fullSource = "#version 320 es\n";
|
||||||
|
if (type == GL_FRAGMENT_SHADER) {
|
||||||
|
fullSource += "precision mediump float;\n";
|
||||||
|
}
|
||||||
|
fullSource += source;
|
||||||
|
} else {
|
||||||
|
fullSource = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* src = fullSource.c_str();
|
||||||
|
glShaderSource(shader, 1, &src, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
|
||||||
|
// 检查编译状态
|
||||||
|
GLint success;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
|
||||||
|
if (!success) {
|
||||||
|
char infoLog[512];
|
||||||
|
glGetShaderInfoLog(shader, 512, nullptr, infoLog);
|
||||||
|
Logger::log(LogLevel::Error, "Shader compilation failed: {}", infoLog);
|
||||||
|
glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint Shader::getUniformLocation(const std::string& name) {
|
||||||
|
if (program_ == 0) return -1;
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
auto it = uniformCache_.find(name);
|
||||||
|
if (it != uniformCache_.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询 Uniform 位置
|
||||||
|
GLint location = glGetUniformLocation(program_, name.c_str());
|
||||||
|
uniformCache_[name] = location;
|
||||||
|
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
#include <resource/text.h>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
Text::Text() = default;
|
||||||
|
|
||||||
|
Text::~Text() = default;
|
||||||
|
|
||||||
|
void Text::setFont(Ptr<Font> font) {
|
||||||
|
font_ = font;
|
||||||
|
dirty_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Text::setText(const std::string& text) {
|
||||||
|
text_ = text;
|
||||||
|
dirty_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Text::setFontSize(float size) {
|
||||||
|
fontSize_ = size;
|
||||||
|
dirty_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Text::setColor(const Color& color) {
|
||||||
|
color_ = color;
|
||||||
|
dirty_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Text::setAlign(TextAlign align) {
|
||||||
|
align_ = align;
|
||||||
|
dirty_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Text::setMaxWidth(float width) {
|
||||||
|
maxWidth_ = width;
|
||||||
|
dirty_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Text::setLineSpacing(float spacing) {
|
||||||
|
lineSpacing_ = spacing;
|
||||||
|
dirty_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Text::rebuild() {
|
||||||
|
if (!dirty_) return;
|
||||||
|
buildVertices();
|
||||||
|
dirty_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Text::buildVertices() {
|
||||||
|
lines_.clear();
|
||||||
|
if (!font_ || text_.empty()) {
|
||||||
|
size_ = Vec2(0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float scale = fontSize_ / font_->getConfig().fontSize;
|
||||||
|
float lineHeight = font_->getLineHeight(fontSize_);
|
||||||
|
|
||||||
|
// 解析 UTF-8 文本
|
||||||
|
std::vector<uint32_t> codepoints;
|
||||||
|
const uint8_t* data = reinterpret_cast<const uint8_t*>(text_.data());
|
||||||
|
size_t len = text_.length();
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
while (i < len) {
|
||||||
|
uint32_t codepoint = 0;
|
||||||
|
uint8_t byte = data[i];
|
||||||
|
|
||||||
|
if ((byte & 0x80) == 0) {
|
||||||
|
codepoint = byte;
|
||||||
|
i++;
|
||||||
|
} else if ((byte & 0xE0) == 0xC0 && i + 1 < len) {
|
||||||
|
codepoint = ((byte & 0x1F) << 6) | (data[i + 1] & 0x3F);
|
||||||
|
i += 2;
|
||||||
|
} else if ((byte & 0xF0) == 0xE0 && i + 2 < len) {
|
||||||
|
codepoint = ((byte & 0x0F) << 12) | ((data[i + 1] & 0x3F) << 6) | (data[i + 2] & 0x3F);
|
||||||
|
i += 3;
|
||||||
|
} else if ((byte & 0xF8) == 0xF0 && i + 3 < len) {
|
||||||
|
codepoint = ((byte & 0x07) << 18) | ((data[i + 1] & 0x3F) << 12) |
|
||||||
|
((data[i + 2] & 0x3F) << 6) | (data[i + 3] & 0x3F);
|
||||||
|
i += 4;
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
codepoints.push_back(codepoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分行处理
|
||||||
|
std::vector<std::vector<uint32_t>> lines;
|
||||||
|
std::vector<uint32_t> currentLine;
|
||||||
|
float currentWidth = 0.0f;
|
||||||
|
uint32_t prevCodepoint = 0;
|
||||||
|
|
||||||
|
for (uint32_t codepoint : codepoints) {
|
||||||
|
if (codepoint == '\n') {
|
||||||
|
lines.push_back(currentLine);
|
||||||
|
currentLine.clear();
|
||||||
|
currentWidth = 0.0f;
|
||||||
|
prevCodepoint = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GlyphInfo* glyph = font_->getGlyph(codepoint);
|
||||||
|
if (!glyph) {
|
||||||
|
prevCodepoint = codepoint;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float advance = glyph->advance * scale;
|
||||||
|
|
||||||
|
if (prevCodepoint != 0) {
|
||||||
|
advance += font_->getKerning(prevCodepoint, codepoint) * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要换行
|
||||||
|
if (maxWidth_ > 0 && currentWidth + advance > maxWidth_ && !currentLine.empty()) {
|
||||||
|
lines.push_back(currentLine);
|
||||||
|
currentLine.clear();
|
||||||
|
currentWidth = 0.0f;
|
||||||
|
prevCodepoint = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLine.push_back(codepoint);
|
||||||
|
currentWidth += advance;
|
||||||
|
prevCodepoint = codepoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentLine.empty()) {
|
||||||
|
lines.push_back(currentLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建每行的顶点数据
|
||||||
|
float totalHeight = 0.0f;
|
||||||
|
float maxLineWidth = 0.0f;
|
||||||
|
|
||||||
|
for (size_t lineIdx = 0; lineIdx < lines.size(); ++lineIdx) {
|
||||||
|
const auto& line = lines[lineIdx];
|
||||||
|
TextLine textLine;
|
||||||
|
|
||||||
|
float x = 0.0f;
|
||||||
|
float lineWidth = 0.0f;
|
||||||
|
prevCodepoint = 0;
|
||||||
|
|
||||||
|
// 首先计算行宽度
|
||||||
|
for (uint32_t codepoint : line) {
|
||||||
|
const GlyphInfo* glyph = font_->getGlyph(codepoint);
|
||||||
|
if (!glyph) continue;
|
||||||
|
|
||||||
|
float advance = glyph->advance * scale;
|
||||||
|
if (prevCodepoint != 0) {
|
||||||
|
advance += font_->getKerning(prevCodepoint, codepoint) * scale;
|
||||||
|
}
|
||||||
|
lineWidth += advance;
|
||||||
|
prevCodepoint = codepoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据对齐方式计算起始 X
|
||||||
|
if (align_ == TextAlign::Center) {
|
||||||
|
x = -lineWidth * 0.5f;
|
||||||
|
} else if (align_ == TextAlign::Right) {
|
||||||
|
x = -lineWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建顶点
|
||||||
|
prevCodepoint = 0;
|
||||||
|
for (uint32_t codepoint : line) {
|
||||||
|
const GlyphInfo* glyph = font_->getGlyph(codepoint);
|
||||||
|
if (!glyph) continue;
|
||||||
|
|
||||||
|
if (prevCodepoint != 0) {
|
||||||
|
x += font_->getKerning(prevCodepoint, codepoint) * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
float xPos = x + glyph->bearing.x * scale;
|
||||||
|
float yPos = -glyph->bearing.y * scale;
|
||||||
|
float w = glyph->size.x * scale;
|
||||||
|
float h = glyph->size.y * scale;
|
||||||
|
|
||||||
|
// 添加四个顶点
|
||||||
|
uint16_t baseIndex = static_cast<uint16_t>(textLine.vertices.size());
|
||||||
|
|
||||||
|
textLine.vertices.push_back({Vec2(xPos, yPos + h), Vec2(glyph->uv0.x, glyph->uv1.y), color_});
|
||||||
|
textLine.vertices.push_back({Vec2(xPos + w, yPos + h), Vec2(glyph->uv1.x, glyph->uv1.y), color_});
|
||||||
|
textLine.vertices.push_back({Vec2(xPos + w, yPos), Vec2(glyph->uv1.x, glyph->uv0.y), color_});
|
||||||
|
textLine.vertices.push_back({Vec2(xPos, yPos), Vec2(glyph->uv0.x, glyph->uv0.y), color_});
|
||||||
|
|
||||||
|
// 添加两个三角形的索引
|
||||||
|
textLine.indices.push_back(baseIndex);
|
||||||
|
textLine.indices.push_back(baseIndex + 1);
|
||||||
|
textLine.indices.push_back(baseIndex + 2);
|
||||||
|
textLine.indices.push_back(baseIndex);
|
||||||
|
textLine.indices.push_back(baseIndex + 2);
|
||||||
|
textLine.indices.push_back(baseIndex + 3);
|
||||||
|
|
||||||
|
x += glyph->advance * scale;
|
||||||
|
prevCodepoint = codepoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
textLine.width = lineWidth;
|
||||||
|
textLine.yOffset = totalHeight;
|
||||||
|
lines_.push_back(std::move(textLine));
|
||||||
|
|
||||||
|
maxLineWidth = std::max(maxLineWidth, lineWidth);
|
||||||
|
totalHeight += lineHeight * lineSpacing_;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_ = Vec2(maxLineWidth, totalHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,271 @@
|
||||||
|
#include <resource/texture.h>
|
||||||
|
#include <utils/logger.h>
|
||||||
|
|
||||||
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
|
#include <stb/stb_image.h>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
Texture::Texture() = default;
|
||||||
|
|
||||||
|
Texture::~Texture() {
|
||||||
|
if (handle_ != 0) {
|
||||||
|
glDeleteTextures(1, &handle_);
|
||||||
|
handle_ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Texture::create(uint32 width, uint32 height, TextureFormat format) {
|
||||||
|
if (handle_ != 0) {
|
||||||
|
glDeleteTextures(1, &handle_);
|
||||||
|
}
|
||||||
|
|
||||||
|
glGenTextures(1, &handle_);
|
||||||
|
if (handle_ == 0) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to generate texture");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
width_ = width;
|
||||||
|
height_ = height;
|
||||||
|
format_ = format;
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, handle_);
|
||||||
|
|
||||||
|
GLint internalFormat = getGLInternalFormat(format);
|
||||||
|
GLenum glFormat = getGLFormat(format);
|
||||||
|
GLenum glType = getGLType(format);
|
||||||
|
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, glFormat, glType, nullptr);
|
||||||
|
|
||||||
|
// 设置默认过滤和环绕模式
|
||||||
|
setFilter(TextureFilter::Linear, TextureFilter::Linear);
|
||||||
|
setWrap(TextureWrap::Clamp, TextureWrap::Clamp);
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
loaded_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Texture::loadFromFile(const std::string& path) {
|
||||||
|
int width, height, channels;
|
||||||
|
stbi_set_flip_vertically_on_load(true);
|
||||||
|
unsigned char* data = stbi_load(path.c_str(), &width, &height, &channels, 0);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to load texture: {}", path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextureFormat format;
|
||||||
|
if (channels == 1) {
|
||||||
|
format = TextureFormat::R8;
|
||||||
|
} else if (channels == 3) {
|
||||||
|
format = TextureFormat::RGB8;
|
||||||
|
} else {
|
||||||
|
format = TextureFormat::RGBA8;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool result = loadFromMemory(data, width, height, format);
|
||||||
|
stbi_image_free(data);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
name_ = path;
|
||||||
|
Logger::log(LogLevel::Info, "Loaded texture: {} ({}x{})", path.c_str(), width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Texture::loadFromMemory(const uint8* data, uint32 width, uint32 height, TextureFormat format) {
|
||||||
|
if (handle_ != 0) {
|
||||||
|
glDeleteTextures(1, &handle_);
|
||||||
|
}
|
||||||
|
|
||||||
|
glGenTextures(1, &handle_);
|
||||||
|
if (handle_ == 0) {
|
||||||
|
Logger::log(LogLevel::Error, "Failed to generate texture");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
width_ = width;
|
||||||
|
height_ = height;
|
||||||
|
format_ = format;
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, handle_);
|
||||||
|
|
||||||
|
GLint internalFormat = getGLInternalFormat(format);
|
||||||
|
GLenum glFormat = getGLFormat(format);
|
||||||
|
GLenum glType = getGLType(format);
|
||||||
|
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, glFormat, glType, data);
|
||||||
|
|
||||||
|
// 设置默认过滤和环绕模式
|
||||||
|
setFilter(TextureFilter::Linear, TextureFilter::Linear);
|
||||||
|
setWrap(TextureWrap::Clamp, TextureWrap::Clamp);
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
loaded_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Texture::updateRegion(uint32 x, uint32 y, uint32 width, uint32 height, const uint8* data) {
|
||||||
|
if (handle_ == 0 || !data) return;
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, handle_);
|
||||||
|
GLenum glFormat = getGLFormat(format_);
|
||||||
|
GLenum glType = getGLType(format_);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, glFormat, glType, data);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Texture::bind(uint32 slot) const {
|
||||||
|
if (handle_ != 0) {
|
||||||
|
glActiveTexture(GL_TEXTURE0 + slot);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, handle_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Texture::unbind() const {
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Texture::setFilter(TextureFilter minFilter, TextureFilter magFilter) {
|
||||||
|
if (handle_ == 0) return;
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, handle_);
|
||||||
|
|
||||||
|
GLenum minFilterGL = GL_LINEAR;
|
||||||
|
GLenum magFilterGL = GL_LINEAR;
|
||||||
|
|
||||||
|
switch (minFilter) {
|
||||||
|
case TextureFilter::Nearest:
|
||||||
|
minFilterGL = GL_NEAREST;
|
||||||
|
break;
|
||||||
|
case TextureFilter::Linear:
|
||||||
|
minFilterGL = GL_LINEAR;
|
||||||
|
break;
|
||||||
|
case TextureFilter::MipmapNearest:
|
||||||
|
minFilterGL = GL_NEAREST_MIPMAP_NEAREST;
|
||||||
|
break;
|
||||||
|
case TextureFilter::MipmapLinear:
|
||||||
|
minFilterGL = GL_LINEAR_MIPMAP_LINEAR;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
minFilterGL = GL_LINEAR;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (magFilter) {
|
||||||
|
case TextureFilter::Nearest:
|
||||||
|
magFilterGL = GL_NEAREST;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
magFilterGL = GL_LINEAR;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilterGL);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilterGL);
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Texture::setWrap(TextureWrap wrapS, TextureWrap wrapT) {
|
||||||
|
if (handle_ == 0) return;
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, handle_);
|
||||||
|
|
||||||
|
GLenum wrapSGL = GL_CLAMP_TO_EDGE;
|
||||||
|
GLenum wrapTGL = GL_CLAMP_TO_EDGE;
|
||||||
|
|
||||||
|
switch (wrapS) {
|
||||||
|
case TextureWrap::Repeat:
|
||||||
|
wrapSGL = GL_REPEAT;
|
||||||
|
break;
|
||||||
|
case TextureWrap::Clamp:
|
||||||
|
wrapSGL = GL_CLAMP_TO_EDGE;
|
||||||
|
break;
|
||||||
|
case TextureWrap::Mirror:
|
||||||
|
wrapSGL = GL_MIRRORED_REPEAT;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
wrapSGL = GL_CLAMP_TO_EDGE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (wrapT) {
|
||||||
|
case TextureWrap::Repeat:
|
||||||
|
wrapTGL = GL_REPEAT;
|
||||||
|
break;
|
||||||
|
case TextureWrap::Clamp:
|
||||||
|
wrapTGL = GL_CLAMP_TO_EDGE;
|
||||||
|
break;
|
||||||
|
case TextureWrap::Mirror:
|
||||||
|
wrapTGL = GL_MIRRORED_REPEAT;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
wrapTGL = GL_CLAMP_TO_EDGE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapSGL);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapTGL);
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Texture::generateMipmap() {
|
||||||
|
if (handle_ != 0) {
|
||||||
|
glBindTexture(GL_TEXTURE_2D, handle_);
|
||||||
|
glGenerateMipmap(GL_TEXTURE_2D);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint Texture::getGLInternalFormat(TextureFormat format) {
|
||||||
|
switch (format) {
|
||||||
|
case TextureFormat::RGB8:
|
||||||
|
return GL_RGB8;
|
||||||
|
case TextureFormat::RGBA8:
|
||||||
|
return GL_RGBA8;
|
||||||
|
case TextureFormat::R8:
|
||||||
|
return GL_R8;
|
||||||
|
case TextureFormat::Depth:
|
||||||
|
return GL_DEPTH_COMPONENT24;
|
||||||
|
case TextureFormat::DepthStencil:
|
||||||
|
return GL_DEPTH24_STENCIL8;
|
||||||
|
default:
|
||||||
|
return GL_RGBA8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLenum Texture::getGLFormat(TextureFormat format) {
|
||||||
|
switch (format) {
|
||||||
|
case TextureFormat::RGB8:
|
||||||
|
return GL_RGB;
|
||||||
|
case TextureFormat::RGBA8:
|
||||||
|
return GL_RGBA;
|
||||||
|
case TextureFormat::R8:
|
||||||
|
return GL_RED;
|
||||||
|
case TextureFormat::Depth:
|
||||||
|
return GL_DEPTH_COMPONENT;
|
||||||
|
case TextureFormat::DepthStencil:
|
||||||
|
return GL_DEPTH_STENCIL;
|
||||||
|
default:
|
||||||
|
return GL_RGBA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLenum Texture::getGLType(TextureFormat format) {
|
||||||
|
switch (format) {
|
||||||
|
case TextureFormat::DepthStencil:
|
||||||
|
return GL_UNSIGNED_INT_24_8;
|
||||||
|
default:
|
||||||
|
return GL_UNSIGNED_BYTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
#include <types/math/color.h>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
|
||||||
|
const Color Color::White = Color(1.0f, 1.0f, 1.0f, 1.0f);
|
||||||
|
const Color Color::Black = Color(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
const Color Color::Red = Color(1.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
const Color Color::Green = Color(0.0f, 1.0f, 0.0f, 1.0f);
|
||||||
|
const Color Color::Blue = Color(0.0f, 0.0f, 1.0f, 1.0f);
|
||||||
|
const Color Color::Yellow = Color(1.0f, 1.0f, 0.0f, 1.0f);
|
||||||
|
const Color Color::Cyan = Color(0.0f, 1.0f, 1.0f, 1.0f);
|
||||||
|
const Color Color::Magenta = Color(1.0f, 0.0f, 1.0f, 1.0f);
|
||||||
|
const Color Color::Transparent = Color(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -78,7 +78,7 @@ end
|
||||||
-- ==============================================
|
-- ==============================================
|
||||||
|
|
||||||
if target_plat == "mingw" then
|
if target_plat == "mingw" then
|
||||||
add_requires("glm", "libsdl2", "libsdl2_mixer")
|
add_requires("glm", "libsdl2", "libsdl2_mixer", "freetype")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ==============================================
|
-- ==============================================
|
||||||
|
|
@ -94,6 +94,7 @@ define_extra2d_engine()
|
||||||
-- 示例程序目标(作为子项目)
|
-- 示例程序目标(作为子项目)
|
||||||
if is_config("examples","true") then
|
if is_config("examples","true") then
|
||||||
includes("examples/hello_world", {rootdir = "examples/hello_world"})
|
includes("examples/hello_world", {rootdir = "examples/hello_world"})
|
||||||
|
includes("examples/resource_loading", {rootdir = "examples/resource_loading"})
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 测试套件目标(作为子项目)
|
-- 测试套件目标(作为子项目)
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,18 @@ function define_extra2d_engine()
|
||||||
if plat == "mingw" then
|
if plat == "mingw" then
|
||||||
add_defines("_UNICODE", "UNICODE")
|
add_defines("_UNICODE", "UNICODE")
|
||||||
-- 使用 xmake 官方包
|
-- 使用 xmake 官方包
|
||||||
add_packages("glm", "libsdl2", "libsdl2_mixer", {public = true})
|
add_packages("glm", "libsdl2", "libsdl2_mixer", "freetype", {public = true})
|
||||||
add_syslinks("winmm", "imm32", "version", "setupapi", {public = true})
|
add_syslinks("winmm", "imm32", "version", "setupapi", {public = true})
|
||||||
elseif plat == "switch" then
|
elseif plat == "switch" then
|
||||||
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
|
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
|
||||||
add_includedirs(devkitPro .. "/portlibs/switch/include", {public = true})
|
add_includedirs(devkitPro .. "/portlibs/switch/include", {public = true})
|
||||||
|
add_includedirs(devkitPro .. "/portlibs/switch/include/freetype2", {public = true})
|
||||||
add_linkdirs(devkitPro .. "/portlibs/switch/lib")
|
add_linkdirs(devkitPro .. "/portlibs/switch/lib")
|
||||||
-- Switch 平台使用 OpenGL ES + EGL + Mesa 驱动
|
-- Switch 平台使用 OpenGL ES + EGL + Mesa 驱动
|
||||||
-- 注意:链接顺序很重要,被依赖的库要放在后面
|
-- 注意:链接顺序很重要,被依赖的库要放在后面
|
||||||
add_syslinks("SDL2_mixer", "SDL2", "opusfile", "opus", "vorbisidec", "ogg",
|
add_syslinks("SDL2_mixer", "SDL2", "opusfile", "opus", "vorbisidec", "ogg",
|
||||||
"modplug", "mpg123", "FLAC", "GLESv2", "EGL", "glapi", "drm_nouveau",
|
"modplug", "mpg123", "FLAC", "freetype", "harfbuzz", "png", "bz2", "z",
|
||||||
|
"GLESv2", "EGL", "glapi", "drm_nouveau",
|
||||||
{public = true})
|
{public = true})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue