SDF Fonts: Appendix

 from Red Blob Games
1 Feb 2026

These are topics that didn’t fit on the main page.

 1  Layout#

In msdf-atlas-gen, the JSON file contains information about the size and layout of each character (“glyph”):

jq '.glyphs[] | select(.unicode == 65)' \
          assets/FiraSans-Regular.json
{
  "unicode": 65,
  "advance": 0.57300000000000006,
  "planeBounds": {
    "left": -0.055197810998398253,
    "bottom": -0.051254671649759741,
    "right": 0.62819781099839844,
    "top": 0.76882007474639624
  },
  "atlasBounds": {
    "left": 0.5,
    "bottom": 34.5,
    "right": 20.5,
    "top": 58.5
  }
}

What do these fields mean? From msdf-atlas-gen’s documentation[1]:

The JSON also contains general information about the font in the metrics field, but I won’t be using them for the examples on this page:

jq '.metrics' assets/FiraSans-Regular.json
{
  "emSize": 1,
  "lineHeight": 1.2,
  "ascender": 0.93500000000000005,
  "descender": -0.26500000000000001,
  "underlineY": -0.10000000000000001,
  "underlineThickness": 0.050000000000000003
}

I will need to use information about the atlas from the atlas field:

jq '.atlas' assets/FiraSans-Regular.json
{
  "type": "sdf",
  "distanceRange": 2.9265625000000002,
  "distanceRangeMiddle": 0,
  "size": 29.265625,
  "width": 192,
  "height": 192,
  "yOrigin": "bottom"
}

We’ll multiply the abstract “em” units by the font size to get sizes in pixels. The first step is to determine where to draw the character relative to the cursor position:

  1. Use planeBounds (multiplied by font size) to determine the area to draw on the screen (blue shaded area). This will be larger than the character itself to leave room for outline effects.
  2. The cursor location is at x. Note that parts of the letter may be drawn to the left of x, especially once outlines and glow effects are included.
  3. The text baseline is at y. Some characters like g will extend below the baseline.

Visually:

left right top bottom y x

The second step is to generate rectangles where the characters will be drawn. Let’s construct a layout for the word dig which gets turned into three glyphs, d, i, g. Use advance (multiplied by font size) to increment x to draw the next character, i. There will be overlap between the drawing areas for d and i. Visually:

d's advance i's advance y x

Suppose we have a string and a starting x,y position. We can calculate the rectangles needed for each character, as well as the corresponding atlas locations:

Let’s calculate the layout boxes for each character:

(interactive!)

There can be overlap, and it will be more the larger your distance range (aemrange in msdfgen, differently named in each SDF library). The extra space is where the outlines, glow effects, and drop shadows go. Most characters extend slightly below the baseline but descenders like g extend well below the baseline.

I didn’t explore these topics:

 2  SDF vs MSDF#

One downside of SDF fonts is mentioned in the original papers from 2006 and 2007: the corners of shapes end up rounded instead of sharp. The 2006 paper uses a relatively complex data structure to calculate the SDF in a way that keeps corners sharp. The 2007 paper suggests using multiple channels but say “As it stands, we like the rounded style of this text” so they didn’t go into details about how to keep corners sharp. The 2016 paper goes into extensive detail about how to use multiple channels.

Screenshot showing how corners of an SDF rendered font can be round
256✕256 SDF font atlas, showing rounded corners

To get sharper corners, we can either:

  1. Use a higher resolution SDF:

    1✕ texture size
    4✕ texture size
    16✕ texture size
    Screenshot of SDF with 256x256 font atlasScreenshot of SDF with 512x512 font atlasScreenshot of SDF with 1024x1024 atlas
    SDF font atlas, increasing resolution from 256 to 1024
  2. Use a multi channel distance field (MSDF, 4 channels instead of 1):

    SDF 1 byte/pixel
    MSDF 4 byte/pixel
    Screenshot of SDF with 256x256 font atlasScreenshot of MSDF with 256 atlas
    SDF vs MSDF at 256x256

It’s not surprising that using more texture memory increases the quality, but for a given amount of texture memory, which approach is better? Let’s compare 256✕256 MSDF (4 bytes per pixel) with 512✕512 SDF (1 byte per pixel):

SDF with 256 KiB
MSDF with 256 KiB
Screenshot of SDF with 512x512 atlasScreenshot of MSDF with 256x256 atlas
SDF vs MSF with same texture memory

The SDF has smoother curves, especially noticeable on the dot of the “i”. The MSDF has sharper corners. Neither one is a clear win. At a higher resolution, the MSDF looks better:

SDF with 576 KiB
MSDF with 576 KiB
Screenshot of SDF with 768x768 atlasScreenshot of MSDF with 384x384 atlas
SDF vs MSDF with same texture memory
SDF with 1024 KiB
MSDF with 1024 KiB
Screenshot of SDF with 1024x1024 atlasScreenshot of MSDF with 512x512 atlas
SDF vs MSDF with same texture memory

Rounded corners look better for some of the effects like glow and shadow.

 3  Asymmetric range#

For outline/glow effects we need distances farther away from the font. In the above examples I’ve been using --aemrange -0.05 +0.05. The range depends on what effects we want:

A larger range uses more of the “value space” for outlines and less for the main font, which reduces its quality:

-0.25 to +0.25 aemrange
-0.05 to +0.05 aemrange
Screenshot of SDF with -0.25 to +0.25 aemrangeScreenshot of SDF with -0.05 to +0.05 aemrange
SDF with thick (0.5) vs thin (0.1) distance range

If the font looks bad, make sure 2/scale < atlas.distanceRange < 128 (see issue 11 on msdf-atlas-gen[6]), where atlas.distanceRange = (aemrange[1] - aemrange[0]) * atlas.size and scale is the ratio of the on-screen size to the size in the atlas. For example, if the letter A in the atlas is 32px tall, and you want to render at 16px, then scale is 0.5 and distanceRange needs to be at least 4.

The best range depends on what effects you want. Plan to experiment to find a good range for your needs. I explored this topic more on my blog.

 4  Antialiasing#

I’ve read in multiple places that gamma correction is important. However, when I tried it myself, I had a hard time telling an improvement. Maybe my code or math was buggy.

https://acko.net/blog/subpixel-distance-transform/[8]

The “blurry text” that some people associate with anti-aliasing is usually just text blended with the wrong gamma curve, and without an appropriate bleed for the font in question.

In Use.GPU I prefer to use gamma correct, linear RGB color, even for 2D. What surprised me the most is just how unquestionably superior this looks. Text looks rock solid and readable even at small sizes on low-DPI.

https://news.ycombinator.com/item?id=42191709#42192825[9] → says don’t worry about fonts being tuned for non-gamma-correction, and make sure antialiasing is linear not smoothstep

https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#antialiasing[10] says Photoshop uses 1.42 instead of 2.2

https://www.puredevsoftware.com/blog/2019/01/22/sub-pixel-gamma-correct-font-rendering/[11] uses 1.43 instead of 2.2

https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html[12] says something different. It says to blend in linear space, but compensate for lightness, not by changing gamma but using something different:

I will show why a pixel wide white line and a black background will visual look like a pixel wide line after anti-aliasing in linear-RGB color space

At this point you may think, okay… anti-alias instead in the sRGB color space. The sRGB’s transfer function (gamma), by design, already approximates the perceived lightness.

This will actually work, up to a point. Many font rendering engines do this, probably after having experimented with linear anti-aliasing and not getting the desired results. And even image editors used to compose in a similar non-linear color spaces in the past.

However instead of a uniform gradient between the foreground and the background the colors may get seriously distorted.

However I think the code is just converting back to something close to srgb.

https://github.com/kovidgoyal/kitty/pull/5969#issuecomment-1426707968[13] the Kitty terminal also has a thickness adjustment based on the contrast between the background luminance (underL) and foreground luminance (overL):

// Apply additional gamma-adjustment scaled by the luminance difference, the darker the foreground the more adjustment we apply.
// A multiplicative contrast is also available to increase sasturation.
over.a = clamp(mix(over.a, pow(over.a, text_gamma_adjustment), (1 - overL + underL) * text_gamma_scaling) * text_contrast);

Patrick Walton @pcwalton says on Twitter[14] :

For posterity: Seems that default glyph dilation on macOS is min(vec2(0.3px), vec2(0.015125, 0.0121) * S) where S is the font size in px

I had a lot of trouble getting things to look right, and I am not the only one https://x.com/rygorous/status/512371399542202368[15]

#+begin_comment

 5  TODO Contrast#

The goal of my project was to increase the contrast between the text and the map https://somethingaboutmaps.wordpress.com/2021/06/01/on-edges/[16]

Cartography community calls several of these “halos”

overlap problem with adjacent characters - /blog/2024-09-27-sdf-combining-distance-fields/

#+end_comment

 6  Using msdfgen#

I’m using msdfgen[20], a c++ command line tool. It generates both SDF and MSDF output. The companion project msdf-atlas-gen[21] generates a font atlas and a JSON file with the layout information. On Windows, get a binary from msdf-atlas-gen/releases[22]. On Linux or Mac, compile it from source. First install cmake and vcpkg:

  1. On Mac,

    cd ~/Projects/src
       
    brew install cmake vcpkg
    export VCPKG_ROOT="$HOME/vcpkg"
    git clone https://github.com/microsoft/vcpkg $VCPKG_ROOT
    
  2. On Ubuntu Linux,

    sudo apt update
    sudo apt -y install curl git cmake zip build-essential pkg-config libfreetype-dev
    export VCPKG_ROOT="$HOME/vcpkg"
    git clone https://github.com/microsoft/vcpkg.git $VCPKG_ROOT
    (cd $VCPKG_ROOT; ./bootstrap-vcpkg.sh)
    
  3. On Fedora Linux,

    sudo yum install -y git vcpkg zip cmake
    source /etc/profile
    git clone https://github.com/microsoft/vcpkg.git $VCPKG_ROOT
    
  4. On Amazon Linux,

    sudo yum install -y git vcpkg zip cmake
    export VCPKG_ROOT="$HOME/vcpkg"
    git clone https://github.com/microsoft/vcpkg.git $VCPKG_ROOT
    (cd $VCPKG_ROOT; ./bootstrap-vcpkg.sh)
    

Then build the binary with:

git clone https://github.com/Chlumsky/msdf-atlas-gen.git
cd msdf-atlas-gen
git submodule update --init
cmake .
make

If that fails with a skia error, try:

cmake -DMSDF_ATLAS_USE_SKIA=OFF .
make

To update the software later:

cd msdf-atlas-gen
git pull
git submodule update
cmake . --fresh
make

I used this command to generate the texture atlas (.png file) and the layout data (.json file):

mkdir -p assets
~/Projects/src/msdf-atlas-gen/bin/msdf-atlas-gen \
    --font fonts/FiraSans-Regular.otf \
    --type sdf --aemrange -0.05 +0.05 --dimensions 192 192 \
    --imageout  assets/FiraSans-Regular.png \
    --json      assets/FiraSans-Regular.json

I used a low resolution SDF for the diagrams on this page. Depending on what sizes you render at and what effects you want, you will want a higher resolution and/or MSDF instead of SDF. See the section on rounded fonts for a comparison.

 7  My pages#

{TODO do I need this?}

 8  TODO Other pages#

{TODO need to organize and prune this list}

https://github.com/Blatko1/awesome-msdf[23] - is indeed awesome as its name suggests - lots of resources, including shaders with thickness, outlines, glow, drop shadow, gamma correction, plus links to discussions

wrappers around msdfgen:

- msdf-atlas-gen[24]

other implementations

other links

non distance field based approaches:

Other shaders I looked at:

Email me , or comment here: