feat(渲染): 添加SDF字体渲染支持

实现基于有符号距离场(SDF)的高质量字体渲染功能,包括:
1. 新增SDF专用着色器及配置文件
2. 扩展着色器管理器支持vec2/vec3类型uniform
3. 修改精灵批处理系统以支持自定义着色器
4. 更新渲染器实现SDF字体特殊处理逻辑
5. 替换示例中的字体资源为SDF字体
This commit is contained in:
ChestnutYueyue 2026-02-19 03:33:47 +08:00
parent c32c2dd60d
commit 080fb56003
12 changed files with 231 additions and 29 deletions

View File

@ -134,6 +134,7 @@ private:
IWindow* window_; IWindow* window_;
GLSpriteBatch spriteBatch_; GLSpriteBatch spriteBatch_;
Ptr<IShader> shapeShader_; Ptr<IShader> shapeShader_;
Ptr<IShader> sdfFontShader_; // SDF字体专用着色器
GLuint shapeVao_; // 形状 VAO手动管理用于顶点属性配置 GLuint shapeVao_; // 形状 VAO手动管理用于顶点属性配置
GLBuffer shapeBuffer_; // 形状 VBO使用 GLBuffer 管理) GLBuffer shapeBuffer_; // 形状 VBO使用 GLBuffer 管理)

View File

@ -4,6 +4,7 @@
#include <extra2d/graphics/backends/opengl/gl_texture.h> #include <extra2d/graphics/backends/opengl/gl_texture.h>
#include <extra2d/graphics/batch/sprite_batch.h> #include <extra2d/graphics/batch/sprite_batch.h>
#include <extra2d/graphics/shader/shader_interface.h> #include <extra2d/graphics/shader/shader_interface.h>
#include <extra2d/graphics/shader/shader_manager.h>
#include <glad/glad.h> #include <glad/glad.h>
#include <vector> #include <vector>
@ -27,6 +28,9 @@ public:
void begin(const glm::mat4 &viewProjection); void begin(const glm::mat4 &viewProjection);
void end(); void end();
// 使用指定着色器开始批处理
void begin(const glm::mat4 &viewProjection, Ptr<IShader> shader);
// 绘制单个精灵 // 绘制单个精灵
void draw(const Texture &texture, const SpriteData &data); void draw(const Texture &texture, const SpriteData &data);
@ -37,6 +41,16 @@ public:
// 获取绘制调用次数 // 获取绘制调用次数
uint32_t getDrawCallCount() const { return drawCallCount_; } uint32_t getDrawCallCount() const { return drawCallCount_; }
// 设置自定义着色器用于SDF字体等特殊渲染
void setShader(Ptr<IShader> shader);
// 获取当前着色器
Ptr<IShader> getShader() const { return shader_; }
// 设置额外的uniform值用于SDF字体等特殊渲染
void setExtraUniforms(const UniformValueMap &uniforms) { extraUniforms_ = uniforms; }
void clearExtraUniforms() { extraUniforms_.clear(); }
private: private:
// OpenGL 对象 // OpenGL 对象
GLuint vao_; GLuint vao_;
@ -60,6 +74,9 @@ private:
uint32_t drawCallCount_; uint32_t drawCallCount_;
glm::mat4 viewProjection_; glm::mat4 viewProjection_;
// 额外的uniform值用于SDF字体等特殊渲染
UniformValueMap extraUniforms_;
// 内部方法 // 内部方法
void flush(); void flush();
void submitBatch(); void submitBatch();

View File

@ -27,6 +27,8 @@ struct ShaderUniformDef {
std::string type; std::string type;
std::string description; std::string description;
float defaultValue = 0.0f; // 默认值用于float类型 float defaultValue = 0.0f; // 默认值用于float类型
float defaultVec2[2] = {0, 0}; // 默认值用于vec2类型
float defaultVec3[3] = {0, 0, 0}; // 默认值用于vec3类型
float defaultVec4[4] = {0, 0, 0, 0}; // 默认值用于vec4类型 float defaultVec4[4] = {0, 0, 0, 0}; // 默认值用于vec4类型
float defaultMat4[16] = { float defaultMat4[16] = {
1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0,

View File

@ -0,0 +1,39 @@
#version 300 es
precision highp float;
in vec2 v_texCoord;
in vec4 v_color;
uniform sampler2D u_texture;
uniform float u_opacity;
uniform float u_sdfThreshold;
uniform float u_sdfSmoothness;
uniform vec2 u_textureSize;
out vec4 fragColor;
void main() {
// 采样 SDF 纹理SDF 值存储在 alpha 通道,范围 0-255 已映射到 0-1
float sdfValue = texture(u_texture, v_texCoord).a;
// 使用 fwidth 计算屏幕空间的变化率
float fw = fwidth(sdfValue);
// 平衡的抗锯齿:根据屏幕空间变化率调整平滑范围
// 在放大时更平滑,缩小时更锐利
float smoothRange = max(u_sdfSmoothness, fw * 0.5);
// 使用 smoothstep 进行抗锯齿
float alpha = smoothstep(u_sdfThreshold - smoothRange,
u_sdfThreshold + smoothRange,
sdfValue);
// 应用颜色和透明度
fragColor = v_color;
fragColor.a *= alpha * u_opacity;
// 丢弃完全透明的像素
if (fragColor.a < 0.001) {
discard;
}
}

View File

@ -0,0 +1,60 @@
{
"name": "sdf_font",
"category": "builtin",
"version": "1.0",
"description": "SDF字体渲染Shader支持高质量抗锯齿",
"uniforms": {
"u_viewProjection": {
"type": "mat4",
"default": [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
],
"description": "视图投影矩阵"
},
"u_model": {
"type": "mat4",
"default": [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
],
"description": "模型矩阵"
},
"u_opacity": {
"type": "float",
"default": 1.0,
"description": "透明度"
},
"u_sdfThreshold": {
"type": "float",
"default": 0.5,
"description": "SDF阈值默认0.5范围0-1。值越小字形越粗越大越细"
},
"u_sdfSmoothness": {
"type": "float",
"default": 0.02,
"description": "SDF平滑度控制抗锯齿范围越小越锐利默认0.02"
},
"u_textureSize": {
"type": "vec2",
"default": [512, 512],
"description": "SDF纹理尺寸用于计算像素级平滑"
}
},
"samplers": {
"u_texture": {
"binding": 0,
"description": "SDF纹理采样器"
}
},
"backends": {
"opengl": {
"vertex": "backends/opengl/builtin/sprite.vert",
"fragment": "backends/opengl/builtin/sdf_font.frag"
}
}
}

View File

@ -165,6 +165,7 @@ void GLFontAtlas::createAtlas() {
std::vector<uint8_t> emptyData(ATLAS_WIDTH * ATLAS_HEIGHT * channels, 0); std::vector<uint8_t> emptyData(ATLAS_WIDTH * ATLAS_HEIGHT * channels, 0);
texture_ = std::make_unique<GLTexture>(ATLAS_WIDTH, ATLAS_HEIGHT, texture_ = std::make_unique<GLTexture>(ATLAS_WIDTH, ATLAS_HEIGHT,
emptyData.data(), channels); emptyData.data(), channels);
// 所有字体都使用线性过滤SDF的抗锯齿由着色器处理
texture_->setFilter(true); texture_->setFilter(true);
// 初始化矩形打包上下文 - 持久化以支持增量打包 // 初始化矩形打包上下文 - 持久化以支持增量打包

View File

@ -654,18 +654,42 @@ void GLRenderer::drawText(const FontAtlas &font, const std::string &text,
float cursorY = y; float cursorY = y;
float baselineY = cursorY + font.getAscent(); float baselineY = cursorY + font.getAscent();
// 确保批处理已激活(自动批处理) // 检查是否为SDF字体
if (autoBatchEnabled_) { bool isSDF = font.isSDF();
ensureBatchActive();
}
// 检查纹理变化,如果纹理不同则先提交当前批次 // 如果是SDF字体切换到SDF着色器
if (autoBatchEnabled_ && currentBatchTexture_ != nullptr && if (isSDF && sdfFontShader_) {
currentBatchTexture_ != font.getTexture()) { // 先提交当前普通批处理
submitPendingSprites(); if (autoBatchEnabled_ && !pendingSprites_.empty()) {
} submitPendingSprites();
if (autoBatchEnabled_) { }
currentBatchTexture_ = font.getTexture();
// 使用SDF着色器开始新的批处理
// 设置需要动态计算的uniform值
UniformValueMap sdfUniformValues;
sdfUniformValues["u_viewProjection"] = viewProjection_;
if (font.getTexture()) {
sdfUniformValues["u_textureSize"] = ShaderUniformValue(
glm::vec2(static_cast<float>(font.getTexture()->getWidth()),
static_cast<float>(font.getTexture()->getHeight())));
}
// 设置额外的uniform值让submitBatch使用
spriteBatch_.setExtraUniforms(sdfUniformValues);
spriteBatch_.begin(viewProjection_, sdfFontShader_);
} else {
// 确保批处理已激活(自动批处理)
if (autoBatchEnabled_) {
ensureBatchActive();
}
// 检查纹理变化,如果纹理不同则先提交当前批次
if (autoBatchEnabled_ && currentBatchTexture_ != nullptr &&
currentBatchTexture_ != font.getTexture()) {
submitPendingSprites();
}
if (autoBatchEnabled_) {
currentBatchTexture_ = font.getTexture();
}
} }
// 收集所有字符数据用于批处理 // 收集所有字符数据用于批处理
@ -677,7 +701,10 @@ void GLRenderer::drawText(const FontAtlas &font, const std::string &text,
if (codepoint == '\n') { if (codepoint == '\n') {
// 换行时,将当前行添加到待处理列表 // 换行时,将当前行添加到待处理列表
if (!sprites.empty()) { if (!sprites.empty()) {
if (autoBatchEnabled_) { if (isSDF && sdfFontShader_) {
// SDF模式直接提交
spriteBatch_.drawBatch(*font.getTexture(), sprites);
} else if (autoBatchEnabled_) {
pendingSprites_.insert(pendingSprites_.end(), sprites.begin(), pendingSprites_.insert(pendingSprites_.end(), sprites.begin(),
sprites.end()); sprites.end());
} else { } else {
@ -724,7 +751,7 @@ void GLRenderer::drawText(const FontAtlas &font, const std::string &text,
sprites.push_back(data); sprites.push_back(data);
// 自动批处理:如果缓冲区满,先提交当前批次 // 自动批处理:如果缓冲区满,先提交当前批次
if (autoBatchEnabled_ && sprites.size() >= MAX_BATCH_SPRITES) { if (!isSDF && autoBatchEnabled_ && sprites.size() >= MAX_BATCH_SPRITES) {
pendingSprites_.insert(pendingSprites_.end(), sprites.begin(), pendingSprites_.insert(pendingSprites_.end(), sprites.begin(),
sprites.end()); sprites.end());
sprites.clear(); sprites.clear();
@ -734,7 +761,10 @@ void GLRenderer::drawText(const FontAtlas &font, const std::string &text,
// 提交剩余的字符 // 提交剩余的字符
if (!sprites.empty()) { if (!sprites.empty()) {
if (autoBatchEnabled_) { if (isSDF && sdfFontShader_) {
// SDF模式直接提交
spriteBatch_.drawBatch(*font.getTexture(), sprites);
} else if (autoBatchEnabled_) {
pendingSprites_.insert(pendingSprites_.end(), sprites.begin(), pendingSprites_.insert(pendingSprites_.end(), sprites.begin(),
sprites.end()); sprites.end());
} else { } else {
@ -742,6 +772,18 @@ void GLRenderer::drawText(const FontAtlas &font, const std::string &text,
spriteBatch_.drawBatch(*font.getTexture(), sprites); spriteBatch_.drawBatch(*font.getTexture(), sprites);
} }
} }
// 如果是SDF字体结束批处理并恢复普通着色器
if (isSDF && sdfFontShader_) {
spriteBatch_.end();
// 清除额外的uniform值
spriteBatch_.clearExtraUniforms();
// 恢复默认的sprite着色器
auto defaultShader = ShaderManager::getInstance().getBuiltin("sprite");
if (defaultShader) {
spriteBatch_.setShader(defaultShader);
}
}
} }
/** /**
@ -762,6 +804,14 @@ void GLRenderer::initShapeRendering() {
} }
} }
// 加载SDF字体着色器
sdfFontShader_ = ShaderManager::getInstance().getBuiltin("sdf_font");
if (!sdfFontShader_) {
E2D_LOG_WARN("获取SDF字体着色器失败SDF字体将使用普通渲染");
} else {
E2D_LOG_INFO("SDF字体着色器加载成功");
}
// 初始化形状 GLBuffer使用 Dynamic 使用模式,因为每帧都会更新) // 初始化形状 GLBuffer使用 Dynamic 使用模式,因为每帧都会更新)
BufferDesc shapeBufferDesc; BufferDesc shapeBufferDesc;
shapeBufferDesc.type = BufferType::Vertex; shapeBufferDesc.type = BufferType::Vertex;
@ -889,14 +939,15 @@ void GLRenderer::flushShapeBatch() {
if (shapeShader_) { if (shapeShader_) {
shapeShader_->bind(); shapeShader_->bind();
// 只提供需要动态计算的值其他值使用JSON中定义的默认值 // 只提供需要动态计算的值其他值使用JSON中定义的默认值
UniformValueMap uniformValues; UniformValueMap uniformValues;
uniformValues["u_viewProjection"] = viewProjection_; uniformValues["u_viewProjection"] = viewProjection_;
// 使用ShaderManager自动应用uniform值未提供的值使用JSON中的默认值 // 使用ShaderManager自动应用uniform值未提供的值使用JSON中的默认值
// 使用着色器自己的名称从JSON中解析的name字段 // 使用着色器自己的名称从JSON中解析的name字段
ShaderManager::getInstance().applyUniforms(shapeShader_, shapeShader_->getName(), uniformValues); ShaderManager::getInstance().applyUniforms(
shapeShader_, shapeShader_->getName(), uniformValues);
} }
// 使用 GLBuffer::updateData() 更新缓冲区数据 // 使用 GLBuffer::updateData() 更新缓冲区数据
@ -925,14 +976,15 @@ void GLRenderer::flushLineBatch() {
glLineWidth(currentLineWidth_); glLineWidth(currentLineWidth_);
if (shapeShader_) { if (shapeShader_) {
shapeShader_->bind(); shapeShader_->bind();
// 只提供需要动态计算的值其他值使用JSON中定义的默认值 // 只提供需要动态计算的值其他值使用JSON中定义的默认值
UniformValueMap uniformValues; UniformValueMap uniformValues;
uniformValues["u_viewProjection"] = viewProjection_; uniformValues["u_viewProjection"] = viewProjection_;
// 使用ShaderManager自动应用uniform值未提供的值使用JSON中的默认值 // 使用ShaderManager自动应用uniform值未提供的值使用JSON中的默认值
// 使用着色器自己的名称从JSON中解析的name字段 // 使用着色器自己的名称从JSON中解析的name字段
ShaderManager::getInstance().applyUniforms(shapeShader_, shapeShader_->getName(), uniformValues); ShaderManager::getInstance().applyUniforms(
shapeShader_, shapeShader_->getName(), uniformValues);
} }
// 使用 GLBuffer::updateData() 更新缓冲区数据 // 使用 GLBuffer::updateData() 更新缓冲区数据

View File

@ -93,6 +93,22 @@ void GLSpriteBatch::begin(const glm::mat4 &viewProjection) {
ebo_.bind(); ebo_.bind();
} }
void GLSpriteBatch::begin(const glm::mat4 &viewProjection, Ptr<IShader> shader) {
// 设置自定义着色器
if (shader) {
shader_ = shader;
}
begin(viewProjection);
}
void GLSpriteBatch::setShader(Ptr<IShader> shader) {
// 如果当前有未提交的批次,先提交
if (batch_.getSpriteCount() > 0) {
flush();
}
shader_ = shader;
}
void GLSpriteBatch::end() { void GLSpriteBatch::end() {
if (batch_.getSpriteCount() > 0) { if (batch_.getSpriteCount() > 0) {
flush(); flush();
@ -166,6 +182,11 @@ void GLSpriteBatch::submitBatch() {
UniformValueMap uniformValues; UniformValueMap uniformValues;
uniformValues["u_viewProjection"] = viewProjection_; uniformValues["u_viewProjection"] = viewProjection_;
// 合并额外的uniform值如SDF字体的u_textureSize
for (const auto& [name, value] : extraUniforms_) {
uniformValues[name] = value;
}
// 使用ShaderManager自动应用uniform值未提供的值使用JSON中的默认值 // 使用ShaderManager自动应用uniform值未提供的值使用JSON中的默认值
// 使用着色器自己的名称从JSON中解析的name字段 // 使用着色器自己的名称从JSON中解析的name字段
ShaderManager::getInstance().applyUniforms(shader_, shader_->getName(), ShaderManager::getInstance().applyUniforms(shader_, shader_->getName(),

View File

@ -549,6 +549,16 @@ Ptr<IShader> ShaderManager::loadFromMetadata(const std::string &jsonPath,
def.defaultInt = value["default"].get<int>(); def.defaultInt = value["default"].get<int>();
} else if (def.type == "bool") { } else if (def.type == "bool") {
def.defaultBool = value["default"].get<bool>(); def.defaultBool = value["default"].get<bool>();
} else if (def.type == "vec2" && value["default"].is_array()) {
auto arr = value["default"].get<std::vector<float>>();
for (size_t i = 0; i < arr.size() && i < 2; ++i) {
def.defaultVec2[i] = arr[i];
}
} else if (def.type == "vec3" && value["default"].is_array()) {
auto arr = value["default"].get<std::vector<float>>();
for (size_t i = 0; i < arr.size() && i < 3; ++i) {
def.defaultVec3[i] = arr[i];
}
} else if (def.type == "vec4" && value["default"].is_array()) { } else if (def.type == "vec4" && value["default"].is_array()) {
auto arr = value["default"].get<std::vector<float>>(); auto arr = value["default"].get<std::vector<float>>();
for (size_t i = 0; i < arr.size() && i < 4; ++i) { for (size_t i = 0; i < arr.size() && i < 4; ++i) {
@ -806,6 +816,12 @@ void ShaderManager::applyUniforms(Ptr<IShader> shader,
shader->setInt(name, def.defaultInt); shader->setInt(name, def.defaultInt);
} else if (def.type == "bool") { } else if (def.type == "bool") {
shader->setBool(name, def.defaultBool); shader->setBool(name, def.defaultBool);
} else if (def.type == "vec2") {
shader->setVec2(name,
glm::vec2(def.defaultVec2[0], def.defaultVec2[1]));
} else if (def.type == "vec3") {
shader->setVec3(name, glm::vec3(def.defaultVec3[0], def.defaultVec3[1],
def.defaultVec3[2]));
} else if (def.type == "vec4") { } else if (def.type == "vec4") {
shader->setVec4(name, shader->setVec4(name,
glm::vec4(def.defaultVec4[0], def.defaultVec4[1], glm::vec4(def.defaultVec4[0], def.defaultVec4[1],

Binary file not shown.

View File

@ -23,17 +23,10 @@ public:
} }
try { try {
font_ = renderer_->createFontAtlas("assets/fonts/arial.ttf", 24); font_ = renderer_->createFontAtlas("assets/fonts/fonts.ttf", 26, true);
} catch (...) { } catch (...) {
std::cerr << "Failed to load font from assets/fonts/arial.ttf" std::cerr << "Failed to load font from assets/fonts/fonts.ttf"
<< std::endl; << std::endl;
// 尝试使用备用路径
try {
font_ = renderer_->createFontAtlas("C:/Windows/Fonts/arial.ttf", 24);
} catch (...) {
std::cerr << "Failed to load font from system path!" << std::endl;
font_ = nullptr;
}
} }
if (font_) { if (font_) {