diff options
Diffstat (limited to 'src/client/render/render.cc')
| -rw-r--r-- | src/client/render/render.cc | 602 |
1 files changed, 602 insertions, 0 deletions
diff --git a/src/client/render/render.cc b/src/client/render/render.cc new file mode 100644 index 0000000..18aa32a --- /dev/null +++ b/src/client/render/render.cc @@ -0,0 +1,602 @@ +#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", 0, 0, 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); +} + +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<glm::vec3, 8> 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<unsigned, 24> 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<glm::vec2, 4> 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<unsigned, 6> 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<char, character_info> 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<int>(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<unsigned>(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<unsigned>(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<unsigned long>(c), + flags | FT_LOAD_RENDER)) { + return this->operator[]('?'); // placeholder + } + + const int width = static_cast<int>(face->glyph->bitmap.width); + const int height = static_cast<int>(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<float>(face->glyph->advance.x >> 6), + .advance_y = static_cast<float>(face->glyph->advance.y >> 6), + .bitmap_width = static_cast<float>(face->glyph->bitmap.width), + .bitmap_height = static_cast<float>(face->glyph->bitmap.rows), + .bitmap_left = static_cast<float>(face->glyph->bitmap_left), + .bitmap_top = static_cast<float>(face->glyph->bitmap_top), + .x_offset = static_cast<float>(this->texture.last_offset.x), + .y_offset = static_cast<float>(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); } + + 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<void*>(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<unsigned, font_atlas> 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<unsigned, unsigned>(size, size)); + return emplace.first->second; + }(); + + const std::vector<vertex> vertices = [&]() { + std::vector<vertex> vertices; + + const float atlas_width = static_cast<float>(atlas.get_width()); + const float atlas_height = static_cast<float>(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<float>(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<glm::vec4, 6> 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(); + + 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<unsigned>( + (now - start) / std::chrono::milliseconds(1)); + }(), + .xwindow = static_cast<float>(get_window_size().x), + .ywindow = static_cast<float>(get_window_size().y)}; + glBindBuffer(GL_UNIFORM_BUFFER, ubo); + glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(upload), &upload); +} + +} // namespace render +} // namespace client |
