Implementing SDF/MSDF Font In OpenGL

Caleb Faith
5 min readDec 16, 2017

--

Top: Texture, Bottom: Result after shader processing

When looking for resources to render text I stumbled upon this GitHub repo which allows you to generate Multi-Signed Distance Fields (MSDF) from any source image or font. Using MSDF allows you to render text efficiently at almost any size including extremely large text without any pixelization. It also works on mitigating the traditional shortcomings of SDF such as rounded edges. However I couldn’t find a step-by-step guide on how to implement it in my game, hence this article.

Generating Textures

Using the .exe from this GitHub repo it’s extremely easy to generate textures from any source. I used the ‘Lato’ font from Google. The example command from the repo renders great textures but they are not adequate for our use as they are scaled and centred losing the natural proportions of the font. Here’s the example command:

msdfgen.exe msdf -font C:\Windows\Fonts\arialbd.ttf 'M' -o msdf.png -size 32 32 -pxrange 4 -autoframe -testrender render.png 1024 1024

This was the command I ended up using:

msdfgen.exe msdf -font Lato-Regular.ttf 'a' -size 96 96 -pxrange 12 -scale 2  -o a.png -translate 3 20
Result Of The Above Command

Make sure you have msdfgen.exe, freetype.dll and zlib.dll in the same directory. And make sure to change the output file name as well.

The character is off-centre but when you include other letters such as ‘W’ or ‘p’ you will need the extra space. The most important part is that the natural proportions, height and baseline is retained — making our job much easier! As far as the size I also wanted a power of 2 width and height. I originally tried 64x96 but some letters were cut off. The image above is only 2.32KB.

To generate textures for each individual character I created a batch file which runs a command for each character and outputs a PNG per character.

2D_Texture_Array

“Array textures are an alternative to atlases, as they can store multiple layers of texture data for objects without some of the problems of atlases.” — OpenGL Wiki

A 2D Texture Array is a collection of same size images which you can access with 3 texture coordinates (u, v, w) where w corresponds to the index of the texture array. This eliminates the need to switch textures for each character thus vastly improving our text rendering performance.

The implementation of these textures is beyond the scope of this article but a good place to start is the OpenGL Wiki page which even includes example code.

Kerning

Kerning is a subtle but very important detail needed to create natural looking text. I used FreeType to provide the kerning. Here is some simple psuedocode showing how to use FreeType:

// Set our starting position
x = startPos.x;
y = startPos.y;
// Set character size
FT_Set_Char_Size(face, 0, size * 64, dpi, 0);
for each character c in string
{
// Load the character in FreeType
FT_Load_Char(face, c, FT_LOAD_DEFAULT);
if(c != ' ') // Don't draw spaces
{
SetGlyphPos(x, y);
SetGlyphChar(c);
AddToOpenGLBuffer(Glyph);
}
// Get the space to advance by (kerning)
// And move our x position along after adding to the buffer
x += slot->advance.x >> 6;
}

Checkout the FreeType tutorial for more info.

Drawing

For rendering I created an orthographic matrix which matches the screen resolution. Here’s an example using GLM:

// left, right, bottom, top, near, far
matrix = ortho(0.0f, width, height, 0.0f, -100.0f, 100.0f);

I then rendered each character with a sprite/plane and set it’s width and height to the character size provided to FreeType in the kerning section above.

Shaders

The shaders are surprisingly simple and I used a modified shader from the example at the MSDFGEN repo as well as paulhoux example in this thread. I’m quite new to writing GLSL so please feel free to point out any mistakes and I’ll correct them.

Vertex Shader GLSL

#version 330 corelayout(location = 0) in vec3 vertexPosition_modelspace;
layout(location = 1) in vec3 textureCoordinates;
layout(location = 2) in vec4 vertexColor;
out vec3 texCoords;
out vec4 fragColor;
uniform mat4 MVP;void main()
{
gl_Position = MVP * vec4(vertexPosition_modelspace, 1);
texCoords = textureCoordinates;
fragColor = vertexColor;
}

Fragment Shader GLSL

#version 330 corein vec3 texCoords;
in vec4 fragColor;
out vec4 color;uniform sampler2DArray msdf;float median(float r, float g, float b) {
return max(min(r, g), min(max(r, g), b));
}
void main()
{
vec3 flipped_texCoords = vec3(texCoords.x, 1.0 - texCoords.y, texCoords.z);
vec2 pos = flipped_texCoords.xy;
vec3 sample = texture(msdf, flipped_texCoords).rgb;
ivec2 sz = textureSize(msdf, 0).xy;
float dx = dFdx(pos.x) * sz.x;
float dy = dFdy(pos.y) * sz.y;
float toPixels = 8.0 * inversesqrt(dx * dx + dy * dy);
float sigDist = median(sample.r, sample.g, sample.b);
float w = fwidth(sigDist);
float opacity = smoothstep(0.5 - w, 0.5 + w, sigDist);
color = vec4(fragColor.rgb, opacity);
}

Troubleshooting

If it isn’t working as expected somethings to check are:

  1. You’re using a linear filter with your textures. Use GL_LINEAR not GL_NEAREST.
  2. Don’t compress your textures otherwise you’ll have some weird artefacts. It took me an hour of troubleshooting to realise that using DDX compression was messing with the SDF.

Final Words

Final Result

After putting all of this together you should have some wonderful looking text at any size! Please let me know if you have any improvements or issues in the comments.

--

--