Table of Contents
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.
Drawing with Fields
cl-sdf makes fields; drawing them is the renderer's work. This page gives the arithmetic, in GLSL for the shader parts. Pyssel's SDF painter does the same in cl-gsl.
The quad
Draw a quad over the field's bounds, transformed into screen space like the shape itself. For bounds (xmin ymin xmax ymax), the corners and their texture coordinates are:
| Corner | Shape position | u | v with :y-axis :up |
v with :y-axis :down |
|---|---|---|---|---|
| bottom left | (xmin, ymin) | 0 | 0 | 1 |
| bottom right | (xmax, ymin) | 1 | 0 | 1 |
| top left | (xmin, ymax) | 0 | 1 | 0 |
| top right | (xmax, ymax) | 1 | 1 | 0 |
v = 0 is the texture's first row, as uploaded. In an atlas, map 0..1 into the field's region of the atlas.
For a glyph at a pen position, the shape position is the outline's position relative to the glyph's origin; with outlines in ems, multiply by the font size in pixels and add the pen position. cl-textrender's Distance Field Atlas page works through it for a y-down screen.
From a sample to coverage
A sample s decodes to a distance in shape units, positive inside:
distance = (s − 0.5) × 2 × spread
Multiplied by the number of screen pixels per shape unit, that is a distance in pixels, and a pixel whose center is d pixels inside the edge is covered by about clamp(d + 0.5, 0, 1). For an MSDF or MTSDF, s is the median of the red, green and blue samples, each interpolated by the texture unit first; for an SDF it is the one channel.
The pixels per shape unit are known for flat 2D drawing at a fixed scale. For any transform, including rotation and perspective, the shader can find them from the derivatives of the texture coordinates. The full range of a field, 2 × spread, is a fixed number of texels:
range in texels = 2 × spread × width / (xmax − xmin)
which is 2 × px-spread when the spread was left to :px-spread. Pass it to the shader:
#version 450
layout(set = 0, binding = 0) uniform sampler2D field; // UNORM, linear filtering
layout(push_constant) uniform Params {
vec4 fillColor;
float rangeTexels; // 2 × spread × texels per shape unit
} params;
layout(location = 0) in vec2 uv;
layout(location = 0) out vec4 outColor;
float median(float r, float g, float b) {
return max(min(r, g), min(max(r, g), b));
}
// The field's full range, 2 × spread, in screen pixels at this fragment.
float screenPxRange() {
vec2 unitRange = vec2(params.rangeTexels) / vec2(textureSize(field, 0));
vec2 screenTexSize = vec2(1.0) / fwidth(uv);
return max(0.5 * dot(unitRange, screenTexSize), 1.0);
}
void main() {
vec4 s = texture(field, uv);
float distance = screenPxRange() * (median(s.r, s.g, s.b) - 0.5); // pixels, + inside
float fill = clamp(distance + 0.5, 0.0, 1.0);
outColor = vec4(params.fillColor.rgb, params.fillColor.a * fill);
}
screenPxRange() is msdfgen's: it works in atlas texture coordinates too, since the range in texels does not depend on where the field is. The max(…, 1.0) keeps the edge from vanishing when a field is drawn smaller than its texels. For an SDF, replace the median with s.r.
Recipes does the same on the CPU in Lisp, which is a convenient way to check a field before writing any shader.
Effects from the true distance
The median is only right near the edge. For anything that reaches away from it, use an MTSDF's alpha channel, the true distance, decoded the same way. The snippets below continue main() above, and assume Params also holds outlineColor, outlineWidth and softness (in pixels) and shadowOffsetUV:
float trueDistance = screenPxRange() * (s.a - 0.5); // pixels, + inside
Outline. An outline w pixels wide outside the glyph is the glyph grown by w:
float outline = clamp(trueDistance + params.outlineWidth + 0.5, 0.0, 1.0);
vec4 color = mix(params.outlineColor, params.fillColor, fill);
outColor = vec4(color.rgb, color.a * outline);
Glow and soft shadow. Sample alpha again, at an offset for a shadow or none for a glow, and fade it over the softness k pixels outside the edge:
float shadowDistance = screenPxRange() * (texture(field, uv - params.shadowOffsetUV).a - 0.5);
float shadow = smoothstep(-params.softness, 0.0, shadowDistance);
// Draw the shadow color with alpha × shadow, under the fill.
Bolder or lighter. Add a few tenths of a pixel to distance before taking coverage to embolden, or subtract to thin. Large offsets round the corners, since they move the edge into the region where the median is no longer a distance.
Limits
- Reach. Nothing beyond the spread can be seen: the samples are clamped there. An outline's width plus a shadow's softness and offset must stay within the spread, measured in the same units. Pyssel limits them to its fields' quarter em.
- Size. A field drawn larger than its texels shows smooth curves and sharp corners for an MSDF, but features thinner than a texel or two cannot be represented at all; make the field larger. A field drawn very small, where its whole range is under a pixel or two, can no longer anti-alias. Give it a larger
:px-spread, or use a bitmap at small sizes. - Filtering. Fields need linear filtering, a UNORM format, no sRGB conversion, and no mipmaps for MSDF; see Reading Fields.
- Quad size. The quad covers the bounds, so the padding is the room an effect has to be drawn in. With padding at least the spread, effects are never clipped by the quad's edge.