diff --git a/1.jpg b/1.jpg new file mode 100644 index 0000000..b75ea16 Binary files /dev/null and b/1.jpg differ diff --git a/Extra2D/include/extra2d/graphics/backends/opengl/gl_font_atlas.h b/Extra2D/include/extra2d/graphics/backends/opengl/gl_font_atlas.h index 61c8288..72d286c 100644 --- a/Extra2D/include/extra2d/graphics/backends/opengl/gl_font_atlas.h +++ b/Extra2D/include/extra2d/graphics/backends/opengl/gl_font_atlas.h @@ -13,6 +13,7 @@ namespace extra2d { // ============================================================================ // OpenGL 字体图集实现 (使用 STB 库) +// 使用 stb_rect_pack 进行动态矩形打包,支持动态缓存字形 // ============================================================================ class GLFontAtlas : public FontAtlas { public: @@ -31,6 +32,7 @@ public: Vec2 measureText(const std::string& text) override; private: + // 字形数据内部结构 struct GlyphData { float width; float height; @@ -40,17 +42,16 @@ private: float u0, v0, u1, v1; }; - struct PackedCharData { - stbtt_packedchar packedChar; - bool valid; - }; + // 图集配置 - 增大尺寸以支持更多字符 + static constexpr int ATLAS_WIDTH = 1024; + static constexpr int ATLAS_HEIGHT = 1024; + static constexpr int PADDING = 2; // 字形之间的间距 bool useSDF_; int fontSize_; Ptr texture_; std::unordered_map glyphs_; - std::unordered_map packedChars_; float lineHeight_; float ascent_; float descent_; @@ -61,15 +62,21 @@ private: stbtt_fontinfo fontInfo_; float scale_; - // 图集打包参数 - static constexpr int ATLAS_WIDTH = 512; - static constexpr int ATLAS_HEIGHT = 512; + // stb_rect_pack 上下文 - 持久化以支持增量打包 + mutable stbrp_context packContext_; + mutable std::vector packNodes_; + + // 预分配缓冲区,避免每次动态分配 + mutable std::vector glyphBitmapCache_; + mutable std::vector glyphRgbaCache_; // 初始化字体 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, const std::vector& data); }; diff --git a/Extra2D/include/extra2d/graphics/backends/opengl/gl_sprite_batch.h b/Extra2D/include/extra2d/graphics/backends/opengl/gl_sprite_batch.h index 2679e6b..2808b00 100644 --- a/Extra2D/include/extra2d/graphics/backends/opengl/gl_sprite_batch.h +++ b/Extra2D/include/extra2d/graphics/backends/opengl/gl_sprite_batch.h @@ -57,6 +57,7 @@ private: // 着色器和矩阵 Ptr shader_; uint32_t drawCallCount_; + glm::mat4 viewProjection_; // 内部方法 void flush(); diff --git a/Extra2D/src/graphics/backends/opengl/gl_font_atlas.cpp b/Extra2D/src/graphics/backends/opengl/gl_font_atlas.cpp index bb91b42..a578afe 100644 --- a/Extra2D/src/graphics/backends/opengl/gl_font_atlas.cpp +++ b/Extra2D/src/graphics/backends/opengl/gl_font_atlas.cpp @@ -17,54 +17,60 @@ namespace extra2d { // ============================================================================ // GLFontAtlas 构造函数 -// 使用 STB 库加载字体并创建纹理图集 +// 加载字体文件并初始化图集 // ============================================================================ GLFontAtlas::GLFontAtlas(const std::string &filepath, int fontSize, bool useSDF) - : useSDF_(useSDF), fontSize_(fontSize), - lineHeight_(0.0f), ascent_(0.0f), descent_(0.0f), lineGap_(0.0f), - scale_(0.0f) { + : useSDF_(useSDF), fontSize_(fontSize), lineHeight_(0.0f), ascent_(0.0f), + descent_(0.0f), lineGap_(0.0f), scale_(0.0f) { + + // 加载字体文件 if (!initFont(filepath)) { E2D_LOG_ERROR("Failed to initialize font: {}", filepath); return; } - // 创建纹理图集 (使用单通道格式) - texture_ = std::make_unique(ATLAS_WIDTH, ATLAS_HEIGHT, nullptr, 1); + // 计算字体缩放比例和度量 + scale_ = stbtt_ScaleForPixelHeight(&fontInfo_, static_cast(fontSize_)); - // 预加载 ASCII 字符 - std::string asciiChars = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; - for (char c : asciiChars) { - char32_t codepoint = static_cast(static_cast(c)); - renderGlyph(codepoint); - } - - // 计算字体度量 int ascent, descent, lineGap; stbtt_GetFontVMetrics(&fontInfo_, &ascent, &descent, &lineGap); - - scale_ = stbtt_ScaleForPixelHeight(&fontInfo_, static_cast(fontSize)); - + ascent_ = static_cast(ascent) * scale_; descent_ = static_cast(descent) * scale_; lineGap_ = static_cast(lineGap) * scale_; lineHeight_ = ascent_ - descent_ + lineGap_; + + // 创建图集纹理和打包上下文 + createAtlas(); + + // 预加载常用 ASCII 字符 + std::string asciiChars = " !\"#$%&'()*+,-./" + "0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`" + "abcdefghijklmnopqrstuvwxyz{|}~"; + for (char c : asciiChars) { + char32_t codepoint = static_cast(static_cast(c)); + cacheGlyph(codepoint); + } + + E2D_LOG_INFO("Font atlas created: {} ({}px, {}x{})", filepath, fontSize_, + ATLAS_WIDTH, ATLAS_HEIGHT); } // ============================================================================ // GLFontAtlas 析构函数 // ============================================================================ GLFontAtlas::~GLFontAtlas() { - // STB 不需要显式清理,字体数据由 vector 自动管理 + // 智能指针自动管理纹理资源 } // ============================================================================ -// 获取字形信息 +// 获取字形信息 - 如果字形不存在则动态缓存 // ============================================================================ const Glyph *GLFontAtlas::getGlyph(char32_t codepoint) const { auto it = glyphs_.find(codepoint); if (it == glyphs_.end()) { - // 尝试渲染该字符 - const_cast(this)->renderGlyph(codepoint); + // 动态缓存新字形 + const_cast(this)->cacheGlyph(codepoint); it = glyphs_.find(codepoint); if (it == glyphs_.end()) { return nullptr; @@ -88,37 +94,42 @@ const Glyph *GLFontAtlas::getGlyph(char32_t codepoint) const { } // ============================================================================ -// 测量文本尺寸 +// 测量文本尺寸 - 支持多行文本 // ============================================================================ Vec2 GLFontAtlas::measureText(const std::string &text) { float width = 0.0f; float maxWidth = 0.0f; float height = lineHeight_; - + float currentWidth = 0.0f; + for (size_t i = 0; i < text.length();) { // 处理 UTF-8 编码 char32_t codepoint = 0; unsigned char c = static_cast(text[i]); - + if ((c & 0x80) == 0) { // 单字节 ASCII codepoint = c; i++; } else if ((c & 0xE0) == 0xC0) { // 2字节 UTF-8 - if (i + 1 >= text.length()) break; - codepoint = ((c & 0x1F) << 6) | (static_cast(text[i + 1]) & 0x3F); + if (i + 1 >= text.length()) + break; + codepoint = + ((c & 0x1F) << 6) | (static_cast(text[i + 1]) & 0x3F); i += 2; } else if ((c & 0xF0) == 0xE0) { // 3字节 UTF-8 - if (i + 2 >= text.length()) break; - codepoint = ((c & 0x0F) << 12) | + if (i + 2 >= text.length()) + break; + codepoint = ((c & 0x0F) << 12) | ((static_cast(text[i + 1]) & 0x3F) << 6) | (static_cast(text[i + 2]) & 0x3F); i += 3; } else if ((c & 0xF8) == 0xF0) { // 4字节 UTF-8 - if (i + 3 >= text.length()) break; + if (i + 3 >= text.length()) + break; codepoint = ((c & 0x07) << 18) | ((static_cast(text[i + 1]) & 0x3F) << 12) | ((static_cast(text[i + 2]) & 0x3F) << 6) | @@ -129,27 +140,27 @@ Vec2 GLFontAtlas::measureText(const std::string &text) { i++; continue; } - + // 处理换行 if (codepoint == '\n') { - maxWidth = std::max(maxWidth, width); - width = 0.0f; + maxWidth = std::max(maxWidth, currentWidth); + currentWidth = 0.0f; height += lineHeight_; continue; } - - const Glyph* glyph = getGlyph(codepoint); + + const Glyph *glyph = getGlyph(codepoint); if (glyph) { - width += glyph->advance; + currentWidth += glyph->advance; } } - - maxWidth = std::max(maxWidth, width); + + maxWidth = std::max(maxWidth, currentWidth); return Vec2(maxWidth, height); } // ============================================================================ -// 初始化字体 (使用 STB) +// 初始化字体 - 加载字体文件到内存 // ============================================================================ bool GLFontAtlas::initFont(const std::string &filepath) { // 读取字体文件到内存 @@ -178,97 +189,203 @@ bool GLFontAtlas::initFont(const std::string &filepath) { } // ============================================================================ -// 渲染字形到图集 +// 创建图集纹理 - 初始化空白纹理和矩形打包上下文 // ============================================================================ -bool GLFontAtlas::renderGlyph(char32_t codepoint) { - if (glyphs_.find(codepoint) != glyphs_.end()) { - return true; // 已存在 - } +void GLFontAtlas::createAtlas() { + // 统一使用 4 通道格式 (RGBA) + int channels = 4; + std::vector emptyData(ATLAS_WIDTH * ATLAS_HEIGHT * channels, 0); + texture_ = std::make_unique(ATLAS_WIDTH, ATLAS_HEIGHT, + emptyData.data(), channels); + texture_->setFilter(true); - // 获取字形索引 - int glyphIndex = stbtt_FindGlyphIndex(&fontInfo_, static_cast(codepoint)); - if (glyphIndex == 0) { - return false; // 字符不存在 - } + // 初始化矩形打包上下文 - 持久化以支持增量打包 + packNodes_.resize(ATLAS_WIDTH); + stbrp_init_target(&packContext_, ATLAS_WIDTH, ATLAS_HEIGHT, packNodes_.data(), + ATLAS_WIDTH); - // 获取字形位图尺寸 - int width, height, xoff, yoff; - unsigned char *bitmap = stbtt_GetCodepointBitmap( - &fontInfo_, 0, scale_, static_cast(codepoint), - &width, &height, &xoff, &yoff); - - if (!bitmap) { - // 空白字符(如空格) - int advance, leftSideBearing; - stbtt_GetCodepointHMetrics(&fontInfo_, static_cast(codepoint), &advance, &leftSideBearing); - - GlyphData data; - data.width = 0; - data.height = 0; - data.bearingX = 0; - data.bearingY = 0; - data.advance = static_cast(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(codepoint); - rect.w = static_cast(width); - rect.h = static_cast(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(bitmap, bitmap + width * height)); - stbtt_FreeBitmap(bitmap, nullptr); - - // 获取字形度量 - int advance, leftSideBearing; - stbtt_GetCodepointHMetrics(&fontInfo_, static_cast(codepoint), &advance, &leftSideBearing); - - int ix0, iy0, ix1, iy1; - stbtt_GetCodepointBitmapBox(&fontInfo_, static_cast(codepoint), scale_, scale_, &ix0, &iy0, &ix1, &iy1); - - // 存储字形数据 - GlyphData data; - data.width = static_cast(width); - data.height = static_cast(height); - data.bearingX = static_cast(xoff); - data.bearingY = static_cast(-yoff); // STB 使用不同的坐标系 - data.advance = static_cast(advance) * scale_; - - // 计算 UV 坐标 - data.u0 = static_cast(x) / ATLAS_WIDTH; - data.v0 = static_cast(y) / ATLAS_HEIGHT; - data.u1 = static_cast(x + width) / ATLAS_WIDTH; - data.v1 = static_cast(y + height) / ATLAS_HEIGHT; - - glyphs_[codepoint] = data; - return true; + // 预分配字形缓冲区 + // 假设最大字形尺寸为 fontSize * fontSize * 4 (RGBA) + size_t maxGlyphSize = static_cast(fontSize_ * fontSize_ * 4 * 4); + glyphBitmapCache_.reserve(maxGlyphSize); + glyphRgbaCache_.reserve(maxGlyphSize); } // ============================================================================ -// 更新图集纹理 +// 缓存字形 - 渲染字形到图集并存储信息 +// 使用 stb_rect_pack 进行矩形打包 +// ============================================================================ +void GLFontAtlas::cacheGlyph(char32_t codepoint) { + // 检查是否已存在 + if (glyphs_.find(codepoint) != glyphs_.end()) { + return; + } + + // 获取字形水平度量 + int advance, leftSideBearing; + stbtt_GetCodepointHMetrics(&fontInfo_, static_cast(codepoint), &advance, + &leftSideBearing); + float advancePx = static_cast(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(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(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(w); + data.height = static_cast(h); + data.bearingX = static_cast(xoff); + data.bearingY = static_cast(yoff); + data.advance = advancePx; + + // 计算 UV 坐标 + // stb_rect_pack 使用左上角为原点,OpenGL纹理使用左下角为原点 + // 需要翻转V坐标 + float v0 = static_cast(atlasY) / ATLAS_HEIGHT; + float v1 = static_cast(atlasY + h) / ATLAS_HEIGHT; + data.u0 = static_cast(atlasX) / ATLAS_WIDTH; + data.v0 = 1.0f - v1; // 翻转V坐标 + data.u1 = static_cast(atlasX + w) / ATLAS_WIDTH; + data.v1 = 1.0f - v0; // 翻转V坐标 + + glyphs_[codepoint] = data; + + // 将 SDF 单通道数据转换为 RGBA 格式(统一格式) + size_t pixelCount = static_cast(w) * static_cast(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(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(w) * static_cast(h); + glyphBitmapCache_.resize(pixelCount); + stbtt_MakeCodepointBitmap(&fontInfo_, glyphBitmapCache_.data(), w, h, w, + scale_, scale_, static_cast(codepoint)); + + // 使用 stb_rect_pack 打包矩形 + stbrp_rect rect; + rect.id = static_cast(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(w); + data.height = static_cast(h); + data.bearingX = static_cast(xoff); + data.bearingY = static_cast(yoff); + data.advance = advancePx; + + // 计算 UV 坐标 + // stb_rect_pack 使用左上角为原点,OpenGL纹理使用左下角为原点 + // 需要翻转V坐标 + float v0 = static_cast(atlasY) / ATLAS_HEIGHT; + float v1 = static_cast(atlasY + h) / ATLAS_HEIGHT; + data.u0 = static_cast(atlasX) / ATLAS_WIDTH; + data.v0 = 1.0f - v1; // 翻转V坐标 + data.u1 = static_cast(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, - const std::vector &data) { + const std::vector &data) { if (texture_) { 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()); } } diff --git a/Extra2D/src/graphics/backends/opengl/gl_renderer.cpp b/Extra2D/src/graphics/backends/opengl/gl_renderer.cpp index b34097a..11474b6 100644 --- a/Extra2D/src/graphics/backends/opengl/gl_renderer.cpp +++ b/Extra2D/src/graphics/backends/opengl/gl_renderer.cpp @@ -599,15 +599,21 @@ void GLRenderer::drawText(const FontAtlas &font, const std::string &text, float penX = cursorX; 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; } + // 计算字形位置 + // bearingX: 水平偏移(从左边缘到字形左边缘) + // bearingY: 垂直偏移(从基线到字形顶部,通常为负值) float xPos = penX + glyph->bearingX; float yPos = baselineY + glyph->bearingY; 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.uvRect = Rect( Vec2(glyph->u0, glyph->v0), @@ -615,7 +621,8 @@ void GLRenderer::drawText(const FontAtlas &font, const std::string &text, ); data.color = color; 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); } diff --git a/Extra2D/src/graphics/backends/opengl/gl_sprite_batch.cpp b/Extra2D/src/graphics/backends/opengl/gl_sprite_batch.cpp index 3ca1ef3..c605202 100644 --- a/Extra2D/src/graphics/backends/opengl/gl_sprite_batch.cpp +++ b/Extra2D/src/graphics/backends/opengl/gl_sprite_batch.cpp @@ -76,6 +76,8 @@ void GLSpriteBatch::begin(const glm::mat4 &viewProjection) { batches_.clear(); currentTexture_ = nullptr; drawCallCount_ = 0; + // 保存 viewProjection 矩阵供后续使用 + viewProjection_ = viewProjection; } void GLSpriteBatch::end() { @@ -141,8 +143,15 @@ void GLSpriteBatch::submitBatch() { // 绑定着色器并设置uniform if (shader_) { 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(); } - // 绑定着色器并设置uniform - if (shader_) { - shader_->bind(); - // 这里需要从某处获取 viewProjection - // 简化处理:假设 shader 已经在 begin 时设置了 viewProjection - shader_->setInt("u_texture", 0); - shader_->setFloat("u_opacity", 1.0f); - } - // 重置状态 batches_.clear(); currentTexture_ = nullptr; diff --git a/Extra2D/src/graphics/batch/sprite_batch.cpp b/Extra2D/src/graphics/batch/sprite_batch.cpp index cd17b8b..45189ba 100644 --- a/Extra2D/src/graphics/batch/sprite_batch.cpp +++ b/Extra2D/src/graphics/batch/sprite_batch.cpp @@ -169,15 +169,20 @@ void SpriteBatch::generateVertices(const SpriteData& sprite, size_t vertexOffset } // 设置纹理坐标 - float u1 = sprite.uvRect.origin.x; - float v1 = sprite.uvRect.origin.y; - float u2 = u1 + sprite.uvRect.size.width; - float v2 = v1 + sprite.uvRect.size.height; + // uvRect.origin = (u0, v0) - 左下 + // uvRect.size = (width, height) - 从左上到右下的尺寸 + float u0 = sprite.uvRect.origin.x; + 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); // 右下 - vertices_[vertexOffset + 2].texCoord = Vec2(u2, v1); // 右上 - vertices_[vertexOffset + 3].texCoord = Vec2(u1, v1); // 左上 + // 顶点顺序: 左下, 右下, 右上, 左上 + // 注意: 在 gl_font_atlas 中 v0 > v1 (因为翻转了V坐标) + // 所以 v0 对应底部,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) {