feat(渲染): 优化字体图集和精灵批处理系统

- 在GLFontAtlas中重构字体图集实现,使用stb_rect_pack进行动态矩形打包
- 增加图集尺寸至1024x1024并添加字形间距
- 改进字形UV计算和纹理坐标处理
- 在GLSpriteBatch中保存viewProjection矩阵并优化着色器uniform设置
- 修正文本渲染中的字形位置计算和精灵中心点处理
- 优化纹理坐标生成逻辑以正确处理UV翻转
This commit is contained in:
ChestnutYueyue 2026-02-17 19:55:47 +08:00
parent 30b677f192
commit 6b4ce69657
7 changed files with 289 additions and 152 deletions

BIN
1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@ -13,6 +13,7 @@ namespace extra2d {
// ============================================================================ // ============================================================================
// OpenGL 字体图集实现 (使用 STB 库) // OpenGL 字体图集实现 (使用 STB 库)
// 使用 stb_rect_pack 进行动态矩形打包,支持动态缓存字形
// ============================================================================ // ============================================================================
class GLFontAtlas : public FontAtlas { class GLFontAtlas : public FontAtlas {
public: public:
@ -31,6 +32,7 @@ public:
Vec2 measureText(const std::string& text) override; Vec2 measureText(const std::string& text) override;
private: private:
// 字形数据内部结构
struct GlyphData { struct GlyphData {
float width; float width;
float height; float height;
@ -40,17 +42,16 @@ private:
float u0, v0, u1, v1; float u0, v0, u1, v1;
}; };
struct PackedCharData { // 图集配置 - 增大尺寸以支持更多字符
stbtt_packedchar packedChar; static constexpr int ATLAS_WIDTH = 1024;
bool valid; static constexpr int ATLAS_HEIGHT = 1024;
}; static constexpr int PADDING = 2; // 字形之间的间距
bool useSDF_; bool useSDF_;
int fontSize_; int fontSize_;
Ptr<GLTexture> texture_; Ptr<GLTexture> texture_;
std::unordered_map<char32_t, GlyphData> glyphs_; std::unordered_map<char32_t, GlyphData> glyphs_;
std::unordered_map<char32_t, PackedCharData> packedChars_;
float lineHeight_; float lineHeight_;
float ascent_; float ascent_;
float descent_; float descent_;
@ -61,15 +62,21 @@ private:
stbtt_fontinfo fontInfo_; stbtt_fontinfo fontInfo_;
float scale_; float scale_;
// 图集打包参数 // stb_rect_pack 上下文 - 持久化以支持增量打包
static constexpr int ATLAS_WIDTH = 512; mutable stbrp_context packContext_;
static constexpr int ATLAS_HEIGHT = 512; mutable std::vector<stbrp_node> packNodes_;
// 预分配缓冲区,避免每次动态分配
mutable std::vector<uint8_t> glyphBitmapCache_;
mutable std::vector<uint8_t> glyphRgbaCache_;
// 初始化字体 // 初始化字体
bool initFont(const std::string& filepath); bool initFont(const std::string& filepath);
// 渲染字形到图集 // 创建空白图集纹理
bool renderGlyph(char32_t codepoint); void createAtlas();
// 更新图集纹理 // 缓存字形到图集
void cacheGlyph(char32_t codepoint);
// 更新图集纹理区域
void updateAtlas(int x, int y, int width, int height, void updateAtlas(int x, int y, int width, int height,
const std::vector<uint8_t>& data); const std::vector<uint8_t>& data);
}; };

View File

@ -57,6 +57,7 @@ private:
// 着色器和矩阵 // 着色器和矩阵
Ptr<IShader> shader_; Ptr<IShader> shader_;
uint32_t drawCallCount_; uint32_t drawCallCount_;
glm::mat4 viewProjection_;
// 内部方法 // 内部方法
void flush(); void flush();

View File

@ -17,54 +17,60 @@ namespace extra2d {
// ============================================================================ // ============================================================================
// GLFontAtlas 构造函数 // GLFontAtlas 构造函数
// 使用 STB 库加载字体并创建纹理图集 // 加载字体文件并初始化图集
// ============================================================================ // ============================================================================
GLFontAtlas::GLFontAtlas(const std::string &filepath, int fontSize, bool useSDF) GLFontAtlas::GLFontAtlas(const std::string &filepath, int fontSize, bool useSDF)
: useSDF_(useSDF), fontSize_(fontSize), : useSDF_(useSDF), fontSize_(fontSize), lineHeight_(0.0f), ascent_(0.0f),
lineHeight_(0.0f), ascent_(0.0f), descent_(0.0f), lineGap_(0.0f), descent_(0.0f), lineGap_(0.0f), scale_(0.0f) {
scale_(0.0f) {
// 加载字体文件
if (!initFont(filepath)) { if (!initFont(filepath)) {
E2D_LOG_ERROR("Failed to initialize font: {}", filepath); E2D_LOG_ERROR("Failed to initialize font: {}", filepath);
return; return;
} }
// 创建纹理图集 (使用单通道格式) // 计算字体缩放比例和度量
texture_ = std::make_unique<GLTexture>(ATLAS_WIDTH, ATLAS_HEIGHT, nullptr, 1); scale_ = stbtt_ScaleForPixelHeight(&fontInfo_, static_cast<float>(fontSize_));
// 预加载 ASCII 字符
std::string asciiChars = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
for (char c : asciiChars) {
char32_t codepoint = static_cast<char32_t>(static_cast<unsigned char>(c));
renderGlyph(codepoint);
}
// 计算字体度量
int ascent, descent, lineGap; int ascent, descent, lineGap;
stbtt_GetFontVMetrics(&fontInfo_, &ascent, &descent, &lineGap); stbtt_GetFontVMetrics(&fontInfo_, &ascent, &descent, &lineGap);
scale_ = stbtt_ScaleForPixelHeight(&fontInfo_, static_cast<float>(fontSize));
ascent_ = static_cast<float>(ascent) * scale_; ascent_ = static_cast<float>(ascent) * scale_;
descent_ = static_cast<float>(descent) * scale_; descent_ = static_cast<float>(descent) * scale_;
lineGap_ = static_cast<float>(lineGap) * scale_; lineGap_ = static_cast<float>(lineGap) * scale_;
lineHeight_ = ascent_ - descent_ + lineGap_; lineHeight_ = ascent_ - descent_ + lineGap_;
// 创建图集纹理和打包上下文
createAtlas();
// 预加载常用 ASCII 字符
std::string asciiChars = " !\"#$%&'()*+,-./"
"0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`"
"abcdefghijklmnopqrstuvwxyz{|}~";
for (char c : asciiChars) {
char32_t codepoint = static_cast<char32_t>(static_cast<unsigned char>(c));
cacheGlyph(codepoint);
}
E2D_LOG_INFO("Font atlas created: {} ({}px, {}x{})", filepath, fontSize_,
ATLAS_WIDTH, ATLAS_HEIGHT);
} }
// ============================================================================ // ============================================================================
// GLFontAtlas 析构函数 // GLFontAtlas 析构函数
// ============================================================================ // ============================================================================
GLFontAtlas::~GLFontAtlas() { GLFontAtlas::~GLFontAtlas() {
// STB 不需要显式清理,字体数据由 vector 自动管理 // 智能指针自动管理纹理资源
} }
// ============================================================================ // ============================================================================
// 获取字形信息 // 获取字形信息 - 如果字形不存在则动态缓存
// ============================================================================ // ============================================================================
const Glyph *GLFontAtlas::getGlyph(char32_t codepoint) const { const Glyph *GLFontAtlas::getGlyph(char32_t codepoint) const {
auto it = glyphs_.find(codepoint); auto it = glyphs_.find(codepoint);
if (it == glyphs_.end()) { if (it == glyphs_.end()) {
// 尝试渲染该字符 // 动态缓存新字形
const_cast<GLFontAtlas *>(this)->renderGlyph(codepoint); const_cast<GLFontAtlas *>(this)->cacheGlyph(codepoint);
it = glyphs_.find(codepoint); it = glyphs_.find(codepoint);
if (it == glyphs_.end()) { if (it == glyphs_.end()) {
return nullptr; return nullptr;
@ -88,37 +94,42 @@ const Glyph *GLFontAtlas::getGlyph(char32_t codepoint) const {
} }
// ============================================================================ // ============================================================================
// 测量文本尺寸 // 测量文本尺寸 - 支持多行文本
// ============================================================================ // ============================================================================
Vec2 GLFontAtlas::measureText(const std::string &text) { Vec2 GLFontAtlas::measureText(const std::string &text) {
float width = 0.0f; float width = 0.0f;
float maxWidth = 0.0f; float maxWidth = 0.0f;
float height = lineHeight_; float height = lineHeight_;
float currentWidth = 0.0f;
for (size_t i = 0; i < text.length();) { for (size_t i = 0; i < text.length();) {
// 处理 UTF-8 编码 // 处理 UTF-8 编码
char32_t codepoint = 0; char32_t codepoint = 0;
unsigned char c = static_cast<unsigned char>(text[i]); unsigned char c = static_cast<unsigned char>(text[i]);
if ((c & 0x80) == 0) { if ((c & 0x80) == 0) {
// 单字节 ASCII // 单字节 ASCII
codepoint = c; codepoint = c;
i++; i++;
} else if ((c & 0xE0) == 0xC0) { } else if ((c & 0xE0) == 0xC0) {
// 2字节 UTF-8 // 2字节 UTF-8
if (i + 1 >= text.length()) break; if (i + 1 >= text.length())
codepoint = ((c & 0x1F) << 6) | (static_cast<unsigned char>(text[i + 1]) & 0x3F); break;
codepoint =
((c & 0x1F) << 6) | (static_cast<unsigned char>(text[i + 1]) & 0x3F);
i += 2; i += 2;
} else if ((c & 0xF0) == 0xE0) { } else if ((c & 0xF0) == 0xE0) {
// 3字节 UTF-8 // 3字节 UTF-8
if (i + 2 >= text.length()) break; if (i + 2 >= text.length())
codepoint = ((c & 0x0F) << 12) | break;
codepoint = ((c & 0x0F) << 12) |
((static_cast<unsigned char>(text[i + 1]) & 0x3F) << 6) | ((static_cast<unsigned char>(text[i + 1]) & 0x3F) << 6) |
(static_cast<unsigned char>(text[i + 2]) & 0x3F); (static_cast<unsigned char>(text[i + 2]) & 0x3F);
i += 3; i += 3;
} else if ((c & 0xF8) == 0xF0) { } else if ((c & 0xF8) == 0xF0) {
// 4字节 UTF-8 // 4字节 UTF-8
if (i + 3 >= text.length()) break; if (i + 3 >= text.length())
break;
codepoint = ((c & 0x07) << 18) | codepoint = ((c & 0x07) << 18) |
((static_cast<unsigned char>(text[i + 1]) & 0x3F) << 12) | ((static_cast<unsigned char>(text[i + 1]) & 0x3F) << 12) |
((static_cast<unsigned char>(text[i + 2]) & 0x3F) << 6) | ((static_cast<unsigned char>(text[i + 2]) & 0x3F) << 6) |
@ -129,27 +140,27 @@ Vec2 GLFontAtlas::measureText(const std::string &text) {
i++; i++;
continue; continue;
} }
// 处理换行 // 处理换行
if (codepoint == '\n') { if (codepoint == '\n') {
maxWidth = std::max(maxWidth, width); maxWidth = std::max(maxWidth, currentWidth);
width = 0.0f; currentWidth = 0.0f;
height += lineHeight_; height += lineHeight_;
continue; continue;
} }
const Glyph* glyph = getGlyph(codepoint); const Glyph *glyph = getGlyph(codepoint);
if (glyph) { if (glyph) {
width += glyph->advance; currentWidth += glyph->advance;
} }
} }
maxWidth = std::max(maxWidth, width); maxWidth = std::max(maxWidth, currentWidth);
return Vec2(maxWidth, height); return Vec2(maxWidth, height);
} }
// ============================================================================ // ============================================================================
// 初始化字体 (使用 STB) // 初始化字体 - 加载字体文件到内存
// ============================================================================ // ============================================================================
bool GLFontAtlas::initFont(const std::string &filepath) { bool GLFontAtlas::initFont(const std::string &filepath) {
// 读取字体文件到内存 // 读取字体文件到内存
@ -178,97 +189,203 @@ bool GLFontAtlas::initFont(const std::string &filepath) {
} }
// ============================================================================ // ============================================================================
// 渲染字形到图集 // 创建图集纹理 - 初始化空白纹理和矩形打包上下文
// ============================================================================ // ============================================================================
bool GLFontAtlas::renderGlyph(char32_t codepoint) { void GLFontAtlas::createAtlas() {
if (glyphs_.find(codepoint) != glyphs_.end()) { // 统一使用 4 通道格式 (RGBA)
return true; // 已存在 int channels = 4;
} std::vector<uint8_t> emptyData(ATLAS_WIDTH * ATLAS_HEIGHT * channels, 0);
texture_ = std::make_unique<GLTexture>(ATLAS_WIDTH, ATLAS_HEIGHT,
emptyData.data(), channels);
texture_->setFilter(true);
// 获取字形索引 // 初始化矩形打包上下文 - 持久化以支持增量打包
int glyphIndex = stbtt_FindGlyphIndex(&fontInfo_, static_cast<int>(codepoint)); packNodes_.resize(ATLAS_WIDTH);
if (glyphIndex == 0) { stbrp_init_target(&packContext_, ATLAS_WIDTH, ATLAS_HEIGHT, packNodes_.data(),
return false; // 字符不存在 ATLAS_WIDTH);
}
// 获取字形位图尺寸 // 预分配字形缓冲区
int width, height, xoff, yoff; // 假设最大字形尺寸为 fontSize * fontSize * 4 (RGBA)
unsigned char *bitmap = stbtt_GetCodepointBitmap( size_t maxGlyphSize = static_cast<size_t>(fontSize_ * fontSize_ * 4 * 4);
&fontInfo_, 0, scale_, static_cast<int>(codepoint), glyphBitmapCache_.reserve(maxGlyphSize);
&width, &height, &xoff, &yoff); glyphRgbaCache_.reserve(maxGlyphSize);
if (!bitmap) {
// 空白字符(如空格)
int advance, leftSideBearing;
stbtt_GetCodepointHMetrics(&fontInfo_, static_cast<int>(codepoint), &advance, &leftSideBearing);
GlyphData data;
data.width = 0;
data.height = 0;
data.bearingX = 0;
data.bearingY = 0;
data.advance = static_cast<float>(advance) * scale_;
data.u0 = data.v0 = data.u1 = data.v1 = 0;
glyphs_[codepoint] = data;
return true;
}
// 使用 stb_rect_pack 进行图集打包
stbrp_context context;
stbrp_node nodes[ATLAS_WIDTH];
stbrp_init_target(&context, ATLAS_WIDTH, ATLAS_HEIGHT, nodes, ATLAS_WIDTH);
stbrp_rect rect;
rect.id = static_cast<int>(codepoint);
rect.w = static_cast<stbrp_coord>(width);
rect.h = static_cast<stbrp_coord>(height);
if (!stbrp_pack_rects(&context, &rect, 1) || !rect.was_packed) {
E2D_LOG_WARN("Failed to pack glyph for codepoint: {}", codepoint);
stbtt_FreeBitmap(bitmap, nullptr);
return false;
}
int x = rect.x;
int y = rect.y;
// 更新纹理
updateAtlas(x, y, width, height, std::vector<uint8_t>(bitmap, bitmap + width * height));
stbtt_FreeBitmap(bitmap, nullptr);
// 获取字形度量
int advance, leftSideBearing;
stbtt_GetCodepointHMetrics(&fontInfo_, static_cast<int>(codepoint), &advance, &leftSideBearing);
int ix0, iy0, ix1, iy1;
stbtt_GetCodepointBitmapBox(&fontInfo_, static_cast<int>(codepoint), scale_, scale_, &ix0, &iy0, &ix1, &iy1);
// 存储字形数据
GlyphData data;
data.width = static_cast<float>(width);
data.height = static_cast<float>(height);
data.bearingX = static_cast<float>(xoff);
data.bearingY = static_cast<float>(-yoff); // STB 使用不同的坐标系
data.advance = static_cast<float>(advance) * scale_;
// 计算 UV 坐标
data.u0 = static_cast<float>(x) / ATLAS_WIDTH;
data.v0 = static_cast<float>(y) / ATLAS_HEIGHT;
data.u1 = static_cast<float>(x + width) / ATLAS_WIDTH;
data.v1 = static_cast<float>(y + height) / ATLAS_HEIGHT;
glyphs_[codepoint] = data;
return true;
} }
// ============================================================================ // ============================================================================
// 更新图集纹理 // 缓存字形 - 渲染字形到图集并存储信息
// 使用 stb_rect_pack 进行矩形打包
// ============================================================================
void GLFontAtlas::cacheGlyph(char32_t codepoint) {
// 检查是否已存在
if (glyphs_.find(codepoint) != glyphs_.end()) {
return;
}
// 获取字形水平度量
int advance, leftSideBearing;
stbtt_GetCodepointHMetrics(&fontInfo_, static_cast<int>(codepoint), &advance,
&leftSideBearing);
float advancePx = static_cast<float>(advance) * scale_;
// SDF 渲染模式
if (useSDF_) {
constexpr int SDF_PADDING = 8;
constexpr unsigned char ONEDGE_VALUE = 128;
constexpr float PIXEL_DIST_SCALE = 64.0f;
int w = 0, h = 0, xoff = 0, yoff = 0;
unsigned char *sdf = stbtt_GetCodepointSDF(
&fontInfo_, scale_, static_cast<int>(codepoint), SDF_PADDING,
ONEDGE_VALUE, PIXEL_DIST_SCALE, &w, &h, &xoff, &yoff);
if (!sdf || w <= 0 || h <= 0) {
if (sdf)
stbtt_FreeSDF(sdf, nullptr);
// 创建空白字形(如空格)
GlyphData data{};
data.advance = advancePx;
glyphs_[codepoint] = data;
return;
}
// 使用 stb_rect_pack 打包矩形
stbrp_rect rect;
rect.id = static_cast<int>(codepoint);
rect.w = w + PADDING * 2;
rect.h = h + PADDING * 2;
stbrp_pack_rects(&packContext_, &rect, 1);
if (!rect.was_packed) {
E2D_LOG_WARN("Font atlas is full, cannot cache codepoint: {}", codepoint);
stbtt_FreeSDF(sdf, nullptr);
return;
}
int atlasX = rect.x + PADDING;
int atlasY = rect.y + PADDING;
// 创建字形数据
GlyphData data;
data.width = static_cast<float>(w);
data.height = static_cast<float>(h);
data.bearingX = static_cast<float>(xoff);
data.bearingY = static_cast<float>(yoff);
data.advance = advancePx;
// 计算 UV 坐标
// stb_rect_pack 使用左上角为原点OpenGL纹理使用左下角为原点
// 需要翻转V坐标
float v0 = static_cast<float>(atlasY) / ATLAS_HEIGHT;
float v1 = static_cast<float>(atlasY + h) / ATLAS_HEIGHT;
data.u0 = static_cast<float>(atlasX) / ATLAS_WIDTH;
data.v0 = 1.0f - v1; // 翻转V坐标
data.u1 = static_cast<float>(atlasX + w) / ATLAS_WIDTH;
data.v1 = 1.0f - v0; // 翻转V坐标
glyphs_[codepoint] = data;
// 将 SDF 单通道数据转换为 RGBA 格式(统一格式)
size_t pixelCount = static_cast<size_t>(w) * static_cast<size_t>(h);
glyphRgbaCache_.resize(pixelCount * 4);
for (size_t i = 0; i < pixelCount; ++i) {
uint8_t alpha = sdf[i];
glyphRgbaCache_[i * 4 + 0] = 255; // R
glyphRgbaCache_[i * 4 + 1] = 255; // G
glyphRgbaCache_[i * 4 + 2] = 255; // B
glyphRgbaCache_[i * 4 + 3] = alpha; // A - SDF 值存储在 Alpha 通道
}
// 更新纹理 - OpenGL纹理坐标原点在左下角需要将Y坐标翻转
updateAtlas(atlasX, ATLAS_HEIGHT - atlasY - h, w, h, glyphRgbaCache_);
stbtt_FreeSDF(sdf, nullptr);
return;
}
// 普通位图渲染模式
int x0 = 0, y0 = 0, x1 = 0, y1 = 0;
stbtt_GetCodepointBitmapBox(&fontInfo_, static_cast<int>(codepoint), scale_,
scale_, &x0, &y0, &x1, &y1);
int w = x1 - x0;
int h = y1 - y0;
int xoff = x0;
// y0 是相对于基线的偏移(通常为负值,表示在基线上方)
// bearingY 应该是字形顶部相对于基线的偏移
int yoff = y0;
if (w <= 0 || h <= 0) {
// 空白字符(如空格)
GlyphData data{};
data.advance = advancePx;
glyphs_[codepoint] = data;
return;
}
// 使用预分配缓冲区渲染字形
size_t pixelCount = static_cast<size_t>(w) * static_cast<size_t>(h);
glyphBitmapCache_.resize(pixelCount);
stbtt_MakeCodepointBitmap(&fontInfo_, glyphBitmapCache_.data(), w, h, w,
scale_, scale_, static_cast<int>(codepoint));
// 使用 stb_rect_pack 打包矩形
stbrp_rect rect;
rect.id = static_cast<int>(codepoint);
rect.w = w + PADDING * 2;
rect.h = h + PADDING * 2;
stbrp_pack_rects(&packContext_, &rect, 1);
if (!rect.was_packed) {
E2D_LOG_WARN("Font atlas is full, cannot cache codepoint: {}", codepoint);
return;
}
int atlasX = rect.x + PADDING;
int atlasY = rect.y + PADDING;
// 创建字形数据
GlyphData data;
data.width = static_cast<float>(w);
data.height = static_cast<float>(h);
data.bearingX = static_cast<float>(xoff);
data.bearingY = static_cast<float>(yoff);
data.advance = advancePx;
// 计算 UV 坐标
// stb_rect_pack 使用左上角为原点OpenGL纹理使用左下角为原点
// 需要翻转V坐标
float v0 = static_cast<float>(atlasY) / ATLAS_HEIGHT;
float v1 = static_cast<float>(atlasY + h) / ATLAS_HEIGHT;
data.u0 = static_cast<float>(atlasX) / ATLAS_WIDTH;
data.v0 = 1.0f - v1; // 翻转V坐标
data.u1 = static_cast<float>(atlasX + w) / ATLAS_WIDTH;
data.v1 = 1.0f - v0; // 翻转V坐标
glyphs_[codepoint] = data;
// 将单通道字形数据转换为 RGBA 格式白色字形Alpha 通道存储灰度)
glyphRgbaCache_.resize(pixelCount * 4);
for (size_t i = 0; i < pixelCount; ++i) {
uint8_t alpha = glyphBitmapCache_[i];
glyphRgbaCache_[i * 4 + 0] = 255; // R
glyphRgbaCache_[i * 4 + 1] = 255; // G
glyphRgbaCache_[i * 4 + 2] = 255; // B
glyphRgbaCache_[i * 4 + 3] = alpha; // A
}
// 更新纹理 - OpenGL纹理坐标原点在左下角需要将Y坐标翻转
updateAtlas(atlasX, ATLAS_HEIGHT - atlasY - h, w, h, glyphRgbaCache_);
}
// ============================================================================
// 更新图集纹理区域
// ============================================================================ // ============================================================================
void GLFontAtlas::updateAtlas(int x, int y, int width, int height, void GLFontAtlas::updateAtlas(int x, int y, int width, int height,
const std::vector<uint8_t> &data) { const std::vector<uint8_t> &data) {
if (texture_) { if (texture_) {
texture_->bind(); texture_->bind();
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, GL_RED, glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, GL_RGBA,
GL_UNSIGNED_BYTE, data.data()); GL_UNSIGNED_BYTE, data.data());
} }
} }

View File

@ -599,15 +599,21 @@ void GLRenderer::drawText(const FontAtlas &font, const std::string &text,
float penX = cursorX; float penX = cursorX;
cursorX += glyph->advance; cursorX += glyph->advance;
if (glyph->width <= 0.0f || glyph->height <= 0.0f) { // 使用 epsilon 比较浮点数,避免精度问题
constexpr float EPSILON = 0.001f;
if (glyph->width < EPSILON || glyph->height < EPSILON) {
continue; continue;
} }
// 计算字形位置
// bearingX: 水平偏移(从左边缘到字形左边缘)
// bearingY: 垂直偏移(从基线到字形顶部,通常为负值)
float xPos = penX + glyph->bearingX; float xPos = penX + glyph->bearingX;
float yPos = baselineY + glyph->bearingY; float yPos = baselineY + glyph->bearingY;
SpriteData data; SpriteData data;
data.position = Vec2(xPos, yPos); // 设置精灵中心位置(精灵批处理使用中心点)
data.position = Vec2(xPos + glyph->width * 0.5f, yPos + glyph->height * 0.5f);
data.size = Vec2(glyph->width, glyph->height); data.size = Vec2(glyph->width, glyph->height);
data.uvRect = Rect( data.uvRect = Rect(
Vec2(glyph->u0, glyph->v0), Vec2(glyph->u0, glyph->v0),
@ -615,7 +621,8 @@ void GLRenderer::drawText(const FontAtlas &font, const std::string &text,
); );
data.color = color; data.color = color;
data.rotation = 0.0f; data.rotation = 0.0f;
data.pivot = Vec2(0.0f, 0.0f); // pivot (0.5, 0.5) 表示中心点,这样 position 就是精灵中心
data.pivot = Vec2(0.5f, 0.5f);
sprites.push_back(data); sprites.push_back(data);
} }

View File

@ -76,6 +76,8 @@ void GLSpriteBatch::begin(const glm::mat4 &viewProjection) {
batches_.clear(); batches_.clear();
currentTexture_ = nullptr; currentTexture_ = nullptr;
drawCallCount_ = 0; drawCallCount_ = 0;
// 保存 viewProjection 矩阵供后续使用
viewProjection_ = viewProjection;
} }
void GLSpriteBatch::end() { void GLSpriteBatch::end() {
@ -141,8 +143,15 @@ void GLSpriteBatch::submitBatch() {
// 绑定着色器并设置uniform // 绑定着色器并设置uniform
if (shader_) { if (shader_) {
shader_->bind(); shader_->bind();
// 注意batch 层不存储 viewProjection需要外部传入 // 设置视图投影矩阵
// 这里简化处理,实际应该在 begin 时设置 shader_->setMat4("u_viewProjection", viewProjection_);
// 设置模型矩阵为单位矩阵(精灵位置已经在顶点生成时计算)
glm::mat4 model(1.0f);
shader_->setMat4("u_model", model);
// 设置纹理采样器
shader_->setInt("u_texture", 0);
// 设置透明度
shader_->setFloat("u_opacity", 1.0f);
} }
// 上传顶点数据 // 上传顶点数据
@ -172,15 +181,6 @@ void GLSpriteBatch::flush() {
submitBatch(); submitBatch();
} }
// 绑定着色器并设置uniform
if (shader_) {
shader_->bind();
// 这里需要从某处获取 viewProjection
// 简化处理:假设 shader 已经在 begin 时设置了 viewProjection
shader_->setInt("u_texture", 0);
shader_->setFloat("u_opacity", 1.0f);
}
// 重置状态 // 重置状态
batches_.clear(); batches_.clear();
currentTexture_ = nullptr; currentTexture_ = nullptr;

View File

@ -169,15 +169,20 @@ void SpriteBatch::generateVertices(const SpriteData& sprite, size_t vertexOffset
} }
// 设置纹理坐标 // 设置纹理坐标
float u1 = sprite.uvRect.origin.x; // uvRect.origin = (u0, v0) - 左下
float v1 = sprite.uvRect.origin.y; // uvRect.size = (width, height) - 从左上到右下的尺寸
float u2 = u1 + sprite.uvRect.size.width; float u0 = sprite.uvRect.origin.x;
float v2 = v1 + sprite.uvRect.size.height; float v0 = sprite.uvRect.origin.y;
float u1 = u0 + sprite.uvRect.size.width;
float v1 = v0 + sprite.uvRect.size.height;
vertices_[vertexOffset + 0].texCoord = Vec2(u1, v2); // 左下 // 顶点顺序: 左下, 右下, 右上, 左上
vertices_[vertexOffset + 1].texCoord = Vec2(u2, v2); // 右下 // 注意: 在 gl_font_atlas 中 v0 > v1 (因为翻转了V坐标)
vertices_[vertexOffset + 2].texCoord = Vec2(u2, v1); // 右上 // 所以 v0 对应底部v1 对应顶部
vertices_[vertexOffset + 3].texCoord = Vec2(u1, v1); // 左上 vertices_[vertexOffset + 0].texCoord = Vec2(u0, v0); // 左下
vertices_[vertexOffset + 1].texCoord = Vec2(u1, v0); // 右下
vertices_[vertexOffset + 2].texCoord = Vec2(u1, v1); // 右上
vertices_[vertexOffset + 3].texCoord = Vec2(u0, v1); // 左上
// 设置颜色 // 设置颜色
for (int i = 0; i < 4; ++i) { for (int i = 0; i < 4; ++i) {