Tutorial/OpenGL font rendering

Intro

This is a tutorial on rendering fonts in OpenGL using instancing. Instancing was introduced with version 3.1, but is often supported by earlier version as an extension.

It will also use dynamic color palette technique inspired by Allen Webster (Mr4thDimention) for compressing font color data.

This tutorial is for arbitrary fonts, but it can easily be specialized for monospaced fonts by use of uniforms for font width/height and texture width/height.

Assumptions

Some OpenGL knowledge Already compiled and linked shaders

What is a dynamic color palette?

Every frame we will create a list of colors that we send as a 1D texture. That way we can access it from a shader with just an index independently of how the color is represented.

This also allows us to specify 1 color per, for example, a string.

That way, using a single compare we can reuse last used color.

Types

1
2
3
struct v2 { GLfloat X, Y; };
struct v4 { GLfloat R, G, B, A; };
struct color_rgba { uint8_t R, G, B, A; };

We also need to define a structure that will hold per-glyph data:

1
2
3
4
5
6
7
8
struct font_shader_instance
{
    v2 Position;
    v2 Size;
    v2 TextureCoordinates;
    v2 TextureSize;
    int32_t Color; // Index into the color palette texture.
};

Setup

In this tutorial it is assumed that you have loaded OpenGL 3.0 (GLSL 1.30) or higher. It is also assumed that your driver supports instancing.

Loading necessary functions with GLX

If you're using OpenGL 3.0 you will need to check whether your driver supports instancing. If that is the case you should be able to load these functions like so:

1
2
3
4
5
void (* _glDrawArraysInstanced)(GLenum, GLint, GLsizei, GLsizei);
void (* _glVertexAttribDivisor)(GLuint, GLuint);
_glDrawArraysInstanced = (void (*)(GLenum, GLint, GLsizei, GLsizei))glXGetProcAddress((unsigned char *)"glDrawArraysInstanced");
_glVertexAttribDivisor = (void (*)(GLuint, GLuint))glXGetProcAddress((unsigned char *)"glVertexAttribDivisor");
Assert(_glVertexAttribDivisor && _glDrawArraysInstanced);

Implementation

Glyph data buffers:

1
2
3
4
5
6
font_shader_instance FontInstanceBuffer[Kibi(64)];
uint32_t             FontInstanceCount;

color_rgba FontColorPaletteBuffer[1024] = {};
int32_t FontColorPaletteAt;
v4 LastColor = {};

OpenGL buffer handles:

1
2
3
4
GLuint FontColorPalette;
GLuint FontVAO;
GLuint GlyphArrayBuffer;
GLuint GlyphInstanceBuffer;

Font rendering procedure:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
internal void
RenderFontBuffer()
{
    glUseProgram(FontShaderProgram);

    glActiveTexture(GL_TEXTURE0+0);
    glBindTexture(GL_TEXTURE_2D, TextureFont);
    glActiveTexture(GL_TEXTURE0+1);
    glBindTexture(GL_TEXTURE_1D, FontColorPalette);

    glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexImage1D(GL_TEXTURE_1D, 0,
                 GL_RGBA8, FontColorPaletteAt, 0,
                 GL_RGBA, GL_UNSIGNED_BYTE, FontColorPaletteBuffer);

    glBindVertexArray(FontVAO);
    glBindBuffer(GL_ARRAY_BUFFER, GlyphInstanceBuffer); 
    glBufferData(GL_ARRAY_BUFFER, sizeof(*FontInstanceBuffer)*FontInstanceCount, FontInstanceBuffer, GL_DYNAMIC_DRAW);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(font_shader_instance), (void *)OffsetOf(font_shader_instance, Position));
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(font_shader_instance), (void *)OffsetOf(font_shader_instance, Size));
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(font_shader_instance), (void *)OffsetOf(font_shader_instance, TextureCoordinates));
    glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE, sizeof(font_shader_instance), (void *)OffsetOf(font_shader_instance, TextureSize));
    glVertexAttribIPointer(4, 1, GL_INT,            sizeof(font_shader_instance), (void *)OffsetOf(font_shader_instance, Color));

    glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, FontInstanceCount);

    glBindVertexArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0); 

    FontColorPaletteAt = 1;
    LastColor = {};

    FontInstanceCount = 0;
}

internal void
SetupFontShader()
{
    // Assumes FontShaderProgram is already compiled
    glUseProgram(FontShaderProgram);
    glUniform1i(FontShaderUniform.FontTexture, 0);
    glUniform1i(FontShaderUniform.FontColor, 1);

    glGenBuffers(1, &GlyphInstanceBuffer);
    glGenVertexArrays(1, &FontVAO);

    glBindVertexArray(FontVAO);
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    glEnableVertexAttribArray(2);
    glEnableVertexAttribArray(3);
    glEnableVertexAttribArray(4);
    glVertexAttribDivisor(0, 1);
    glVertexAttribDivisor(1, 1);
    glVertexAttribDivisor(2, 1);
    glVertexAttribDivisor(3, 1);
    glVertexAttribDivisor(4, 1);
    glBindVertexArray(0);

    glGenTextures(1, &FontColorPalette);
    CreateTextureFont();
}

Vertex shader:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#version 130

// Per instance
in vec2 GlyphPosition;
in vec2 GlyphSize;
in vec2 GlyphTextureCoordinates;
in vec2 GlyphTextureSize;
in int VSColor;

out vec2 FSTextureCoordinates;
out vec4 FSColor;

uniform sampler1D FontColor;

void main()
{
    vec2 VSPosition = vec2(gl_VertexID >> 1, gl_VertexID & 1);
    FSTextureCoordinates = GlyphTextureCoordinates + GlyphTextureSize * VSPosition;
    FSColor = texelFetch(FontColor, VSColor, 0);

    gl_Position = vec4(GlyphPosition + GlyphSize * VSPosition, 0.0, 1.0);
}

Fragment shader:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#version 130

in vec2 FSTextureCoordinates;
in vec4 FSColor;

out vec4 Color;

uniform sampler2D FontTexture;

void main()
{
    float C = texture(FontTexture, FSTextureCoordinates).r;
    Color =  C * FSColor;
}

Using this to render fonts

Using this from a render queue processor should be relatively easy. Here is example code that handles a single render queue element that contains a full string and a single color for the whole string, with monospaced text layout and font glyphs stored as a sorted 16*16 ASCII texture:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
case RQE_String:
{
    ELEMENT(rqe_string);
    {
        if(LastColor != E->Color)
        {
            LastColor = E->Color;
#define SetChannel(x) FontColorPaletteBuffer[FontColorPaletteAt].x = (u8)(E->Color.x * 255.0f)
            SetChannel(R);
            SetChannel(G);
            SetChannel(B);
            SetChannel(A);
#undef SetChannel
            ++FontColorPaletteAt;
        }

        v2 P = E->P;
        GLfloat XOffset = E->Size.X * (1-E->Shrink);
        for(uint32_t i = 0;
            i < E->S.Length;
            ++i)
        {
            char C = E->S.C[i];
            v2 PT = V2(1.0f/16*(C & 15), 1.0f-1.0f/16-1.0f/16*(C >> 4));

            FontInstanceBuffer[FontInstanceCount++] =
            {
                .Position = P,
                .Size = E->Size,
                .TextureCoordinates = PT,
#define PD (1.0f/16)
                .TextureSize = {PD, PD},
#undef PD
                .Color = FontColorPaletteAt-1,
            };

            P.X += XOffset;
        }
    }
    At = (u8 *)(E+1) + E->S.Length;
} break;

Edited by Dautor on Reason: typo fix