This commit is contained in:
Vladislav Khorev 2026-02-05 07:33:13 +03:00
commit 82ca994fac
25 changed files with 497 additions and 27 deletions

View File

@ -398,8 +398,10 @@ 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 )
"$<IF:$<CONFIG:Debug>,${FREETYPE_BASE_DIR}-Debug/include,${FREETYPE_BASE_DIR}-Release/include>" 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>"
) )
# =========================================== # ===========================================
@ -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

View File

@ -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
@ -78,8 +80,8 @@ target_compile_definitions(space-game001 PRIVATE
WIN32_LEAN_AND_MEAN WIN32_LEAN_AND_MEAN
PNG_ENABLED PNG_ENABLED
SDL_MAIN_HANDLED SDL_MAIN_HANDLED
# NETWORK NETWORK
# SIMPLIFIED SIMPLIFIED
) )
# Линкуем с SDL2main, если он вообще установлен # Линкуем с SDL2main, если он вообще установлен

Binary file not shown.

View File

@ -1,4 +1,4 @@
precision highp float; //precisionhighp float;
// Фрагментный шейдер: // Фрагментный шейдер:
uniform vec3 uColor; uniform vec3 uColor;

View File

@ -1,4 +1,4 @@
precision mediump float; //precisionmediump float;
varying vec3 color; varying vec3 color;
void main() void main()

View File

@ -1,4 +1,4 @@
precision mediump float; //precisionmediump float;
uniform samplerCube Texture; uniform samplerCube Texture;
varying vec3 dir; varying vec3 dir;

View File

@ -1,4 +1,4 @@
precision mediump float; //precisionmediump float;
uniform sampler2D Texture; uniform sampler2D Texture;
varying vec2 texCoord; varying vec2 texCoord;

View File

@ -1,4 +1,4 @@
precision mediump float; //precisionmediump float;
uniform sampler2D Texture; uniform sampler2D Texture;
varying vec2 texCoord; varying vec2 texCoord;

View File

@ -1,4 +1,4 @@
precision mediump float; //precisionmediump float;
uniform samplerCube Texture; uniform samplerCube Texture;
uniform float skyPercent; uniform float skyPercent;
uniform float uPlayerLightFactor; // Глобальный фактор дня/ночи для позиции игрока uniform float uPlayerLightFactor; // Глобальный фактор дня/ночи для позиции игрока

View File

@ -1,4 +1,4 @@
precision mediump float; //precisionmediump float;
varying vec2 TexCoord; varying vec2 TexCoord;
varying float vHeight; varying float vHeight;

View File

@ -15,7 +15,7 @@ varying vec3 vWorldNormal;
uniform mat4 ProjectionModelViewMatrix; uniform mat4 ProjectionModelViewMatrix;
uniform mat4 ModelViewMatrix; uniform mat4 ModelViewMatrix;
uniform highp vec3 uViewPos; uniform vec3 uViewPos;
void main() { void main() {
gl_Position = ProjectionModelViewMatrix * vec4(vPosition, 1.0); gl_Position = ProjectionModelViewMatrix * vec4(vPosition, 1.0);

View File

@ -1,4 +1,4 @@
precision highp float; //precisionhighp float;
varying vec2 TexCoord; varying vec2 TexCoord;
varying vec3 vViewDirTangent; varying vec3 vViewDirTangent;
varying vec3 Color; varying vec3 Color;

View File

@ -14,7 +14,7 @@ varying vec3 vWorldNormal;
uniform mat4 ProjectionModelViewMatrix; uniform mat4 ProjectionModelViewMatrix;
uniform mat4 ModelViewMatrix; uniform mat4 ModelViewMatrix;
uniform highp vec3 uViewPos; uniform vec3 uViewPos;
void main() { void main() {
vWorldNormal = vNormal; vWorldNormal = vNormal;
@ -23,8 +23,6 @@ void main() {
vec3 viewDirWorld = normalize(uViewPos - vPosition); vec3 viewDirWorld = normalize(uViewPos - vPosition);
// Строим матрицу перехода из атрибутов
// Так как базис ортонормирован, TBN^-1 == TBN_transpose
vViewDirTangent = vec3( vViewDirTangent = vec3(
dot(viewDirWorld, vTangent), dot(viewDirWorld, vTangent),
dot(viewDirWorld, vBinormal), dot(viewDirWorld, vBinormal),

View File

@ -1,4 +1,4 @@
precision highp float; //precisionhighp float;
// planetStone фрагментный шейдер // planetStone фрагментный шейдер
varying vec2 TexCoord; varying vec2 TexCoord;

View File

@ -1,4 +1,4 @@
precision mediump float; //precisionmediump float;
uniform sampler2D Texture; uniform sampler2D Texture;
varying vec2 texCoord; varying vec2 texCoord;

View File

@ -0,0 +1,14 @@
#version 330 core
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D uText;
uniform vec4 uColor;
uniform vec4 uOutlineColor;
uniform float uOutlineWidth = 0.7;
void main() {
float dist = texture(uText, TexCoord).r;
float outline = smoothstep(0.5 - uOutlineWidth, 0.5 + uOutlineWidth, dist);
float text = smoothstep(0.5, 0.5 + 0.1, dist);
float glow = exp(-pow(dist - 0.5, 2.0) / 0.02);
FragColor = vec4(uColor.rgb * (text + glow * 0.6), uColor.a * outline);
}

View 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);
}

View File

@ -28,6 +28,7 @@ add_subdirectory("${BOOST_SRC_DIR}/libs/predef" boost-predef-build EXCLUDE_FROM_
add_executable(Server add_executable(Server
server.cpp server.cpp
../src/network/ClientState.h ../src/network/ClientState.h
../src/network/ClientState.cpp
) )
target_include_directories(Server PRIVATE ${BOOST_SRC_DIR}) target_include_directories(Server PRIVATE ${BOOST_SRC_DIR})

View File

@ -112,6 +112,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)
@ -142,7 +246,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);
@ -151,14 +255,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);
@ -338,6 +442,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!");
@ -590,7 +703,9 @@ namespace ZL
} }
drawShip(); drawShip();
drawRemoteShips(); drawRemoteShips();
drawRemoteShipsLabels();
drawBoxes(); drawBoxes();
drawBoxesLabels();
drawUI(); drawUI();
CheckGlError(); CheckGlError();
@ -683,6 +798,62 @@ namespace ZL
CheckGlError(); CheckGlError();
} }
void Game::drawRemoteShipsLabels()
{
if (!textRenderer) return;
#ifdef NETWORK
// 2D поверх 3D
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Берем удаленных игроков
latestRemotePlayers = networkClient->getRemotePlayers();
auto now = std::chrono::system_clock::now();
now -= std::chrono::milliseconds(CLIENT_DELAY);
for (auto const& [id, remotePlayer] : latestRemotePlayers)
{
if (!remotePlayer.canFetchClientStateAtTime(now))
continue;
ClientState st = remotePlayer.fetchClientStateAtTime(now);
// Позиция корабля в мире
Vector3f shipWorld = st.position;
float distSq = (Environment::shipState.position - shipWorld).squaredNorm();
/*if (distSq > MAX_DIST_SQ) // дальность прорисовки никнейма
continue;*/
float dist = sqrt(distSq);
float alpha = 1.0f; // постоянная видимость
/*float alpha = std::clamp(1.f - (dist - FADE_START) / FADE_RANGE, 0.f, 1.f); // дальность прорисовки никнейма
if (alpha < 0.01f)
continue; */
Vector3f labelWorld = shipWorld + Vector3f{ 0.f, -4.f, 0.f }; // регулировка высоты
float sx, sy, depth;
if (!worldToScreen(labelWorld, sx, sy, depth))
continue;
float uiX = sx, uiY = sy;
float scale = std::clamp(BASE_SCALE / (dist * PERSPECTIVE_K + 1.f), MIN_SCALE, MAX_SCALE);
// Дефолтный лейбл
std::string label = "Player (" + std::to_string(st.id) + ") " + std::to_string((int)dist) + "m";
// TODO: nickname sync
textRenderer->drawText(label, uiX + 1.f, uiY + 1.f, scale, true, {0.f, 0.f, 0.f, alpha}); // color param
textRenderer->drawText(label, uiX, uiY, scale, true, { 1.f, 1.f, 1.f, alpha });
}
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
#endif
}
void Game::processTickCount() { void Game::processTickCount() {
if (lastTickCount == 0) { if (lastTickCount == 0) {
@ -703,7 +874,7 @@ namespace ZL
size_t delta = newTickCount - lastTickCount; size_t delta = newTickCount - lastTickCount;
if (delta > CONST_MAX_TIME_INTERVAL) if (delta > CONST_MAX_TIME_INTERVAL)
{ {
throw std::runtime_error("Synchronization is lost"); //throw std::runtime_error("Synchronization is lost");
} }
auto now_ms = newTickCount; auto now_ms = newTickCount;

View File

@ -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,14 @@ 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 drawRemoteShipsLabels();
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 +70,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;
@ -106,6 +117,13 @@ namespace ZL {
const uint64_t explosionDurationMs = 500; const uint64_t explosionDurationMs = 500;
bool serverBoxesApplied = false; bool serverBoxesApplied = false;
static constexpr float MAX_DIST_SQ = 10000.f * 10000.f;
static constexpr float FADE_START = 6000.f;
static constexpr float FADE_RANGE = 4000.f;
static constexpr float BASE_SCALE = 140.f;
static constexpr float PERSPECTIVE_K = 0.05f; // Tune
static constexpr float MIN_SCALE = 0.4f;
static constexpr float MAX_SCALE = 1.5f;
}; };

View File

@ -835,6 +835,15 @@ namespace ZL {
} }
} }
void Renderer::RenderUniform4fv(const std::string& uniformName, const float* value)
{
auto shader = shaderManager.GetCurrentShader();
auto uniform = shader->uniformList.find(uniformName);
if (uniform != shader->uniformList.end()) {
glUniform4fv(uniform->second, 1, value);
}
}
void Renderer::RenderUniform1i(const std::string& uniformName, const int value) void Renderer::RenderUniform1i(const std::string& uniformName, const int value)
{ {
auto shader = shaderManager.GetCurrentShader(); auto shader = shaderManager.GetCurrentShader();

View File

@ -138,6 +138,7 @@ namespace ZL {
void RenderUniformMatrix4fv(const std::string& uniformName, bool transpose, const float* value); void RenderUniformMatrix4fv(const std::string& uniformName, bool transpose, const float* value);
void RenderUniform1i(const std::string& uniformName, const int value); void RenderUniform1i(const std::string& uniformName, const int value);
void RenderUniform3fv(const std::string& uniformName, const float* value); void RenderUniform3fv(const std::string& uniformName, const float* value);
void RenderUniform4fv(const std::string& uniformName, const float* value);
void RenderUniform1f(const std::string& uniformName, float value); void RenderUniform1f(const std::string& uniformName, float value);
void VertexAttribPointer2fv(const std::string& attribName, int stride, const char* pointer); void VertexAttribPointer2fv(const std::string& attribName, int stride, const char* pointer);

View File

@ -14,7 +14,8 @@ namespace ZL {
char infoLog[CONST_INFOLOG_LENGTH]; char infoLog[CONST_INFOLOG_LENGTH];
int infoLogLength; int infoLogLength;
char infoLog2[CONST_INFOLOG_LENGTH];
int infoLogLength2;
int vertexShaderCompiled; int vertexShaderCompiled;
int fragmentShaderCompiled; int fragmentShaderCompiled;
int programLinked; int programLinked;
@ -40,7 +41,8 @@ namespace ZL {
glCompileShader(fragmentShader); glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentShaderCompiled); glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentShaderCompiled);
glGetShaderInfoLog(fragmentShader, CONST_INFOLOG_LENGTH, &infoLogLength, infoLog); glGetShaderInfoLog(fragmentShader, CONST_INFOLOG_LENGTH, &infoLogLength2, infoLog2);
if (!vertexShaderCompiled) { if (!vertexShaderCompiled) {
#ifdef __ANDROID__ #ifdef __ANDROID__

198
src/render/TextRenderer.cpp Normal file
View File

@ -0,0 +1,198 @@
#include "render/TextRenderer.h"
#include <ft2build.h>
#include FT_FREETYPE_H
#include "Environment.h"
#include "render/OpenGlExtensions.h"
#include <iostream>
#include <array>
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;
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, std::array<float, 4> color)
{
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)
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);
r->RenderUniform4fv("uColor", color.data());
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

42
src/render/TextRenderer.h Normal file
View File

@ -0,0 +1,42 @@
#pragma once
#include <string>
#include <memory>
#include <vector>
#include <unordered_map>
#include <Eigen/Dense>
#include "render/Renderer.h"
#include <array>
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, std::array<float, 4> color = { 1.f,1.f,1.f,1.f });
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