/* * See FTGL/ftgl.h. The printable ASCII characters are baked once, at BAKE_SIZE pixels, into a * power-of-two atlas (mipmapped, so text stays smooth when the game scales it down) that becomes a * texture the first time text is drawn, when the game's GL context exists; each * character is drawn as a quad scaled to the face size, with the baseline at y = 0 and y pointing up, * as FTGL does. The game builds without translations, so ASCII covers all of its text. * * Written in 2026 by FredyGames for the fredygames.com build of Chromium B.S.U.; distributed with the * game under the Clarified Artistic License (see COPYING). */ #include "FTGL/ftgl.h" #include #include #include #define STB_TRUETYPE_IMPLEMENTATION #define STBTT_STATIC #include "stb_truetype.h" namespace { const float BAKE_SIZE = 64.0f; const int ATLAS_W = 1024, ATLAS_H = 512, FIRST = 32, COUNT = 96; } FTFont::FTFont(const char *path) : error(1), size(24), fontData(NULL), glyphs(NULL), atlas(NULL), texture(0), lineHeight(BAKE_SIZE) { FILE *file = path ? fopen(path, "rb") : NULL; if (file == NULL) return; fseek(file, 0, SEEK_END); long length = ftell(file); fseek(file, 0, SEEK_SET); fontData = (unsigned char *)malloc(length); const bool ok = fread(fontData, 1, length, file) == (size_t)length; fclose(file); stbtt_fontinfo info; if (!ok || !stbtt_InitFont(&info, fontData, stbtt_GetFontOffsetForIndex(fontData, 0))) return; int ascent, descent, gap; stbtt_GetFontVMetrics(&info, &ascent, &descent, &gap); lineHeight = (ascent - descent + gap) * stbtt_ScaleForPixelHeight(&info, BAKE_SIZE); glyphs = calloc(COUNT, sizeof(stbtt_bakedchar)); atlas = (unsigned char *)calloc(ATLAS_W * ATLAS_H, 1); stbtt_BakeFontBitmap(fontData, stbtt_GetFontOffsetForIndex(fontData, 0), BAKE_SIZE, atlas, ATLAS_W, ATLAS_H, FIRST, COUNT, (stbtt_bakedchar *)glyphs); error = 0; } FTFont::~FTFont() { if (texture != 0) glDeleteTextures(1, &texture); free(atlas); free(glyphs); free(fontData); } bool FTFont::FaceSize(unsigned int s) { size = s; return true; } void FTFont::upload() { unsigned char *rgba = (unsigned char *)malloc(ATLAS_W * ATLAS_H * 4); for (int i = 0; i < ATLAS_W * ATLAS_H; ++i) { rgba[i * 4] = rgba[i * 4 + 1] = rgba[i * 4 + 2] = 255; rgba[i * 4 + 3] = atlas[i]; } glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); 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_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); gluBuild2DMipmaps(GL_TEXTURE_2D, 4, ATLAS_W, ATLAS_H, GL_RGBA, GL_UNSIGNED_BYTE, rgba); free(rgba); free(atlas); atlas = NULL; } static int length(const char *str, int len) { const int n = (int)strlen(str); return len < 0 || len > n ? n : len; } void FTFont::Render(const char *str, int len) { if (error || str == NULL) return; if (texture == 0) upload(); const float scale = size / BAKE_SIZE; const int n = length(str, len); glBindTexture(GL_TEXTURE_2D, texture); glBegin(GL_QUADS); float x = 0.0f, y = 0.0f; for (int i = 0; i < n; ++i) { int c = (unsigned char)str[i]; if (c < FIRST || c >= FIRST + COUNT) c = '?'; stbtt_aligned_quad q; stbtt_GetBakedQuad((stbtt_bakedchar *)glyphs, ATLAS_W, ATLAS_H, c - FIRST, &x, &y, &q, 1); // stb_truetype's y grows downwards; OpenGL's grows upwards. glTexCoord2f(q.s0, q.t1); glVertex3f(q.x0 * scale, -q.y1 * scale, 0.0f); glTexCoord2f(q.s1, q.t1); glVertex3f(q.x1 * scale, -q.y1 * scale, 0.0f); glTexCoord2f(q.s1, q.t0); glVertex3f(q.x1 * scale, -q.y0 * scale, 0.0f); glTexCoord2f(q.s0, q.t0); glVertex3f(q.x0 * scale, -q.y0 * scale, 0.0f); } glEnd(); } float FTFont::Advance(const char *str, int len) { if (error || str == NULL) return 0.0f; const stbtt_bakedchar *g = (const stbtt_bakedchar *)glyphs; const int n = length(str, len); float advance = 0.0f; for (int i = 0; i < n; ++i) { int c = (unsigned char)str[i]; if (c < FIRST || c >= FIRST + COUNT) c = '?'; advance += g[c - FIRST].xadvance; } return advance * size / BAKE_SIZE; } float FTFont::LineHeight() const { return lineHeight * size / BAKE_SIZE; }