#include "render.hh" namespace client { namespace render { static void check_sdl_call(const auto& sdl_call) { if (sdl_call()) { throw std::runtime_error{SDL_GetError()}; } } static void check_sdl_pointer(const void* const ptr) { if (ptr == nullptr) { throw std::runtime_error{SDL_GetError()}; } } SDL_Window* const& get_sdl_window() noexcept { static SDL_Window* const window = []() -> SDL_Window* { check_sdl_call(std::bind(SDL_Init, SDL_INIT_VIDEO)); check_sdl_call(std::bind(SDL_GL_SetAttribute, SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE)); check_sdl_call( std::bind(SDL_GL_SetAttribute, SDL_GL_CONTEXT_MAJOR_VERSION, 4)); check_sdl_call( std::bind(SDL_GL_SetAttribute, SDL_GL_CONTEXT_MINOR_VERSION, 2)); SDL_Window* const ret = SDL_CreateWindow("blockgame_linux", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 0, 0, SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN_DESKTOP | SDL_WINDOW_BORDERLESS); check_sdl_pointer(ret); return ret; }(); return window; } static SDL_GLContext& get_sdl_glcontext() noexcept { static SDL_GLContext context = []() -> SDL_GLContext { const SDL_GLContext ret = SDL_GL_CreateContext(get_sdl_window()); check_sdl_pointer(ret); check_sdl_call(std::bind(SDL_GL_SetSwapInterval, 0)); check_sdl_call(std::bind(SDL_SetRelativeMouseMode, SDL_TRUE)); // Default gl attributes. glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); glEnable(GL_BLEND); glEnable(GL_DEBUG_OUTPUT); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDebugMessageCallback( [](GLenum, GLenum, GLuint, GLenum severity, GLsizei, const GLchar* message, const void*) { if (severity == GL_DEBUG_SEVERITY_NOTIFICATION) { return; } throw std::runtime_error{std::string{"gl severe error: "} + message}; }, nullptr); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // anti ub first frame return ret; }(); return context; } // When we support window resizing, just make this non const etc. const glm::ivec2& get_window_size() noexcept { static glm::ivec2 window_size = []() -> glm::ivec2 { int width, height; SDL_GetWindowSize(get_sdl_window(), &width, &height); return {width, height}; }(); return window_size; } static int& get_framecount() noexcept { static int framecount = 0; return framecount; } int get_fps() noexcept { constexpr auto rate = std::chrono::milliseconds(500); // rate of updates static auto prev = std::chrono::steady_clock::now(); static int fps = []() { get_framecount() = 0; // so we don't report absurdly high numbers @ init return 0; }(); if (const auto now = std::chrono::steady_clock::now(); now >= prev + rate) { const auto ms_elapsed = (now - prev) / std::chrono::milliseconds(1); int& framecount = get_framecount(); fps = int(float(framecount) * (1000.0f / float(ms_elapsed))); framecount = 0; prev = now; } return fps; } void init() { // Creates the glcontext, traverses the tree of sdl calls required for our // window to initialise. get_sdl_glcontext(); } void quit() noexcept { // Epoxy doesn't like this, so we're commenting it out for now. /* SDL_GL_DeleteContext(get_sdl_glcontext()); SDL_DestroyWindow(get_sdl_window()); SDL_Quit(); */ } void swap_window() noexcept { SDL_GL_SwapWindow(get_sdl_window()); ++get_framecount(); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } // Additional sanity checks for our atlas. static void check_atlas(const client::render::texture& texture) { if (texture.width % 6) { throw std::runtime_error("invalid atlas; WIDTH is not divisible by 6"); } if (texture.height % (texture.width / 6)) { throw std::runtime_error( "invalid atlas, HEIGHT is not divisible by (WIDTH / 6)"); } } GLuint get_texture_atlas() noexcept { static const auto atlas = []() -> GLuint { GLuint texture = 0; glActiveTexture(GL_TEXTURE1); glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D_ARRAY, texture); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_ANISOTROPY, 16.0f); const client::render::texture stbi{"res/textures/atlas.png"}; check_atlas(stbi); const int face_size = stbi.width / 6; // 2D texture array, where our depth is our block face. glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, face_size, face_size, 6 * (stbi.height / face_size), 0, stbi.channels == 3 ? GL_RGB : GL_RGBA, GL_UNSIGNED_BYTE, nullptr); // Fill the 2D texture array. // Because our image has multiple images on the x-axis and opengl // expects a single image per axis, we must fill it in row by row. const auto get_pixel_xy = [&stbi](const int x, const int y) { return stbi.image + 4 * (y * stbi.width + x); }; for (int x = 0; x < 6; ++x) { const int x_pixel = x * face_size; for (int y = 0; y < stbi.height / face_size; ++y) { const int y_pixel = y * face_size; for (auto row = 0; row < face_size; ++row) { glTexSubImage3D( GL_TEXTURE_2D_ARRAY, 0, 0, row, x + y * 6, face_size, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, get_pixel_xy(x_pixel, row + y_pixel)); // pixel } } } glGenerateMipmap(GL_TEXTURE_2D_ARRAY); return texture; }(); return atlas; } void render_cube_outline(const glm::vec3& pos, const glm::vec4& colour) noexcept { const auto generate_vbo = [](const auto& vertices) -> GLuint { GLuint vbo = 0; glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), std::data(vertices), GL_STATIC_DRAW); return vbo; }; const auto generate_vao = []() -> GLuint { GLuint vao = 0; glGenVertexArrays(1, &vao); glBindVertexArray(vao); // position glVertexAttribPointer(0, sizeof(glm::vec3) / sizeof(float), GL_FLOAT, GL_FALSE, sizeof(glm::vec3), nullptr); glEnableVertexAttribArray(0); return vao; }; const auto generate_ebo = [](const auto& indices) -> GLuint { GLuint ebo = 0; glGenBuffers(1, &ebo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), &indices, GL_STATIC_DRAW); return ebo; }; const auto make_matrix = [&]() -> glm::mat4 { constexpr auto identity = glm::mat4(1.0f); const auto proj = camera::get_proj(); const auto trans = glm::translate(identity, pos); const auto& view = camera::get_view(); return proj * view * trans; }; static constexpr std::array vertices = { glm::vec3{-0.5f, -0.5f, -0.5f}, glm::vec3{0.5f, -0.5f, -0.5f}, glm::vec3{-0.5f, -0.5f, 0.5f}, glm::vec3{0.5f, -0.5f, 0.5f}, glm::vec3{-0.5f, 0.5f, -0.5f}, glm::vec3{0.5f, 0.5f, -0.5f}, glm::vec3{-0.5f, 0.5f, 0.5f}, glm::vec3{0.5f, 0.5f, 0.5f}}; static constexpr std::array indices = { 0, 1, 0, 2, 0, 4, 6, 4, 6, 7, 6, 2, 5, 4, 5, 1, 5, 7, 3, 2, 3, 1, 3, 7}; static const program program{"res/shaders/line.vs", "res/shaders/line.fs"}; static const GLuint vbo [[maybe_unused]] = generate_vbo(vertices); static const GLuint vao = generate_vao(); static const GLuint ebo [[maybe_unused]] = generate_ebo(indices); static const GLint u_colour = glGetUniformLocation(program, "_u_colour"); static const GLint u_matrix = glGetUniformLocation(program, "_u_matrix"); glEnable(GL_BLEND); glUseProgram(program); glBindVertexArray(vao); glUniform4fv(u_colour, 1, glm::value_ptr(colour)); glUniformMatrix4fv(u_matrix, 1, GL_FALSE, glm::value_ptr(make_matrix())); glDrawElements(GL_LINES, std::size(indices), GL_UNSIGNED_INT, nullptr); } void render_rectangle(const glm::vec2& pos, const glm::vec2& size, const glm::vec4& colour) noexcept { const auto generate_vbo = [](const auto& vertices) -> GLuint { GLuint vbo = 0; glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), std::data(vertices), GL_STATIC_DRAW); return vbo; }; const auto generate_vao = []() -> GLuint { GLuint vao = 0; glGenVertexArrays(1, &vao); glBindVertexArray(vao); // position glVertexAttribPointer(0, sizeof(glm::vec2) / sizeof(float), GL_FLOAT, GL_FALSE, sizeof(glm::vec2), nullptr); glEnableVertexAttribArray(0); return vao; }; const auto generate_ebo = [](const auto& indices) -> GLuint { GLuint ebo = 0; glGenBuffers(1, &ebo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), &indices, GL_STATIC_DRAW); return ebo; }; const auto make_matrix = [&]() -> glm::mat4 { constexpr auto identity = glm::mat4(1.0f); const glm::vec2& window_size = get_window_size(); const auto proj = glm::ortho(0.f, window_size.x, 0.f, window_size.y); const auto scale = glm::scale(identity, glm::vec3(size.x, size.y, 0.0f)); const auto trans = glm::translate(identity, glm::vec3(pos.x, pos.y, 0.0f)); return proj * trans * scale; }; static constexpr std::array vertices = { glm::vec2{-0.5f, -0.5f}, glm::vec2{0.5f, -0.5f}, glm::vec2{0.5f, 0.5f}, glm::vec2{-0.5f, 0.5f}}; static constexpr std::array indices = {0, 1, 2, 2, 3, 0}; static const program program{"res/shaders/rectangle.vs", "res/shaders/rectangle.fs"}; static const GLuint vbo [[maybe_unused]] = generate_vbo(vertices); static const GLuint vao = generate_vao(); static const GLuint ebo [[maybe_unused]] = generate_ebo(indices); static const GLint u_colour = glGetUniformLocation(program, "_u_colour"); static const GLint u_matrix = glGetUniformLocation(program, "_u_matrix"); glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); glUseProgram(program); glBindVertexArray(vao); glUniform4fv(u_colour, 1, glm::value_ptr(colour)); glUniformMatrix4fv(u_matrix, 1, GL_FALSE, glm::value_ptr(make_matrix())); glDrawElements(GL_TRIANGLES, std::size(indices), GL_UNSIGNED_INT, nullptr); } // http://blog.wolfire.com/2013/03/High-quality-text-rendering // When rendering our fonts, despite what usually occurs in graphics, // downscaling will result in a decrease in visual quality (even if by a small // margin). If we want nice looking fonts, we must NEVER scale it - meaning if // we want a change in size, we will have to generate a new font atlas each time // with a different size. In addition, we are limited only to rendering at // integers. // This class solves this problem by lazily loading characters when necessary, // so we can avoid the runtime penalty of generating a couple thousand chars // in our atlas every time we request a new size. class font_atlas { public: struct character_info { float advance_x; float advance_y; float bitmap_width; float bitmap_height; float bitmap_left; float bitmap_top; float x_offset; // offset in texture atlas. float y_offset; }; struct texture_info { GLuint texture; GLint width; // max width of a row in our atlas GLint height; unsigned cur_max_glyph_height; // max height of a row in our atlas glm::ivec2 last_offset; // where to upload next operator GLuint() const noexcept { return this->texture; } }; private: static constexpr const /*CPP SUCKS*/ char* const font_dir = "res/fonts/NotoSansMono-SemiBold.ttf"; static constexpr auto flags = 0u; // FT_LOAD_TARGET_LIGHT | FT_LOAD_FORCE_AUTOHINT; unsigned font_size; texture_info texture; std::unordered_map characters; private: static FT_Library get_ft_library() noexcept { static FT_Library library = []() -> FT_Library { FT_Library library; if (FT_Init_FreeType(&library)) { throw std::runtime_error("failed to init freetype2"); } return library; }(); return library; } static GLint get_row_size() noexcept { static const GLint row_size = []() -> GLint { GLint row_size; glGetIntegerv(GL_MAX_TEXTURE_SIZE, &row_size); return row_size; }(); return row_size; } // This will set cur_max_glyph_height to size and our last_offset to {0, 0} // so it may be necessary to set them manually. static struct texture_info make_texture_info(const unsigned size) noexcept { GLuint texture; glActiveTexture(GL_TEXTURE0); glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); // Allocate a big strip so that we do not frequently reallocate. const GLint row_size = get_row_size(); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, row_size, size, 0, GL_ALPHA, GL_UNSIGNED_BYTE, nullptr); return {.texture = texture, .width = row_size, .height = static_cast(size), .cur_max_glyph_height = size, .last_offset = {0, 0}}; } private: // Occasionally we will fill the texture atlas too much. This may occur if // we stuff too many characters in and we run out of x, or if a glyph just // happens to be bigger on the y than any other glyph we have inserted. // We must allocate a new texture with the appropriate dimensions that // accomodates for this new glyph while copying over our atlas's previous // contents. void prepare_texture_info(const glm::ivec2 glyph) noexcept { const bool over_x = this->texture.last_offset.x + glyph.x > this->texture.width; const bool over_y = this->texture.last_offset.y + glyph.y > this->texture.height; this->texture.cur_max_glyph_height = std::max( this->texture.cur_max_glyph_height, static_cast(glyph.y)); if (!over_x && !over_y) { return; } const struct texture_info old = this->texture; if (over_x) { this->texture = make_texture_info( static_cast(old.height) + old.cur_max_glyph_height); this->texture.last_offset = {0.0f, old.last_offset.y + old.height}; } else if (over_y) { this->texture = make_texture_info(old.cur_max_glyph_height); this->texture.last_offset = old.last_offset; } this->texture.cur_max_glyph_height = old.cur_max_glyph_height; glCopyImageSubData(old.texture, GL_TEXTURE_2D, 0, 0, 0, 0, this->texture, GL_TEXTURE_2D, 0, 0, 0, 0, old.width, old.height, 1); glDeleteTextures(1, &old.texture); } struct character_info make_character_info(const char c) noexcept { const FT_Library library = this->get_ft_library(); FT_Face face; if (FT_New_Face(library, font_dir, 0, &face)) { throw std::runtime_error("failed to load font"); } FT_Set_Pixel_Sizes(face, 0, this->font_size); if (FT_Load_Char(face, static_cast(c), flags | FT_LOAD_RENDER)) { return this->operator[]('?'); // placeholder } const int width = static_cast(face->glyph->bitmap.width); const int height = static_cast(face->glyph->bitmap.rows); this->prepare_texture_info(glm::ivec2{width, height}); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, this->texture.texture); glTexSubImage2D(GL_TEXTURE_2D, 0, this->texture.last_offset.x, this->texture.last_offset.y, width, height, GL_RED, GL_UNSIGNED_BYTE, face->glyph->bitmap.buffer); character_info character_info = { .advance_x = static_cast(face->glyph->advance.x >> 6), .advance_y = static_cast(face->glyph->advance.y >> 6), .bitmap_width = static_cast(face->glyph->bitmap.width), .bitmap_height = static_cast(face->glyph->bitmap.rows), .bitmap_left = static_cast(face->glyph->bitmap_left), .bitmap_top = static_cast(face->glyph->bitmap_top), .x_offset = static_cast(this->texture.last_offset.x), .y_offset = static_cast(this->texture.last_offset.y)}; this->texture.last_offset.x += width; FT_Done_Face(face); return character_info; } public: // Use this operator to get specific chars. const struct character_info& operator[](const char c) noexcept { if (const auto find_it = this->characters.find(c); find_it != std::end(this->characters)) { return find_it->second; } const auto emplace = this->characters.emplace(c, make_character_info(c)); return emplace.first->second; } font_atlas(const unsigned size) noexcept : font_size(size), texture(make_texture_info(size)) {} ~font_atlas() noexcept { glDeleteTextures(1, &this->texture.texture); } font_atlas(font_atlas&&) = delete; font_atlas(const font_atlas&) = delete; GLint get_height() const noexcept { return this->texture.height; } GLint get_width() const noexcept { return this->texture.width; } unsigned get_glyph_height() const noexcept { return this->texture.cur_max_glyph_height; } GLuint get_texture() const noexcept { return this->texture; } }; void render_text(const std::string_view text, const unsigned int size, const bool is_centered, const bool is_vcentered, const glm::vec4& colour, const glm::mat4& matrix) noexcept { const auto generate_vao = []() -> GLuint { GLuint vao = 0; glGenVertexArrays(1, &vao); glBindVertexArray(vao); return vao; }; struct vertex { glm::vec2 pos; glm::vec2 tex; }; const auto generate_vbo = []() -> GLuint { GLuint vbo = 0; glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 5, nullptr, GL_DYNAMIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, sizeof(glm::vec2) / sizeof(float), GL_FLOAT, GL_FALSE, sizeof(vertex), 0); glEnableVertexAttribArray(1); glVertexAttribPointer(1, sizeof(glm::vec2) / sizeof(float), GL_FLOAT, GL_FALSE, sizeof(vertex), reinterpret_cast(sizeof(glm::vec2))); return vbo; }; static const program program("res/shaders/text.vs", "res/shaders/text.fs"); static const GLuint vao = generate_vao(); static const GLuint vbo = generate_vbo(); static const GLint u_matrix = glGetUniformLocation(program, "_u_matrix"); static const GLint u_colour = glGetUniformLocation(program, "_u_colour"); static std::unordered_map font_atlases; // Find or generate a font atlas in our unordered_map. font_atlas& atlas = [&]() -> font_atlas& { if (const auto find_it = font_atlases.find(size); find_it != std::end(font_atlases)) { return find_it->second; } const auto emplace = font_atlases.emplace(std::pair(size, size)); return emplace.first->second; }(); const std::vector vertices = [&]() { std::vector vertices; const float atlas_width = static_cast(atlas.get_width()); const float atlas_height = static_cast(atlas.get_height()); float total_width = 0.0f; glm::vec3 pos{0.0f}; for (const auto& c : text) { const auto& character = atlas[c]; const float w = character.bitmap_width; const float h = character.bitmap_height; const float x = pos.x + character.bitmap_left; const float y = -pos.y - (character.bitmap_top); pos.x += character.advance_x; pos.y += character.advance_y; total_width += character.advance_x; if (w == 0.0f || h == 0.0f) { continue; } // clang-format off vertices.push_back(vertex{{x , -y }, {character.x_offset / atlas_width , character.y_offset / atlas_height}}); vertices.push_back(vertex{{x , -y - h}, {character.x_offset / atlas_width , (character.y_offset + character.bitmap_height) / atlas_height}}); vertices.push_back(vertex{{x + w, -y }, {(character.x_offset + character.bitmap_width) / atlas_width, character.y_offset / atlas_height}}); vertices.push_back(vertex{{x + w, -y }, {(character.x_offset + character.bitmap_width) / atlas_width, character.y_offset / atlas_height}}); vertices.push_back(vertex{{x , -y - h}, {character.x_offset / atlas_width , (character.y_offset + character.bitmap_height) / atlas_height}}); vertices.push_back(vertex{{x + w, -y - h}, {(character.x_offset + character.bitmap_width) / atlas_width, (character.y_offset + character.bitmap_height) / atlas_height}}); // clang-format on } const float xoffset = is_centered ? std::floor(total_width / 2.0f) : 0.0f; const float yoffset = is_vcentered ? std::floor(static_cast(atlas.get_glyph_height()) / 3.0f) : 0.0f; for (auto& v : vertices) { v.pos.x -= xoffset; v.pos.y -= yoffset; } return vertices; }(); glEnable(GL_BLEND); glDisable(GL_CULL_FACE); glUseProgram(program); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, atlas.get_texture()); glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, std::size(vertices) * sizeof(vertex), std::data(vertices), GL_DYNAMIC_DRAW); glUniform4f(u_colour, colour.x, colour.y, colour.z, colour.w); glUniformMatrix4fv(u_matrix, 1, GL_FALSE, glm::value_ptr(matrix)); glDrawArrays(GL_TRIANGLES, 0, std::size(vertices)); glEnable(GL_CULL_FACE); } // Updates our ebos, we want to call this once per frame. void update_uniforms() noexcept { struct ubo_data { glm::mat4 proj; glm::mat4 view; std::array frustum; float znear; float zfar; float xfov; // IN RADIANS float yfov; // IN RADIANS unsigned time; // resolution of 1000/s float xwindow; float ywindow; }; const auto make_ubo = [&]() -> GLuint { GLuint ubo; glGenBuffers(1, &ubo); glBindBuffer(GL_UNIFORM_BUFFER, ubo); glBufferData(GL_UNIFORM_BUFFER, sizeof(ubo_data), nullptr, GL_DYNAMIC_DRAW); glBindBufferBase(GL_UNIFORM_BUFFER, 0, ubo); // binding @ 0 return ubo; }; static const GLuint ubo = make_ubo(); const ubo_data upload = { .proj = camera::get_proj(), .view = camera::get_view(), .frustum = camera::get_frustum(), .znear = camera::get_znear(), .zfar = camera::get_zfar(), .xfov = glm::radians(camera::get_xfov()), .yfov = glm::radians(camera::get_yfov()), .time = []() -> unsigned { static const auto start = std::chrono::steady_clock::now(); const auto now = std::chrono::steady_clock::now(); return static_cast((now - start) / std::chrono::milliseconds(1)); }(), .xwindow = static_cast(get_window_size().x), .ywindow = static_cast(get_window_size().y)}; glBindBuffer(GL_UNIFORM_BUFFER, ubo); glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(upload), &upload); } void draw_rectangle(const rectangle_args& ra) noexcept { const glm::vec2 pos = ra.pos.to_vec2(); const glm::vec2 size = ra.size.to_vec2(); // We want to render from the bottom left corner. render::render_rectangle(pos + (size / 2.0f), size, ra.colour); } void draw_colour(const glm::vec4& colour) noexcept { const rectangle_args args = { .pos = {.extent = {0.0f, 0.0f}}, .size = {.extent = {1.0f, 1.0f}}, .colour = colour, }; draw_rectangle(args); } void draw_text(const std::string_view text, const text_args& args) noexcept { const glm::vec2& window = render::get_window_size(); const float x = floorf(window.x * args.pos.extent.x + args.pos.offset.x); const float y = floorf(window.y * args.pos.extent.y + args.pos.offset.y); const unsigned height = static_cast( args.extent_height * window.y + args.offset_height); const auto make_matrix = [&](const float xo, const float yo) -> glm::mat4 { constexpr auto identity = glm::mat4(1.0f); const auto proj = glm::ortho(0.f, window.x, 0.f, window.y); const auto tran = glm::translate(identity, glm::vec3{x + xo, y + yo, 0.0f}); return proj * tran; }; glDisable(GL_DEPTH_TEST); glDisable(GL_BLEND); if (args.has_backing) { constexpr glm::vec4 black{0.0f, 0.0f, 0.0f, 1.0f}; render_text(text, height, args.is_centered, args.is_vcentered, black, make_matrix(2.0f, -2.0f)); } glEnable(GL_BLEND); render_text(text, height, args.is_centered, args.is_vcentered, args.colour, make_matrix(0, 0)); glEnable(GL_DEPTH_TEST); } } // namespace render } // namespace client