diff --git a/cmake/ThirdParty.cmake b/cmake/ThirdParty.cmake index 99bc399..5ba633a 100644 --- a/cmake/ThirdParty.cmake +++ b/cmake/ThirdParty.cmake @@ -397,9 +397,11 @@ endif() add_library(freetype_external_lib UNKNOWN IMPORTED GLOBAL) set_target_properties(freetype_external_lib PROPERTIES IMPORTED_LOCATION_DEBUG "${_ft_debug_lib}" - IMPORTED_LOCATION_RELEASE "${_ft_release_lib}" - INTERFACE_INCLUDE_DIRECTORIES - "$,${FREETYPE_BASE_DIR}-Debug/include,${FREETYPE_BASE_DIR}-Release/include>" + IMPORTED_LOCATION_RELEASE "${_ft_release_lib}" +) +target_include_directories(freetype_external_lib INTERFACE + "$,${FREETYPE_BASE_DIR}-Debug/include/freetype2,${FREETYPE_BASE_DIR}-Release/include/freetype2>" + "$,${FREETYPE_BASE_DIR}-Debug/include,${FREETYPE_BASE_DIR}-Release/include>" ) # =========================================== @@ -453,8 +455,8 @@ if(NOT _have_sdl2ttf) -DSDL2_LIBRARY=${_SDL2_LIB} -DSDL2_INCLUDE_DIR=${SDL2_INSTALL_DIR}/include/SDL2 -DFREETYPE_LIBRARY=${_FT_LIB} - -DFREETYPE_INCLUDE_DIR=${_FT_PREFIX}/include - -DFREETYPE_INCLUDE_DIRS=${_FT_PREFIX}/include + -DFREETYPE_INCLUDE_DIR=${_FT_PREFIX}/include/freetype2 + -DFREETYPE_INCLUDE_DIRS=${_FT_PREFIX}/include/freetype2 -DSDL2TTF_VENDORED=OFF -DSDL2TTF_SAMPLES=OFF RESULT_VARIABLE _ttf_cfg_res diff --git a/proj-windows/CMakeLists.txt b/proj-windows/CMakeLists.txt index 36e6ee7..2355739 100644 --- a/proj-windows/CMakeLists.txt +++ b/proj-windows/CMakeLists.txt @@ -57,6 +57,8 @@ add_executable(space-game001 ../src/network/ClientState.cpp ../src/network/WebSocketClient.h ../src/network/WebSocketClient.cpp + ../src/render/TextRenderer.h + ../src/render/TextRenderer.cpp ) # Установка проекта по умолчанию для Visual Studio @@ -78,8 +80,8 @@ target_compile_definitions(space-game001 PRIVATE WIN32_LEAN_AND_MEAN PNG_ENABLED SDL_MAIN_HANDLED -# NETWORK -# SIMPLIFIED + NETWORK + SIMPLIFIED ) # Линкуем с SDL2main, если он вообще установлен diff --git a/resources/fonts/DroidSans.ttf b/resources/fonts/DroidSans.ttf new file mode 100644 index 0000000..ad1efca Binary files /dev/null and b/resources/fonts/DroidSans.ttf differ diff --git a/resources/shaders/defaultAtmosphere_web.fragment b/resources/shaders/defaultAtmosphere_web.fragment index 8ad78af..718e19b 100644 --- a/resources/shaders/defaultAtmosphere_web.fragment +++ b/resources/shaders/defaultAtmosphere_web.fragment @@ -1,4 +1,4 @@ -precision highp float; +//precisionhighp float; // Фрагментный шейдер: uniform vec3 uColor; diff --git a/resources/shaders/defaultColor_web.fragment b/resources/shaders/defaultColor_web.fragment index 1e3d8c1..9dfd488 100644 --- a/resources/shaders/defaultColor_web.fragment +++ b/resources/shaders/defaultColor_web.fragment @@ -1,4 +1,4 @@ -precision mediump float; +//precisionmediump float; varying vec3 color; void main() diff --git a/resources/shaders/default_env_web.fragment b/resources/shaders/default_env_web.fragment index 505814f..be588de 100644 --- a/resources/shaders/default_env_web.fragment +++ b/resources/shaders/default_env_web.fragment @@ -1,4 +1,4 @@ -precision mediump float; +//precisionmediump float; uniform samplerCube Texture; varying vec3 dir; diff --git a/resources/shaders/default_texture_web.fragment b/resources/shaders/default_texture_web.fragment index 1c3c571..f59b4f7 100644 --- a/resources/shaders/default_texture_web.fragment +++ b/resources/shaders/default_texture_web.fragment @@ -1,4 +1,4 @@ -precision mediump float; +//precisionmediump float; uniform sampler2D Texture; varying vec2 texCoord; diff --git a/resources/shaders/default_web.fragment b/resources/shaders/default_web.fragment index 80c01d7..7548e9e 100644 --- a/resources/shaders/default_web.fragment +++ b/resources/shaders/default_web.fragment @@ -1,4 +1,4 @@ -precision mediump float; +//precisionmediump float; uniform sampler2D Texture; varying vec2 texCoord; diff --git a/resources/shaders/env_sky_web.fragment b/resources/shaders/env_sky_web.fragment index 971d78c..18a1169 100644 --- a/resources/shaders/env_sky_web.fragment +++ b/resources/shaders/env_sky_web.fragment @@ -1,4 +1,4 @@ -precision mediump float; +//precisionmediump float; uniform samplerCube Texture; uniform float skyPercent; uniform float uPlayerLightFactor; // Глобальный фактор дня/ночи для позиции игрока diff --git a/resources/shaders/planet_bake_web.fragment b/resources/shaders/planet_bake_web.fragment index c08c976..cd404ba 100644 --- a/resources/shaders/planet_bake_web.fragment +++ b/resources/shaders/planet_bake_web.fragment @@ -1,4 +1,4 @@ -precision mediump float; +//precisionmediump float; varying vec2 TexCoord; varying float vHeight; diff --git a/resources/shaders/planet_land.vertex b/resources/shaders/planet_land.vertex index 1eb3120..bbec690 100644 --- a/resources/shaders/planet_land.vertex +++ b/resources/shaders/planet_land.vertex @@ -15,7 +15,7 @@ varying vec3 vWorldNormal; uniform mat4 ProjectionModelViewMatrix; uniform mat4 ModelViewMatrix; -uniform highp vec3 uViewPos; +uniform vec3 uViewPos; void main() { gl_Position = ProjectionModelViewMatrix * vec4(vPosition, 1.0); diff --git a/resources/shaders/planet_land_web.fragment b/resources/shaders/planet_land_web.fragment index d2047cc..a569efa 100644 --- a/resources/shaders/planet_land_web.fragment +++ b/resources/shaders/planet_land_web.fragment @@ -1,4 +1,4 @@ -precision highp float; +//precisionhighp float; varying vec2 TexCoord; varying vec3 vViewDirTangent; varying vec3 Color; diff --git a/resources/shaders/planet_stone.vertex b/resources/shaders/planet_stone.vertex index ab7d17f..d0c0d57 100644 --- a/resources/shaders/planet_stone.vertex +++ b/resources/shaders/planet_stone.vertex @@ -14,7 +14,7 @@ varying vec3 vWorldNormal; uniform mat4 ProjectionModelViewMatrix; uniform mat4 ModelViewMatrix; -uniform highp vec3 uViewPos; +uniform vec3 uViewPos; void main() { vWorldNormal = vNormal; @@ -23,8 +23,6 @@ void main() { vec3 viewDirWorld = normalize(uViewPos - vPosition); - // Строим матрицу перехода из атрибутов - // Так как базис ортонормирован, TBN^-1 == TBN_transpose vViewDirTangent = vec3( dot(viewDirWorld, vTangent), dot(viewDirWorld, vBinormal), diff --git a/resources/shaders/planet_stone_web.fragment b/resources/shaders/planet_stone_web.fragment index 64333e4..e394f56 100644 --- a/resources/shaders/planet_stone_web.fragment +++ b/resources/shaders/planet_stone_web.fragment @@ -1,4 +1,4 @@ -precision highp float; +//precisionhighp float; // planetStone фрагментный шейдер varying vec2 TexCoord; diff --git a/resources/shaders/spark.fragment b/resources/shaders/spark.fragment index 1c3c571..f59b4f7 100644 --- a/resources/shaders/spark.fragment +++ b/resources/shaders/spark.fragment @@ -1,4 +1,4 @@ -precision mediump float; +//precisionmediump float; uniform sampler2D Texture; varying vec2 texCoord; diff --git a/resources/shaders/text2d.fragment b/resources/shaders/text2d.fragment new file mode 100644 index 0000000..0c69bb0 --- /dev/null +++ b/resources/shaders/text2d.fragment @@ -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); +} \ No newline at end of file diff --git a/resources/shaders/text2d.vertex b/resources/shaders/text2d.vertex new file mode 100644 index 0000000..7483062 --- /dev/null +++ b/resources/shaders/text2d.vertex @@ -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); +} \ No newline at end of file diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 5e12ff8..641ed8d 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -28,6 +28,7 @@ add_subdirectory("${BOOST_SRC_DIR}/libs/predef" boost-predef-build EXCLUDE_FROM_ add_executable(Server server.cpp ../src/network/ClientState.h +../src/network/ClientState.cpp ) target_include_directories(Server PRIVATE ${BOOST_SRC_DIR}) diff --git a/src/Game.cpp b/src/Game.cpp index 51c3eaa..42c843d 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -112,6 +112,110 @@ namespace ZL 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(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; + + 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() : window(nullptr) , glContext(nullptr) @@ -142,7 +246,7 @@ namespace ZL 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("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); @@ -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("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("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("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("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 projCfgLoaded = projectileEmitter.loadFromJsonFile("resources/config/spark_projectile_config.json", renderer, CONST_ZIP_FILE); @@ -338,6 +442,15 @@ namespace ZL boxAlive.resize(boxCoordsArr.size(), true); + textRenderer = std::make_unique(); + 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) { throw std::runtime_error("Failed to load spark emitter config file!"); @@ -590,7 +703,9 @@ namespace ZL } drawShip(); drawRemoteShips(); + drawRemoteShipsLabels(); drawBoxes(); + drawBoxesLabels(); drawUI(); CheckGlError(); @@ -683,6 +798,62 @@ namespace ZL 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() { if (lastTickCount == 0) { @@ -703,7 +874,7 @@ namespace ZL size_t delta = newTickCount - lastTickCount; if (delta > CONST_MAX_TIME_INTERVAL) { - throw std::runtime_error("Synchronization is lost"); + //throw std::runtime_error("Synchronization is lost"); } auto now_ms = newTickCount; diff --git a/src/Game.h b/src/Game.h index 5c5ba20..10177c4 100644 --- a/src/Game.h +++ b/src/Game.h @@ -10,6 +10,10 @@ #include "utils/TaskManager.h" #include "network/NetworkInterface.h" #include +#include +#include +#include +#include namespace ZL { @@ -43,10 +47,14 @@ namespace ZL { void drawCubemap(float skyPercent); void drawShip(); void drawBoxes(); + void drawBoxesLabels(); void drawUI(); void drawRemoteShips(); + void drawRemoteShipsLabels(); void fireProjectiles(); + bool worldToScreen(const Vector3f& world, float& outX, float& outY, float& outDepth) const; + void handleDown(int mx, int my); void handleUp(int mx, int my); void handleMotion(int mx, int my); @@ -62,6 +70,9 @@ namespace ZL { std::vector boxCoordsArr; std::vector boxRenderArr; + std::vector boxLabels; + std::unique_ptr textRenderer; + std::unordered_map latestRemotePlayers; float newShipVelocity = 0; @@ -106,6 +117,13 @@ namespace ZL { const uint64_t explosionDurationMs = 500; 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; }; diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index 805c3c9..a11ed19 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -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) { auto shader = shaderManager.GetCurrentShader(); diff --git a/src/render/Renderer.h b/src/render/Renderer.h index cbf9dbe..e95adb4 100644 --- a/src/render/Renderer.h +++ b/src/render/Renderer.h @@ -138,6 +138,7 @@ namespace ZL { void RenderUniformMatrix4fv(const std::string& uniformName, bool transpose, const float* value); void RenderUniform1i(const std::string& uniformName, const int 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 VertexAttribPointer2fv(const std::string& attribName, int stride, const char* pointer); diff --git a/src/render/ShaderManager.cpp b/src/render/ShaderManager.cpp index 8e27bb8..a45cf64 100644 --- a/src/render/ShaderManager.cpp +++ b/src/render/ShaderManager.cpp @@ -14,7 +14,8 @@ namespace ZL { char infoLog[CONST_INFOLOG_LENGTH]; int infoLogLength; - + char infoLog2[CONST_INFOLOG_LENGTH]; + int infoLogLength2; int vertexShaderCompiled; int fragmentShaderCompiled; int programLinked; @@ -40,7 +41,8 @@ namespace ZL { glCompileShader(fragmentShader); glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentShaderCompiled); - glGetShaderInfoLog(fragmentShader, CONST_INFOLOG_LENGTH, &infoLogLength, infoLog); + glGetShaderInfoLog(fragmentShader, CONST_INFOLOG_LENGTH, &infoLogLength2, infoLog2); + if (!vertexShaderCompiled) { #ifdef __ANDROID__ diff --git a/src/render/TextRenderer.cpp b/src/render/TextRenderer.cpp new file mode 100644 index 0000000..e081bef --- /dev/null +++ b/src/render/TextRenderer.cpp @@ -0,0 +1,198 @@ +#include "render/TextRenderer.h" +#include +#include FT_FREETYPE_H + +#include "Environment.h" +#include "render/OpenGlExtensions.h" +#include +#include + +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 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 \ No newline at end of file diff --git a/src/render/TextRenderer.h b/src/render/TextRenderer.h new file mode 100644 index 0000000..7064f7e --- /dev/null +++ b/src/render/TextRenderer.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include +#include +#include +#include +#include "render/Renderer.h" +#include + + +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 color = { 1.f,1.f,1.f,1.f }); + +private: + bool loadGlyphs(const std::string& ttfPath, int pixelSize); + + Renderer* r = nullptr; + + std::unordered_map glyphs; + + // OpenGL objects for a dynamic quad + unsigned int vao = 0; + unsigned int vbo = 0; + + std::string shaderName = "text2d"; +}; + +} // namespace ZL \ No newline at end of file