diff --git a/CMakeLists.txt b/CMakeLists.txt index 05261370..51d6002f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,7 @@ set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) include(${PROJECT_SOURCE_DIR}/external/cmake-conan/conan.cmake) conan_cmake_configure(REQUIRES boost/1.76.0 + freetype/2.10.4 geographiclib/1.52 glm/0.9.9.8 gtest/cci.20210126 diff --git a/scwx-qt/gl/text.frag b/scwx-qt/gl/text.frag new file mode 100644 index 00000000..23298e8e --- /dev/null +++ b/scwx-qt/gl/text.frag @@ -0,0 +1,12 @@ +#version 330 core +in vec2 texCoords; +out vec4 color; + +uniform sampler2D text; +uniform vec4 textColor; + +void main() +{ + vec4 sampled = vec4(1.0f, 1.0f, 1.0f, texture(text, texCoords).r); + color = textColor * sampled; +} diff --git a/scwx-qt/gl/text.vert b/scwx-qt/gl/text.vert new file mode 100644 index 00000000..ffc3a8be --- /dev/null +++ b/scwx-qt/gl/text.vert @@ -0,0 +1,11 @@ +#version 330 core +layout (location = 0) in vec4 vertex; +out vec2 texCoords; + +uniform mat4 projection; + +void main() +{ + gl_Position = projection * vec4(vertex.xy, 0.0f, 1.0f); + texCoords = vertex.zw; +} diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 6ab4d10c..8c28f3e0 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -12,6 +12,7 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Boost) +find_package(Freetype) find_package(geographiclib) find_package(glm) @@ -51,6 +52,8 @@ set(HDR_MAIN source/scwx/qt/main/main_window.hpp) set(SRC_MAIN source/scwx/qt/main/main.cpp source/scwx/qt/main/main_window.cpp) set(UI_MAIN source/scwx/qt/main/main_window.ui) +set(HDR_GL source/scwx/qt/gl/text_shader.hpp) +set(SRC_GL source/scwx/qt/gl/text_shader.cpp) set(HDR_MANAGER source/scwx/qt/manager/radar_manager.hpp) set(SRC_MANAGER source/scwx/qt/manager/radar_manager.cpp) set(HDR_MAP source/scwx/qt/map/map_widget.hpp @@ -61,16 +64,20 @@ set(SRC_MAP source/scwx/qt/map/map_widget.cpp source/scwx/qt/map/radar_layer.cpp source/scwx/qt/map/radar_range_layer.cpp source/scwx/qt/map/triangle_layer.cpp) -set(HDR_UTIL source/scwx/qt/util/gl.hpp +set(HDR_UTIL source/scwx/qt/util/font.hpp + source/scwx/qt/util/gl.hpp source/scwx/qt/util/shader_program.hpp) -set(SRC_UTIL source/scwx/qt/util/shader_program.cpp) +set(SRC_UTIL source/scwx/qt/util/font.cpp + source/scwx/qt/util/shader_program.cpp) set(HDR_VIEW source/scwx/qt/view/radar_view.hpp) set(SRC_VIEW source/scwx/qt/view/radar_view.cpp) set(RESOURCE_FILES scwx-qt.qrc) set(SHADER_FILES gl/radar.frag - gl/radar.vert) + gl/radar.vert + gl/text.frag + gl/text.vert) set(TS_FILES ts/scwx_en_US.ts) @@ -122,6 +129,7 @@ target_link_libraries(scwx-qt PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Boost::timer qmapboxgl opengl32 + Freetype::Freetype GeographicLib::GeographicLib glm::glm wxdata) diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index 39f00ae6..2638956d 100644 --- a/scwx-qt/scwx-qt.qrc +++ b/scwx-qt/scwx-qt.qrc @@ -2,5 +2,7 @@ gl/radar.frag gl/radar.vert + gl/text.frag + gl/text.vert diff --git a/scwx-qt/source/scwx/qt/gl/text_shader.cpp b/scwx-qt/source/scwx/qt/gl/text_shader.cpp new file mode 100644 index 00000000..c4ad61a5 --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/text_shader.cpp @@ -0,0 +1,155 @@ +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace gl +{ + +static const std::string logPrefix_ = "[scwx::qt::gl::text_shader] "; + +class TextShaderImpl +{ +public: + explicit TextShaderImpl(OpenGLFunctions& gl) : + gl_ {gl}, + projectionLocation_(GL_INVALID_INDEX), + textColorLocation_(GL_INVALID_INDEX), + vao_ {GL_INVALID_INDEX}, + vbo_ {GL_INVALID_INDEX} + { + } + + ~TextShaderImpl() {} + + OpenGLFunctions& gl_; + + GLint projectionLocation_; + GLint textColorLocation_; + + GLuint vao_; + GLuint vbo_; +}; + +TextShader::TextShader(OpenGLFunctions& gl) : + ShaderProgram(gl), p(std::make_unique(gl)) +{ +} +TextShader::~TextShader() = default; + +TextShader::TextShader(TextShader&&) noexcept = default; +TextShader& TextShader::operator=(TextShader&&) noexcept = default; + +bool TextShader::Initialize() +{ + OpenGLFunctions& gl = p->gl_; + + // Load and configure shader + bool success = Load(":/gl/text.vert", ":/gl/text.frag"); + + p->projectionLocation_ = gl.glGetUniformLocation(id(), "projection"); + if (p->projectionLocation_ == -1) + { + BOOST_LOG_TRIVIAL(warning) << logPrefix_ << "Could not find projection"; + } + + p->textColorLocation_ = gl.glGetUniformLocation(id(), "textColor"); + if (p->textColorLocation_ == -1) + { + BOOST_LOG_TRIVIAL(warning) << logPrefix_ << "Could not find textColor"; + } + + gl.glGenVertexArrays(1, &p->vao_); + gl.glGenBuffers(1, &p->vbo_); + gl.glBindVertexArray(p->vao_); + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_); + gl.glBufferData( + GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, nullptr, GL_DYNAMIC_DRAW); + gl.glEnableVertexAttribArray(0); + gl.glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0); + gl.glBindBuffer(GL_ARRAY_BUFFER, 0); + gl.glBindVertexArray(0); + + return success; +} + +void TextShader::RenderText(const std::string& text, + float x, + float y, + float scale, + const glm::mat4& projection, + const boost::gil::rgba8_pixel_t& color, + const std::unordered_map& glyphs) +{ + OpenGLFunctions& gl = p->gl_; + + Use(); + + gl.glEnable(GL_BLEND); + gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + gl.glUniformMatrix4fv( + p->projectionLocation_, 1, GL_FALSE, glm::value_ptr(projection)); + gl.glUniform4f( + p->textColorLocation_, color[0], color[1], color[2], color[3]); + + gl.glActiveTexture(GL_TEXTURE0); + gl.glBindVertexArray(p->vao_); + + for (auto c = text.cbegin(); c != text.cend(); c++) + { + if (glyphs.find(*c) == glyphs.end()) + { + continue; + } + + const util::Glyph& g = glyphs.at(*c); + + float xpos = x + g.bearing.x * scale; + float ypos = y - (g.size.y - g.bearing.y) * scale; + + float w = g.size.x * scale; + float h = g.size.y * scale; + + // Glyph vertices + float vertices[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}}; + + // Render glyph texture + gl.glBindTexture(GL_TEXTURE_2D, g.textureId); + + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_); + gl.glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); + gl.glBindBuffer(GL_ARRAY_BUFFER, 0); + + gl.glDrawArrays(GL_TRIANGLES, 0, 6); + + // Advance to the next glyph + x += (g.advance >> 6) * scale; + } +} + +void TextShader::SetProjection(const glm::mat4& projection) +{ + p->gl_.glUniformMatrix4fv( + p->projectionLocation_, 1, GL_FALSE, glm::value_ptr(projection)); +} + +void TextShader::SetTextColor(const boost::gil::rgba8_pixel_t color) +{ + p->gl_.glUniform4f( + p->textColorLocation_, color[0], color[1], color[2], color[3]); +} + +} // namespace gl +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/gl/text_shader.hpp b/scwx-qt/source/scwx/qt/gl/text_shader.hpp new file mode 100644 index 00000000..0e3c5090 --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/text_shader.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace gl +{ + +class TextShaderImpl; + +class TextShader : public ShaderProgram +{ +public: + explicit TextShader(OpenGLFunctions& gl); + ~TextShader(); + + TextShader(const TextShader&) = delete; + TextShader& operator=(const TextShader&) = delete; + + TextShader(TextShader&&) noexcept; + TextShader& operator=(TextShader&&) noexcept; + + bool Initialize(); + void RenderText(const std::string& text, + float x, + float y, + float scale, + const glm::mat4& projection, + const boost::gil::rgba8_pixel_t& color, + const std::unordered_map& glyphs); + void SetProjection(const glm::mat4& projection); + void SetTextColor(const boost::gil::rgba8_pixel_t color); + +private: + std::unique_ptr p; +}; + +} // namespace gl +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/font.cpp b/scwx-qt/source/scwx/qt/util/font.cpp new file mode 100644 index 00000000..e5ce57c1 --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/font.cpp @@ -0,0 +1,179 @@ +#include + +#include +#include + +#include +#include +#include + +#include FT_FREETYPE_H + +namespace scwx +{ +namespace qt +{ +namespace util +{ + +static const std::string logPrefix_ = "[scwx::qt::util::font] "; + +static std::unordered_map> fontMap_; + +static FT_Library ft_ {nullptr}; +static std::mutex ftMutex_; + +static bool InitializeFreeType(); + +class FontImpl +{ +public: + explicit FontImpl(const std::string& resource) : + resource_(resource), fontData_(), face_ {nullptr} + { + } + + ~FontImpl() {} + + const std::string resource_; + + QByteArray fontData_; + FT_Face face_; +}; + +Font::Font(const std::string& resource) : + p(std::make_unique(resource)) +{ +} +Font::~Font() +{ + FT_Done_Face(p->face_); +} + +void Font::GenerateGlyphs(OpenGLFunctions& gl, + std::unordered_map& glyphs, + unsigned int height) +{ + FT_Error error; + FT_Face& face = p->face_; + + // Allow single-byte texture colors + gl.glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + FT_Set_Pixel_Sizes(p->face_, 0, 48); + + for (unsigned char c = 0; c < 128; c++) + { + if (glyphs.find(c) != glyphs.end()) + { + BOOST_LOG_TRIVIAL(warning) << logPrefix_ << "Found glyph " + << static_cast(c) << ", skipping"; + continue; + } + + if ((error = FT_Load_Char(face, c, FT_LOAD_RENDER)) != 0) + { + BOOST_LOG_TRIVIAL(error) << logPrefix_ << "Failed to load glyph " + << static_cast(c) << ": " << error; + continue; + } + + GLuint texture; + gl.glGenTextures(1, &texture); + gl.glBindTexture(GL_TEXTURE_2D, texture); + + gl.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); + + gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glyphs.insert( + {c, + Glyph { + texture, + glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows), + glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top), + face->glyph->advance.x}}); + } +} + +std::shared_ptr Font::Create(const std::string& resource) +{ + std::shared_ptr font = nullptr; + FT_Error error; + + if (!InitializeFreeType()) + { + return font; + } + + auto it = fontMap_.find(resource); + if (it != fontMap_.end()) + { + return it->second; + } + + QFile fontFile(resource.c_str()); + fontFile.open(QIODevice::ReadOnly); + if (!fontFile.isOpen()) + { + BOOST_LOG_TRIVIAL(error) + << logPrefix_ << "Could not read font file: " << resource; + return font; + } + + font = std::make_shared(resource); + font->p->fontData_ = fontFile.readAll(); + + { + std::scoped_lock(ftMutex_); + if ((error = FT_New_Memory_Face( + ft_, + reinterpret_cast(font->p->fontData_.data()), + font->p->fontData_.size(), + 0, + &font->p->face_)) != 0) + { + BOOST_LOG_TRIVIAL(error) + << logPrefix_ << "Failed to load font: " << error; + font.reset(); + } + } + + if (font != nullptr) + { + fontMap_.insert({resource, font}); + } + + return font; +} + +static bool InitializeFreeType() +{ + std::scoped_lock(ftMutex_); + + FT_Error error; + + if (ft_ == nullptr && (error = FT_Init_FreeType(&ft_)) != 0) + { + BOOST_LOG_TRIVIAL(error) + << logPrefix_ << "Could not init FreeType library: " << error; + ft_ = nullptr; + } + + return (ft_ != nullptr); +} + +} // namespace util +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/font.hpp b/scwx-qt/source/scwx/qt/util/font.hpp new file mode 100644 index 00000000..0c123e79 --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/font.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include + +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace util +{ + +struct Glyph +{ + GLuint textureId; + glm::ivec2 size; // pixels + glm::ivec2 bearing; // pixels + GLint advance; // 1/64 pixels +}; + +class FontImpl; + +class Font +{ +public: + explicit Font(const std::string& resource); + ~Font(); + + Font(const Font&) = delete; + Font& operator=(const Font&) = delete; + + Font(Font&&) = delete; + Font& operator=(Font&&) = delete; + + void GenerateGlyphs(OpenGLFunctions& gl, + std::unordered_map& glyphs, + unsigned int height); + + static std::shared_ptr Create(const std::string& resource); + +private: + std::unique_ptr p; +}; + +} // namespace util +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/shader_program.cpp b/scwx-qt/source/scwx/qt/util/shader_program.cpp index 1b707360..a69578c1 100644 --- a/scwx-qt/source/scwx/qt/util/shader_program.cpp +++ b/scwx-qt/source/scwx/qt/util/shader_program.cpp @@ -147,7 +147,7 @@ bool ShaderProgram::Load(const std::string& vertexPath, gl.glDeleteShader(vertexShader); gl.glDeleteShader(fragmentShader); - return false; + return success; } void ShaderProgram::Use() const diff --git a/scwx-qt/source/scwx/qt/util/shader_program.hpp b/scwx-qt/source/scwx/qt/util/shader_program.hpp index a10a6f12..b05524ca 100644 --- a/scwx-qt/source/scwx/qt/util/shader_program.hpp +++ b/scwx-qt/source/scwx/qt/util/shader_program.hpp @@ -20,7 +20,7 @@ class ShaderProgram { public: explicit ShaderProgram(OpenGLFunctions& gl); - ~ShaderProgram(); + virtual ~ShaderProgram(); ShaderProgram(const ShaderProgram&) = delete; ShaderProgram& operator=(const ShaderProgram&) = delete;