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

View File

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

View File

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

View File

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

View File

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