add freetype-based text rendering and box labels
This commit is contained in:
parent
96d879076a
commit
f41228acf8
@ -398,7 +398,9 @@ add_library(freetype_external_lib UNKNOWN IMPORTED GLOBAL)
|
|||||||
set_target_properties(freetype_external_lib PROPERTIES
|
set_target_properties(freetype_external_lib PROPERTIES
|
||||||
IMPORTED_LOCATION_DEBUG "${_ft_debug_lib}"
|
IMPORTED_LOCATION_DEBUG "${_ft_debug_lib}"
|
||||||
IMPORTED_LOCATION_RELEASE "${_ft_release_lib}"
|
IMPORTED_LOCATION_RELEASE "${_ft_release_lib}"
|
||||||
INTERFACE_INCLUDE_DIRECTORIES
|
)
|
||||||
|
target_include_directories(freetype_external_lib INTERFACE
|
||||||
|
"$<IF:$<CONFIG:Debug>,${FREETYPE_BASE_DIR}-Debug/include/freetype2,${FREETYPE_BASE_DIR}-Release/include/freetype2>"
|
||||||
"$<IF:$<CONFIG:Debug>,${FREETYPE_BASE_DIR}-Debug/include,${FREETYPE_BASE_DIR}-Release/include>"
|
"$<IF:$<CONFIG:Debug>,${FREETYPE_BASE_DIR}-Debug/include,${FREETYPE_BASE_DIR}-Release/include>"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -453,8 +455,8 @@ if(NOT _have_sdl2ttf)
|
|||||||
-DSDL2_LIBRARY=${_SDL2_LIB}
|
-DSDL2_LIBRARY=${_SDL2_LIB}
|
||||||
-DSDL2_INCLUDE_DIR=${SDL2_INSTALL_DIR}/include/SDL2
|
-DSDL2_INCLUDE_DIR=${SDL2_INSTALL_DIR}/include/SDL2
|
||||||
-DFREETYPE_LIBRARY=${_FT_LIB}
|
-DFREETYPE_LIBRARY=${_FT_LIB}
|
||||||
-DFREETYPE_INCLUDE_DIR=${_FT_PREFIX}/include
|
-DFREETYPE_INCLUDE_DIR=${_FT_PREFIX}/include/freetype2
|
||||||
-DFREETYPE_INCLUDE_DIRS=${_FT_PREFIX}/include
|
-DFREETYPE_INCLUDE_DIRS=${_FT_PREFIX}/include/freetype2
|
||||||
-DSDL2TTF_VENDORED=OFF
|
-DSDL2TTF_VENDORED=OFF
|
||||||
-DSDL2TTF_SAMPLES=OFF
|
-DSDL2TTF_SAMPLES=OFF
|
||||||
RESULT_VARIABLE _ttf_cfg_res
|
RESULT_VARIABLE _ttf_cfg_res
|
||||||
|
|||||||
@ -57,6 +57,8 @@ add_executable(space-game001
|
|||||||
../src/network/ClientState.cpp
|
../src/network/ClientState.cpp
|
||||||
../src/network/WebSocketClient.h
|
../src/network/WebSocketClient.h
|
||||||
../src/network/WebSocketClient.cpp
|
../src/network/WebSocketClient.cpp
|
||||||
|
../src/render/TextRenderer.h
|
||||||
|
../src/render/TextRenderer.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
# Установка проекта по умолчанию для Visual Studio
|
# Установка проекта по умолчанию для Visual Studio
|
||||||
@ -79,7 +81,7 @@ target_compile_definitions(space-game001 PRIVATE
|
|||||||
PNG_ENABLED
|
PNG_ENABLED
|
||||||
SDL_MAIN_HANDLED
|
SDL_MAIN_HANDLED
|
||||||
# NETWORK
|
# NETWORK
|
||||||
# SIMPLIFIED
|
SIMPLIFIED
|
||||||
)
|
)
|
||||||
|
|
||||||
# Линкуем с SDL2main, если он вообще установлен
|
# Линкуем с SDL2main, если он вообще установлен
|
||||||
|
|||||||
BIN
resources/fonts/DroidSans.ttf
Normal file
BIN
resources/fonts/DroidSans.ttf
Normal file
Binary file not shown.
11
resources/shaders/text2d.fragment
Normal file
11
resources/shaders/text2d.fragment
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#version 330 core
|
||||||
|
in vec2 TexCoord;
|
||||||
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
uniform sampler2D uText;
|
||||||
|
uniform vec3 uColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float a = texture(uText, TexCoord).r; // glyph alpha in RED channel
|
||||||
|
FragColor = vec4(uColor, a);
|
||||||
|
}
|
||||||
12
resources/shaders/text2d.vertex
Normal file
12
resources/shaders/text2d.vertex
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#version 330 core
|
||||||
|
in vec2 vPosition;
|
||||||
|
in vec2 vTexCoord;
|
||||||
|
|
||||||
|
out vec2 TexCoord;
|
||||||
|
|
||||||
|
uniform mat4 uProjection;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TexCoord = vTexCoord;
|
||||||
|
gl_Position = uProjection * vec4(vPosition.xy, 0.0, 1.0);
|
||||||
|
}
|
||||||
120
src/Game.cpp
120
src/Game.cpp
@ -111,6 +111,110 @@ namespace ZL
|
|||||||
return boxCoordsArr;
|
return boxCoordsArr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Eigen::Matrix4f makeViewMatrix_FromYourCamera()
|
||||||
|
{
|
||||||
|
Eigen::Matrix4f Tz = Eigen::Matrix4f::Identity();
|
||||||
|
Tz(2, 3) = -1.0f * ZL::Environment::zoom;
|
||||||
|
|
||||||
|
Eigen::Matrix4f R = Eigen::Matrix4f::Identity();
|
||||||
|
R.block<3, 3>(0, 0) = ZL::Environment::inverseShipMatrix;
|
||||||
|
|
||||||
|
Eigen::Matrix4f Tship = Eigen::Matrix4f::Identity();
|
||||||
|
Tship(0, 3) = -ZL::Environment::shipState.position.x();
|
||||||
|
Tship(1, 3) = -ZL::Environment::shipState.position.y();
|
||||||
|
Tship(2, 3) = -ZL::Environment::shipState.position.z();
|
||||||
|
|
||||||
|
return Tz * R * Tship;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Eigen::Matrix4f makePerspective(float fovyRadians, float aspect, float zNear, float zFar)
|
||||||
|
{
|
||||||
|
// Стандартная перспектива
|
||||||
|
float f = 1.0f / std::tan(fovyRadians * 0.5f);
|
||||||
|
|
||||||
|
Eigen::Matrix4f P = Eigen::Matrix4f::Zero();
|
||||||
|
P(0, 0) = f / aspect;
|
||||||
|
P(1, 1) = f;
|
||||||
|
P(2, 2) = (zFar + zNear) / (zNear - zFar);
|
||||||
|
P(2, 3) = (2.0f * zFar * zNear) / (zNear - zFar);
|
||||||
|
P(3, 2) = -1.0f;
|
||||||
|
return P;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Game::worldToScreen(const Vector3f& world, float& outX, float& outY, float& outDepth) const
|
||||||
|
{
|
||||||
|
// Матрицы должны совпасть с drawBoxes/drawShip по смыслу
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (clip.w() <= 0.0001f) return false; // позади камеры
|
||||||
|
|
||||||
|
Eigen::Vector3f ndc = clip.head<3>() / clip.w(); // [-1..1]
|
||||||
|
outDepth = ndc.z();
|
||||||
|
|
||||||
|
// В пределах экрана?
|
||||||
|
// (можно оставить, можно клампить)
|
||||||
|
float sx = (ndc.x() * 0.5f + 0.5f) * Environment::width;
|
||||||
|
float sy = (ndc.y() * 0.5f + 0.5f) * Environment::height;
|
||||||
|
|
||||||
|
outX = sx;
|
||||||
|
outY = sy;
|
||||||
|
|
||||||
|
// Можно отсеять те, что вне:
|
||||||
|
if (sx < -200 || sx > Environment::width + 200) return false;
|
||||||
|
if (sy < -200 || sy > Environment::height + 200) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::drawBoxesLabels()
|
||||||
|
{
|
||||||
|
if (!textRenderer) return;
|
||||||
|
|
||||||
|
// Текст рисуем как 2D поверх всего 3D, но ДО drawUI или после — как хочешь.
|
||||||
|
// Чтобы подписи были поверх — делай после drawBoxes и до drawUI (как мы и вставили).
|
||||||
|
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < boxCoordsArr.size(); ++i)
|
||||||
|
{
|
||||||
|
if (i >= boxAlive.size() || !boxAlive[i]) continue;
|
||||||
|
if (i >= boxLabels.size()) continue;
|
||||||
|
|
||||||
|
// ВАЖНО: твои боксы рисуются с Translate({0,0,45000}) + pos
|
||||||
|
Vector3f boxWorld = boxCoordsArr[i].pos + Vector3f{ 0.0f, 0.0f, 45000.0f };
|
||||||
|
|
||||||
|
// Чуть выше бокса по Y (или по Z — как нравится)
|
||||||
|
Vector3f labelWorld = boxWorld + Vector3f{ 0.0f, 2.2f, 0.0f };
|
||||||
|
|
||||||
|
float sx, sy, depth;
|
||||||
|
if (!worldToScreen(labelWorld, sx, sy, depth)) continue;
|
||||||
|
|
||||||
|
// В твоей UI-системе Y обычно перевёрнут (ты делаешь uiY = height - my).
|
||||||
|
// Наш worldToScreen отдаёт Y в системе "низ=0, верх=height" (NDC->screen).
|
||||||
|
// Чтобы совпало с твоей UI-логикой, перевернём:
|
||||||
|
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);
|
||||||
|
|
||||||
|
textRenderer->drawText(boxLabels[i], uiX, uiY, scale, /*centered*/true);
|
||||||
|
}
|
||||||
|
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glEnable(GL_DEPTH_TEST);
|
||||||
|
}
|
||||||
|
|
||||||
Game::Game()
|
Game::Game()
|
||||||
: window(nullptr)
|
: window(nullptr)
|
||||||
, glContext(nullptr)
|
, glContext(nullptr)
|
||||||
@ -141,7 +245,7 @@ namespace ZL
|
|||||||
ZL::CheckGlError();
|
ZL::CheckGlError();
|
||||||
|
|
||||||
|
|
||||||
#ifndef SIMPLIFIED
|
//#ifndef SIMPLIFIED
|
||||||
renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/env_sky.vertex", "resources/shaders/env_sky_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/env_sky.vertex", "resources/shaders/env_sky_web.fragment", CONST_ZIP_FILE);
|
||||||
@ -150,14 +254,14 @@ namespace ZL
|
|||||||
renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/planet_stone.vertex", "resources/shaders/planet_stone_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/planet_stone.vertex", "resources/shaders/planet_stone_web.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/planet_land.vertex", "resources/shaders/planet_land_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/planet_land.vertex", "resources/shaders/planet_land_web.fragment", CONST_ZIP_FILE);
|
||||||
|
|
||||||
#else
|
/*#else
|
||||||
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/default_env.vertex", "resources/shaders/default_env_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("env_sky", "resources/shaders/default_env.vertex", "resources/shaders/default_env_web.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("planetBake", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("planetBake", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("planetStone", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
|
||||||
renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
|
renderer.shaderManager.AddShaderFromFiles("planetLand", "resources/shaders/default_texture.vertex", "resources/shaders/default_texture_web.fragment", CONST_ZIP_FILE);
|
||||||
#endif
|
#endif*/
|
||||||
|
|
||||||
bool cfgLoaded = sparkEmitter.loadFromJsonFile("resources/config/spark_config.json", renderer, CONST_ZIP_FILE);
|
bool cfgLoaded = sparkEmitter.loadFromJsonFile("resources/config/spark_config.json", renderer, CONST_ZIP_FILE);
|
||||||
bool projCfgLoaded = projectileEmitter.loadFromJsonFile("resources/config/spark_projectile_config.json", renderer, CONST_ZIP_FILE);
|
bool projCfgLoaded = projectileEmitter.loadFromJsonFile("resources/config/spark_projectile_config.json", renderer, CONST_ZIP_FILE);
|
||||||
@ -294,6 +398,15 @@ namespace ZL
|
|||||||
|
|
||||||
boxAlive.resize(boxCoordsArr.size(), true);
|
boxAlive.resize(boxCoordsArr.size(), true);
|
||||||
|
|
||||||
|
textRenderer = std::make_unique<ZL::TextRenderer>();
|
||||||
|
textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32);
|
||||||
|
|
||||||
|
boxLabels.clear();
|
||||||
|
boxLabels.reserve(boxCoordsArr.size());
|
||||||
|
for (size_t i = 0; i < boxCoordsArr.size(); ++i) {
|
||||||
|
boxLabels.push_back("Box " + std::to_string(i + 1));
|
||||||
|
}
|
||||||
|
|
||||||
if (!cfgLoaded)
|
if (!cfgLoaded)
|
||||||
{
|
{
|
||||||
throw std::runtime_error("Failed to load spark emitter config file!");
|
throw std::runtime_error("Failed to load spark emitter config file!");
|
||||||
@ -544,6 +657,7 @@ namespace ZL
|
|||||||
drawShip();
|
drawShip();
|
||||||
drawRemoteShips();
|
drawRemoteShips();
|
||||||
drawBoxes();
|
drawBoxes();
|
||||||
|
drawBoxesLabels();
|
||||||
|
|
||||||
drawUI();
|
drawUI();
|
||||||
CheckGlError();
|
CheckGlError();
|
||||||
|
|||||||
10
src/Game.h
10
src/Game.h
@ -10,6 +10,10 @@
|
|||||||
#include "utils/TaskManager.h"
|
#include "utils/TaskManager.h"
|
||||||
#include "network/NetworkInterface.h"
|
#include "network/NetworkInterface.h"
|
||||||
#include <queue>
|
#include <queue>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
#include <render/TextRenderer.h>
|
||||||
|
|
||||||
namespace ZL {
|
namespace ZL {
|
||||||
|
|
||||||
@ -43,10 +47,13 @@ namespace ZL {
|
|||||||
void drawCubemap(float skyPercent);
|
void drawCubemap(float skyPercent);
|
||||||
void drawShip();
|
void drawShip();
|
||||||
void drawBoxes();
|
void drawBoxes();
|
||||||
|
void drawBoxesLabels();
|
||||||
void drawUI();
|
void drawUI();
|
||||||
void drawRemoteShips();
|
void drawRemoteShips();
|
||||||
void fireProjectiles();
|
void fireProjectiles();
|
||||||
|
|
||||||
|
bool worldToScreen(const Vector3f& world, float& outX, float& outY, float& outDepth) const;
|
||||||
|
|
||||||
void handleDown(int mx, int my);
|
void handleDown(int mx, int my);
|
||||||
void handleUp(int mx, int my);
|
void handleUp(int mx, int my);
|
||||||
void handleMotion(int mx, int my);
|
void handleMotion(int mx, int my);
|
||||||
@ -62,6 +69,9 @@ namespace ZL {
|
|||||||
std::vector<BoxCoords> boxCoordsArr;
|
std::vector<BoxCoords> boxCoordsArr;
|
||||||
std::vector<VertexRenderStruct> boxRenderArr;
|
std::vector<VertexRenderStruct> boxRenderArr;
|
||||||
|
|
||||||
|
std::vector<std::string> boxLabels;
|
||||||
|
std::unique_ptr<TextRenderer> textRenderer;
|
||||||
|
|
||||||
std::unordered_map<int, ClientStateInterval> latestRemotePlayers;
|
std::unordered_map<int, ClientStateInterval> latestRemotePlayers;
|
||||||
|
|
||||||
float newShipVelocity = 0;
|
float newShipVelocity = 0;
|
||||||
|
|||||||
205
src/render/TextRenderer.cpp
Normal file
205
src/render/TextRenderer.cpp
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
#include "render/TextRenderer.h"
|
||||||
|
#include <ft2build.h>
|
||||||
|
#include FT_FREETYPE_H
|
||||||
|
|
||||||
|
#include "Environment.h"
|
||||||
|
#include "render/OpenGlExtensions.h" // если у тебя там gl* указатели
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace ZL {
|
||||||
|
|
||||||
|
TextRenderer::~TextRenderer()
|
||||||
|
{
|
||||||
|
for (auto& kv : glyphs) {
|
||||||
|
if (kv.second.texID) glDeleteTextures(1, &kv.second.texID);
|
||||||
|
}
|
||||||
|
glyphs.clear();
|
||||||
|
|
||||||
|
if (vbo) glDeleteBuffers(1, &vbo);
|
||||||
|
// if (vao) glDeleteVertexArrays(1, &vao);
|
||||||
|
vao = 0;
|
||||||
|
vbo = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextRenderer::init(Renderer& renderer, const std::string& ttfPath, int pixelSize)
|
||||||
|
{
|
||||||
|
r = &renderer;
|
||||||
|
|
||||||
|
// Добавим шейдер (если у тебя shaderManager работает так же, как с default)
|
||||||
|
// Подстрой под твой AddShaderFromFiles: тут без zip.
|
||||||
|
r->shaderManager.AddShaderFromFiles(shaderName,
|
||||||
|
"resources/shaders/text2d.vertex",
|
||||||
|
"resources/shaders/text2d.fragment",
|
||||||
|
"" // ZIP пустой
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!loadGlyphs(ttfPath, pixelSize)) return false;
|
||||||
|
|
||||||
|
// VAO/VBO для 6 вершин (2 треугольника) * (pos.xy + uv.xy) = 4 float
|
||||||
|
// glGenVertexArrays(1, &vao);
|
||||||
|
glGenBuffers(1, &vbo);
|
||||||
|
|
||||||
|
//glBindVertexArray(vao);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize)
|
||||||
|
{
|
||||||
|
FT_Library ft;
|
||||||
|
if (FT_Init_FreeType(&ft)) {
|
||||||
|
std::cerr << "FreeType: FT_Init_FreeType failed\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_Face face;
|
||||||
|
if (FT_New_Face(ft, ttfPath.c_str(), 0, &face)) {
|
||||||
|
std::cerr << "FreeType: failed to load font: " << ttfPath << "\n";
|
||||||
|
FT_Done_FreeType(ft);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_Set_Pixel_Sizes(face, 0, pixelSize);
|
||||||
|
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
|
|
||||||
|
glyphs.clear();
|
||||||
|
for (unsigned char c = 32; c < 128; ++c) {
|
||||||
|
if (FT_Load_Char(face, c, FT_LOAD_RENDER)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int tex;
|
||||||
|
glGenTextures(1, &tex);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, tex);
|
||||||
|
glTexImage2D(
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
GL_RED,
|
||||||
|
face->glyph->bitmap.width,
|
||||||
|
face->glyph->bitmap.rows,
|
||||||
|
0,
|
||||||
|
GL_RED,
|
||||||
|
GL_UNSIGNED_BYTE,
|
||||||
|
face->glyph->bitmap.buffer
|
||||||
|
);
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
|
||||||
|
GlyphInfo g;
|
||||||
|
g.texID = 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);
|
||||||
|
g.advance = (unsigned int)face->glyph->advance.x;
|
||||||
|
|
||||||
|
glyphs.emplace((char)c, g);
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_Done_Face(face);
|
||||||
|
FT_Done_FreeType(ft);
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered)
|
||||||
|
{
|
||||||
|
if (!r) return;
|
||||||
|
|
||||||
|
// Считаем ширину строки для центрирования
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
x -= totalW * 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
r->shaderManager.PushShader(shaderName);
|
||||||
|
|
||||||
|
// Орто-проекция в пикселях: (0..W, 0..H)
|
||||||
|
// ВАЖНО: мы рисуем в координатах Game::drawBoxesLabels() как uiX/uiY
|
||||||
|
// Здесь предположим, что (0,0) внизу слева.
|
||||||
|
float W = (float)Environment::width;
|
||||||
|
float H = (float)Environment::height;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// uProjection
|
||||||
|
r->RenderUniformMatrix4fv("uProjection", false, proj.data());
|
||||||
|
r->RenderUniform1i("uText", 0);
|
||||||
|
|
||||||
|
// белый цвет
|
||||||
|
float color[3] = {1.0f, 1.0f, 1.0f};
|
||||||
|
r->RenderUniform3fv("uColor", color);
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
// glBindVertexArray(vao);
|
||||||
|
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||||
|
|
||||||
|
r->EnableVertexAttribArray("vPosition");
|
||||||
|
r->EnableVertexAttribArray("vTexCoord");
|
||||||
|
|
||||||
|
r->VertexAttribPointer2fv("vPosition", 4 * sizeof(float), (const char*)0);
|
||||||
|
r->VertexAttribPointer2fv("vTexCoord", 4 * sizeof(float), (const char*)(2 * sizeof(float)));
|
||||||
|
|
||||||
|
float penX = x;
|
||||||
|
float penY = y;
|
||||||
|
|
||||||
|
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 треугольника
|
||||||
|
float verts[6][4] = {
|
||||||
|
{ xpos, ypos + h, 0.0f, 0.0f },
|
||||||
|
{ xpos, ypos, 0.0f, 1.0f },
|
||||||
|
{ xpos + w, ypos, 1.0f, 1.0f },
|
||||||
|
|
||||||
|
{ xpos, ypos + h, 0.0f, 0.0f },
|
||||||
|
{ xpos + w, ypos, 1.0f, 1.0f },
|
||||||
|
{ xpos + w, ypos + h, 1.0f, 0.0f }
|
||||||
|
};
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g.texID);
|
||||||
|
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(verts), verts);
|
||||||
|
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
|
||||||
|
penX += (g.advance >> 6) * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
|
||||||
|
r->DisableVertexAttribArray("vPosition");
|
||||||
|
r->DisableVertexAttribArray("vTexCoord");
|
||||||
|
|
||||||
|
r->shaderManager.PopShader();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZL
|
||||||
41
src/render/TextRenderer.h
Normal file
41
src/render/TextRenderer.h
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <Eigen/Dense>
|
||||||
|
#include "render/Renderer.h"
|
||||||
|
|
||||||
|
|
||||||
|
namespace ZL {
|
||||||
|
|
||||||
|
struct GlyphInfo {
|
||||||
|
unsigned int texID = 0; // GL texture for glyph
|
||||||
|
Eigen::Vector2f size; // glyph size in pixels
|
||||||
|
Eigen::Vector2f bearing; // offset from baseline
|
||||||
|
unsigned int advance = 0; // advance.x in 1/64 pixels
|
||||||
|
};
|
||||||
|
|
||||||
|
class TextRenderer {
|
||||||
|
public:
|
||||||
|
TextRenderer() = default;
|
||||||
|
~TextRenderer();
|
||||||
|
|
||||||
|
bool init(Renderer& renderer, const std::string& ttfPath, int pixelSize);
|
||||||
|
void drawText(const std::string& text, float x, float y, float scale, bool centered);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool loadGlyphs(const std::string& ttfPath, int pixelSize);
|
||||||
|
|
||||||
|
Renderer* r = nullptr;
|
||||||
|
|
||||||
|
std::unordered_map<char, GlyphInfo> glyphs;
|
||||||
|
|
||||||
|
// OpenGL objects for a dynamic quad
|
||||||
|
unsigned int vao = 0;
|
||||||
|
unsigned int vbo = 0;
|
||||||
|
|
||||||
|
std::string shaderName = "text2d";
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZL
|
||||||
Loading…
Reference in New Issue
Block a user