merge
This commit is contained in:
commit
ed6e1bacc7
309
src/Game.cpp
309
src/Game.cpp
@ -178,6 +178,25 @@ namespace ZL
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Game::projectToNDC(const Vector3f& world, float& ndcX, float& ndcY, float& ndcZ, float& clipW) const
|
||||||
|
{
|
||||||
|
float aspect = static_cast<float>(Environment::width) / static_cast<float>(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()
|
void Game::drawBoxesLabels()
|
||||||
{
|
{
|
||||||
if (!textRenderer) return;
|
if (!textRenderer) return;
|
||||||
@ -209,10 +228,10 @@ namespace ZL
|
|||||||
float uiX = sx;
|
float uiX = sx;
|
||||||
float uiY = sy; // если окажется вверх ногами — замени на (Environment::height - sy)
|
float uiY = sy; // если окажется вверх ногами — замени на (Environment::height - sy)
|
||||||
|
|
||||||
// Можно делать масштаб по дальности: чем дальше — тем меньше.
|
|
||||||
// depth в NDC: ближе к -1 (near) и к 1 (far). Стабильнее считать по расстоянию:
|
|
||||||
float dist = (Environment::shipState.position - boxWorld).norm();
|
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);
|
textRenderer->drawText(boxLabels[i], uiX, uiY, scale, /*centered*/true);
|
||||||
}
|
}
|
||||||
@ -461,9 +480,6 @@ namespace ZL
|
|||||||
|
|
||||||
boxAlive.resize(boxCoordsArr.size(), true);
|
boxAlive.resize(boxCoordsArr.size(), true);
|
||||||
ZL::CheckGlError();
|
ZL::CheckGlError();
|
||||||
textRenderer = std::make_unique<ZL::TextRenderer>();
|
|
||||||
textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE);
|
|
||||||
ZL::CheckGlError();
|
|
||||||
boxLabels.clear();
|
boxLabels.clear();
|
||||||
boxLabels.reserve(boxCoordsArr.size());
|
boxLabels.reserve(boxCoordsArr.size());
|
||||||
for (size_t i = 0; i < boxCoordsArr.size(); ++i) {
|
for (size_t i = 0; i < boxCoordsArr.size(); ++i) {
|
||||||
@ -476,6 +492,14 @@ namespace ZL
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderer.InitOpenGL();
|
renderer.InitOpenGL();
|
||||||
|
|
||||||
|
// TextRenderer создаём/инициализируем ПОСЛЕ инициализации OpenGL
|
||||||
|
textRenderer = std::make_unique<ZL::TextRenderer>();
|
||||||
|
if (!textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE)) {
|
||||||
|
std::cerr << "Failed to init TextRenderer\n";
|
||||||
|
}
|
||||||
|
ZL::CheckGlError();
|
||||||
|
|
||||||
glEnable(GL_BLEND);
|
glEnable(GL_BLEND);
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
@ -738,6 +762,7 @@ namespace ZL
|
|||||||
drawShip();
|
drawShip();
|
||||||
|
|
||||||
drawUI();
|
drawUI();
|
||||||
|
drawTargetHud();
|
||||||
CheckGlError();
|
CheckGlError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -889,6 +914,270 @@ namespace ZL
|
|||||||
return localNow + networkClient->getTimeOffset(); // Нужно добавить геттер в интерфейс
|
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<float>(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() {
|
void Game::processTickCount() {
|
||||||
|
|
||||||
if (lastTickCount == 0) {
|
if (lastTickCount == 0) {
|
||||||
@ -1367,6 +1656,14 @@ namespace ZL
|
|||||||
if (event.type == SDL_QUIT) {
|
if (event.type == SDL_QUIT) {
|
||||||
Environment::exitGameLoop = true;
|
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__
|
#ifdef __ANDROID__
|
||||||
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_AC_BACK) {
|
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_AC_BACK) {
|
||||||
Environment::exitGameLoop = true;
|
Environment::exitGameLoop = true;
|
||||||
|
|||||||
11
src/Game.h
11
src/Game.h
@ -135,7 +135,18 @@ namespace ZL {
|
|||||||
|
|
||||||
std::unordered_set<int> deadRemotePlayers;
|
std::unordered_set<int> 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; // выбирает цель (пока: ближайший живой удаленный игрок)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -6,13 +6,15 @@
|
|||||||
#include "render/OpenGlExtensions.h"
|
#include "render/OpenGlExtensions.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
namespace ZL {
|
namespace ZL {
|
||||||
|
|
||||||
TextRenderer::~TextRenderer()
|
TextRenderer::~TextRenderer()
|
||||||
{
|
{
|
||||||
glyphs.clear();
|
glyphs.clear();
|
||||||
|
atlasTexture.reset();
|
||||||
textMesh.positionVBO.reset();
|
textMesh.positionVBO.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +46,11 @@ bool TextRenderer::init(Renderer& renderer, const std::string& ttfPath, int pixe
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TextRenderer::ClearCache()
|
||||||
|
{
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const std::string& zipfilename)
|
bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const std::string& zipfilename)
|
||||||
{
|
{
|
||||||
// 1. Загружаем сырые данные из ZIP
|
// 1. Загружаем сырые данные из ZIP
|
||||||
@ -81,38 +88,188 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s
|
|||||||
}
|
}
|
||||||
|
|
||||||
FT_Set_Pixel_Sizes(face, 0, pixelSize);
|
FT_Set_Pixel_Sizes(face, 0, pixelSize);
|
||||||
|
|
||||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
|
|
||||||
glyphs.clear();
|
// glyphs.clear();
|
||||||
// Проходим по стандартным ASCII символам
|
|
||||||
|
// Сначала собираем все глифы в память
|
||||||
|
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) {
|
for (unsigned char c = 32; c < 128; ++c) {
|
||||||
|
if (FT_Load_Char(face, c, FT_LOAD_RENDER)) {
|
||||||
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;
|
GlyphBitmap gb;
|
||||||
glyphData.width = face->glyph->bitmap.width;
|
gb.width = face->glyph->bitmap.width;
|
||||||
glyphData.height = face->glyph->bitmap.rows;
|
gb.height = face->glyph->bitmap.rows;
|
||||||
glyphData.format = TextureDataStruct::R8;
|
gb.bearing = Eigen::Vector2f((float)face->glyph->bitmap_left, (float)face->glyph->bitmap_top);
|
||||||
glyphData.mipmap = TextureDataStruct::NONE;
|
gb.advance = static_cast<unsigned int>(face->glyph->advance.x);
|
||||||
|
|
||||||
// Копируем буфер FreeType в вектор данных
|
size_t dataSize = static_cast<size_t>(gb.width) * static_cast<size_t>(gb.height);
|
||||||
size_t dataSize = glyphData.width * glyphData.height;
|
if (dataSize > 0) {
|
||||||
glyphData.data.assign(face->glyph->bitmap.buffer, face->glyph->bitmap.buffer + dataSize);
|
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));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Пакуем глифы в один атлас (упрощённый алгоритм строковой укладки)
|
||||||
|
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_Face(face);
|
||||||
FT_Done_FreeType(ft);
|
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<float, 4> color)
|
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;
|
||||||
if (!r || text.empty()) 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) {
|
for (char ch : text) {
|
||||||
auto it = glyphs.find(ch);
|
auto git = glyphs.find(ch);
|
||||||
if (it == glyphs.end()) continue;
|
if (git == glyphs.end()) continue;
|
||||||
totalW += (it->second.advance >> 6) * scale;
|
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;
|
CachedText& cached = itCache->second;
|
||||||
float penX = x;
|
|
||||||
float penY = y;
|
|
||||||
|
|
||||||
for (char ch : text) {
|
// Вычисляем смещение для проекции (оставляем Y как есть)
|
||||||
auto it = glyphs.find(ch);
|
float tx = x;
|
||||||
if (it == glyphs.end()) continue;
|
if (centered) {
|
||||||
|
tx = x - cached.width * 0.5f;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
float ty = y;
|
||||||
|
|
||||||
// 3. Обновляем VBO через наш стандартный механизм
|
// 3. Обновляем VBO через наш стандартный механизм
|
||||||
// Примечание: для текста лучше использовать GL_DYNAMIC_DRAW,
|
// Примечание: для текста лучше использовать GL_DYNAMIC_DRAW,
|
||||||
// но RefreshVBO сейчас жестко зашит на GL_STATIC_DRAW.
|
// но RefreshVBO сейчас жестко зашит на GL_STATIC_DRAW.
|
||||||
// Для UI это обычно не критично, если строк не тысячи.
|
// Для UI это обычно не критично, если строк не тысячи.
|
||||||
textMesh.AssignFrom(textData);
|
// textMesh.AssignFrom(textData);
|
||||||
|
|
||||||
// 4. Рендеринг
|
// 4. Рендеринг
|
||||||
r->shaderManager.PushShader(shaderName);
|
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();
|
Eigen::Matrix4f proj = Eigen::Matrix4f::Identity();
|
||||||
proj(0, 0) = 2.0f / W;
|
proj(0, 0) = 2.0f / W;
|
||||||
proj(1, 1) = 2.0f / H;
|
proj(1, 1) = 2.0f / H;
|
||||||
proj(0, 3) = -1.0f;
|
// Сдвигаем проекцию так, чтобы локальные координаты меша (pen-origin=0,0) оказались в (tx,ty)
|
||||||
proj(1, 3) = -1.0f;
|
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->RenderUniformMatrix4fv("uProjection", false, proj.data());
|
||||||
r->RenderUniform1i("uText", 0);
|
r->RenderUniform1i("uText", 0);
|
||||||
r->RenderUniform4fv("uColor", color.data());
|
r->RenderUniform4fv("uColor", color.data());
|
||||||
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, atlasTexture->getTexID());
|
||||||
|
|
||||||
// ВНИМАНИЕ: Так как у тебя каждый символ — это отдельная текстура,
|
|
||||||
// нам всё равно придется делать glDrawArrays в цикле, ЛИБО использовать атлас.
|
|
||||||
// Если оставляем текущую систему с разными текстурами:
|
|
||||||
|
|
||||||
r->EnableVertexAttribArray("vPosition");
|
r->EnableVertexAttribArray("vPosition");
|
||||||
r->EnableVertexAttribArray("vTexCoord");
|
r->EnableVertexAttribArray("vTexCoord");
|
||||||
|
|
||||||
for (size_t i = 0; i < text.length(); ++i) {
|
//for (size_t i = 0; i < text.length(); ++i) {
|
||||||
auto it = glyphs.find(text[i]);
|
// auto it = glyphs.find(text[i]);
|
||||||
if (it == glyphs.end()) continue;
|
// if (it == glyphs.end()) continue;
|
||||||
|
|
||||||
glBindTexture(GL_TEXTURE_2D, it->second.texture->getTexID());
|
// glBindTexture(GL_TEXTURE_2D, it->second.texture->getTexID());
|
||||||
|
|
||||||
// Отрисовываем по 6 вершин за раз
|
// // Отрисовываем по 6 вершин за раз
|
||||||
// Нам нужно вручную биндить VBO, так как DrawVertexRenderStruct рисует всё сразу
|
// // Нам нужно вручную биндить VBO, так как DrawVertexRenderStruct рисует всё сразу
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, textMesh.positionVBO->getBuffer());
|
// glBindBuffer(GL_ARRAY_BUFFER, textMesh.positionVBO->getBuffer());
|
||||||
r->VertexAttribPointer3fv("vPosition", 0, (const char*)(i * 6 * sizeof(Vector3f)));
|
// r->VertexAttribPointer3fv("vPosition", 0, (const char*)(i * 6 * sizeof(Vector3f)));
|
||||||
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, textMesh.texCoordVBO->getBuffer());
|
// glBindBuffer(GL_ARRAY_BUFFER, textMesh.texCoordVBO->getBuffer());
|
||||||
r->VertexAttribPointer2fv("vTexCoord", 0, (const char*)(i * 6 * sizeof(Vector2f)));
|
// 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("vPosition");
|
||||||
r->DisableVertexAttribArray("vTexCoord");
|
r->DisableVertexAttribArray("vTexCoord");
|
||||||
r->shaderManager.PopShader();
|
r->shaderManager.PopShader();
|
||||||
|
|
||||||
|
// Сброс бинда текстуры не обязателен, но можно для чистоты
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
}
|
}
|
||||||
} // namespace ZL
|
} // namespace ZL
|
||||||
@ -12,7 +12,10 @@
|
|||||||
namespace ZL {
|
namespace ZL {
|
||||||
|
|
||||||
struct GlyphInfo {
|
struct GlyphInfo {
|
||||||
std::shared_ptr<Texture> texture; // Texture for glyph
|
// std::shared_ptr<Texture> 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 size; // glyph size in pixels
|
||||||
Eigen::Vector2f bearing; // offset from baseline
|
Eigen::Vector2f bearing; // offset from baseline
|
||||||
unsigned int advance = 0; // advance.x in 1/64 pixels
|
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);
|
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<float, 4> color = { 1.f,1.f,1.f,1.f });
|
void drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color = { 1.f,1.f,1.f,1.f });
|
||||||
|
|
||||||
|
// Clear cached meshes (call on window resize / DPI change)
|
||||||
|
void ClearCache();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool loadGlyphs(const std::string& ttfPath, int pixelSize, const std::string& zipfilename);
|
bool loadGlyphs(const std::string& ttfPath, int pixelSize, const std::string& zipfilename);
|
||||||
|
|
||||||
@ -37,9 +43,24 @@ private:
|
|||||||
//unsigned int vao = 0;
|
//unsigned int vao = 0;
|
||||||
//unsigned int vbo = 0;
|
//unsigned int vbo = 0;
|
||||||
|
|
||||||
|
// единый атлас для всех глифов
|
||||||
|
std::shared_ptr<Texture> atlasTexture;
|
||||||
|
size_t atlasWidth = 0;
|
||||||
|
size_t atlasHeight = 0;
|
||||||
|
|
||||||
VertexRenderStruct textMesh;
|
VertexRenderStruct textMesh;
|
||||||
|
|
||||||
std::string shaderName = "text2d";
|
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<std::string, CachedText> cache;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZL
|
} // namespace ZL
|
||||||
Loading…
Reference in New Issue
Block a user