/* * MIT License * * Copyright (c) 2026 Mark Allyn * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ /* * 02_text_description.cpp * Author: Mark Allyn * Component: 02_text_description * * FreeType-based text renderer. Loads ASCII glyphs into GL_RED textures * and renders them as textured quads into whichever viewport is active. */ #include "02_text_description.h" #include #include FT_FREETYPE_H #include #include #include /* Global singleton used by all components. */ TextRenderer g_text_renderer; /* ------------------------------------------------------------------ */ /* Internal helpers */ /* ------------------------------------------------------------------ */ static bool read_file(const std::string& path, std::string& out) { std::ifstream f(path); if (!f.is_open()) { fprintf(stderr, "[02_text] cannot open shader: %s\n", path.c_str()); return false; } std::ostringstream ss; ss << f.rdbuf(); out = ss.str(); return true; } static GLuint compile_shader_src(const char* source, GLenum type) { GLuint shader = glCreateShader(type); glShaderSource(shader, 1, &source, nullptr); glCompileShader(shader); GLint ok = 0; glGetShaderiv(shader, GL_COMPILE_STATUS, &ok); if (!ok) { char log[512]; glGetShaderInfoLog(shader, 512, nullptr, log); fprintf(stderr, "[02_text] shader compile error:\n%s\n", log); } return shader; } /* ------------------------------------------------------------------ */ /* TextRenderer public methods */ /* ------------------------------------------------------------------ */ bool TextRenderer::initialize_shaders() { std::string vert_src, frag_src; if (!read_file(SHADER_DIR "02_text_description_vert.glsl", vert_src)) return false; if (!read_file(SHADER_DIR "02_text_description_frag.glsl", frag_src)) return false; GLuint vert = compile_shader_src(vert_src.c_str(), GL_VERTEX_SHADER); GLuint frag = compile_shader_src(frag_src.c_str(), GL_FRAGMENT_SHADER); shader_program_ = glCreateProgram(); glAttachShader(shader_program_, vert); glAttachShader(shader_program_, frag); glLinkProgram(shader_program_); GLint ok = 0; glGetProgramiv(shader_program_, GL_LINK_STATUS, &ok); if (!ok) { char log[512]; glGetProgramInfoLog(shader_program_, 512, nullptr, log); fprintf(stderr, "[02_text] shader link error:\n%s\n", log); glDeleteShader(vert); glDeleteShader(frag); return false; } glDeleteShader(vert); glDeleteShader(frag); /* VAO/VBO for one glyph quad: 6 vertices × 4 floats (xy + uv). */ glGenVertexArrays(1, &vao_); glGenBuffers(1, &vbo_); glBindVertexArray(vao_); glBindBuffer(GL_ARRAY_BUFFER, vbo_); glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, nullptr, GL_DYNAMIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), reinterpret_cast(0)); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); /* Bind text_sampler uniform to texture unit 0. */ glUseProgram(shader_program_); glUniform1i(glGetUniformLocation(shader_program_, "text_sampler"), 0); glUseProgram(0); #if DEBUG_SHADER_COMPILE fprintf(stdout, "[02_text] shaders compiled and linked OK\n"); #endif return true; } bool TextRenderer::initialize_font(const std::string& font_path, unsigned int font_size) { FT_Library ft_lib; if (FT_Init_FreeType(&ft_lib)) { fprintf(stderr, "[02_text] FreeType init failed\n"); return false; } FT_Face ft_face; if (FT_New_Face(ft_lib, font_path.c_str(), 0, &ft_face)) { fprintf(stderr, "[02_text] cannot load font: %s\n", font_path.c_str()); FT_Done_FreeType(ft_lib); return false; } FT_Set_Pixel_Sizes(ft_face, 0, font_size); /* Upload each printable ASCII glyph as a GL_RED texture. */ glPixelStorei(GL_UNPACK_ALIGNMENT, 1); for (unsigned char c = 32; c < 127; c++) { if (FT_Load_Char(ft_face, c, FT_LOAD_RENDER)) { fprintf(stderr, "[02_text] failed to load glyph 0x%02x\n", c); continue; } GLuint tex = 0; glGenTextures(1, &tex); glBindTexture(GL_TEXTURE_2D, tex); glTexImage2D( GL_TEXTURE_2D, 0, GL_RED, static_cast(ft_face->glyph->bitmap.width), static_cast(ft_face->glyph->bitmap.rows), 0, GL_RED, GL_UNSIGNED_BYTE, ft_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); glyph_character ch; ch.texture_id = tex; ch.size_x = static_cast(ft_face->glyph->bitmap.width); ch.size_y = static_cast(ft_face->glyph->bitmap.rows); ch.bearing_x = ft_face->glyph->bitmap_left; ch.bearing_y = ft_face->glyph->bitmap_top; ch.advance = static_cast(ft_face->glyph->advance.x); characters_[static_cast(c)] = ch; } glBindTexture(GL_TEXTURE_2D, 0); FT_Done_Face(ft_face); FT_Done_FreeType(ft_lib); fprintf(stdout, "[02_text] font loaded: %s at %u px\n", font_path.c_str(), font_size); return true; } void TextRenderer::render_text(const std::string& text, float x, float y, float scale, float color_r, float color_g, float color_b) { glUseProgram(shader_program_); glUniform3f(glGetUniformLocation(shader_program_, "text_color"), color_r, color_g, color_b); glActiveTexture(GL_TEXTURE0); glBindVertexArray(vao_); for (std::string::const_iterator it = text.cbegin(); it != text.cend(); ++it) { char c = *it; if (characters_.count(c) == 0) { /* Advance by a fixed amount for unknown characters. */ x += 10.0f * scale; continue; } const glyph_character& ch = characters_.at(c); float xpos = x + static_cast(ch.bearing_x) * scale; float ypos = y - static_cast(ch.size_y - ch.bearing_y) * scale; float w = static_cast(ch.size_x) * scale; float h = static_cast(ch.size_y) * scale; if (w > 0.0f && h > 0.0f) { 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); glDrawArrays(GL_TRIANGLES, 0, 6); } x += static_cast(ch.advance >> 6) * scale; } glBindVertexArray(0); glBindTexture(GL_TEXTURE_2D, 0); glUseProgram(0); } float TextRenderer::get_text_width(const std::string& text, float scale) const { float width = 0.0f; for (std::string::const_iterator it = text.cbegin(); it != text.cend(); ++it) { char c = *it; if (characters_.count(c) == 0) { width += 10.0f * scale; continue; } width += static_cast(characters_.at(c).advance >> 6) * scale; } return width; } void TextRenderer::set_projection(int w, int h) { /* Column-major orthographic matrix mapping pixel space [0,w]x[0,h] * to NDC. Near=-1, far=1 (depth unused for 2D text). */ float fw = static_cast(w); float fh = static_cast(h); float proj[16] = { 2.0f / fw, 0.0f, 0.0f, 0.0f, 0.0f, 2.0f / fh, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f }; glUseProgram(shader_program_); glUniformMatrix4fv( glGetUniformLocation(shader_program_, "projection"), 1, GL_FALSE, proj ); glUseProgram(0); } void TextRenderer::cleanup() { for (std::map::iterator it = characters_.begin(); it != characters_.end(); ++it) { glDeleteTextures(1, &it->second.texture_id); } characters_.clear(); if (vao_) { glDeleteVertexArrays(1, &vao_); vao_ = 0; } if (vbo_) { glDeleteBuffers(1, &vbo_); vbo_ = 0; } if (shader_program_) { glDeleteProgram(shader_program_); shader_program_ = 0; } }