From 4eda57b4e490a9cc9d5d1f739c54ae3c35fb62f4 Mon Sep 17 00:00:00 2001 From: vottozi Date: Thu, 12 Feb 2026 00:08:52 +0600 Subject: [PATCH] single texture atlas for all font glyphs in TextRenderer --- src/Game.cpp | 4 +- src/render/TextRenderer.cpp | 264 +++++++++++++++++++++++++++++------- src/render/TextRenderer.h | 10 +- 3 files changed, 227 insertions(+), 51 deletions(-) diff --git a/src/Game.cpp b/src/Game.cpp index 5ce7e6c..6b39c63 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -26,8 +26,8 @@ namespace ZL #ifdef EMSCRIPTEN const char* CONST_ZIP_FILE = "resources.zip"; #else - const char* CONST_ZIP_FILE = "C:\\Work\\Projects\\space-game001\\resources.zip"; - //const char* CONST_ZIP_FILE = ""; + // const char* CONST_ZIP_FILE = "C:\\Work\\Projects\\space-game001\\resources.zip"; + const char* CONST_ZIP_FILE = ""; #endif static bool g_exitBgAnimating = false; diff --git a/src/render/TextRenderer.cpp b/src/render/TextRenderer.cpp index 067086e..7cd52c4 100644 --- a/src/render/TextRenderer.cpp +++ b/src/render/TextRenderer.cpp @@ -6,6 +6,8 @@ #include "render/OpenGlExtensions.h" #include #include +#include +#include namespace ZL { @@ -15,7 +17,7 @@ TextRenderer::~TextRenderer() if (kv.second.texID) glDeleteTextures(1, &kv.second.texID); }*/ glyphs.clear(); - + atlasTexture.reset(); textMesh.positionVBO.reset(); /* if (vbo) glDeleteBuffers(1, &vbo); @@ -97,38 +99,188 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s } FT_Set_Pixel_Sizes(face, 0, pixelSize); - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - glyphs.clear(); - // Проходим по стандартным ASCII символам + // glyphs.clear(); + + // Сначала собираем все глифы в память + struct GlyphBitmap { + int width = 0; + int height = 0; + std::vector data; // R8 байты (ширина*height) + Eigen::Vector2f bearing; + unsigned int advance = 0; + }; + + std::vector> glyphList; + glyphList.reserve(128 - 32); + + int maxGlyphHeight = 0; + for (unsigned char c = 32; c < 128; ++c) { - - FT_Load_Char(face, c, FT_LOAD_RENDER); + if (FT_Load_Char(face, c, FT_LOAD_RENDER)) { + // пропускаем если не удалось загрузить, но сохраняем пустой запись с advance + GlyphBitmap gb; + gb.width = 0; + gb.height = 0; + gb.bearing = { 0.f, 0.f }; + gb.advance = 0; + glyphList.emplace_back((char)c, std::move(gb)); + continue; + } - TextureDataStruct glyphData; - glyphData.width = face->glyph->bitmap.width; - glyphData.height = face->glyph->bitmap.rows; - glyphData.format = TextureDataStruct::R8; - glyphData.mipmap = TextureDataStruct::NONE; + GlyphBitmap gb; + gb.width = face->glyph->bitmap.width; + gb.height = face->glyph->bitmap.rows; + gb.bearing = Eigen::Vector2f((float)face->glyph->bitmap_left, (float)face->glyph->bitmap_top); + gb.advance = static_cast(face->glyph->advance.x); - // Копируем буфер FreeType в вектор данных - size_t dataSize = glyphData.width * glyphData.height; - glyphData.data.assign(face->glyph->bitmap.buffer, face->glyph->bitmap.buffer + dataSize); + size_t dataSize = static_cast(gb.width) * static_cast(gb.height); + if (dataSize > 0) { + gb.data.assign(face->glyph->bitmap.buffer, face->glyph->bitmap.buffer + dataSize); + maxGlyphHeight = max(maxGlyphHeight, gb.height); + } - // Теперь создание текстуры — это одна строка! - auto tex = std::make_shared(glyphData); - - GlyphInfo g; - g.texture = tex; - g.size = Eigen::Vector2f((float)face->glyph->bitmap.width, (float)face->glyph->bitmap.rows); - g.bearing = Eigen::Vector2f((float)face->glyph->bitmap_left, (float)face->glyph->bitmap_top); - // Advance во FreeType измеряется в 1/64 пикселя - g.advance = (unsigned int)face->glyph->advance.x; - - glyphs.emplace((char)c, g); + glyphList.emplace_back((char)c, std::move(gb)); } + // Пакуем глифы в один атлас (упрощённый алгоритм строковой укладки) + const int padding = 1; + const int maxAtlasWidth = 1024; // безопасное значение для большинства устройств + int curX = padding; + int curY = padding; + int rowHeight = 0; + int neededWidth = 0; + int neededHeight = 0; + + // Предварительно вычислим требуемый размер, укладывая в maxAtlasWidth + for (auto& p : glyphList) { + const GlyphBitmap& gb = p.second; + int w = gb.width; + int h = gb.height; + if (curX + w + padding > maxAtlasWidth) { + // новая строка + neededWidth = max(neededWidth, curX); + curX = padding; + curY += rowHeight + padding; + rowHeight = 0; + } + curX += w + padding; + rowHeight = max(rowHeight, h); + } + neededWidth = max(neededWidth, curX); + neededHeight = curY + rowHeight + padding; + + // Подгоняем к степеням двух (необязательно, но часто удобно) + auto nextPow2 = [](int v) { + int p = 1; + while (p < v) p <<= 1; + return p; + }; + + atlasWidth = static_cast(nextPow2(max(16, neededWidth))); + atlasHeight = static_cast(nextPow2(max(16, neededHeight))); + + // Ограничение - если получилось слишком большое, попробуем без power-of-two + if (atlasWidth > 4096) atlasWidth = static_cast(neededWidth); + if (atlasHeight > 4096) atlasHeight = static_cast(neededHeight); + + // Создаём буфер атласа, инициализируем нулями (прозрачность) + std::vector atlasData(atlasWidth * atlasHeight, 0); + + // Второй проход - размещаем глифы и заполняем atlasData + curX = padding; + curY = padding; + rowHeight = 0; + + for (auto &p : glyphList) { + char ch = p.first; + GlyphBitmap &gb = p.second; + + if (gb.width == 0 || gb.height == 0) { + // пустой глиф — записываем UV с нулевым размером и метрики + GlyphInfo gi; + gi.size = Eigen::Vector2f(0.f, 0.f); + gi.bearing = gb.bearing; + gi.advance = gb.advance; + gi.uv = Eigen::Vector2f(0.f, 0.f); + gi.uvSize = Eigen::Vector2f(0.f, 0.f); + glyphs.emplace(ch, gi); + continue; + } + + if (curX + gb.width + padding > static_cast(atlasWidth)) { + // новая строка + curX = padding; + curY += rowHeight + padding; + rowHeight = 0; + } + + // Копируем строки глифа в atlasData + for (int row = 0; row < gb.height; ++row) { + // FreeType буфер, как мы ранее использовали, хранит строки подряд. + // Копируем gb.width байт из gb.data на позицию (curX, curY + row) + int destY = curY + row; + int destX = curX; + char* destPtr = atlasData.data() + destY * atlasWidth + destX; + const char* srcPtr = gb.data.data() + row * gb.width; + std::memcpy(destPtr, srcPtr, static_cast(gb.width)); + } + + // Сохраняем информацию о глифе (в пикселях и UV) + GlyphInfo gi; + gi.size = Eigen::Vector2f((float)gb.width, (float)gb.height); + gi.bearing = gb.bearing; + gi.advance = gb.advance; + + // UV: нормализуем относительно размера атласа. Здесь uv указывает на верх-лево. + gi.uv = Eigen::Vector2f((float)curX / (float)atlasWidth, (float)curY / (float)atlasHeight); + gi.uvSize = Eigen::Vector2f((float)gb.width / (float)atlasWidth, (float)gb.height / (float)atlasHeight); + + glyphs.emplace(ch, gi); + + curX += gb.width + padding; + rowHeight = max(rowHeight, gb.height); + } + + // // Проходим по стандартным ASCII символам + // for (unsigned char c = 32; c < 128; ++c) { + // + // FT_Load_Char(face, c, FT_LOAD_RENDER); + + // TextureDataStruct glyphData; + // glyphData.width = face->glyph->bitmap.width; + // glyphData.height = face->glyph->bitmap.rows; + // glyphData.format = TextureDataStruct::R8; + // glyphData.mipmap = TextureDataStruct::NONE; + + // // Копируем буфер FreeType в вектор данных + // size_t dataSize = glyphData.width * glyphData.height; + // glyphData.data.assign(face->glyph->bitmap.buffer, face->glyph->bitmap.buffer + dataSize); + + // // Теперь создание текстуры — это одна строка! + // auto tex = std::make_shared(glyphData); + + //GlyphInfo g; + // g.texture = tex; + // g.size = Eigen::Vector2f((float)face->glyph->bitmap.width, (float)face->glyph->bitmap.rows); + // g.bearing = Eigen::Vector2f((float)face->glyph->bitmap_left, (float)face->glyph->bitmap_top); + // // Advance во FreeType измеряется в 1/64 пикселя + // g.advance = (unsigned int)face->glyph->advance.x; + + // glyphs.emplace((char)c, g); + // } + + // Создаём Texture из atlasData (R8) + TextureDataStruct atlasTex; + atlasTex.width = atlasWidth; + atlasTex.height = atlasHeight; + atlasTex.format = TextureDataStruct::R8; + atlasTex.mipmap = TextureDataStruct::NONE; + atlasTex.data = std::move(atlasData); + + atlasTexture = std::make_shared(atlasTex); + // Очистка FT_Done_Face(face); FT_Done_FreeType(ft); @@ -142,7 +294,7 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array color) { - if (!r || text.empty()) return; + if (!r || text.empty() || !atlasTexture) return; // 1. Считаем ширину для центрирования float totalW = 0.0f; @@ -180,13 +332,27 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca textData.PositionData.push_back({ xpos + w, ypos, 0.0f }); textData.PositionData.push_back({ xpos + w, ypos + h, 0.0f }); - // UV-координаты (здесь есть нюанс с атласом, ниже поясню) - textData.TexCoordData.push_back({ 0.0f, 0.0f }); - textData.TexCoordData.push_back({ 0.0f, 1.0f }); - textData.TexCoordData.push_back({ 1.0f, 1.0f }); - textData.TexCoordData.push_back({ 0.0f, 0.0f }); - textData.TexCoordData.push_back({ 1.0f, 1.0f }); - textData.TexCoordData.push_back({ 1.0f, 0.0f }); + // TexCoords — на основе UV позиции и размера в атласе (uv указывает на верх-лево) + float u0 = g.uv.x(); + float v0 = g.uv.y(); + float u1 = u0 + g.uvSize.x(); + float v1 = v0 + g.uvSize.y(); + + //// UV-координаты (здесь есть нюанс с атласом, ниже поясню) + //textData.TexCoordData.push_back({ 0.0f, 0.0f }); + //textData.TexCoordData.push_back({ 0.0f, 1.0f }); + //textData.TexCoordData.push_back({ 1.0f, 1.0f }); + //textData.TexCoordData.push_back({ 0.0f, 0.0f }); + //textData.TexCoordData.push_back({ 1.0f, 1.0f }); + //textData.TexCoordData.push_back({ 1.0f, 0.0f }); + + // Соответствие прежней системе UV: (0,0)=верх-лево, (0,1)=ниж-лево + textData.TexCoordData.push_back({ u0, v0 }); + textData.TexCoordData.push_back({ u0, v1 }); + textData.TexCoordData.push_back({ u1, v1 }); + textData.TexCoordData.push_back({ u0, v0 }); + textData.TexCoordData.push_back({ u1, v1 }); + textData.TexCoordData.push_back({ u1, v0 }); penX += (g.advance >> 6) * scale; } @@ -214,33 +380,35 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca r->RenderUniform4fv("uColor", color.data()); glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, atlasTexture->getTexID()); - // ВНИМАНИЕ: Так как у тебя каждый символ — это отдельная текстура, - // нам всё равно придется делать glDrawArrays в цикле, ЛИБО использовать атлас. - // Если оставляем текущую систему с разными текстурами: r->EnableVertexAttribArray("vPosition"); r->EnableVertexAttribArray("vTexCoord"); - for (size_t i = 0; i < text.length(); ++i) { - auto it = glyphs.find(text[i]); - if (it == glyphs.end()) continue; + //for (size_t i = 0; i < text.length(); ++i) { + // auto it = glyphs.find(text[i]); + // if (it == glyphs.end()) continue; - glBindTexture(GL_TEXTURE_2D, it->second.texture->getTexID()); + // glBindTexture(GL_TEXTURE_2D, it->second.texture->getTexID()); - // Отрисовываем по 6 вершин за раз - // Нам нужно вручную биндить VBO, так как DrawVertexRenderStruct рисует всё сразу - glBindBuffer(GL_ARRAY_BUFFER, textMesh.positionVBO->getBuffer()); - r->VertexAttribPointer3fv("vPosition", 0, (const char*)(i * 6 * sizeof(Vector3f))); + // // Отрисовываем по 6 вершин за раз + // // Нам нужно вручную биндить VBO, так как DrawVertexRenderStruct рисует всё сразу + // glBindBuffer(GL_ARRAY_BUFFER, textMesh.positionVBO->getBuffer()); + // r->VertexAttribPointer3fv("vPosition", 0, (const char*)(i * 6 * sizeof(Vector3f))); - glBindBuffer(GL_ARRAY_BUFFER, textMesh.texCoordVBO->getBuffer()); - r->VertexAttribPointer2fv("vTexCoord", 0, (const char*)(i * 6 * sizeof(Vector2f))); + // glBindBuffer(GL_ARRAY_BUFFER, textMesh.texCoordVBO->getBuffer()); + // r->VertexAttribPointer2fv("vTexCoord", 0, (const char*)(i * 6 * sizeof(Vector2f))); - glDrawArrays(GL_TRIANGLES, 0, 6); - } + // glDrawArrays(GL_TRIANGLES, 0, 6); + //} + r->DrawVertexRenderStruct(textMesh); r->DisableVertexAttribArray("vPosition"); r->DisableVertexAttribArray("vTexCoord"); r->shaderManager.PopShader(); + + // Сброс бинда текстуры не обязателен, но можно для чистоты + glBindTexture(GL_TEXTURE_2D, 0); } } // namespace ZL \ No newline at end of file diff --git a/src/render/TextRenderer.h b/src/render/TextRenderer.h index 4f78efc..e55e80e 100644 --- a/src/render/TextRenderer.h +++ b/src/render/TextRenderer.h @@ -12,7 +12,10 @@ namespace ZL { struct GlyphInfo { - std::shared_ptr texture; // Texture for glyph + // std::shared_ptr texture; // Texture for glyph + Eigen::Vector2f uv; // u,v координата левого верхнего угла в атласе (0..1) + Eigen::Vector2f uvSize; // ширина/высота в UV (0..1) + Eigen::Vector2f size; // glyph size in pixels Eigen::Vector2f bearing; // offset from baseline unsigned int advance = 0; // advance.x in 1/64 pixels @@ -37,6 +40,11 @@ private: //unsigned int vao = 0; //unsigned int vbo = 0; + // единый атлас для всех глифов + std::shared_ptr atlasTexture; + size_t atlasWidth = 0; + size_t atlasHeight = 0; + VertexRenderStruct textMesh; std::string shaderName = "text2d";