Files
Frostbite2D/Fostbite2D/src/fostbite2D/render/opengl/gl_font_atlas.cpp
2026-02-17 13:28:38 +08:00

314 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <SDL.h>
#include <fostbite2D/render/opengl/gl_font_atlas.h>
#include <fstream>
#define STB_TRUETYPE_IMPLEMENTATION
#include <stb/stb_truetype.h>
#define STB_RECT_PACK_IMPLEMENTATION
#include <algorithm>
#include <stb/stb_rect_pack.h>
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<char *>(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<float>(fontSize_));
int ascent, descent, lineGap;
stbtt_GetFontVMetrics(&fontInfo_, &ascent, &descent, &lineGap);
ascent_ = static_cast<float>(ascent) * scale_;
descent_ = static_cast<float>(descent) * scale_;
lineGap_ = static_cast<float>(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<char32_t>(static_cast<unsigned char>(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<uint8_t> emptyData(ATLAS_WIDTH * ATLAS_HEIGHT * channels, 0);
texture_ = std::make_unique<GLTexture>(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<size_t>(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<int>(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<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);
Glyph glyph{};
glyph.advance = advancePx;
glyphs_[codepoint] = glyph;
return;
}
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) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Font atlas is full, cannot cache codepoint: %d",
static_cast<int>(codepoint));
stbtt_FreeSDF(sdf, nullptr);
return;
}
int atlasX = rect.x + PADDING;
int atlasY = rect.y + PADDING;
Glyph glyph;
glyph.width = static_cast<float>(w);
glyph.height = static_cast<float>(h);
glyph.bearingX = static_cast<float>(xoff);
glyph.bearingY = static_cast<float>(yoff);
glyph.advance = advancePx;
// stb_rect_pack 使用左上角为原点OpenGL纹理使用左下角为原点
// 需要翻转V坐标
float v0 = static_cast<float>(atlasY) / ATLAS_HEIGHT;
float v1 = static_cast<float>(atlasY + h) / ATLAS_HEIGHT;
glyph.u0 = static_cast<float>(atlasX) / ATLAS_WIDTH;
glyph.v0 = 1.0f - v1; // 翻转V坐标
glyph.u1 = static_cast<float>(atlasX + w) / ATLAS_WIDTH;
glyph.v1 = 1.0f - v0; // 翻转V坐标
glyphs_[codepoint] = glyph;
// 将 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 通道
}
// 直接设置像素对齐为 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<int>(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<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) {
// 图集已满,无法缓存更多字形
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Font atlas is full, cannot cache codepoint: %d",
static_cast<int>(codepoint));
return;
}
int atlasX = rect.x + PADDING;
int atlasY = rect.y + PADDING;
// 创建字形信息
Glyph glyph;
glyph.width = static_cast<float>(w);
glyph.height = static_cast<float>(h);
glyph.bearingX = static_cast<float>(xoff);
glyph.bearingY = static_cast<float>(yoff);
glyph.advance = advancePx;
// 计算纹理坐标(相对于图集)
// stb_rect_pack 使用左上角为原点OpenGL纹理使用左下角为原点
// 需要翻转V坐标
float v0 = static_cast<float>(atlasY) / ATLAS_HEIGHT;
float v1 = static_cast<float>(atlasY + h) / ATLAS_HEIGHT;
glyph.u0 = static_cast<float>(atlasX) / ATLAS_WIDTH;
glyph.v0 = 1.0f - v1; // 翻转V坐标
glyph.u1 = static_cast<float>(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