#include #include #include #define STB_TRUETYPE_IMPLEMENTATION #include #define STB_RECT_PACK_IMPLEMENTATION #include #include namespace frostbite2D { // ============================================================================ // 构造函数 - 初始化字体图集 // ============================================================================ /** * @brief 构造函数,从字体文件初始化字体图集 * @param filepath 字体文件路径 * @param fontSize 字体大小(像素) * @param useSDF 是否使用有符号距离场渲染 */ GLFontAtlas::GLFontAtlas(const std::string &filepath, int fontSize, bool useSDF) : fontSize_(fontSize), useSDF_(useSDF), currentY_(0), scale_(0.0f), ascent_(0.0f), descent_(0.0f), lineGap_(0.0f) { // 加载字体文件 std::ifstream file(filepath, std::ios::binary | std::ios::ate); if (!file.is_open()) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load font: %s", filepath.c_str()); return; } std::streamsize size = file.tellg(); file.seekg(0, std::ios::beg); fontData_.resize(size); if (!file.read(reinterpret_cast(fontData_.data()), size)) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to read font file: %s", filepath.c_str()); return; } // 初始化 stb_truetype if (!stbtt_InitFont(&fontInfo_, fontData_.data(), stbtt_GetFontOffsetForIndex(fontData_.data(), 0))) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to init font: %s", filepath.c_str()); return; } scale_ = stbtt_ScaleForPixelHeight(&fontInfo_, static_cast(fontSize_)); int ascent, descent, lineGap; stbtt_GetFontVMetrics(&fontInfo_, &ascent, &descent, &lineGap); ascent_ = static_cast(ascent) * scale_; descent_ = static_cast(descent) * scale_; lineGap_ = static_cast(lineGap) * scale_; createAtlas(); } // ============================================================================ // 析构函数 // ============================================================================ /** * @brief 析构函数 */ GLFontAtlas::~GLFontAtlas() = default; // ============================================================================ // 获取字形 - 如果字形不存在则缓存它 // ============================================================================ /** * @brief 获取字形信息,如果字形不存在则动态缓存 * @param codepoint Unicode码点 * @return 字形信息指针,如果获取失败返回nullptr */ const Glyph *GLFontAtlas::getGlyph(char32_t codepoint) const { auto it = glyphs_.find(codepoint); if (it == glyphs_.end()) { cacheGlyph(codepoint); it = glyphs_.find(codepoint); } return (it != glyphs_.end()) ? &it->second : nullptr; } // ============================================================================ // 测量文本尺寸 // ============================================================================ /** * @brief 测量文本渲染后的尺寸 * @param text 要测量的文本 * @return 文本的宽度和高度 */ Vec2 GLFontAtlas::measureText(const std::string &text) { float width = 0.0f; float height = getAscent() - getDescent(); float currentWidth = 0.0f; for (char c : text) { char32_t codepoint = static_cast(static_cast(c)); if (codepoint == '\n') { width = std::max(width, currentWidth); currentWidth = 0.0f; height += getLineHeight(); continue; } const Glyph *glyph = getGlyph(codepoint); if (glyph) { currentWidth += glyph->advance; } } width = std::max(width, currentWidth); return Vec2(width, height); } // ============================================================================ // 创建图集纹理 - 初始化空白纹理和矩形打包上下文 // ============================================================================ /** * @brief 创建字体图集纹理,初始化空白纹理和矩形打包上下文 */ void GLFontAtlas::createAtlas() { // 统一使用 4 通道格式 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); // 初始化矩形打包上下文 packNodes_.resize(ATLAS_WIDTH); stbrp_init_target(&packContext_, ATLAS_WIDTH, ATLAS_HEIGHT, packNodes_.data(), ATLAS_WIDTH); // 预分配字形缓冲区 // 假设最大字形尺寸为 fontSize * fontSize * 4 (RGBA) size_t maxGlyphSize = static_cast(fontSize_ * fontSize_ * 4 * 4); glyphBitmapCache_.reserve(maxGlyphSize); glyphRgbaCache_.reserve(maxGlyphSize); } // ============================================================================ // 缓存字形 - 渲染字形到图集并存储信息 // 使用 stb_rect_pack 进行矩形打包 // ============================================================================ /** * @brief 缓存字形到图集,渲染字形位图并存储字形信息 * @param codepoint Unicode码点 */ void GLFontAtlas::cacheGlyph(char32_t codepoint) const { int advance = 0; stbtt_GetCodepointHMetrics(&fontInfo_, static_cast(codepoint), &advance, nullptr); float advancePx = advance * scale_; 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); Glyph glyph{}; glyph.advance = advancePx; glyphs_[codepoint] = glyph; return; } 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) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Font atlas is full, cannot cache codepoint: %d", static_cast(codepoint)); stbtt_FreeSDF(sdf, nullptr); return; } int atlasX = rect.x + PADDING; int atlasY = rect.y + PADDING; Glyph glyph; glyph.width = static_cast(w); glyph.height = static_cast(h); glyph.bearingX = static_cast(xoff); glyph.bearingY = static_cast(yoff); glyph.advance = advancePx; // stb_rect_pack 使用左上角为原点,OpenGL纹理使用左下角为原点 // 需要翻转V坐标 float v0 = static_cast(atlasY) / ATLAS_HEIGHT; float v1 = static_cast(atlasY + h) / ATLAS_HEIGHT; glyph.u0 = static_cast(atlasX) / ATLAS_WIDTH; glyph.v0 = 1.0f - v1; // 翻转V坐标 glyph.u1 = static_cast(atlasX + w) / ATLAS_WIDTH; glyph.v1 = 1.0f - v0; // 翻转V坐标 glyphs_[codepoint] = glyph; // 将 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 通道 } // 直接设置像素对齐为 4,无需查询当前状态 glBindTexture(GL_TEXTURE_2D, texture_->getTextureID()); glPixelStorei(GL_UNPACK_ALIGNMENT, 4); // OpenGL纹理坐标原点在左下角,需要将Y坐标翻转 glTexSubImage2D(GL_TEXTURE_2D, 0, atlasX, ATLAS_HEIGHT - atlasY - h, w, h, GL_RGBA, GL_UNSIGNED_BYTE, glyphRgbaCache_.data()); 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; int yoff = y0; if (w <= 0 || h <= 0) { Glyph glyph{}; glyph.advance = advancePx; glyphs_[codepoint] = glyph; 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) { // 图集已满,无法缓存更多字形 SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Font atlas is full, cannot cache codepoint: %d", static_cast(codepoint)); return; } int atlasX = rect.x + PADDING; int atlasY = rect.y + PADDING; // 创建字形信息 Glyph glyph; glyph.width = static_cast(w); glyph.height = static_cast(h); glyph.bearingX = static_cast(xoff); glyph.bearingY = static_cast(yoff); glyph.advance = advancePx; // 计算纹理坐标(相对于图集) // stb_rect_pack 使用左上角为原点,OpenGL纹理使用左下角为原点 // 需要翻转V坐标 float v0 = static_cast(atlasY) / ATLAS_HEIGHT; float v1 = static_cast(atlasY + h) / ATLAS_HEIGHT; glyph.u0 = static_cast(atlasX) / ATLAS_WIDTH; glyph.v0 = 1.0f - v1; // 翻转V坐标 glyph.u1 = static_cast(atlasX + w) / ATLAS_WIDTH; glyph.v1 = 1.0f - v0; // 翻转V坐标 // 存储字形 glyphs_[codepoint] = glyph; // 将单通道字形数据转换为 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 } // 更新纹理 - 将字形数据上传到图集的指定位置 // 直接设置像素对齐为 4,无需查询当前状态 glBindTexture(GL_TEXTURE_2D, texture_->getTextureID()); glPixelStorei(GL_UNPACK_ALIGNMENT, 4); // OpenGL纹理坐标原点在左下角,需要将Y坐标翻转 glTexSubImage2D(GL_TEXTURE_2D, 0, atlasX, ATLAS_HEIGHT - atlasY - h, w, h, GL_RGBA, GL_UNSIGNED_BYTE, glyphRgbaCache_.data()); } } // namespace frostbite2D