perf(渲染): 优化文本渲染和精灵批处理性能
- 在 GLSpriteBatch 中添加 viewProjection 缓存以减少重复设置 - 增大字体图集尺寸以支持更多字符 - 预分配字形缓冲区避免动态内存分配 - 统一使用 RGBA 格式处理 SDF 和普通字形 - 优化文本渲染使用批处理绘制 - 使用编译期生成的静态索引缓冲区 - 优化顶点变换计算减少重复运算 - 硬编码 SDF 常量到着色器中避免运行时传参
This commit is contained in:
parent
f86f9b3966
commit
4f641a2854
|
|
@ -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_;
|
||||
|
|
@ -55,6 +55,10 @@ private:
|
|||
float ascent_;
|
||||
float descent_;
|
||||
float lineGap_;
|
||||
|
||||
// 预分配字形位图缓冲区,避免每次动态分配
|
||||
mutable std::vector<uint8_t> glyphBitmapCache_;
|
||||
mutable std::vector<uint8_t> glyphRgbaCache_;
|
||||
|
||||
void createAtlas();
|
||||
void cacheGlyph(char32_t codepoint) const;
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ private:
|
|||
const Texture *currentTexture_;
|
||||
bool currentIsSDF_;
|
||||
glm::mat4 viewProjection_;
|
||||
|
||||
// 缓存上一帧的 viewProjection,避免重复设置
|
||||
glm::mat4 cachedViewProjection_;
|
||||
bool viewProjectionDirty_ = true;
|
||||
|
||||
uint32_t drawCallCount_;
|
||||
uint32_t spriteCount_;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{}; }
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
vertexBuffer_[vertexCount_++] = v0;
|
||||
vertexBuffer_[vertexCount_++] = v1;
|
||||
vertexBuffer_[vertexCount_++] = v2;
|
||||
vertexBuffer_[vertexCount_++] = v3;
|
||||
// 直接计算变换后的位置
|
||||
float rx0 = -anchorOffsetX;
|
||||
float ry0 = -anchorOffsetY;
|
||||
float rx1 = data.size.x - anchorOffsetX;
|
||||
float ry1 = data.size.y - anchorOffsetY;
|
||||
|
||||
// 预计算旋转后的偏移
|
||||
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_);
|
||||
|
|
|
|||
Loading…
Reference in New Issue