This commit is contained in:
2026-06-17 10:35:08 -07:00
parent e3b09170a4
commit 5355a9eded
9 changed files with 1836 additions and 0 deletions

278
src/02_text_description.cpp Normal file
View File

@@ -0,0 +1,278 @@
/*
* 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 <ft2build.h>
#include FT_FREETYPE_H
#include <cstdio>
#include <fstream>
#include <sstream>
/* 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<void*>(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<GLsizei>(ft_face->glyph->bitmap.width),
static_cast<GLsizei>(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<int>(ft_face->glyph->bitmap.width);
ch.size_y = static_cast<int>(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<unsigned int>(ft_face->glyph->advance.x);
characters_[static_cast<char>(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<float>(ch.bearing_x) * scale;
float ypos = y - static_cast<float>(ch.size_y - ch.bearing_y) * scale;
float w = static_cast<float>(ch.size_x) * scale;
float h = static_cast<float>(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<float>(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<float>(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<float>(w);
float fh = static_cast<float>(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<char, glyph_character>::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; }
}