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.
Distance Field Atlas
The optional system cl-textrender/sdf is for renderers that draw text from
distance fields. A distance field stays sharp over a range of sizes and
makes outlines, glows and soft shadows cheap. This system makes each glyph
into a multi-channel true signed distance field (MTSDF) once, with
cl-sdf, and packs it into a
cl-atlas atlas of four bytes a
texel. Each face has an atlas of its own, which the face keeps and which
goes with it. The renderer uploads the atlases' pages as RGBA8 textures.
(asdf:load-system :cl-textrender/sdf)
(tr:with-font (font "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" :size 16)
(let* ((fields (cl-textrender/sdf:make-field-atlas)) ; :page-size, :texels-per-em, :spread
(region (cl-textrender/sdf:glyph-region fields font
(tr:font-glyph-id font #\A))))
(list (cl-atlas:region-width region) (cl-atlas:region-height region)
(multiple-value-list (cl-textrender/sdf:glyph-field-bounds region)))))
;; => (38 40 (-0.2421875 -0.25 0.9453125 1.0))
The A's field is 38 by 40 texels and covers the rectangle from (-0.24, -0.25) to (0.95, 1.0) in ems, around the glyph's origin.
Making a field atlas
(cl-textrender/sdf:make-field-atlas &key (page-size 512) (texels-per-em 32) (spread 1/4))
- page-size is the width and height of every page of every face's atlas, in texels.
- texels-per-em is the field's resolution: how many texels span an em. More keep corners sharp at larger sizes, and cost memory and time.
- spread is how far, in ems, each field reaches beyond the glyph's outline on every side. It limits how far outlines, glows and shadows can reach. A larger spread lets them reach further, and costs precision.
field-atlas-page-size, field-atlas-texels-per-em and
field-atlas-spread return the settings. A field atlas is a plain Lisp
object that needs no releasing: what it makes is kept with the faces it
draws, and goes with them, or with the field atlas once the collector takes
it. Make one and keep it for the life of the program.
(face-atlas field-atlas face) returns the cl-atlas atlas a face's fields
are packed into, made the first time. The face keeps it and frees it with
itself. Its cl-atlas:atlas-data is the face, so a renderer that holds one
of its pages reaches the face, and keeps it alive.
One field per face
Fields are made in ems, so one field serves every size of a face: the
fonts face-font gives of one face share them.
(let* ((face (tr:make-face "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"))
(fields (cl-textrender/sdf:make-field-atlas))
(small (tr:face-font face 16))
(big (tr:face-font face 64)))
(eq (cl-textrender/sdf:glyph-region fields small (tr:font-glyph-id small #\A))
(cl-textrender/sdf:glyph-region fields big (tr:font-glyph-id big #\A))))
;; => T
Fonts of different faces get fields of their own, even of the same file: make one face and ask it for each size. Weights of a variable font are different faces, and get fields of their own.
What a field holds
(cl-textrender/sdf:glyph-field-bounds region) ; => left, bottom, right, top
glyph-field-bounds returns the rectangle of ems the region covers, y up
from the glyph's origin on the baseline. The region's left texel column
starts at the left edge and its top row at the top. The rectangle reaches
spread ems beyond the outline on every side, and a texel is exactly
1/texels-per-em of an em.
Each texel has four channels:
- Red, green and blue hold the multi-channel distance. Their median draws the glyph with sharp corners.
- Alpha holds the true distance to the outline, for effects that reach away from the edge, where the median is unreliable.
Every channel maps a distance d, in ems, positive inside the glyph, to
0.5 + d / (2 × spread), clamped to the range 0 to 1: −spread ems is 0,
the edge 0.5, and +spread ems 1.
A glyph without ink, such as a space, has an empty region, whose
cl-atlas:region-page is nil, and bounds of zero.
Drawing a layout
(cl-textrender/sdf:map-layout-fields function field-atlas layout
&key (scale 1) (x 0) (y 0))
map-layout-fields calls function for each glyph of layout that has
ink, drawn at scale pixels per layout unit with the layout's top left at
pixel (x, y). It passes eight arguments:
- the glyph's region in its face's atlas;
- the pixel position x, y of the corner of the field's first texel, its top left in the atlas;
- the vector ux, uy that the field's rows run along;
- the vector vx, vy that its columns run down;
- the
cl-textrender:glyph, for its font, style data and so on.
The field covers the quad from (x, y) to (x + ux + vx, y + uy +
vy). Draw it with the region's texture coordinates, cl-atlas:region-uv,
running from the first corner along u and down v:
| Corner | Texture coordinates |
|---|---|
| (x, y) | (u0, v0) |
| (x + ux, y + uy) | (u1, v0) |
| (x + ux + vx, y + uy + vy) | (u1, v1) |
| (x + vx, y + vy) | (u0, v1) |
(tr:with-font (font "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" :size 16)
(let ((fields (cl-textrender/sdf:make-field-atlas))
(quads '()))
(cl-textrender/sdf:map-layout-fields
(lambda (region x y ux uy vx vy glyph)
(declare (ignore region glyph))
(push (list x y ux uy vx vy) quads))
fields (tr:layout-text "Hi" font) :scale 2 :x 10 :y 20)
(nreverse quads)))
;; => ((5.140625 17.703125 34.0 0.0 0.0 40.0)
;; (29.078125 16.703125 19.0 0.0 0.0 41.0))
Upright glyphs have their rows along x and their columns down y, as here.
A glyph turned in vertical text (glyph-rotated-p) gets its quad turned with
it: its rows run down and its columns to the left, and the same drawing
code handles it. Positions are exact, not snapped to pixels, and rows grow
downwards from the layout's top whatever its y axis. Fields are made as
they are first met, which takes milliseconds each; once they all have
been, map-layout-fields allocates nothing.
Placing one glyph yourself
For a single upright glyph at pixel (gx, gy), y down, in a font drawn at s pixels per em (the font's size times the scale), the field's quad is
left = gx + field-left × s top = gy - field-top × s
right = gx + field-right × s bottom = gy - field-bottom × s
where glyph-field-bounds gives the field's left, bottom, right and top in
ems.
The shader
In the fragment shader, sample with linear filtering and turn the sample into a distance in pixels:
d = (median(r, g, b) - 0.5) × 2 × spread × s ; pixels, positive inside
alpha = clamp(d + 0.5, 0, 1) ; coverage of the glyph
For effects, use the true distance in the alpha channel the same way:
(a - 0.5) × 2 × spread × s pixels. An outline of width w pixels covers
where that distance is above −w; a glow fades out as it falls; a shadow
samples it at an offset. None can reach further than spread ems.
Recipes draws a glyph this way on the CPU, which shows the arithmetic in full.
The pixels per em, s, are the scale times the glyph font's size: 32 in the example above, where a field's texel lands on exactly one pixel.
Uploading pages
Pages are four bytes a texel, red, green, blue and alpha: an RGBA8
texture, without sRGB conversion. Upload the pages of face-atlas for each
face you draw with as for the glyph atlas, with
cl-atlas:take-page-changes and cl-atlas:copy-page-region; see
Glyph Atlas.
Threads and time
Making a field takes a few milliseconds per glyph. A page of new text made
on the render thread would stall a frame, so a renderer that must not stall
calls glyph-region on worker threads, and uploads at a frame
boundary. Finding a field made before allocates nothing, so a renderer may
look fields up every frame.
A field atlas may be used from any number of threads at once. Fields are made outside the atlas's lock, so fields for different glyphs are made in parallel; if two threads make the same field, the first packed is kept.
Errors
glyph-region signals textrender-error if a glyph's outline cannot
be made into a field, which happens only for a malformed outline. Draw that
glyph another way, such as rasterized. A glyph ID the font does not have is
no error: its region is .notdef's, made once for them all.