diff options
-rw-r--r-- | Jamroot.jam | 5 | ||||
-rw-r--r-- | gfx/gl/shaders/uiShaderFont.fs | 12 | ||||
-rw-r--r-- | gfx/gl/uiShader.cpp | 30 | ||||
-rw-r--r-- | gfx/gl/uiShader.h | 6 | ||||
-rw-r--r-- | iwyu.json | 16 | ||||
-rw-r--r-- | lib/unicode.c | 13 | ||||
-rw-r--r-- | lib/unicode.h | 20 | ||||
-rw-r--r-- | test/Jamfile.jam | 1 | ||||
-rw-r--r-- | test/test-text.cpp | 91 | ||||
-rw-r--r-- | ui/font.cpp | 182 | ||||
-rw-r--r-- | ui/font.h | 48 | ||||
-rw-r--r-- | ui/text.cpp | 59 | ||||
-rw-r--r-- | ui/text.h | 31 | ||||
-rw-r--r-- | ui/toolbar.cpp | 2 | ||||
-rw-r--r-- | ui/window.cpp | 2 |
15 files changed, 508 insertions, 10 deletions
diff --git a/Jamroot.jam b/Jamroot.jam index ce4b342..d4476e3 100644 --- a/Jamroot.jam +++ b/Jamroot.jam @@ -10,6 +10,8 @@ import sequence ; pkg-config.import sdl2 ; pkg-config.import glew ; +pkg-config.import freetype2 ; +pkg-config.import glib-2.0 ; lib pthread ; variant coverage : debug ; @@ -72,6 +74,7 @@ lib ilt : <include>. <include>lib <link>static + <use>glib-2.0 <cflags>-fPIC ] : @@ -96,6 +99,8 @@ lib ilt : <include>lib <library>sdl2 <library>glew + <library>freetype2 + <library>glib-2.0 <library>pthread : : <include>. diff --git a/gfx/gl/shaders/uiShaderFont.fs b/gfx/gl/shaders/uiShaderFont.fs new file mode 100644 index 0000000..bee455d --- /dev/null +++ b/gfx/gl/shaders/uiShaderFont.fs @@ -0,0 +1,12 @@ +#version 130 + +in vec2 texCoord0; + +uniform sampler2D sampler; +uniform vec3 colour; + +void +main() +{ + gl_FragColor = vec4(colour, texture(sampler, texCoord0).r); +} diff --git a/gfx/gl/uiShader.cpp b/gfx/gl/uiShader.cpp index 1c2a0e1..fbcdd15 100644 --- a/gfx/gl/uiShader.cpp +++ b/gfx/gl/uiShader.cpp @@ -2,20 +2,36 @@ #include <gfx/gl/glSource.h> #include <gfx/gl/programHandle.h> #include <gfx/gl/shaders/fs-uiShader.h> +#include <gfx/gl/shaders/fs-uiShaderFont.h> #include <gfx/gl/shaders/vs-uiShader.h> #include <glm/glm.hpp> #include <glm/gtc/type_ptr.hpp> +#include <initializer_list> -UIShader::UIShader(size_t width, size_t height) : program {uiShader_vs.compile(), uiShader_fs.compile()} +UIShader::UIShader(size_t width, size_t height) : + progDefault {uiShader_vs.compile(), uiShader_fs.compile()}, progText {uiShader_vs.compile(), + uiShaderFont_fs.compile()} { - if (auto loc = glGetUniformLocation(program.m_program, "uiProjection"); loc >= 0) { - glUseProgram(program.m_program); - const auto uiProjection = glm::ortho<float>(0, static_cast<float>(width), 0, static_cast<float>(height)); - glUniformMatrix4fv(loc, 1, GL_FALSE, glm::value_ptr(uiProjection)); + for (const auto prog : {&progDefault, &progText}) { + if (auto loc = glGetUniformLocation(prog->m_program, "uiProjection"); loc >= 0) { + glUseProgram(prog->m_program); + const auto uiProjection = glm::ortho<float>(0, static_cast<float>(width), 0, static_cast<float>(height)); + glUniformMatrix4fv(loc, 1, GL_FALSE, glm::value_ptr(uiProjection)); + } } } + +void +UIShader::useDefault() const +{ + glUseProgram(progDefault.m_program); +} + void -UIShader::use() const +UIShader::useText(glm::vec3 colour) const { - glUseProgram(program.m_program); + glUseProgram(progText.m_program); + if (auto loc = glGetUniformLocation(progText.m_program, "colour"); loc >= 0) { + glUniform3fv(loc, 1, glm::value_ptr(colour)); + } } diff --git a/gfx/gl/uiShader.h b/gfx/gl/uiShader.h index 45ccc23..502ba13 100644 --- a/gfx/gl/uiShader.h +++ b/gfx/gl/uiShader.h @@ -4,17 +4,19 @@ #include "programHandle.h" #include <GL/glew.h> #include <cstddef> +#include <glm/glm.hpp> class UIShader { public: UIShader(std::size_t width, std::size_t height); - void use() const; + void useDefault() const; + void useText(glm::vec3) const; private: class UIProgramHandle : public ProgramHandleBase { using ProgramHandleBase::ProgramHandleBase; }; - UIProgramHandle program; + UIProgramHandle progDefault, progText; }; #endif @@ -17,6 +17,14 @@ }, { "include": [ + "@\"freetype/.*\"", + "private", + "<freetype/freetype.h>", + "public" + ] + }, + { + "include": [ "@<glm/detail/.*>", "private", "<glm/glm.hpp>", @@ -121,6 +129,14 @@ }, { "symbol": [ + "FT_FREETYPE_H", + "private", + "<ft2build.h>", + "public" + ] + }, + { + "symbol": [ "std::abs", "private", "<cmath>", diff --git a/lib/unicode.c b/lib/unicode.c new file mode 100644 index 0000000..b52431f --- /dev/null +++ b/lib/unicode.c @@ -0,0 +1,13 @@ +#include "unicode.h" +#include <glib.h> + +const char * +next_char(const char * c) +{ + return g_utf8_next_char(c); +} +uint32_t +get_codepoint(const char * c) +{ + return g_utf8_get_char(c); +} diff --git a/lib/unicode.h b/lib/unicode.h new file mode 100644 index 0000000..8bd841b --- /dev/null +++ b/lib/unicode.h @@ -0,0 +1,20 @@ +#ifndef UNICODE_H +#define UNICODE_H + +// Wrappers of some glib functions (why are we using glib then?) which we want, but glib.h is a bit C like + +#ifdef __cplusplus +# include <cstdint> +extern "C" { +#else +# include <stdint.h> +#endif + +const char * next_char(const char *); +uint32_t get_codepoint(const char *); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/test/Jamfile.jam b/test/Jamfile.jam index 82ca894..f8987d0 100644 --- a/test/Jamfile.jam +++ b/test/Jamfile.jam @@ -24,3 +24,4 @@ run test-obj.cpp ; run test-maths.cpp ; run test-network.cpp ; run test-persistence.cpp : -- : [ sequence.insertion-sort [ glob fixtures/json/*.json fixtures/json/bad/*.json ] ] ; +run test-text.cpp ; diff --git a/test/test-text.cpp b/test/test-text.cpp new file mode 100644 index 0000000..93a831e --- /dev/null +++ b/test/test-text.cpp @@ -0,0 +1,91 @@ +#define BOOST_TEST_MODULE test_text + +#include "test-helpers.hpp" +#include <boost/test/data/test_case.hpp> +#include <boost/test/unit_test.hpp> +#include <stream_support.hpp> + +#include <ui/font.h> + +struct FontTest : public Font { + FontTest() : Font {"/usr/share/fonts/corefonts/arial.ttf", 48} { } +}; + +BOOST_TEST_DONT_PRINT_LOG_VALUE(Font::CharData); + +using TextureSizeTestData = std::tuple<unsigned, unsigned, unsigned>; +BOOST_DATA_TEST_CASE(fontTextureSize, boost::unit_test::data::make<unsigned>({2, 3, 10, 50, 250}), fontHeight) +{ + auto isPowerOfTwo = [](auto x) { + return (x & (x - 1)) == 0; + }; + const auto res = Font::getTextureSize(fontHeight); + // Power of 2 dimensions... + BOOST_CHECK(isPowerOfTwo(res.x)); + BOOST_CHECK(isPowerOfTwo(res.y)); + // No bigger than max texture size... + BOOST_CHECK_LE(res.x, GL_MAX_TEXTURE_SIZE); + BOOST_CHECK_LE(res.y, GL_MAX_TEXTURE_SIZE); + // Big enough to hold the raster... + BOOST_CHECK_GE(res.y, 8); // Sensible minimum + BOOST_CHECK_GE(res.y, fontHeight); + // Keep the requested size + BOOST_CHECK_EQUAL(res.z, fontHeight); +} + +BOOST_FIXTURE_TEST_SUITE(ft, FontTest); + +BOOST_AUTO_TEST_CASE(initialize_chardata) +{ + BOOST_CHECK_GE(charsData.size(), 72); + BOOST_CHECK_EQUAL(fontTextures.size(), 2); +} + +using CharDataTest = std::tuple<char, Font::CharData>; +BOOST_DATA_TEST_CASE(initialize_chardata_A, + boost::unit_test::data::make<CharDataTest>({ + {'A', {0, {34, 35}, {627, 0}, {-1, 35}, 32}}, + {'I', {0, {6, 35}, {862, 0}, {4, 35}, 13}}, + {'j', {0, {11, 45}, {1656, 0}, {-3, 35}, 11}}, + {'o', {0, {24, 27}, {1748, 0}, {1, 27}, 27}}, + }), + character, expected) +{ + const auto & cd = charsData.at(character); + BOOST_CHECK_EQUAL(cd.textureIdx, expected.textureIdx); + BOOST_CHECK_EQUAL(cd.size, expected.size); + BOOST_CHECK_EQUAL(cd.position, expected.position); + BOOST_CHECK_EQUAL(cd.bearing, expected.bearing); + BOOST_CHECK_EQUAL(cd.advance, expected.advance); +} + +static_assert(glm::vec2 {862, 0} / glm::vec2 {2048, 64} == glm::vec2 {0.4208984375, 0}); +static_assert(glm::vec2 {866, 35} / glm::vec2 {2048, 64} == glm::vec2 {0.4228515625, 0.546875}); + +BOOST_AUTO_TEST_CASE(render_text) +{ + constexpr std::string_view text {"I Like Trains"}; + const auto spaces = std::count_if(text.begin(), text.end(), isspace); + const auto tqs = render(text); + BOOST_REQUIRE_EQUAL(tqs.size(), 1); + const auto & t1 = tqs.begin(); + BOOST_CHECK_EQUAL(t1->first, fontTextures.front().texture); + const auto & v = t1->second; + BOOST_CHECK_EQUAL(v.size(), text.size() - spaces); + + BOOST_TEST_CONTEXT(size) { + // I + BOOST_CHECK_CLOSE_VEC(v[0][0], glm::vec4(4, 0, 0.42, 0.54)); + BOOST_CHECK_CLOSE_VEC(v[0][1], glm::vec4(10, 0, 0.42, 0.54)); + BOOST_CHECK_CLOSE_VEC(v[0][2], glm::vec4(10, 35, 0.42, 0)); + BOOST_CHECK_CLOSE_VEC(v[0][3], glm::vec4(4, 35, 0.42, 0)); + // (space, no glyph) + // L + BOOST_CHECK_CLOSE_VEC(v[1][0], glm::vec4(32, 0, 0.42, 0.54)); + BOOST_CHECK_CLOSE_VEC(v[1][1], glm::vec4(54, 0, 0.42, 0.54)); + BOOST_CHECK_CLOSE_VEC(v[1][2], glm::vec4(54, 35, 0.42, 0)); + BOOST_CHECK_CLOSE_VEC(v[1][3], glm::vec4(32, 35, 0.42, 0)); + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/ui/font.cpp b/ui/font.cpp new file mode 100644 index 0000000..5596f20 --- /dev/null +++ b/ui/font.cpp @@ -0,0 +1,182 @@ +#include "font.h" +#include <algorithm> +#include <cctype> +#include <ft2build.h> +#include FT_FREETYPE_H +#include <glRef.hpp> +#include <maths.h> +#include <memory> +#include <optional> +#include <stdexcept> +#include <unicode.h> +#include <utility> +// IWYU pragma: no_forward_declare FT_LibraryRec_ + +std::string +FT_Error_StringSafe(FT_Error err) +{ + if (const auto errstr = FT_Error_String(err)) { + return {errstr}; + } + return std::to_string(err); +} + +template<auto Func, typename... Args> +void +FT_Check(Args &&... args) +{ + if (const auto err = Func(std::forward<Args>(args)...)) { + throw std::runtime_error {std::string {"FreeType error: "} + FT_Error_StringSafe(err)}; + } +} + +const std::string BASIC_CHARS = []() { + std::string chars; + for (char c {}; c >= 0; c++) { + if (isgraph(c)) { + chars += c; + } + } + return chars + "£€²³"; +}(); + +using FT = glRef<FT_Library, + []() { + FT_Library ft {}; + FT_Check<FT_Init_FreeType>(&ft); + return ft; + }, + FT_Done_FreeType>; + +using Face = glRef<FT_Face, + [](FT_Library ft, const char * const name) { + FT_Face face {}; + FT_Check<FT_New_Face>(ft, name, 0, &face); + return face; + }, + FT_Done_Face>; + +Font::Font(const char * const p, unsigned s) : path {p}, size {getTextureSize(s)} +{ + generateChars(BASIC_CHARS); +} + +void +Font::generateChars(const std::string_view chars) const +{ + std::optional<FT> ft; + std::optional<Face> face; + + for (auto c = chars.data(); c <= &chars.back(); c = next_char(c)) { + const auto codepoint = get_codepoint(c); + if (charsData.find(codepoint) == charsData.end()) { + if (!ft) { + ft.emplace(); + } + if (!face) { + face.emplace(*ft, path.c_str()); + FT_Set_Pixel_Sizes(*face, 0, size.z); + } + FT_UInt glyph_index = FT_Get_Char_Index(*face, codepoint); + if (FT_Load_Glyph(*face, glyph_index, FT_LOAD_RENDER)) { + charsData.emplace(codepoint, CharData {}); + continue; + } + + const auto & glyph = (*face)->glyph; + const auto textureIdx = getTextureWithSpace(glyph->bitmap.width); + auto & texture = fontTextures[textureIdx]; + + glTexSubImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(texture.used), 0, + static_cast<GLsizei>(glyph->bitmap.width), static_cast<GLsizei>(glyph->bitmap.rows), GL_RED, + GL_UNSIGNED_BYTE, glyph->bitmap.buffer); + + const auto & cd = charsData + .emplace(codepoint, + CharData {textureIdx, {glyph->bitmap.width, glyph->bitmap.rows}, + {texture.used, 0}, {glyph->bitmap_left, glyph->bitmap_top}, + glyph->advance.x >> 6}) + .first->second; + texture.used += cd.size.x; + } + } +} + +std::size_t +Font::getTextureWithSpace(unsigned int adv) const +{ + if (auto itr = std::find_if(fontTextures.begin(), fontTextures.end(), + [adv, this](const FontTexture & ft) { + return (ft.used + adv) < size.x; + }); + itr != fontTextures.end()) { + glBindTexture(GL_TEXTURE_2D, itr->texture); + return static_cast<std::size_t>(itr - fontTextures.begin()); + } + + auto & texture = fontTextures.emplace_back(); + glGenTextures(1, &texture.texture); + glBindTexture(GL_TEXTURE_2D, texture.texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, static_cast<GLsizei>(size.x), static_cast<GLsizei>(size.y), 0, GL_RED, + GL_UNSIGNED_BYTE, nullptr); + 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); + + return fontTextures.size() - 1; +} + +glm::uvec3 +Font::getTextureSize(unsigned int height) +{ + auto pow2 = [](unsigned int target) { + unsigned int v {8}; + do { + v <<= 1; + } while (v << 1 < GL_MAX_TEXTURE_SIZE && v < target); + return v; + }; + + constexpr const unsigned int WIDTH_PER_HEIGHT {64}; + return {pow2(height * WIDTH_PER_HEIGHT), pow2(height), height}; +} + +Font::TextureQuads +Font::render(const std::string_view chars) const +{ + constexpr static const std::array<std::pair<glm::vec2, glm::vec2>, 4> C {{ + {{0, 0}, {0, 1}}, + {{1, 0}, {1, 1}}, + {{1, 1}, {1, 0}}, + {{0, 1}, {0, 0}}, + }}; + + generateChars(chars); + + glm::vec2 pos {}; + TextureQuads out; + for (auto c = chars.data(); c <= &chars.back(); c = next_char(c)) { + if (isspace(*c)) { + pos.x += static_cast<float>(size.y) / 4.F; + continue; + } + const auto & ch = charsData.at(get_codepoint(c)); + if (!ch.advance) { + continue; + } + + const auto charPos = pos + glm::vec2 {ch.bearing.x, ch.bearing.y - static_cast<int>(ch.size.y)}; + const auto size = glm::vec2 {ch.size}; + + Quad q; + std::transform(C.begin(), C.end(), q.begin(), [&size, &charPos, &ch, this](const auto & c) { + return (charPos + (size * c.first)) + || ((glm::vec2 {ch.position} + (glm::vec2 {ch.size} * c.second)) / glm::vec2 {this->size}); + }); + out[fontTextures[ch.textureIdx].texture].emplace_back(q); + + pos.x += static_cast<float>(ch.advance); + } + return out; +} diff --git a/ui/font.h b/ui/font.h new file mode 100644 index 0000000..c9d834a --- /dev/null +++ b/ui/font.h @@ -0,0 +1,48 @@ +#ifndef FONT_H +#define FONT_H + +#include <GL/glew.h> +#include <array> +#include <cstddef> +#include <cstdint> +#include <glm/glm.hpp> +#include <map> +#include <string> +#include <string_view> +#include <vector> + +class Font { +public: + Font(const char * const path, unsigned int height); + + using Quad = std::array<glm::vec4, 4>; + using Quads = std::vector<Quad>; + using TextureQuads = std::map<GLuint /*textureId*/, Quads>; + TextureQuads render(const std::string_view text) const; + + struct CharData { + size_t textureIdx; + glm::uvec2 size; + glm::uvec2 position; + glm::ivec2 bearing; + long advance; + }; + struct FontTexture { + GLuint texture; + unsigned int used; + }; + + static glm::uvec3 getTextureSize(unsigned int height); + +protected: + void generateChars(const std::string_view text) const; + const CharData getChar(char) const; + std::size_t getTextureWithSpace(unsigned int adv) const; + + std::string path; + glm::uvec3 size; + mutable std::map<uint32_t, CharData> charsData; + mutable std::vector<FontTexture> fontTextures; +}; + +#endif diff --git a/ui/text.cpp b/ui/text.cpp new file mode 100644 index 0000000..be696c3 --- /dev/null +++ b/ui/text.cpp @@ -0,0 +1,59 @@ +#include "text.h" +#include "font.h" +#include "gfx/gl/uiShader.h" +#include "uiComponent.h" +#include <array> +#include <glBuffers.h> +#include <glVertexArrays.h> +#include <glm/gtc/type_ptr.hpp> +#include <map> +#include <utility> + +const auto font {"/usr/share/fonts/hack/Hack-Regular.ttf"}; +Text::Text(std::string_view s, Position pos, glm::vec3 c) : UIComponent {pos}, colour {c} +{ + for (const auto & textureQuads : Font {font, static_cast<unsigned int>(pos.size.y)}.render(s)) { + auto & rendering + = models.emplace_back(textureQuads.first, static_cast<GLsizei>(6 * textureQuads.second.size())); + glBindVertexArray(rendering.vao); + + glBindBuffer(GL_ARRAY_BUFFER, rendering.vbo); + std::vector<glm::vec4> vertices; + vertices.reserve(6 * textureQuads.second.size()); + for (const auto & quad : textureQuads.second) { + for (auto offset = 0U; offset < 3; offset += 2) { + for (auto vertex = 0U; vertex < 3; vertex += 1) { + vertices.emplace_back(quad[(vertex + offset) % 4] + glm::vec4 {position.origin, 0, 0}); + } + } + }; + glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(sizeof(glm::vec4)) * rendering.count, + glm::value_ptr(vertices.front()), GL_STATIC_DRAW); + + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), nullptr); + + glBindVertexArray(0); + } +} + +void +Text::render(const UIShader & shader, const Position &) const +{ + shader.useText(colour); + for (const auto & m : models) { + glBindTexture(GL_TEXTURE_2D, m.texture); + glBindVertexArray(m.vao); + glDrawArrays(GL_TRIANGLES, 0, m.count); + } + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); +} + +bool +Text::handleInput(const SDL_Event &, const Position &) +{ + return false; +} + +Text::Model::Model(GLuint t, GLsizei c) : texture {t}, count {c} { } diff --git a/ui/text.h b/ui/text.h new file mode 100644 index 0000000..81122de --- /dev/null +++ b/ui/text.h @@ -0,0 +1,31 @@ +#pragma once + +#include "uiComponent.h" +#include <GL/glew.h> +#include <glBuffers.h> +#include <glVertexArrays.h> +#include <glm/glm.hpp> +#include <string_view> +#include <vector> + +class UIShader; +union SDL_Event; + +class Text : public UIComponent { +public: + Text(std::string_view s, Position, glm::vec3 colour); + + void render(const UIShader &, const Position & parentPos) const override; + bool handleInput(const SDL_Event &, const Position & parentPos) override; + +private: + struct Model { + Model(GLuint, GLsizei); + GLuint texture; + GLsizei count; + glVertexArray vao; + glBuffer vbo; + }; + std::vector<Model> models; + glm::vec3 colour; +}; diff --git a/ui/toolbar.cpp b/ui/toolbar.cpp index c8febce..ada97b0 100644 --- a/ui/toolbar.cpp +++ b/ui/toolbar.cpp @@ -1,4 +1,5 @@ #include "toolbar.h" +#include "gfx/gl/uiShader.h" #include "ui/iconButton.h" #include "ui/uiComponent.h" #include "uiComponentPlacer.h" @@ -17,6 +18,7 @@ Toolbar::Toolbar(const std::initializer_list<InitInfo> & initInfo) : UIComponent void Toolbar::render(const UIShader & uiShader, const Position & parentPos) const { + uiShader.useDefault(); const auto absPos = this->position + parentPos; icons.apply(&UIComponent::render, uiShader, absPos); } diff --git a/ui/window.cpp b/ui/window.cpp index 13c7d95..6a263aa 100644 --- a/ui/window.cpp +++ b/ui/window.cpp @@ -13,6 +13,7 @@ SDL_GL_CreateContextAndGlewInit(SDL_Window * w) if (glewInit() != GLEW_OK) {
throw std::runtime_error {"Glew failed to initialize!"};
}
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glEnable(GL_BLEND);
@@ -89,7 +90,6 @@ Window::refresh(const GameState * gameState) const void
Window::render(const GameState *) const
{
- uiShader.use();
glDisable(GL_DEPTH_TEST);
uiComponents.apply(&UIComponent::render, uiShader, UIComponent::Position {});
}
|