渲染后端加入
This commit is contained in:
313
Fostbite2D/src/fostbite2D/render/opengl/gl_font_atlas.cpp
Normal file
313
Fostbite2D/src/fostbite2D/render/opengl/gl_font_atlas.cpp
Normal file
@@ -0,0 +1,313 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user