1 Concepts
dxtr edited this page 2026-10-12 00:19:57 +02:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Concepts

Distance fields

A bitmap of a shape stores, for each pixel, how much of it the shape covers. Scaled up, it blurs or turns blocky. A distance field stores, for each texel, how far the texel's center is from the shape's edge instead: positive inside, negative outside. Distance changes smoothly, so the GPU's bilinear filter can interpolate it between texels, and the edge can be found again wherever the interpolated distance is zero. A shader turns that distance into coverage at whatever size the shape is drawn, so a field of 32 by 32 texels can draw a glyph cleanly at many sizes.

The same distance also says where the outline of a given width lies, how far a glow should fade, or where a soft shadow ends. These effects cost one texture sample.

SDF, MSDF and MTSDF

SDF (generate-sdf) has one channel, the true signed distance to the nearest point of the edge. Interpolating it rounds off corners: a texel near a corner only knows how far the corner is, not where its two edges go. The radius of the rounding is about a texel.

MSDF (generate-msdf), the multi-channel signed distance field from Viktor Chlumský's msdfgen, has three channels. Each edge of the outline is given a color, which says which of the red, green and blue channels it contributes to (see Edge Coloring). At a sharp corner, the two edges have different colors. Each channel holds the distance to the nearest edge that feeds it. The shader takes the median of the three interpolated channels, and the median follows both edges right into the corner.

The median is only a distance near the edge. Further out, it measures distance to the lines the edges lie on, not to the edges, so it grows wrong away from corners. It is good for the fill, and unreliable for anything that reaches away from the edge.

MTSDF (generate-mtsdf), multi-channel and true signed distance field, is the MSDF in red, green and blue plus the true SDF in alpha. The median draws the shape with sharp corners; alpha drives outlines, glows and shadows. Its alpha is exactly what generate-sdf would produce.

Here is a triangle drawn from fields of 8 by 8 texels, scaled up five times, by the CPU drawing code in Recipes. The SDF has lost the apex and rounded the base corners; the MSDF keeps them:

SDF                                         MSDF



                                                               ..
                                                              .##.
                 ......                                      .####.
                .######.                                    .######.
               .########.                                  .########.
              .##########.                                .##########.
             .############.                              .############.
            .##############.                            .##############.
           .################.                          .################.
          .##################.                        .##################.
         .####################.                      .####################.
         =####################=                     .######################.
       .=*####################*=.                  .########################.
       -==+******************+==-                 .**************************.

Which to use

Kind Channels Sharp corners Effects reaching away from the edge Use for
SDF 1 No Yes Shapes without sharp corners, or drawn small; effects only; the smallest textures.
MSDF 3 Yes No Text and icons without effects. Pad to four channels for upload.
MTSDF 4 Yes Yes Text and icons with outlines, glows or shadows. What Pyssel uses.

MTSDF costs a third more memory than MSDF and the same to generate, and it is a natural RGBA8 texture, so it is the usual choice.

Shape units, bounds and texels

Outlines are given in shape units: whatever units the contours use. Glyph outlines from a font of size 1 are in ems; an icon might be in its own design units. cl-sdf never assumes a unit.

A field covers a rectangle of shape space, its bounds, (xmin ymin xmax ymax). By default this is the outline's bounding box (taken from its control points), grown on every side by :padding; or you give :bounds yourself. The rectangle is cut into width × height texels, and each texel stores the distance at its center: texel column i samples at xmin + (i + 1/2) × (xmax − xmin) / width.

Row zero is at the bottom of the bounds by default (:y-axis :up), as in a y-up shape space and OpenGL's texture convention. :y-axis :down puts row zero at the top, which is what a texture that is uploaded top row first, as in Vulkan and Direct3D, usually wants. See Reading Fields.

Spread

Distances are stored normalized to 0..1 by default. The spread is the distance, in shape units, that maps to the ends of that range:

stored = 0.5 + distance / (2 × spread),  clamped to 0..1

  −spread  →  0.0     (spread units outside)
        0  →  0.5     (on the edge)
  +spread  →  1.0     (spread units inside)

So spread is a half range. Tools such as msdfgen describe the full range instead, with an option like -pxrange 8; the same field in cl-sdf has a spread of 4 texels.

When you give no :spread, cl-sdf uses :px-spread texels, 4 by default, converted to shape units with the larger of a texel's width and height. field-spread returns the spread that was used; a shader needs it to turn samples back into distances.

The spread is a trade:

  • A shader can only see distances within the spread. Outlines, glows and shadows cannot reach further than the spread outside the edge.
  • An 8-bit texture has 256 steps across 2 × spread. A larger spread makes each step coarser.
  • The spread shrinks on screen with the shape. When a field is drawn so small that its whole range, 2 × spread, covers less than a pixel or two, the shader can no longer anti-alias the edge. A larger spread lets a field be drawn smaller.

:normalize nil stores raw distances in shape units instead, for uses other than textures: physics, procedural geometry, or your own encoding.

Signs and orientation

Distances are positive inside the filled shape and negative outside. What is inside is decided by the fill rule, :nonzero by default, or :even-odd, over all the contours together. cl-sdf works out the filled region's boundary itself, so:

  • Contours may run clockwise or counterclockwise. Reversing every contour draws the same shape.
  • Contours may overlap or cross themselves. The field is of their union, as the fill rule fills it.

See Contours and Fill Rules.

What generating does

  1. Checks the arguments, and works out the bounds and the spread.
  2. Flattens curves into short lines, within 1/32 of a texel by default.
  3. Resolves the boundary of the filled region: splits edges where they cross, drops edges with fill on both sides or neither, and orients the rest with the fill on their left.
  4. For SDF and alpha: at every texel center, the distance to the nearest boundary edge, signed by whether the center is inside.
  5. For MSDF: colors the edges, and at every texel center, each channel's distance to its own nearest edge, extended past the edge's ends so corners stay sharp. Where the channels' median would put the texel on the wrong side, all three are set to the true distance.
  6. For MSDF: error correction, which flattens texels that would draw stray artifacts when interpolated with their neighbors. See Error Correction.
  7. Encodes the distances with the spread, and stores them.

How It Works explains each step in detail.

Threads

cl-sdf has no global state. Generators and queries may run on any number of threads at once, and fields of different glyphs can be made in parallel on worker threads, which is what Pyssel does. Shapes and colored edges are immutable and may be shared. A field may be read from many threads; writing one with (setf field-ref) while others read it needs your own lock.