#include "../../include/mgl/graphics/font.h" #include #define STB_TRUETYPE_IMPLEMENTATION #include "../../external/stb_truetype.h" /* TODO: Show a rectangle for unsupported glyphs or if |self| is NULL */ /* TODO: Pack fonts tighter */ /* Need padding so filtering doesn't touch pixels in another glyphs area */ #define GLYPH_PADDING 2 #define GLYPH_UPSAMPLE 1 #define GLYPH_SHIFT 0.5f static int round_float(float value) { return value + 0.5f; } static int max_int(int a, int b) { return a >= b ? a : b; } int mgl_font_load_from_file(mgl_font *self, const mgl_memory_mapped_file *mapped_file, unsigned int character_size) { self->texture.id = 0; self->font_atlas.width = 0; self->font_atlas.height = 0; self->font_atlas.prev_width = 0; self->font_atlas.prev_height = 0; self->font_atlas.pointer_position = (mgl_vec2i){ GLYPH_PADDING, GLYPH_PADDING }; self->font_atlas.render_section = MGL_ATLAS_SECTION_INITIAL; self->font_atlas.initial_section_height = 0; self->font_atlas.right_section_height = 0; self->character_size = character_size; mgl_font_char_map_init(&self->char_map); self->current_line_max_height = 0; self->font_info = NULL; self->font_info = malloc(sizeof(stbtt_fontinfo)); if(!self->font_info) { fprintf(stderr, "Error: failed to load font, error: out of memory\n"); return -1; } /* TODO: Handle font offset with ttc */ if(!stbtt_InitFont(self->font_info, mapped_file->data, stbtt_GetFontOffsetForIndex(mapped_file->data, 0))) { fprintf(stderr, "Error: failed to load font, error: stbtt_InitFont failed\n"); mgl_font_unload(self); return -1; } int ascent = 0; int descent = 0; int linegap = 0; stbtt_GetFontVMetrics(self->font_info, &ascent, &descent, &linegap); const float font_scale = stbtt_ScaleForPixelHeight(self->font_info, self->character_size*GLYPH_UPSAMPLE); self->ascent = round_float(font_scale * ascent); self->descent = round_float(font_scale * descent); self->linegap = round_float(font_scale * linegap); self->max_glyph_height = self->character_size - self->descent; /* TODO: Use stbtt_GetCodepointSDF */ return 0; } void mgl_font_unload(mgl_font *self) { mgl_texture_unload(&self->texture); mgl_font_char_map_deinit(&self->char_map); self->current_line_max_height = 0; self->ascent = 0; self->descent = 0; self->linegap = 0; self->max_glyph_height = 0; free(self->font_info); self->font_info = NULL; } static int align_up_to_next_value_of(int value, int alignment) { int offset_to_next_alignment = value % alignment; if(offset_to_next_alignment == 0) return value; else return value + (alignment - offset_to_next_alignment); } static void mgl_font_handle_new_render_position(mgl_font *self, int glyph_width) { if(self->font_atlas.pointer_position.x + glyph_width + GLYPH_PADDING >= self->font_atlas.width) { if(self->font_atlas.pointer_position.y + self->current_line_max_height + (int)self->character_size + GLYPH_PADDING >= self->font_atlas.height && self->font_atlas.render_section != MGL_ATLAS_SECTION_RIGHT) { /* Texture atlas looks like this with glyphs filling the texture: |----| |ABCD| |EFGH| |----| And then texture atlas gets resized: |----|----| | |ABCD| | <-| 1. The right side gets filled in with new glyphs. |EDFG| | <-| |----|----| | | | | | | | | | | | | | |----|----| | | ^^^^^^^^^^^ | ---------------| 2. The bottom side gets filled in with new glyphs after the right side is filled with glyphs. */ /* Texture overflow, enlargen the font atlas! */ /* TODO: Create a new texture and copy the texture content instead of rendering all glyphs again */ const int new_width = align_up_to_next_value_of(self->font_atlas.width * 2, self->character_size + GLYPH_PADDING); const int new_height = align_up_to_next_value_of(self->font_atlas.height * 2, self->character_size + GLYPH_PADDING); if(mgl_texture_resize(&self->texture, new_width, new_height, NULL) == 0) { self->font_atlas.initial_section_height = self->font_atlas.pointer_position.y + self->current_line_max_height; self->font_atlas.prev_width = self->font_atlas.width; self->font_atlas.prev_height = self->font_atlas.height; self->font_atlas.width = new_width; self->font_atlas.height = new_height; self->font_atlas.render_section = MGL_ATLAS_SECTION_RIGHT; self->font_atlas.pointer_position.x = self->font_atlas.prev_width; self->font_atlas.pointer_position.y = GLYPH_PADDING; mgl_font_char_map_clear_rendered(&self->char_map); mgl_font_glyph glyph_dummy; mgl_font_char_iterator font_char_it = mgl_font_char_map_begin(&self->char_map); while(font_char_it.value) { /* TODO: Update glyph without calling this, since this needs to get glyph from hash map again */ mgl_font_get_glyph(self, font_char_it.value->key, &glyph_dummy); mgl_font_char_iterator_next(&font_char_it); } } else { fprintf(stderr, "Error: failed to resize font atlas\n"); } } else if(self->font_atlas.render_section == MGL_ATLAS_SECTION_RIGHT && self->font_atlas.pointer_position.y + self->current_line_max_height + (int)self->character_size + GLYPH_PADDING >= self->font_atlas.prev_height) { self->font_atlas.right_section_height = self->font_atlas.pointer_position.y + self->current_line_max_height; self->font_atlas.render_section = MGL_ATLAS_SECTION_BOTTOM; self->font_atlas.pointer_position.x = GLYPH_PADDING; self->font_atlas.pointer_position.y = max_int(self->font_atlas.initial_section_height, self->font_atlas.right_section_height) + GLYPH_PADDING; } else { if(self->font_atlas.render_section != MGL_ATLAS_SECTION_RIGHT) { self->font_atlas.pointer_position.x = GLYPH_PADDING; } else { self->font_atlas.pointer_position.x = self->font_atlas.prev_width; } self->font_atlas.pointer_position.y += self->current_line_max_height + GLYPH_PADDING; } self->current_line_max_height = 0; } } int mgl_font_get_glyph(mgl_font *self, uint32_t codepoint, mgl_font_glyph *glyph) { if(!self) return -1; if(self->font_atlas.width == 0) { if(self->texture.id == 0 && mgl_texture_init(&self->texture) != 0) return -1; const int initial_atlas_size = align_up_to_next_value_of(128, self->character_size + GLYPH_PADDING); /* TODO: Scale by character size */ mgl_texture_load_options load_options = { .compressed = false, .pixel_coordinates = true }; if(mgl_texture_load_from_memory(&self->texture, NULL, initial_atlas_size, initial_atlas_size, MGL_IMAGE_FORMAT_ALPHA, &load_options) != 0) { return -1; } else { /*fprintf(stderr, "Error: failed to create font atlas texture, error: mgl_texture_load_from_memory failed\n");*/ self->font_atlas.width = initial_atlas_size; self->font_atlas.height = initial_atlas_size; } } mgl_font_char_entry *existing_char_entry = mgl_font_char_map_get(&self->char_map, codepoint); if(existing_char_entry) { *glyph = existing_char_entry->value; if(existing_char_entry->rendered) return 0; } /* TODO: Maybe somehow use multiple textures? that would be needed when using a lot of glyphs (such as when using chinese fonts). TODO: Map texture buffer and rasterize the glyph directly into the texture. */ const int glyph_index = stbtt_FindGlyphIndex(self->font_info, codepoint); const float font_scale = stbtt_ScaleForPixelHeight(self->font_info, self->character_size*GLYPH_UPSAMPLE); int advance = 0; int lsb; stbtt_GetGlyphHMetrics(self->font_info, glyph_index, &advance, &lsb); const float glyph_advance = font_scale * advance; int x0, y0, x1, y1; stbtt_GetGlyphBitmapBoxSubpixel(self->font_info, glyph_index, font_scale, font_scale, GLYPH_SHIFT, GLYPH_SHIFT, &x0, &y0, &x1, &y1); int width = x1 - x0; int height = y1 - y0; int xoff = x0; int yoff = y0; /* TODO: Use stbtt_MakeGlyphBitmapSubpixelPrefilter instead for better text quality */ const size_t pixels_width = (width + GLYPH_PADDING * 2); const size_t pixels_height = (height + GLYPH_PADDING * 2); const size_t pixels_size = pixels_width * pixels_height; unsigned char *pixels = calloc(pixels_size, 1); if(pixels) { const int top_padding = GLYPH_PADDING; stbtt_MakeGlyphBitmapSubpixel(self->font_info, pixels + pixels_width * top_padding + GLYPH_PADDING, width, height, pixels_width, font_scale, font_scale, GLYPH_SHIFT, GLYPH_SHIFT, glyph_index); } mgl_vec2i render_offset; if(existing_char_entry) { render_offset = existing_char_entry->render_offset; } else { mgl_font_handle_new_render_position(self, width); render_offset.x = self->font_atlas.pointer_position.x; render_offset.y = self->font_atlas.pointer_position.y; mgl_font_glyph new_glyph; new_glyph.position = (mgl_vec2i){ xoff/GLYPH_UPSAMPLE, self->ascent + yoff/GLYPH_UPSAMPLE }; new_glyph.size = (mgl_vec2i){ width/GLYPH_UPSAMPLE, height/GLYPH_UPSAMPLE }; new_glyph.texture_position = (mgl_vec2i){ render_offset.x, render_offset.y }; new_glyph.texture_size = (mgl_vec2i){ width, height }; new_glyph.advance = round_float(glyph_advance/GLYPH_UPSAMPLE); *glyph = new_glyph; if(mgl_font_char_map_insert(&self->char_map, codepoint, &new_glyph, &existing_char_entry) != 0) { free(pixels); memset(glyph, 0, sizeof(mgl_font_glyph)); return 0; } existing_char_entry->render_offset = render_offset; if(height > self->current_line_max_height) self->current_line_max_height = height; self->font_atlas.pointer_position.x += width + GLYPH_PADDING; } existing_char_entry->rendered = true; if(!pixels) { if(codepoint == ' ' || codepoint == '\t') { return 0; } else { memset(glyph, 0, sizeof(mgl_font_glyph)); return -1; } } const int res = mgl_texture_update(&self->texture, pixels, render_offset.x - GLYPH_PADDING, render_offset.y - GLYPH_PADDING, pixels_width, pixels_height, MGL_IMAGE_FORMAT_ALPHA); free(pixels); return res; } float mgl_font_get_kerning(const mgl_font *self, uint32_t prev_codepoint, uint32_t codepoint) { if(!self || self->texture.id == 0) return 0.0f; return round_float(stbtt_GetCodepointKernAdvance(self->font_info, prev_codepoint, codepoint) * stbtt_ScaleForPixelHeight(self->font_info, self->character_size)); }