Sharp Text at Any Size with MSDF
Rendering text on the GPU usually starts with a glyph atlas: rasterise each character into a texture once, then draw textured quads. It’s fast, but every glyph is baked at a single size. Scale it up and the edges blur; scale it down and they shimmer.
Signed distance fields
A signed distance field stores something different in each texel: the distance to the nearest edge of the glyph, positive inside and negative outside. Because distance interpolates well, bilinear filtering still gives meaningful values between texels, and the fragment shader can find the edge at any scale by thresholding.
Valve popularised the technique in 2007, and it works remarkably well — a 32×32 field can draw a glyph cleanly at hundreds of pixels. Its weakness is corners. A single distance value can’t describe a sharp corner, so corners come out rounded.
Adding channels
Multi-channel signed distance fields, developed by Viktor Chlumský, fix this by splitting a glyph’s edges across the red, green and blue channels. Each channel stores the distance to a different subset of edges, and the shader takes the median of the three:
float median(float r, float g, float b) {
return max(min(r, g), min(max(r, g), b));
}
fragment float4 glyphFragment(VertexOut in [[stage_in]],
texture2d<float> atlas [[texture(0)]],
sampler s [[sampler(0)]]) {
float3 msd = atlas.sample(s, in.uv).rgb;
float distance = median(msd.r, msd.g, msd.b) - 0.5;
float width = fwidth(distance);
float alpha = smoothstep(-width, width, distance);
return float4(in.color.rgb, in.color.a * alpha);
}
Where two edges meet, the channels disagree in just the right way for the median to reconstruct a sharp point.
In practice
You don’t generate the fields yourself. Chlumský’s msdfgen and its companion atlas generator produce a texture plus a JSON file of glyph metrics. From there it’s an ordinary textured-quad renderer with the shader above.
Good results mostly come down to two details: generating the atlas with enough distance range that edges stay smooth when text is small, and using fwidth so the edge stays one pixel wide when text is large.