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..ba8c6d0 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 @@ -79,7 +81,7 @@ target_compile_definitions(space-game001 PRIVATE PNG_ENABLED SDL_MAIN_HANDLED # NETWORK -# SIMPLIFIED + 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/text2d.fragment b/resources/shaders/text2d.fragment new file mode 100644 index 0000000..a548f01 --- /dev/null +++ b/resources/shaders/text2d.fragment @@ -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); +} \ 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/src/Game.cpp b/src/Game.cpp index 7afcadb..e849a84 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -111,6 +111,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) @@ -141,7 +245,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); @@ -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("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); @@ -294,6 +398,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!"); @@ -544,6 +657,7 @@ namespace ZL drawShip(); drawRemoteShips(); drawBoxes(); + drawBoxesLabels(); drawUI(); CheckGlError(); diff --git a/src/Game.h b/src/Game.h index 597c223..5488563 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,13 @@ namespace ZL { void drawCubemap(float skyPercent); void drawShip(); void drawBoxes(); + void drawBoxesLabels(); void drawUI(); void drawRemoteShips(); 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 +69,9 @@ namespace ZL { std::vector boxCoordsArr; std::vector boxRenderArr; + std::vector boxLabels; + std::unique_ptr textRenderer; + std::unordered_map latestRemotePlayers; float newShipVelocity = 0; diff --git a/src/render/TextRenderer.cpp b/src/render/TextRenderer.cpp new file mode 100644 index 0000000..ff964e8 --- /dev/null +++ b/src/render/TextRenderer.cpp @@ -0,0 +1,205 @@ +#include "render/TextRenderer.h" +#include +#include FT_FREETYPE_H + +#include "Environment.h" +#include "render/OpenGlExtensions.h" // если у тебя там gl* указатели +#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; + + // Добавим шейдер (если у тебя 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 \ No newline at end of file diff --git a/src/render/TextRenderer.h b/src/render/TextRenderer.h new file mode 100644 index 0000000..0cf85bb --- /dev/null +++ b/src/render/TextRenderer.h @@ -0,0 +1,41 @@ +#pragma once +#include +#include +#include +#include +#include +#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 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