diff options
author | Leo Tenenbaum <pommicket@gmail.com> | 2020-11-23 18:29:58 -0500 |
---|---|---|
committer | Leo Tenenbaum <pommicket@gmail.com> | 2020-11-23 18:29:58 -0500 |
commit | 55d0ece0a9072ca409bdf6ff2f3b6d0b268e2952 (patch) | |
tree | 5df0cbfacf3d9442ddb34fdf5fb3b59f4e74d82e /text.c | |
parent | 5e458dff3bcc832b0b28d83bd3ef482174d1dc09 (diff) |
unicode text rendering working
Diffstat (limited to 'text.c')
-rw-r--r-- | text.c | 230 |
1 files changed, 182 insertions, 48 deletions
@@ -3,25 +3,34 @@ #define STB_TRUETYPE_IMPLEMENTATION #define STBTT_STATIC no_warn_start -#include "stb_truetype.h" +#include "lib/stb_truetype.h" no_warn_end #include <stdarg.h> #include <stdlib.h> #include <GL/gl.h> +#define UNICODE_CODE_POINTS 0x110000 // number of Unicode code points +// We split up code points into a bunch of pages, so we don't have to load all of the font at +// once into one texture. +#define CHAR_PAGE_SIZE 2048 +#define CHAR_PAGE_COUNT UNICODE_CODE_POINTS / CHAR_PAGE_SIZE + struct Font { float char_height; - GLuint texture; - u32 nchars; - stbtt_bakedchar chars[]; + GLuint textures[CHAR_PAGE_COUNT]; + int tex_widths[CHAR_PAGE_COUNT], tex_heights[CHAR_PAGE_COUNT]; + stbtt_bakedchar *char_pages[CHAR_PAGE_COUNT]; // character pages. NULL if the page hasn't been loaded yet. + // TTF data (i.e. the contents of the TTF file) + u8 *ttf_data; + int curr_page; }; static char text_err[200]; -static void text_clear_err(void) { +void text_clear_err(void) { text_err[0] = '\0'; } -static bool text_has_err(void) { +bool text_has_err(void) { return text_err[0] != '\0'; } @@ -38,9 +47,57 @@ static void text_set_err(char const *fmt, ...) { } } +static void text_load_char_page(Font *font, int page) { + if (font->char_pages[page]) { + // already loaded + return; + } + font->char_pages[page] = calloc(CHAR_PAGE_SIZE, sizeof *font->char_pages[page]); + for (int bitmap_width = 128, bitmap_height = 128; bitmap_width <= 4096; bitmap_width *= 2, bitmap_height *= 2) { + u8 *bitmap = calloc((size_t)bitmap_width, (size_t)bitmap_height); + if (bitmap) { + int err = stbtt_BakeFontBitmap(font->ttf_data, 0, font->char_height, bitmap, + bitmap_width, bitmap_height, page * CHAR_PAGE_SIZE, CHAR_PAGE_SIZE, font->char_pages[page]); + if (err > 0) { + // font converted to bitmap successfully. + GLuint texture = 0; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, bitmap_width, bitmap_height, 0, GL_ALPHA, GL_UNSIGNED_BYTE, bitmap); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + #if DEBUG + debug_println("Loaded font page %d with %dx%d bitmap as texture %u.", page, bitmap_width, bitmap_height, texture); + #endif + font->textures[page] = texture; + font->tex_widths[page] = bitmap_width; + font->tex_heights[page] = bitmap_height; + GLenum glerr = glGetError(); + if (glerr) { + text_set_err("Couldn't create texture for font (GL error %u).", glerr); + if (texture) glDeleteTextures(1, &texture); + break; + } + } + } else { + text_set_err("Not enough memory for font bitmap."); + } + free(bitmap); + if (font->textures[page]) { // if font loaded successfully + break; + } + } + if (!font->textures[page] && !text_has_err()) { + text_set_err("Couldn't convert font to bitmap."); + } + if (text_has_err()) { + free(font->char_pages[page]); + font->char_pages[page] = NULL; + } +} + Font *text_font_load(char const *ttf_filename, float font_size) { Font *font = NULL; - u32 nchars = 128; FILE *ttf_file = fopen(ttf_filename, "rb"); text_clear_err(); @@ -48,54 +105,25 @@ Font *text_font_load(char const *ttf_filename, float font_size) { if (ttf_file) { fseek(ttf_file, 0, SEEK_END); u32 file_size = (u32)ftell(ttf_file); + fseek(ttf_file, 0, SEEK_SET); if (file_size < (50UL<<20)) { // fonts aren't usually bigger than 50 MB u8 *file_data = calloc(1, file_size); - font = calloc(1, sizeof *font + nchars * sizeof *font->chars); + font = calloc(1, sizeof *font); if (file_data && font) { - if (fread(file_data, 1, file_size, ttf_file) == file_size) { - font->nchars = nchars; + size_t bytes_read = fread(file_data, 1, file_size, ttf_file); + if (bytes_read == file_size) { font->char_height = font_size; - - for (int bitmap_width = 256, bitmap_height = 256; bitmap_width <= 4096; bitmap_width *= 2, bitmap_height *= 2) { - u8 *bitmap = calloc((size_t)bitmap_width, (size_t)bitmap_height); - if (bitmap) { - int err = stbtt_BakeFontBitmap(file_data, 0, font->char_height, bitmap, - bitmap_width, bitmap_height, 0, (int)font->nchars, font->chars); - if (err > 0) { - // font converted to bitmap successfully. - GLuint texture = 0; - glGenTextures(1, &texture); - glBindTexture(GL_TEXTURE_2D, texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, bitmap_width, bitmap_height, 0, GL_ALPHA, GL_UNSIGNED_BYTE, bitmap); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - #if DEBUG - debug_println("Loaded font %s with %dx%d bitmap.", ttf_filename, bitmap_width, bitmap_height); - #endif - font->texture = texture; - if (glGetError()) { - text_set_err("Couldn't create texture for font."); - } - } - } else { - text_set_err("Not enough memory for font bitmap."); - } - free(bitmap); - if (font->texture) { // if font loaded successfully - break; - } - } - if (!font->texture && !text_has_err()) { - text_set_err("Couldn't convert font to bitmap."); - } + font->ttf_data = file_data; + text_load_char_page(font, 0); // load page with Latin text, etc. + font->curr_page = -1; } else { - text_set_err("Couldn't read font file.", ttf_filename); + text_set_err("Couldn't read font file."); } } else { text_set_err("Not enough memory for font."); } - free(file_data); if (text_has_err()) { + free(file_data); free(font); font = NULL; } @@ -109,11 +137,117 @@ Font *text_font_load(char const *ttf_filename, float font_size) { return font; } -#if 0 +typedef struct { + float x, y; +} TextRenderState; + +static void text_render_with_page(Font *font, int page) { + if (font->curr_page != page) { + if (font->curr_page != -1) { + // we were rendering chars from another page. + glEnd(); // stop doing that + } + text_load_char_page(font, page); // load the page if necessary + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, font->textures[page]); + glBegin(GL_QUADS); + font->curr_page = page; + } +} + +void text_chars_begin(Font *font) { + text_render_with_page(font, 0); // start by loading Latin text +} + +void text_chars_end(Font *font) { + glEnd(); + glDisable(GL_TEXTURE_2D); + font->curr_page = -1; +} + +static void text_render_char_internal(Font *font, char32_t c, TextRenderState *state) { + if (c >= 0x30000 && c < 0xE0000){ + // these Unicode code points are currently unassigned. replace them with ☐. + // (specifically, we don't want to use extra memory for pages which + // won't even have any valid characters in them) + c = 0x2610; + } + if (c >= UNICODE_CODE_POINTS) c = 0x2610; // code points this big should never appear in valid Unicode + uint page = c / CHAR_PAGE_SIZE; + uint index = c % CHAR_PAGE_SIZE; + text_render_with_page(font, (int)page); + stbtt_bakedchar *char_data = font->char_pages[page]; + if (char_data) { // if page was successfully loaded + stbtt_aligned_quad q = {0}; + // because stb_truetype uses down is positive, we need to negate the y + // coordinate, pass it into the function, then negate it back. + state->y = -state->y; + stbtt_GetBakedQuad(char_data, font->tex_widths[page], font->tex_heights[page], + (int)index, &state->x, &state->y, &q, 1); + state->y = -state->y; + glTexCoord2f(q.s0,q.t1); glVertex2f(q.x0,-q.y1); + glTexCoord2f(q.s1,q.t1); glVertex2f(q.x1,-q.y1); + glTexCoord2f(q.s1,q.t0); glVertex2f(q.x1,-q.y0); + glTexCoord2f(q.s0,q.t0); glVertex2f(q.x0,-q.y0); + } +} + +void text_render_char(Font *font, char32_t c, float *x, float *y) { + TextRenderState state = {*x, *y}; + text_render_char_internal(font, c, &state); + *x = state.x; + *y = state.y; +} + +static void text_render_internal(Font *font, char const *text, float *x, float *y) { + mbstate_t mbstate = {0}; + TextRenderState render_state = {*x, *y}; + text_chars_begin(font); + char32_t c = 0; + char const *end = text + strlen(text); + while (text != end) { + size_t ret = mbrtoc32(&c, text, (size_t)(end - text), &mbstate); + if (ret == 0) break; + if (ret == (size_t)(-2)) { // incomplete multi-byte character + text_render_char_internal(font, '?', &render_state); + text = end; // done reading text + } else if (ret == (size_t)(-1)) { + // invalid UTF-8; skip this byte + text_render_char_internal(font, '?', &render_state); + ++text; + } else { + if (ret != (size_t)(-3)) + text += ret; // character consists of `ret` bytes + switch (c) { + default: + text_render_char_internal(font, (char32_t)c, &render_state); + break; + } + } + } + text_chars_end(font); + *x = render_state.x; + *y = render_state.y; +} + void text_render(Font *font, char const *text, float x, float y) { - + text_render_internal(font, text, &x, &y); } void text_get_size(Font *font, char const *text, float *width, float *height) { + float x = 0, y = 0; + text_render_internal(font, text, &x, &y); + if (width) *width = x; + if (height) *height = y + font->char_height * (2/3.0f); +} + +void text_font_free(Font *font) { + free(font->ttf_data); + stbtt_bakedchar **char_pages = font->char_pages; + for (int i = 0; i < CHAR_PAGE_COUNT; ++i) { + if (char_pages[i]) { + free(char_pages[i]); + } + } + free(font); } -#endif |