diff --git a/assets/shaders/text.frag b/assets/shaders/text.frag index a4a5658..db8c677 100644 --- a/assets/shaders/text.frag +++ b/assets/shaders/text.frag @@ -1,11 +1,11 @@ #version 330 core -in vec2 TexCoords; +in vec2 TexCoords; +in vec3 ourColor; out vec4 color; uniform sampler2D text; -uniform vec3 textColor; void main() { vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r); - color = vec4(textColor, 1.0) * sampled; + color = vec4(ourColor, 1.0) * sampled; } diff --git a/assets/shaders/text.vert b/assets/shaders/text.vert index 73b96d1..d9d6592 100644 --- a/assets/shaders/text.vert +++ b/assets/shaders/text.vert @@ -1,11 +1,15 @@ #version 330 core -layout(location = 0) in vec4 vertex; +layout (location = 0) in vec2 aPos; +layout (location = 1) in vec2 aTexCoords; +layout (location = 2) in vec3 aColor; out vec2 TexCoords; +out vec3 ourColor; uniform mat4 projection; void main() { - gl_Position = projection * vec4(vertex.xy, 0.0, 1.0); - TexCoords = vertex.zw; + gl_Position = projection * vec4(aPos, 0.0, 1.0); + TexCoords = aTexCoords; + ourColor = aColor; } diff --git a/client/src/game_state.h b/client/src/game_state.h index 318a133..7b032dd 100644 --- a/client/src/game_state.h +++ b/client/src/game_state.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include "gfx/types.h" diff --git a/client/src/gfx/txt_renderer.cpp b/client/src/gfx/txt_renderer.cpp index 7f52680..1ded38d 100644 --- a/client/src/gfx/txt_renderer.cpp +++ b/client/src/gfx/txt_renderer.cpp @@ -17,24 +17,37 @@ TextRenderer::TextRenderer(unsigned int screen_width, unsigned int screen_height _txt_shader->use(); _txt_shader->set_mat4("projection", _projecton); - /* Configure VAO/VBO for texture coords. */ + /* Configure VAO/VBO for batch rendering. */ glGenVertexArrays(1, &_vao); glGenBuffers(1, &_vbo); glBindVertexArray(_vao); glBindBuffer(GL_ARRAY_BUFFER, _vbo); - glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, NULL, GL_DYNAMIC_DRAW); + /* Pre-allocate buffer memory. */ + glBufferData(GL_ARRAY_BUFFER, sizeof(TextVertex) * MAX_VERTICES, nullptr, GL_DYNAMIC_DRAW); + + /* Position attribute. */ glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(TextVertex), (void*)0); + /* Texture coord attribute. */ + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(TextVertex), (void*)offsetof(TextVertex, s)); + /* Colour attribute. */ + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(TextVertex), (void*)offsetof(TextVertex, r)); + glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); + + /* Pre-allocate vector capacity. */ + _vertices.reserve(MAX_VERTICES); } TextRenderer::~TextRenderer(void) { delete _txt_shader; + glDeleteTextures(1, &_atlas_texture_id); } void TextRenderer::load_font(const char* font_path, unsigned int font_size) { - _chars.clear(); FT_Library ft; if(FT_Init_FreeType(&ft)) { printf("Could not init FreeType Library\n"); @@ -50,77 +63,120 @@ void TextRenderer::load_font(const char* font_path, unsigned int font_size) { FT_Set_Pixel_Sizes(face, 0, font_size); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + /* Calculate atlas dimensions. */ + unsigned int atlas_width = 0; + unsigned int atlas_height = 0; + unsigned int max_height = 0; + for(unsigned char c = 0; c < 128; c++) { if(FT_Load_Char(face, c, FT_LOAD_RENDER)) { printf("Failed to load Glyph for char %c\n", c); continue; } - unsigned int texture; - glGenTextures(1, &texture); - glBindTexture(GL_TEXTURE_2D, texture); - 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); - - character ch = { - texture, - { (int)face->glyph->bitmap.width, (int)face->glyph->bitmap.rows }, - { face->glyph->bitmap_left, face->glyph->bitmap_top }, - (unsigned int)face->glyph->advance.x - }; - _chars.insert(std::pair(c, ch)); + atlas_width += face->glyph->bitmap.width; + max_height = std::max(max_height, face->glyph->bitmap.rows); } + + atlas_height = max_height; + + /* Create the atlas texture. */ + glGenTextures(1, &_atlas_texture_id); + glBindTexture(GL_TEXTURE_2D, _atlas_texture_id); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, atlas_width, atlas_height, 0, GL_RED, + GL_UNSIGNED_BYTE, 0); + + 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); + + /* Fill the atlas texture with data. */ + int x_offset = 0; + for(unsigned char c = 0; c < 128; c++) { + if(FT_Load_Char(face, c, FT_LOAD_RENDER)) { + continue; + } + + /* Copy glyph bitmap to atlas. */ + glTexSubImage2D( + GL_TEXTURE_2D, 0, x_offset, 0, + face->glyph->bitmap.width, face->glyph->bitmap.rows, + GL_RED, GL_UNSIGNED_BYTE, face->glyph->bitmap.buffer + ); + + /* Store character metrics and texture coords. */ + _chars[c] = { + {(int)face->glyph->bitmap.width, (int)face->glyph->bitmap.rows }, + {face->glyph->bitmap_left, face->glyph->bitmap_top}, + (unsigned int)face->glyph->advance.x, + (float)x_offset / atlas_width, + 0.0f, + (float)face->glyph->bitmap.width / atlas_width, + (float)face->glyph->bitmap.rows / atlas_height + }; + x_offset += face->glyph->bitmap.width; + } + glBindTexture(GL_TEXTURE_2D, 0); FT_Done_Face(face); FT_Done_FreeType(ft); } -void TextRenderer::render_text(const char* text, float x, float y, float scale, - const Color& color) { +void TextRenderer::begin(void) { + _vertices.clear(); +} + +void TextRenderer::flush(void) { + if(_vertices.empty()) { + return; + } + _txt_shader->use(); - _txt_shader->set_vec3("textColor", color.r, color.g, color.b); glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, _atlas_texture_id); glBindVertexArray(_vao); - for(const char* p = text; *p; p++) { - character ch = _chars[*p]; + glBindBuffer(GL_ARRAY_BUFFER, _vbo); + glBufferSubData(GL_ARRAY_BUFFER, 0, _vertices.size() * sizeof(TextVertex), _vertices.data()); - float xpos = x + ch.bearing[0] * scale; - float ypos = y - (ch.size[1] - ch.bearing[1]) * scale; - float w = ch.size[0] * scale; - float h = ch.size[1] * scale; + glDrawArrays(GL_TRIANGLES, 0, _vertices.size()); - 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 } - }; - - glBindTexture(GL_TEXTURE_2D, ch.texture_id); - glBindBuffer(GL_ARRAY_BUFFER, _vbo); - glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); - glBindBuffer(GL_ARRAY_BUFFER, 0); - glDrawArrays(GL_TRIANGLES, 0, 6); - x += (ch.advance >> 6) * scale; - } + glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); glBindTexture(GL_TEXTURE_2D, 0); } +void TextRenderer::render_text(const char* text, float x, float y, const Color& color) { + for(const char* p = text; *p; p++) { + unsigned char c = *p; + character ch = _chars[c]; + + float xpos = x + ch.bearing[0]; + float ypos = y - (ch.size[1] - ch.bearing[1]); + float w = ch.size[0]; + float h = ch.size[1]; + + _vertices.push_back({xpos, ypos+h, ch.tx, ch.ty, color.r, color.g, color.b}); + _vertices.push_back({xpos, ypos, ch.tx, ch.ty+ch.th, color.r, color.g, color.b}); + _vertices.push_back({xpos+w, ypos, ch.tx+ch.tw, ch.ty+ch.th, color.r, color.g, color.b}); + + _vertices.push_back({xpos, ypos+h, ch.tx, ch.ty, color.r, color.g, color.b}); + _vertices.push_back({xpos+w, ypos, ch.tx+ch.tw, ch.ty+ch.th, color.r, color.g, color.b}); + _vertices.push_back({xpos+w, ypos+h, ch.tx+ch.tw, ch.ty, color.r, color.g, color.b}); + + x += (ch.advance >> 6); + } +} + float TextRenderer::get_text_width(const char* text, float scale) { float width = 0.0f; for(const char* p = text; *p; p++) { - width += (_chars[*p].advance >> 6) * scale; + unsigned char c = *p; + if(c < 128) { + width += (_chars[c].advance >> 6) * scale; + } } return width; } diff --git a/client/src/gfx/txt_renderer.h b/client/src/gfx/txt_renderer.h index 3fd2260..43654bc 100644 --- a/client/src/gfx/txt_renderer.h +++ b/client/src/gfx/txt_renderer.h @@ -1,33 +1,47 @@ #pragma once #include +#include #include FT_FREETYPE_H -#include #include "shader.h" #include "types.h" +struct TextVertex { + float x, y, s, t, r, g, b; +}; + /* State of a single charactrer glyph. */ struct character { - unsigned int texture_id; int size[2]; int bearing[2]; unsigned int advance; + float tx, ty; /* x and y offset of glyph in texture atlas. */ + float tw, th; /* width and height of glyph in texture atlas. */ }; +const int MAX_GLYPHS_PER_BATCH = 10000; +const int VERTICES_PER_GLYPH = 6; +const int MAX_VERTICES = MAX_GLYPHS_PER_BATCH * VERTICES_PER_GLYPH; + class TextRenderer { public: TextRenderer(unsigned int screen_width, unsigned int screen_height); ~TextRenderer(void); void load_font(const char* font_path, unsigned int font_size); - void render_text(const char* text, float x, float y, float scale, const Color& color); + void render_text(const char* text, float x, float y, const Color& color); float get_text_width(const char* text, float scale); + void begin(void); + void flush(void); + private: Shader* _txt_shader; unsigned int _vao, _vbo; - std::map _chars; + unsigned int _atlas_texture_id; + character _chars[128]; + std::vector _vertices; float _projecton[16]; }; diff --git a/client/src/terminal.cpp b/client/src/terminal.cpp index ebb922a..9635b18 100644 --- a/client/src/terminal.cpp +++ b/client/src/terminal.cpp @@ -7,7 +7,6 @@ #include "terminal.h" #include "client_network.h" -#include "gfx/txt_renderer.h" #include "gfx/types.h" #include "ui/window_action.h" @@ -118,6 +117,8 @@ void Terminal::render(const RenderContext& context, int x, int y_screen, int y_g float line_height = 20.0f; float padding = 5.0f; + context.ui_renderer->begin_text(); + /* Enable scissor test to clip rendering to the window content area. */ glEnable(GL_SCISSOR_TEST); glScissor(x, y_gl, width, height); @@ -144,4 +145,6 @@ void Terminal::render(const RenderContext& context, int x, int y_screen, int y_g /* Disable scissor test. */ glDisable(GL_SCISSOR_TEST); + + context.ui_renderer->flush_text(); } diff --git a/client/src/ui/desktop.cpp b/client/src/ui/desktop.cpp index 58e9153..2d3eeb3 100644 --- a/client/src/ui/desktop.cpp +++ b/client/src/ui/desktop.cpp @@ -196,21 +196,28 @@ UIWindow* Desktop::get_focused_window(void) { } void Desktop::render(const RenderContext& context) { + /* Pass 1: Background. */ + context.ui_renderer->begin_text(); _render_wallpaper(context.ui_renderer); - if(_launcher_is_open) { - _launcher->render(context.ui_renderer); - } - _taskbar->render(context.ui_renderer, _windows, _focused_window); - /* Render non-focused windows first. */ + context.ui_renderer->flush_text(); + + /* Pass 2: Windows (back to front). */ for(const auto& win : _windows) { if(win.get() != _focused_window && !win->is_minimized()) { win.get()->render(context); } } - /* Render focused window last so it's on top. */ if(_focused_window && !_focused_window->is_minimized()) { _focused_window->render(context); } + + /* Pass 3: Top-level static UI (taskbar, Launcher, etc.). */ + context.ui_renderer->begin_text(); + if(_launcher_is_open) { + _launcher->render(context.ui_renderer); + } + _taskbar->render(context.ui_renderer, _windows, _focused_window); + context.ui_renderer->flush_text(); } void Desktop::_render_wallpaper(UIRenderer* ui_renderer) { diff --git a/client/src/ui/editor.cpp b/client/src/ui/editor.cpp index 714c3a4..44d437c 100644 --- a/client/src/ui/editor.cpp +++ b/client/src/ui/editor.cpp @@ -52,8 +52,20 @@ void Editor::render(const RenderContext& context, int x, int y_screen, int y_gl, int content_y = y_screen + menu_bar_height; int content_height = height - menu_bar_height; + /* + * Render editor in two passes to ensure the dropdown + * menu appears on top of the main text view. + */ + /* Pass 1: Main Content. */ + context.ui_renderer->begin_text(); + _menu_bar->render_bar(context.ui_renderer, x, y_screen, width); _view->render(context.ui_renderer, x, content_y, width, content_height, context.show_cursor); - _menu_bar->render(context.ui_renderer, x, y_screen, width); + context.ui_renderer->flush_text(); + + /* Pass 2: Dropdown Menu. */ + context.ui_renderer->begin_text(); + _menu_bar->render_dropdown(context.ui_renderer, x, y_screen, width); + context.ui_renderer->flush_text(); } void Editor::scroll(int amount, int content_height) { diff --git a/client/src/ui/main_menu.cpp b/client/src/ui/main_menu.cpp index 1319315..a5f8e38 100644 --- a/client/src/ui/main_menu.cpp +++ b/client/src/ui/main_menu.cpp @@ -95,7 +95,7 @@ Screen MainMenu::update(void) { } void MainMenu::render(UIRenderer* ui_renderer) { - _render_background(ui_renderer->get_text_renderer()); + _render_background(ui_renderer); /* Button colours. */ const Color button_color = { 0.1f, 0.15f, 0.2f }; @@ -130,10 +130,9 @@ void MainMenu::_update_background(void) { } } -void MainMenu::_render_background(TextRenderer* txt_renderer) { +void MainMenu::_render_background(UIRenderer* ui_renderer) { const Color background_text_color = { 0.0f, 0.35f, 0.15f }; /* Dark green. */ for(const auto& line : _background_text) { - txt_renderer->render_text(line.text.c_str(), line.x, line.y, 1.0f, - background_text_color); + ui_renderer->render_text(line.text.c_str(), line.x, line.y, background_text_color); } } diff --git a/client/src/ui/main_menu.h b/client/src/ui/main_menu.h index 2671b3f..9226934 100644 --- a/client/src/ui/main_menu.h +++ b/client/src/ui/main_menu.h @@ -30,7 +30,7 @@ public: private: void _update_background(void); - void _render_background(TextRenderer* txdt_renderer); + void _render_background(UIRenderer* ui_renderer); /* For animated background. */ struct ScrollingText { diff --git a/client/src/ui/menu_bar.cpp b/client/src/ui/menu_bar.cpp index fca7d64..4c1b0ec 100644 --- a/client/src/ui/menu_bar.cpp +++ b/client/src/ui/menu_bar.cpp @@ -72,10 +72,9 @@ void MenuBar::handle_event(SDL_Event* event, int window_x, int window_y) { } } -void MenuBar::render(UIRenderer* ui_renderer, int x, int y, int width) { +void MenuBar::render_bar(UIRenderer* ui_renderer, int x, int y, int width) { const Color bg_color = {0.15f, 0.17f, 0.19f}; const Color text_color = {0.9f, 0.9f, 0.9f}; - const Color hover_color = {0.3f, 0.32f, 0.34f}; ui_renderer->draw_rect(x, y, width, _height, bg_color); @@ -84,16 +83,23 @@ void MenuBar::render(UIRenderer* ui_renderer, int x, int y, int width) { int menu_width = 60; ui_renderer->render_text(_menus[i].label.c_str(), menu_x+10, y+20, text_color); - if(_open_menu_index == (int)i) { - int item_y = y + _height; /* Draw items below the bar. */ - int item_height = 30; - int dropdown_width = 150; - for(const auto& item : _menus[i].items) { - ui_renderer->draw_rect(menu_x, item_y, dropdown_width, item_height, bg_color); - ui_renderer->render_text(item.label.c_str(), menu_x+10, item_y+20, text_color); - item_y += item_height; - } - } menu_x += menu_width; } } + +void MenuBar::render_dropdown(UIRenderer* ui_renderer, int x, int y, int width ) { + if(_open_menu_index == -1) return; + + const Color bg_color = { 0.15f, 0.17f, 0.19f }; + const Color text_color = { 0.9f, 0.9f, 0.9f }; + + int menu_x = x + (_open_menu_index*60); + int item_y = y + _height; + int item_height = 30; + int dropdown_width = 150; + for(const auto& item : _menus[_open_menu_index].items) { + ui_renderer->draw_rect(menu_x, item_y, dropdown_width, item_height, bg_color); + ui_renderer->render_text(item.label.c_str(), menu_x+10, item_y+20, text_color); + item_y += item_height; + } +} diff --git a/client/src/ui/menu_bar.h b/client/src/ui/menu_bar.h index 4cf24f6..06ed1ae 100644 --- a/client/src/ui/menu_bar.h +++ b/client/src/ui/menu_bar.h @@ -28,7 +28,8 @@ public: std::function action); void handle_event(SDL_Event* event, int window_x, int window_y); - void render(UIRenderer* ui_renderer, int x, int y, int width); + void render_bar(UIRenderer* ui_renderer, int x, int y, int width); + void render_dropdown(UIRenderer* ui_renderer, int x, int y, int width); int get_height(void) const; private: diff --git a/client/src/ui/ui_renderer.cpp b/client/src/ui/ui_renderer.cpp index 405e872..d5d4a5b 100644 --- a/client/src/ui/ui_renderer.cpp +++ b/client/src/ui/ui_renderer.cpp @@ -25,7 +25,17 @@ void UIRenderer::render_text(const char* text, int x, int y, const Color& color) if(!_txt_renderer) return; /* Convert the screen-space baseline y-coord to GL-space baseline y-coord. */ int y_gl = _screen_height - y; - _txt_renderer->render_text(text, x, y_gl, 1.0f, color); + _txt_renderer->render_text(text, x, y_gl, color); +} + +void UIRenderer::begin_text(void) { + if(!_txt_renderer) return; + _txt_renderer->begin(); +} + +void UIRenderer::flush_text(void) { + if(!_txt_renderer) return; + _txt_renderer->flush(); } TextRenderer* UIRenderer::get_text_renderer(void) { diff --git a/client/src/ui/ui_renderer.h b/client/src/ui/ui_renderer.h index a638a4a..85bf8e5 100644 --- a/client/src/ui/ui_renderer.h +++ b/client/src/ui/ui_renderer.h @@ -17,6 +17,9 @@ public: void draw_triangle(int x1, int y1, int x2, int y2, int x3, int y3, const Color& color); void render_text(const char* text, int x, int y, const Color& color); + void begin_text(void); + void flush_text(void); + /* Expose underlying text renderer for things like width calculation. */ TextRenderer* get_text_renderer(void); diff --git a/client/src/ui/ui_window.cpp b/client/src/ui/ui_window.cpp index 311f3ee..3ee948c 100644 --- a/client/src/ui/ui_window.cpp +++ b/client/src/ui/ui_window.cpp @@ -79,6 +79,8 @@ bool UIWindow::is_point_inside(int x, int y) { void UIWindow::render(const RenderContext& context) { int title_bar_height = 30; + context.ui_renderer->begin_text(); + /* Define colours. */ const Color frame_color = { 0.2f, 0.2f, 0.25f }; const Color title_bar_color = { 0.15f, 0.15f, 0.2f }; @@ -115,6 +117,8 @@ void UIWindow::render(const RenderContext& context) { context.ui_renderer->draw_triangle(corner_x, corner_y - 10, corner_x - 10, corner_y, corner_x, corner_y, resize_handle_color); + context.ui_renderer->flush_text(); + if(_content) { int content_screen_y = _y + title_bar_height; int content_height = _height - title_bar_height;