space-game001/src/render/TextRenderer.cpp
Vladislav Khorev 928600acd4 Refactoring
2026-03-07 19:41:15 +03:00

405 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "render/TextRenderer.h"
#include <ft2build.h>
#include FT_FREETYPE_H
#include "Environment.h"
#include "render/OpenGlExtensions.h"
#include <iostream>
#include <array>
#include <algorithm>
#include <cmath>
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<char> 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<const FT_Byte*>(fontData.data()),
static_cast<FT_Long>(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<char> data; // R8 байты (ширина*height)
Eigen::Vector2f bearing;
unsigned int advance = 0;
};
std::vector<std::pair<char, GlyphBitmap>> 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<unsigned int>(face->glyph->advance.x);
size_t dataSize = static_cast<size_t>(gb.width) * static_cast<size_t>(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<size_t>(nextPow2(max(16, neededWidth)));
atlasHeight = static_cast<size_t>(nextPow2(max(16, neededHeight)));
// Ограничение - если получилось слишком большое, попробуем без power-of-two
if (atlasWidth > 4096) atlasWidth = static_cast<size_t>(neededWidth);
if (atlasHeight > 4096) atlasHeight = static_cast<size_t>(neededHeight);
// Создаём буфер атласа, инициализируем нулями (прозрачность)
std::vector<char> 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<int>(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<size_t>(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<Texture>(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<Texture>(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<float, 4> 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