diff --git a/src/Game.cpp b/src/Game.cpp index ec7a2d6..6394fea 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -178,6 +178,25 @@ namespace ZL return true; } + bool Game::projectToNDC(const Vector3f& world, float& ndcX, float& ndcY, float& ndcZ, float& clipW) const + { + float aspect = static_cast(Environment::width) / static_cast(Environment::height); + Eigen::Matrix4f V = makeViewMatrix_FromYourCamera(); + Eigen::Matrix4f P = makePerspective(1.0f / 1.5f, aspect, Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); + + Eigen::Vector4f w(world.x(), world.y(), world.z(), 1.0f); + Eigen::Vector4f clip = P * V * w; + + clipW = clip.w(); + if (std::abs(clipW) < 1e-6f) return false; + + Eigen::Vector3f ndc = clip.head<3>() / clipW; + ndcX = ndc.x(); + ndcY = ndc.y(); + ndcZ = ndc.z(); + return true; + } + void Game::drawBoxesLabels() { if (!textRenderer) return; @@ -209,10 +228,10 @@ namespace ZL float uiX = sx; float uiY = sy; // если окажется вверх ногами — замени на (Environment::height - sy) - // Можно делать масштаб по дальности: чем дальше — тем меньше. - // depth в NDC: ближе к -1 (near) и к 1 (far). Стабильнее считать по расстоянию: float dist = (Environment::shipState.position - boxWorld).norm(); - float scale = std::clamp(120.0f / (dist + 1.0f), 0.6f, 1.2f); + float scaleRaw = 120.0f / (dist + 1.0f); + float scale = std::round(scaleRaw * 10.0f) / 10.0f; // округление до 0.1 + scale = std::clamp(scale, 0.6f, 1.2f); textRenderer->drawText(boxLabels[i], uiX, uiY, scale, /*centered*/true); } @@ -461,9 +480,6 @@ namespace ZL boxAlive.resize(boxCoordsArr.size(), true); ZL::CheckGlError(); - textRenderer = std::make_unique(); - textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE); - ZL::CheckGlError(); boxLabels.clear(); boxLabels.reserve(boxCoordsArr.size()); for (size_t i = 0; i < boxCoordsArr.size(); ++i) { @@ -476,6 +492,14 @@ namespace ZL } renderer.InitOpenGL(); + + // TextRenderer создаём/инициализируем ПОСЛЕ инициализации OpenGL + textRenderer = std::make_unique(); + if (!textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE)) { + std::cerr << "Failed to init TextRenderer\n"; + } + ZL::CheckGlError(); + glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); @@ -738,6 +762,7 @@ namespace ZL drawShip(); drawUI(); + drawTargetHud(); CheckGlError(); } @@ -889,6 +914,270 @@ namespace ZL return localNow + networkClient->getTimeOffset(); // Нужно добавить геттер в интерфейс } + int Game::pickTargetId() const + { + int bestId = -1; + constexpr float INF_F = 1e30f; + float bestDistSq = INF_F; + + for (auto const& [id, st] : remotePlayerStates) { + if (deadRemotePlayers.count(id)) continue; + + float d2 = (Environment::shipState.position - st.position).squaredNorm(); + if (d2 < bestDistSq) { + bestDistSq = d2; + bestId = id; + } + } + return bestId; + } + + static VertexDataStruct MakeColoredRect2D(float cx, float cy, float hw, float hh, float z, + const Eigen::Vector4f& rgba) + { + VertexDataStruct v; + // 2 triangles + Vector3f p1{ cx - hw, cy - hh, z }; + Vector3f p2{ cx - hw, cy + hh, z }; + Vector3f p3{ cx + hw, cy + hh, z }; + Vector3f p4{ cx + hw, cy - hh, z }; + + v.PositionData = { p1, p2, p3, p3, p4, p1 }; + + // defaultColor shader likely uses vColor (vec3), но нам нужен alpha. + // У тебя в Renderer есть RenderUniform4fv, но шейдер может брать vColor. + // Поэтому: сделаем ColorData vec3, а alpha дадим через uniform uColor, если есть. + // Если в defaultColor нет uniform uColor — тогда alpha будет 1.0. + // Для совместимости: кладём RGB, alpha будем задавать uniform'ом отдельно. + Vector3f rgb{ rgba.x(), rgba.y(), rgba.z() }; + v.ColorData = { rgb, rgb, rgb, rgb, rgb, rgb }; + return v; + } + + void Game::drawTargetHud() + { + if (!textRenderer) return; + + // 1) выбираем цель + int targetIdNow = pickTargetId(); + if (targetIdNow < 0) { + trackedTargetId = -1; + targetAcquireAnim = 0.f; + targetWasVisible = false; + return; + } + + // если цель сменилась — сброс анимации “схлопывания” + if (trackedTargetId != targetIdNow) { + trackedTargetId = targetIdNow; + targetAcquireAnim = 0.0f; + targetWasVisible = false; + } + + const ClientState& st = remotePlayerStates.at(trackedTargetId); + Vector3f shipWorld = st.position; + + // 2) проекция + float ndcX, ndcY, ndcZ, clipW; + if (!projectToNDC(shipWorld, ndcX, ndcY, ndcZ, clipW)) return; + + // behind camera? + bool behind = (clipW <= 0.0f); + + // on-screen check (NDC) + bool onScreen = (!behind && + ndcX >= -1.0f && ndcX <= 1.0f && + ndcY >= -1.0f && ndcY <= 1.0f); + + // 3) расстояние + float dist = (Environment::shipState.position - shipWorld).norm(); + + // time for arrow bob + float t = static_cast(SDL_GetTicks64()) * 0.001f; + + // 4) Настройки стиля (как X3) + Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный + float thickness = 2.0f; // толщина линий (px) + float z = 0.0f; // 2D слой + + // 5) Если цель в кадре: рисуем скобки + if (onScreen) + { + // перевод NDC -> экран (в пикселях) + float sx = (ndcX * 0.5f + 0.5f) * Environment::width; + float sy = (ndcY * 0.5f + 0.5f) * Environment::height; + + // анимация “снаружи внутрь” + // targetAcquireAnim растёт к 1, быстро (похоже на захват) + float dt = 1.0f / 60.0f; // у тебя нет dt в draw, берём константу, выглядит норм + targetAcquireAnim = min(1.0f, targetAcquireAnim + dt * 6.5f); + + // базовый размер рамки в зависимости от дистанции (как у лейблов) + float size = 220.0f / (dist * 0.01f + 1.0f); // подстройка + size = std::clamp(size, 35.0f, 120.0f); // min/max + + // “схлопывание”: сначала больше, потом ближе к кораблю + // expand 1.6 -> 1.0 + float expand = 1.6f - 0.6f * targetAcquireAnim; + + float half = size * expand; + float cornerLen = max(10.0f, half * 0.35f); + + // точки углов + float left = sx - half; + float right = sx + half; + float bottom = sy - half; + float top = sy + half; + + // рисуем 8 тонких прямоугольников (2 на угол) + auto drawBar = [&](float cx, float cy, float w, float h) + { + VertexDataStruct v = MakeColoredRect2D(cx, cy, w * 0.5f, h * 0.5f, z, enemyColor); + hudTempMesh.AssignFrom(v); + renderer.DrawVertexRenderStruct(hudTempMesh); + }; + + // включаем 2D режим + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + renderer.shaderManager.PushShader("defaultColor"); + renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f); + renderer.PushMatrix(); + renderer.LoadIdentity(); + + // верх-лево: горизонт + вертикаль + drawBar(left + cornerLen * 0.5f, top, cornerLen, thickness); + drawBar(left, top - cornerLen * 0.5f, thickness, cornerLen); + + // верх-право + drawBar(right - cornerLen * 0.5f, top, cornerLen, thickness); + drawBar(right, top - cornerLen * 0.5f, thickness, cornerLen); + + // низ-лево + drawBar(left + cornerLen * 0.5f, bottom, cornerLen, thickness); + drawBar(left, bottom + cornerLen * 0.5f, thickness, cornerLen); + + // низ-право + drawBar(right - cornerLen * 0.5f, bottom, cornerLen, thickness); + drawBar(right, bottom + cornerLen * 0.5f, thickness, cornerLen); + + renderer.PopMatrix(); + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); + + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + + targetWasVisible = true; + return; + } + + // 6) Если цель offscreen: рисуем стрелку на краю + // dir: куда “смотреть” в NDC + float dirX = ndcX; + float dirY = ndcY; + + // если позади камеры — разворачиваем направление + if (behind) { + dirX = -dirX; + dirY = -dirY; + } + + float len = std::sqrt(dirX * dirX + dirY * dirY); + if (len < 1e-5f) return; + dirX /= len; + dirY /= len; + + // пересечение луча с прямоугольником [-1..1] с отступом + float marginNdc = 0.08f; + float maxX = 1.0f - marginNdc; + float maxY = 1.0f - marginNdc; + + float tx = (std::abs(dirX) < 1e-6f) ? 1e9f : (maxX / std::abs(dirX)); + float ty = (std::abs(dirY) < 1e-6f) ? 1e9f : (maxY / std::abs(dirY)); + float k = min(tx, ty); + + float edgeNdcX = dirX * k; + float edgeNdcY = dirY * k; + + float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::width; + float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::height; + + // лёгкая анимация “зова”: смещение по направлению + float bob = std::sin(t * 6.0f) * 6.0f; + edgeX += dirX * bob; + edgeY += dirY * bob; + + // стрелка как треугольник + маленький “хвост” + float arrowLen = 26.0f; + float arrowWid = 14.0f; + + // перпендикуляр + float px = -dirY; + float py = dirX; + + Vector3f tip{ edgeX + dirX * arrowLen, edgeY + dirY * arrowLen, z }; + Vector3f left{ edgeX + px * (arrowWid * 0.5f), edgeY + py * (arrowWid * 0.5f), z }; + Vector3f right{ edgeX - px * (arrowWid * 0.5f), edgeY - py * (arrowWid * 0.5f), z }; + + auto drawTri = [&](const Vector3f& a, const Vector3f& b, const Vector3f& c) + { + VertexDataStruct v; + v.PositionData = { a, b, c }; + Vector3f rgb{ enemyColor.x(), enemyColor.y(), enemyColor.z() }; + v.ColorData = { rgb, rgb, rgb }; + hudTempMesh.AssignFrom(v); + renderer.DrawVertexRenderStruct(hudTempMesh); + }; + + auto drawBar = [&](float cx, float cy, float w, float h) + { + VertexDataStruct v = MakeColoredRect2D(cx, cy, w * 0.5f, h * 0.5f, z, enemyColor); + hudTempMesh.AssignFrom(v); + renderer.DrawVertexRenderStruct(hudTempMesh); + }; + + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + renderer.shaderManager.PushShader("defaultColor"); + renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f); + renderer.PushMatrix(); + renderer.LoadIdentity(); + + // треугольник-стрелка + drawTri(tip, left, right); + + // “хвост” (короткая черта) + float tailLen = 14.0f; + float tailX = edgeX - dirX * 6.0f; + float tailY = edgeY - dirY * 6.0f; + // хвост рисуем как тонкий прямоугольник, ориентированный примерно по направлению: + // (упрощение: горизонт/вертикаль не поворачиваем, но выглядит ок. Хочешь — сделаем поворот матрицей) + drawBar(tailX, tailY, max(thickness, tailLen), thickness); + + renderer.PopMatrix(); + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); + + // дистанция рядом со стрелкой + // (у тебя ещё будет “статично под прицелом” — это просто другой TextView / drawText) + { + std::string d = std::to_string((int)dist) + "m"; + float tx = edgeX + px * 18.0f; + float ty = edgeY + py * 18.0f; + textRenderer->drawText(d, tx, ty, 0.6f, true, { 1.f, 0.f, 0.f, 1.f }); + } + + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + + targetWasVisible = false; + } + void Game::processTickCount() { if (lastTickCount == 0) { @@ -1367,6 +1656,14 @@ namespace ZL if (event.type == SDL_QUIT) { Environment::exitGameLoop = true; } +#if SDL_VERSION_ATLEAST(2,0,5) + else if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) { + // Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях + Environment::width = event.window.data1; + Environment::height = event.window.data2; + if (textRenderer) textRenderer->ClearCache(); + } +#endif #ifdef __ANDROID__ if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_AC_BACK) { Environment::exitGameLoop = true; diff --git a/src/Game.h b/src/Game.h index 979908b..b49280f 100644 --- a/src/Game.h +++ b/src/Game.h @@ -135,7 +135,18 @@ namespace ZL { std::unordered_set deadRemotePlayers; + // --- Target HUD (brackets + offscreen arrow) --- + int trackedTargetId = -1; + bool targetWasVisible = false; + float targetAcquireAnim = 0.0f; // 0..1 схлопывание (0 = далеко, 1 = на месте) + // временный меш для HUD (будем перезаливать VBO маленькими порциями) + VertexRenderStruct hudTempMesh; + + // helpers + bool projectToNDC(const Vector3f& world, float& ndcX, float& ndcY, float& ndcZ, float& clipW) const; + void drawTargetHud(); // рисует рамку или стрелку + int pickTargetId() const; // выбирает цель (пока: ближайший живой удаленный игрок) }; diff --git a/src/render/TextRenderer.cpp b/src/render/TextRenderer.cpp index 0d9d9fc..585ca8d 100644 --- a/src/render/TextRenderer.cpp +++ b/src/render/TextRenderer.cpp @@ -6,13 +6,15 @@ #include "render/OpenGlExtensions.h" #include #include +#include +#include namespace ZL { TextRenderer::~TextRenderer() { glyphs.clear(); - + atlasTexture.reset(); textMesh.positionVBO.reset(); } @@ -44,6 +46,11 @@ bool TextRenderer::init(Renderer& renderer, const std::string& ttfPath, int pixe return true; } +void TextRenderer::ClearCache() +{ + cache.clear(); +} + bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const std::string& zipfilename) { // 1. Загружаем сырые данные из ZIP @@ -81,38 +88,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); @@ -126,61 +283,81 @@ 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; + + // формируем ключ кеша + 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; - // 1. Считаем ширину для центрирования - float totalW = 0.0f; - if (centered) { for (char ch : text) { - auto it = glyphs.find(ch); - if (it == glyphs.end()) continue; - totalW += (it->second.advance >> 6) * scale; + 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); } - x -= totalW * 0.5f; + + // Сохраняем в кеш + CachedText ct; + ct.width = totalW; + ct.height = maxH; + ct.mesh.AssignFrom(textData); + + auto res = cache.emplace(key, std::move(ct)); + itCache = res.first; } - // 2. Подготовка данных (аналог CreateRect2D, но для всей строки) - VertexDataStruct textData; - float penX = x; - float penY = y; + // Используем кешированный меш + CachedText& cached = itCache->second; - for (char ch : text) { - auto it = glyphs.find(ch); - if (it == glyphs.end()) continue; - - const GlyphInfo& g = it->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 вершин) для текущего символа - // Координаты Z ставим 0.0f, так как это 2D - 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 }); - - // 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 }); - - penX += (g.advance >> 6) * scale; + // Вычисляем смещение для проекции (оставляем 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); + // textMesh.AssignFrom(textData); // 4. Рендеринг r->shaderManager.PushShader(shaderName); @@ -191,41 +368,44 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca Eigen::Matrix4f proj = Eigen::Matrix4f::Identity(); proj(0, 0) = 2.0f / W; proj(1, 1) = 2.0f / H; - proj(0, 3) = -1.0f; - proj(1, 3) = -1.0f; + // Сдвигаем проекцию так, чтобы локальные координаты меша (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()); - // ВНИМАНИЕ: Так как у тебя каждый символ — это отдельная текстура, - // нам всё равно придется делать 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(cached.mesh); 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..d7a138a 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 @@ -26,6 +29,9 @@ public: bool init(Renderer& renderer, const std::string& ttfPath, int pixelSize, const std::string& zipfilename); void drawText(const std::string& text, float x, float y, float scale, bool centered, std::array color = { 1.f,1.f,1.f,1.f }); + // Clear cached meshes (call on window resize / DPI change) + void ClearCache(); + private: bool loadGlyphs(const std::string& ttfPath, int pixelSize, const std::string& zipfilename); @@ -37,9 +43,24 @@ 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"; + + // caching for static texts + struct CachedText { + VertexRenderStruct mesh; + float width = 0.f; // in pixels, total advance + float height = 0.f; // optional, not currently used + }; + + // key: text + "|" + scale + "|" + centered + std::unordered_map cache; }; } // namespace ZL \ No newline at end of file