Edge Coloring
An MSDF keeps corners sharp by giving the two edges at each corner to different channels. Which channels each edge feeds is its color. generate-msdf and generate-mtsdf color edges automatically, and most users never need this page. It is for choosing colors yourself, and for understanding what the automatic coloring does.
Colors and channels
| Color | Channels |
|---|---|
:white |
red, green, blue |
:cyan |
green, blue |
:magenta |
red, blue |
:yellow |
red, green |
:red, :green, :blue |
that one channel |
(sdf:edge-color-channels :magenta) ; => (:RED :BLUE)
The type sdf:edge-color is the seven keywords. MSDF edges must feed at least two channels, so only cyan, magenta, yellow and white are allowed in a field; an edge that fed one channel would be outvoted by the other two in the median.
Why it works: away from corners, an edge's two channels agree, and the median follows it. At a corner between, say, a cyan and a magenta edge, the shared blue channel follows whichever edge is nearer, while green follows the cyan edge's line and red the magenta edge's line, both extended past the corner. The median of the three traces the sharp corner.
Automatic coloring
(sdf:color-edges contours &key (colors '(:cyan :magenta :yellow)) (angle-threshold 3))
;; => a list of contours, each a list of colored-edge
color-edges is what the generators use, with its defaults, and its result can be passed to them in place of the contours. For each contour:
- Corners. A joint between consecutive segments, including the joint between the last and the first, is a corner if the outline turns sharply there (see the threshold). Directions are the curves' tangents at their ends; a control point that repeats an end point is skipped.
- No corners, a smooth loop such as a circle: every edge is white. All three channels are the same distance.
- One corner, such as a teardrop: the contour is taken from the corner round to itself. The first third of its edges gets the first palette color, the middle third white, and the last third the second color, so the two sides of the corner differ. A contour of fewer than three segments has each segment split into three equal Bézier pieces first.
- Two or more corners: each run of edges from one corner to the next gets one color, taken in turn from the palette, never the same as the run before; the last run also differs from the first.
Point segments, whose points all coincide, are left out. The input is not modified.
With *square* from Home, *triangle* from Getting Started, and the helpers from Recipes:
(defun colors (contours &rest options)
(mapcar (lambda (contour) (mapcar #'sdf:colored-edge-color contour))
(apply #'sdf:color-edges contours options)))
(colors *square*) ; => ((:CYAN :MAGENTA :YELLOW :MAGENTA))
(colors *triangle*) ; => ((:CYAN :MAGENTA :YELLOW))
(colors (list (circle-contour 0 0 1))) ; => ((:WHITE :WHITE :WHITE :WHITE))
(colors *square* :colors '(:cyan :magenta)) ; => ((:CYAN :MAGENTA :CYAN :MAGENTA))
(colors '(((:cubic 0 0 2 1 -2 1 0 0)))) ; => ((:CYAN :WHITE :MAGENTA)), split in three
The palette
:colors lists the colors to cycle through: at least two distinct colors from :cyan, :magenta and :yellow. A contour with an odd number of corners needs three, because two colors cannot alternate round an odd cycle; with two, it signals an sdf-error that says so. The default palette always works.
The angle threshold
:angle-threshold is in radians, from 0 to π, and defaults to 3, about 172°. It is the angle between two edges, at the joint, below which the joint counts as a corner: a straight joint is π, a hairpin 0. So by default any joint where the outline turns by more than about 8° is a corner, and gentle joints between curve segments are not.
A smaller threshold treats more joints as smooth:
;; A regular hexagon: every joint is 120°, about 2.09 radians.
(defparameter *hexagon*
(list (polygon-contour (loop for i below 6
collect (list (cos (* i (/ pi 3))) (sin (* i (/ pi 3))))))))
(colors *hexagon*) ; => ((:CYAN :MAGENTA :YELLOW :CYAN :MAGENTA :YELLOW))
(colors *hexagon* :angle-threshold 2) ; => ((:WHITE :WHITE :WHITE :WHITE :WHITE :WHITE))
(colors *square* :angle-threshold 1.5) ; => ((:WHITE :WHITE :WHITE :WHITE)), 90° > 1.5
The threshold means the same over the whole range: below π/2, right angles are smooth too, and only spikes sharper than the threshold are corners. For the default and any threshold from π/2 up, this is msdfgen's test.
Treating a real corner as smooth rounds it in the field; treating a smooth joint as a corner costs nothing visible, but uses up color changes. The default is msdfgen's.
Coloring by hand
(sdf:make-colored-edge segment color) ; => colored-edge
(sdf:colored-edge-segment edge) ; => the segment
(sdf:colored-edge-color edge) ; => the color
Build a list of contours of colored edges, from color-edges or with make-colored-edge, and pass it to generate-msdf or generate-mtsdf in place of the contours:
(let ((colored (list (mapcar #'sdf:make-colored-edge
(rectangle-contour 0 0 1 1)
'(:cyan :magenta :cyan :magenta)))))
(sdf:generate-msdf colored 16 16))
The colored edges are the outline: their segments give the true distance, the inside test and the alpha channel, and their colors the red, green and blue channels. One argument holds both, so they cannot disagree. Passing color-edges' output with its defaults gives exactly the field the generator would make from the plain contours.
- Every edge is colored or none is. Mixing colored edges and plain segments signals an
invalid-contours-errorthat names the first one out of place. - Every edge must be cyan, magenta, yellow or white, or the generator signals an
sdf-errornaming the edge. - At every sharp corner, the two edges must have different colors other than white, so that they share exactly one channel. Otherwise that corner comes out rounded.
- Corners made by overlaps count. When contours cross, the filled region's boundary has corners where they cross, and edges disappear where they overlap. Automatic coloring is redone on the resolved boundary when that happens; colors you give are used as they are. Color by hand only outlines that do not overlap, or make sure your colors are valid on the resolved boundary.
generate-sdf accepts colored contours as well and ignores the colors, so an MTSDF's alpha channel is still exactly generate-sdf's field of the same input.
Colored edges are immutable, and may be shared between threads and fields.
When coloring goes wrong
A badly colored corner is drawn rounded, like an SDF corner, or leaves a small artifact near it. Two safety nets limit the damage: where the median of the channels would put a texel on the wrong side of the edge, the generator sets all three channels to the true distance; and error correction flattens texels that would draw artifacts between texels.