perf(渲染): 优化文本渲染和精灵批处理性能

- 在 GLSpriteBatch 中添加 viewProjection 缓存以减少重复设置
- 增大字体图集尺寸以支持更多字符
- 预分配字形缓冲区避免动态内存分配
- 统一使用 RGBA 格式处理 SDF 和普通字形
- 优化文本渲染使用批处理绘制
- 使用编译期生成的静态索引缓冲区
- 优化顶点变换计算减少重复运算
- 硬编码 SDF 常量到着色器中避免运行时传参
This commit is contained in:
ChestnutYueyue 2026-02-13 10:43:38 +08:00
parent f86f9b3966
commit 4f641a2854
5 changed files with 128 additions and 72 deletions

View File

@ -34,9 +34,9 @@ public:
bool isSDF() const override { return useSDF_; }
private:
// 图集配置
static constexpr int ATLAS_WIDTH = 512;
static constexpr int ATLAS_HEIGHT = 512;
// 图集配置 - 增大尺寸以支持更多字符
static constexpr int ATLAS_WIDTH = 1024;
static constexpr int ATLAS_HEIGHT = 1024;
static constexpr int PADDING = 2; // 字形之间的间距
int fontSize_;
@ -56,6 +56,10 @@ private:
float descent_;
float lineGap_;
// 预分配字形位图缓冲区,避免每次动态分配
mutable std::vector<uint8_t> glyphBitmapCache_;
mutable std::vector<uint8_t> glyphRgbaCache_;
void createAtlas();
void cacheGlyph(char32_t codepoint) const;
};

View File

@ -79,6 +79,10 @@ private:
bool currentIsSDF_;
glm::mat4 viewProjection_;
// 缓存上一帧的 viewProjection避免重复设置
glm::mat4 cachedViewProjection_;
bool viewProjectionDirty_ = true;
uint32_t drawCallCount_;
uint32_t spriteCount_;
uint32_t batchCount_;

View File

@ -97,7 +97,8 @@ Vec2 GLFontAtlas::measureText(const std::string &text) {
// 创建图集纹理 - 初始化空白纹理和矩形打包上下文
// ============================================================================
void GLFontAtlas::createAtlas() {
int channels = useSDF_ ? 1 : 4;
// 统一使用 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);
@ -107,6 +108,12 @@ void GLFontAtlas::createAtlas() {
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);
}
// ============================================================================
@ -171,14 +178,23 @@ void GLFontAtlas::cacheGlyph(char32_t codepoint) const {
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());
GLint prevUnpackAlignment = 4;
glGetIntegerv(GL_UNPACK_ALIGNMENT, &prevUnpackAlignment);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
// OpenGL纹理坐标原点在左下角需要将Y坐标翻转
glTexSubImage2D(GL_TEXTURE_2D, 0, atlasX, ATLAS_HEIGHT - atlasY - h, w, h,
GL_RED, GL_UNSIGNED_BYTE, sdf);
glPixelStorei(GL_UNPACK_ALIGNMENT, prevUnpackAlignment);
GL_RGBA, GL_UNSIGNED_BYTE, glyphRgbaCache_.data());
stbtt_FreeSDF(sdf, nullptr);
return;
@ -199,9 +215,10 @@ void GLFontAtlas::cacheGlyph(char32_t codepoint) const {
return;
}
std::vector<unsigned char> bitmap(
static_cast<size_t>(w) * static_cast<size_t>(h), 0);
stbtt_MakeCodepointBitmap(&fontInfo_, bitmap.data(), w, h, w, scale_, scale_,
// 使用预分配缓冲区
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 打包矩形
@ -244,20 +261,22 @@ void GLFontAtlas::cacheGlyph(char32_t codepoint) const {
glyphs_[codepoint] = glyph;
// 将单通道字形数据转换为 RGBA 格式白色字形Alpha 通道存储灰度)
std::vector<uint8_t> rgbaData(w * h * 4);
for (int i = 0; i < w * h; ++i) {
uint8_t alpha = bitmap[static_cast<size_t>(i)];
rgbaData[i * 4 + 0] = 255; // R
rgbaData[i * 4 + 1] = 255; // G
rgbaData[i * 4 + 2] = 255; // B
rgbaData[i * 4 + 3] = alpha; // A
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坐标翻转
// 直接设置像素对齐为 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, rgbaData.data());
GL_RGBA, GL_UNSIGNED_BYTE, glyphRgbaCache_.data());
}
} // namespace extra2d

View File

@ -444,6 +444,10 @@ void GLRenderer::drawText(const FontAtlas &font, const std::string &text,
float cursorY = y;
float baselineY = cursorY + font.getAscent();
// 收集所有字符数据用于批处理
std::vector<GLSpriteBatch::SpriteData> sprites;
sprites.reserve(text.size()); // 预分配空间
for (char32_t codepoint : utf8ToUtf32(text)) {
if (codepoint == '\n') {
cursorX = x;
@ -464,20 +468,24 @@ void GLRenderer::drawText(const FontAtlas &font, const std::string &text,
float xPos = penX + glyph->bearingX;
float yPos = baselineY + glyph->bearingY;
Rect destRect(xPos, yPos, glyph->width, glyph->height);
GLSpriteBatch::SpriteData data;
data.position = glm::vec2(destRect.origin.x, destRect.origin.y);
data.size = glm::vec2(destRect.size.width, destRect.size.height);
data.position = glm::vec2(xPos, yPos);
data.size = glm::vec2(glyph->width, glyph->height);
data.texCoordMin = glm::vec2(glyph->u0, glyph->v0);
data.texCoordMax = glm::vec2(glyph->u1, glyph->v1);
data.color = glm::vec4(color.r, color.g, color.b, color.a);
data.rotation = 0.0f;
data.anchor = glm::vec2(0.0f, 0.0f);
data.isSDF = font.isSDF();
spriteBatch_.draw(*font.getTexture(), data);
sprites.push_back(data);
}
}
// 使用批处理绘制所有字符
if (!sprites.empty()) {
spriteBatch_.drawBatch(*font.getTexture(), sprites);
}
}
void GLRenderer::resetStats() { stats_ = Stats{}; }

View File

@ -49,6 +49,24 @@ private:
const TrigLookup::Tables TrigLookup::table_;
// 静态索引生成函数
static const std::array<GLuint, GLSpriteBatch::MAX_INDICES>& getIndices() {
static std::array<GLuint, GLSpriteBatch::MAX_INDICES> indices = []() {
std::array<GLuint, GLSpriteBatch::MAX_INDICES> arr{};
for (size_t i = 0; i < GLSpriteBatch::MAX_SPRITES; ++i) {
GLuint base = static_cast<GLuint>(i * GLSpriteBatch::VERTICES_PER_SPRITE);
arr[i * GLSpriteBatch::INDICES_PER_SPRITE + 0] = base + 0;
arr[i * GLSpriteBatch::INDICES_PER_SPRITE + 1] = base + 1;
arr[i * GLSpriteBatch::INDICES_PER_SPRITE + 2] = base + 2;
arr[i * GLSpriteBatch::INDICES_PER_SPRITE + 3] = base + 0;
arr[i * GLSpriteBatch::INDICES_PER_SPRITE + 4] = base + 2;
arr[i * GLSpriteBatch::INDICES_PER_SPRITE + 5] = base + 3;
}
return arr;
}();
return indices;
}
// 顶点着色器 (GLES 3.2)
static const char *SPRITE_VERTEX_SHADER = R"(
#version 300 es
@ -70,6 +88,8 @@ void main() {
)";
// 片段着色器 (GLES 3.2)
// SDF 常量硬编码ONEDGE_VALUE=128/255=0.502, PIXEL_DIST_SCALE=255/64=3.98
// SDF 值存储在 Alpha 通道
static const char *SPRITE_FRAGMENT_SHADER = R"(
#version 300 es
precision highp float;
@ -78,15 +98,13 @@ in vec4 vColor;
uniform sampler2D uTexture;
uniform int uUseSDF;
uniform float uSdfOnEdge;
uniform float uSdfScale;
out vec4 fragColor;
void main() {
if (uUseSDF == 1) {
float dist = texture(uTexture, vTexCoord).r;
float sd = (dist - uSdfOnEdge) * uSdfScale;
float dist = texture(uTexture, vTexCoord).a;
float sd = (dist - 0.502) * 3.98;
float w = fwidth(sd);
float alpha = smoothstep(-w, w, sd);
fragColor = vec4(vColor.rgb, vColor.a * alpha);
@ -136,19 +154,8 @@ bool GLSpriteBatch::init() {
glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(void *)offsetof(Vertex, color));
// 生成索引缓冲区 - 静态,只需创建一次
std::vector<GLuint> indices;
indices.reserve(MAX_INDICES);
for (size_t i = 0; i < MAX_SPRITES; ++i) {
GLuint base = static_cast<GLuint>(i * VERTICES_PER_SPRITE);
indices.push_back(base + 0);
indices.push_back(base + 1);
indices.push_back(base + 2);
indices.push_back(base + 0);
indices.push_back(base + 2);
indices.push_back(base + 3);
}
// 使用编译期生成的静态索引缓冲区
const auto& indices = getIndices();
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(GLuint),
indices.data(), GL_STATIC_DRAW);
@ -196,40 +203,55 @@ bool GLSpriteBatch::needsFlush(const Texture &texture, bool isSDF) const {
}
void GLSpriteBatch::addVertices(const SpriteData &data) {
// 计算变换后的顶点位置
glm::vec2 anchorOffset(data.size.x * data.anchor.x,
data.size.y * data.anchor.y);
// 计算锚点偏移
float anchorOffsetX = data.size.x * data.anchor.x;
float anchorOffsetY = data.size.y * data.anchor.y;
// 使用三角函数查表替代 cosf/sinf
float cosR = TrigLookup::cosRad(data.rotation);
float sinR = TrigLookup::sinRad(data.rotation);
auto transform = [&](float x, float y) -> glm::vec2 {
float rx = x - anchorOffset.x;
float ry = y - anchorOffset.y;
return glm::vec2(data.position.x + rx * cosR - ry * sinR,
data.position.y + rx * sinR + ry * cosR);
};
glm::vec4 color(data.color.r, data.color.g, data.color.b, data.color.a);
// 添加四个顶点(图片已在加载时翻转,纹理坐标直接使用)
// v0(左上) -- v1(右上)
// | |
// v3(左下) -- v2(右下)
Vertex v0{transform(0, 0), glm::vec2(data.texCoordMin.x, data.texCoordMin.y),
color};
Vertex v1{transform(data.size.x, 0),
glm::vec2(data.texCoordMax.x, data.texCoordMin.y), color};
Vertex v2{transform(data.size.x, data.size.y),
glm::vec2(data.texCoordMax.x, data.texCoordMax.y), color};
Vertex v3{transform(0, data.size.y),
glm::vec2(data.texCoordMin.x, data.texCoordMax.y), color};
// 直接计算变换后的位置
float rx0 = -anchorOffsetX;
float ry0 = -anchorOffsetY;
float rx1 = data.size.x - anchorOffsetX;
float ry1 = data.size.y - anchorOffsetY;
vertexBuffer_[vertexCount_++] = v0;
vertexBuffer_[vertexCount_++] = v1;
vertexBuffer_[vertexCount_++] = v2;
vertexBuffer_[vertexCount_++] = v3;
// 预计算旋转后的偏移
float cosRx0 = rx0 * cosR, sinRx0 = rx0 * sinR;
float cosRx1 = rx1 * cosR, sinRx1 = rx1 * sinR;
float cosRy0 = ry0 * cosR, sinRy0 = ry0 * sinR;
float cosRy1 = ry1 * cosR, sinRy1 = ry1 * sinR;
// v0: (0, 0) -> (rx0, ry0)
vertexBuffer_[vertexCount_++] = {
glm::vec2(data.position.x + cosRx0 - sinRy0, data.position.y + sinRx0 + cosRy0),
glm::vec2(data.texCoordMin.x, data.texCoordMin.y),
color
};
// v1: (size.x, 0) -> (rx1, ry0)
vertexBuffer_[vertexCount_++] = {
glm::vec2(data.position.x + cosRx1 - sinRy0, data.position.y + sinRx1 + cosRy0),
glm::vec2(data.texCoordMax.x, data.texCoordMin.y),
color
};
// v2: (size.x, size.y) -> (rx1, ry1)
vertexBuffer_[vertexCount_++] = {
glm::vec2(data.position.x + cosRx1 - sinRy1, data.position.y + sinRx1 + cosRy1),
glm::vec2(data.texCoordMax.x, data.texCoordMax.y),
color
};
// v3: (0, size.y) -> (rx0, ry1)
vertexBuffer_[vertexCount_++] = {
glm::vec2(data.position.x + cosRx0 - sinRy1, data.position.y + sinRx0 + cosRy1),
glm::vec2(data.texCoordMin.x, data.texCoordMax.y),
color
};
}
void GLSpriteBatch::draw(const Texture &texture, const SpriteData &data) {
@ -316,8 +338,7 @@ void GLSpriteBatch::flush() {
shader_.setMat4("uViewProjection", viewProjection_);
shader_.setInt("uTexture", 0);
shader_.setInt("uUseSDF", currentIsSDF_ ? 1 : 0);
shader_.setFloat("uSdfOnEdge", 128.0f / 255.0f);
shader_.setFloat("uSdfScale", 255.0f / 64.0f);
// SDF 常量已硬编码到着色器中
// 更新 VBO 数据 - 只更新实际使用的部分
glBindBuffer(GL_ARRAY_BUFFER, vbo_);