#include "render/TextRenderer.h" #include #include FT_FREETYPE_H #include "Environment.h" #include "render/OpenGlExtensions.h" #include #include #include #include namespace ZL { TextRenderer::~TextRenderer() { glyphs.clear(); atlasTexture.reset(); textMesh.positionVBO.reset(); } bool TextRenderer::init(Renderer& renderer, const std::string& ttfPath, int pixelSize, const std::string& zipfilename) { r = &renderer; #ifdef EMSCRIPTEN r->shaderManager.AddShaderFromFiles(shaderName, "resources/shaders/text2d.vertex", "resources/shaders/text2d_web.fragment", zipfilename ); #else r->shaderManager.AddShaderFromFiles(shaderName, "resources/shaders/text2d.vertex", "resources/shaders/text2d_desktop.fragment", zipfilename ); #endif ZL::CheckGlError(); if (!loadGlyphs(ttfPath, pixelSize, zipfilename)) return false; ZL::CheckGlError(); textMesh.data.PositionData.resize(6, Eigen::Vector3f(0, 0, 0)); textMesh.RefreshVBO(); ZL::CheckGlError(); return true; } void TextRenderer::ClearCache() { cache.clear(); } bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const std::string& zipfilename) { // 1. Загружаем сырые данные из ZIP std::vector fontData; try { fontData = !zipfilename.empty() ? readFileFromZIP(ttfPath, zipfilename) : readFile(ttfPath); // Предполагаем наличие функции readFile } catch (const std::exception& e) { std::cerr << "TextRenderer: failed to read font data: " << e.what() << "\n"; return false; } if (fontData.empty()) return false; FT_Library ft; if (FT_Init_FreeType(&ft)) { std::cerr << "FreeType: FT_Init_FreeType failed\n"; return false; } // 2. Инициализируем Face из памяти FT_Face face; // Важно: передаем указатель на буфер и его размер if (FT_New_Memory_Face(ft, reinterpret_cast(fontData.data()), static_cast(fontData.size()), 0, &face)) { std::cerr << "FreeType: failed to load font from memory: " << ttfPath << "\n"; FT_Done_FreeType(ft); return false; } FT_Set_Pixel_Sizes(face, 0, pixelSize); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // 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) { 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; } 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); 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); } 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); // После FT_Done_Face данные из fontData больше не нужны, // вектор сам очистится при выходе из функции. glBindTexture(GL_TEXTURE_2D, 0); return true; } void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array color) { if (!r || text.empty() || !atlasTexture) return; // формируем ключ кеша std::string key = text + "|" + std::to_string(scale) + "|" + (centered ? "1" : "0"); auto itCache = cache.find(key); if (itCache == cache.end()) { VertexDataStruct textData; float penX = 0.0f; float penY = 0.0f; float totalW = 0.0f; float maxH = 0.0f; for (char ch : text) { auto git = glyphs.find(ch); if (git == glyphs.end()) continue; const GlyphInfo& g = git->second; float xpos = penX + g.bearing.x() * scale; float ypos = penY - (g.size.y() - g.bearing.y()) * scale; float w = g.size.x() * scale; float h = g.size.y() * scale; // Добавляем 2 треугольника (6 вершин) для текущего символа textData.PositionData.push_back({ xpos, ypos + h, 0.0f }); textData.PositionData.push_back({ xpos, ypos, 0.0f }); textData.PositionData.push_back({ xpos + w, ypos, 0.0f }); textData.PositionData.push_back({ xpos, ypos + h, 0.0f }); textData.PositionData.push_back({ xpos + w, ypos, 0.0f }); textData.PositionData.push_back({ xpos + w, ypos + h, 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(); 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; totalW = penX; maxH = max(maxH, h); } // Сохраняем в кеш CachedText ct; ct.width = totalW; ct.height = maxH; ct.mesh.AssignFrom(textData); auto res = cache.emplace(key, std::move(ct)); itCache = res.first; } // Используем кешированный меш CachedText& cached = itCache->second; // Вычисляем смещение для проекции (оставляем Y как есть) float tx = x; if (centered) { tx = x - cached.width * 0.5f; } float ty = y; // 3. Обновляем VBO через наш стандартный механизм // Примечание: для текста лучше использовать GL_DYNAMIC_DRAW, // но RefreshVBO сейчас жестко зашит на GL_STATIC_DRAW. // Для UI это обычно не критично, если строк не тысячи. // textMesh.AssignFrom(textData); // 4. Рендеринг r->shaderManager.PushShader(shaderName); // Матрица проекции — используем виртуальные проекционные размеры, // чтобы координаты текста были независимы от физического разрешения экрана. float W = Environment::projectionWidth; float H = Environment::projectionHeight; Eigen::Matrix4f proj = Eigen::Matrix4f::Identity(); proj(0, 0) = 2.0f / W; proj(1, 1) = 2.0f / H; // Сдвигаем проекцию так, чтобы локальные координаты меша (pen-origin=0,0) оказались в (tx,ty) proj(0, 3) = -1.0f + 2.0f * (tx) / W; proj(1, 3) = -1.0f + 2.0f * (ty) / H; r->RenderUniformMatrix4fv("uProjection", false, proj.data()); r->RenderUniform1i("uText", 0); r->RenderUniform4fv("uColor", color.data()); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, atlasTexture->getTexID()); //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()); // // Отрисовываем по 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))); // glDrawArrays(GL_TRIANGLES, 0, 6); //} r->DrawVertexRenderStruct(cached.mesh); r->shaderManager.PopShader(); // Сброс бинда текстуры не обязателен, но можно для чистоты glBindTexture(GL_TEXTURE_2D, 0); } } // namespace ZL