Short blog post I had on substack, which I'm retiring and posting an update on here. The technique I'm describing avoids using a 2D texture and stores the built glyph directly in bytes.
As you can imagine this is pretty bad in comparison to the 2D texture approach. Storing glyphs in 2D Textures has benefits like tightly packing, overriding sections of a texture and most importantly the fragment shader can most likely read faster from a texture than reinterpreting from our storage buffer.
With that said let's just see if we can get this "alternative" working :)
Initialization & Building
I’m using Odin for the code examples and the wonderful stb truetype library for loading and dealing with fonts.
Let’s look into a simple way to implement this when looking at the data.
// mapping codepoint + pixel_size to the built glyph in the glyph buffer Glyph_Key :: struct { codepoint: rune, pixel_size: f32, } // glyph information Glyph_Slot :: struct { offset: u32, // offset into the glyph_buffer xoff, yoff: i16, // glyph visual offset width, height: u16, // glyph dimensions ascent: f32, // based on used pixel_size xadvance: f32, // based on used pixel_size } Renderer_State :: struct { // ... glyph_buffer: []byte, // storing all glyph pixels glyph_buffer_index: int, // write position glyph_buffer_map: map[Glyph_Key]Glyph_Slot, // mapping key to built glyph font_regular: Font_Keep, // storing the font info + font bytes }
Building the wanted codepoint into the glyph_buffer is also pretty simple. rs.vrs is my global Vulkan Renderer State.
vrs_build_glyph :: proc( info: ^stbtt.fontinfo, codepoint: rune, pixel_size: f32, ) -> (res: Glyph_Slot) { m := &rs.vrs.glyph_buffer_map // request codepoint + pixel_size key := Glyph_Key { codepoint, pixel_size, } if glyph, ok := m[key]; ok { res = glyph } else { // get correct offset bytes b := rs.vrs.glyph_buffer[rs.vrs.glyph_buffer_index:] // convert to glyph once, instead of per call glyph := stbtt.FindGlyphIndex(info, codepoint) // glyph bounding box x0, y0, x1, y1: i32 scale := stbtt.ScaleForPixelHeight(info, pixel_size) stbtt.GetGlyphBitmapBox(info, glyph, scale, scale, &x0, &y0, &x1, &y1) w := x1 - x0 h := y1 - y0 // build glyph into bytes // stride is width of glyph! as seen in stbtt internals stbtt.MakeGlyphBitmap(info, raw_data(b), w, h, w, scale, scale, glyph) // gather glyph positional info ascent := font_ascent(info, scale) xadvance, lsb: i32 stbtt.GetGlyphHMetrics(info, glyph, &xadvance, &lsb) // create glyph slot res = Glyph_Slot { offset = u32(rs.vrs.glyph_buffer_index), xoff = i16(x0), yoff = i16(y0), width = u16(w), height = u16(h), ascent = ascent, xadvance = f32(xadvance) * scale, } m[key] = res // offset write index by width and height rs.vrs.glyph_buffer_index += int(w) * int(h) } return }
When rendering glyphs we can call this procedure to retrieve the wanted glyph slot - whenever one doesn't exist, it will be built it into the glyph_buffer.
Per font you will need one of these maps to lookup each codepoint + pixel_size you want. In case you only want to use one map - you could designate a few KB per font and offset on each font. Font 1 takes 0-2KB, Font 2 takes 2-4KB, …
Storage Buffer
I won’t go into the details of how to create a Storage Buffer - as that should be simple enough in most cases.
Update the Storage Buffer to the glyph_buffer data every frame or whenever a new glyph gets built into the buffer.
Shader
Now we have to reconstruct the glyph texture in the fragment shader. The code below will assume you can feed the Glyph_Slot
data into the vertex shader.
// glyphs only store 1 byte per pixel (bpp) so the R (red) value, we need to get the correct value out of each 4 byte value though layout(std430, binding = 3) readonly buffer Glyph_Buffer { uint data[]; } glyph_buffer; // based on https://stackoverflow.com/a/60469946 // provide wanted 1B index you want from 4B (uint) based array uint get_byte(uint byte_idx) { uint byte_in_uint = byte_idx % 4; uint vec_idx = byte_idx / 4; uint bytes = glyph_buffer.data[vec_idx]; return (bytes >> ((byte_in_uint) * 8)) & 0xFF; } void main() { // ... // get screen position, v_* values are provided through vertex shader float x = (gl_FragCoord.x - v_pos_x); float y = (gl_FragCoord.y - v_pos_y); uint y_off = uint(y) * v_glyph_width; uint byte = get_byte(uint(v_glyph_offset + x + y_off)); float alpha = float(byte) / 255; frag_color = v_color_goal * alpha; }
Maybe this could be done better or more efficient, let me know if you know better ways to achieve this kind of extraction.
Result
That’s about it, pretty simple right?
Notes:
What is this even good for?
- Easier to create than a good texture cache but less efficient
- Nothing else
What if you want to remove built glyphs?
- Would need better allocation scheme where you can reset regions like a free list
- Clear
glyph_buffer
and let the system rebuild the wanted glyphs