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.
Shaders
The system cl-slug/clsl holds the Slug pixel shader as clsl shader functions, ported from Eric Lengyel's reference shader. A program's own vertex shader places each shape's quad; its fragment shader calls slug-coverage.
slug-coverage
(cl-slug/clsl:slug-coverage curves bands render banding data) => :float
How much of the pixel at render the shape covers, from 0 to 1.
| Argument | clsl type | What it is |
|---|---|---|
curves |
:sampler2d |
The curve texture, :rgba16-float. |
bands |
:usampler2d |
The band texture, :rg16-uint. |
render |
:vec2 |
The pixel's position in the shape's ems, interpolated. |
banding |
:vec4 |
The first four floats write-shape-data writes: the band scales and offsets. Flat. |
data |
:ivec4 |
The last four: where the shape's data starts, and the indices of its last bands, with the even-odd flag. Flat. |
write-shape-data writes the eight numbers into a vector of single-floats, such as an instance buffer, without allocating:
(cl-slug:write-shape-data shape instances start)
;; INSTANCES[START .. START+3]: banding
;; INSTANCES[START+4 .. START+7]: data, whole numbers, which floats hold exactly
The vertex shader reads the eight as two vec4 attributes and turns the second into an ivec4 with truncate. +shape-data-floats+ is 8.
render's screen-space derivatives give the size of a pixel in ems, through fwidth, so call slug-coverage where control flow is uniform: not inside a branch that neighbouring pixels may take differently. clsl checks this when the shader is defined, and refuses a call that may not be.
A program that calls slug-coverage needs a device that keeps the NaNs of floats, which clsl's guard on turning a float into an integer checks for. program-features reports it as :signed-zero-inf-nan-preserve-float32, the name cl-gfx's device-features gives it; nearly every Vulkan 1.4 device has it.
A minimal fragment shader, in a package that uses CLSL:
(define-shader text-fragment :fragment
((color :vec4 :interpolation :flat) (em :vec2)
(banding :vec4 :interpolation :flat) (data :ivec4 :interpolation :flat)
&uniform (curves :sampler2d) (bands :usampler2d))
(* color (cl-slug/clsl:slug-coverage curves bands em banding data)))
That multiplies every channel, alpha included, by the coverage: a colour of premultiplied alpha, for blending with src = 1, dst = 1 - src alpha. For straight alpha, multiply only the alpha.
The fill rule
The even-odd rule is checked at run time, from bit 12 of data's fourth component, which write-shape-data sets for an :even-odd shape. So one pipeline draws shapes of either rule. With the nonzero rule the coverage is clamped to 0 to 1; with even-odd it folds, so that 2 is 0 and 3 is 1.
The vertex shader
For each shape drawn, the vertex shader:
- Draws a quad over the shape's bounds,
shape-xmintoshape-xmaxandshape-ymintoshape-ymax, a pixel larger on every side, so the anti-aliased edge has room. - Passes each corner's position in ems, which the GPU interpolates to
render. - Passes
bandinganddata, which are the same at every corner.
How many ems a pixel is depends on the transform. For an affine transform, which 2D drawing uses, it is the length of an em's axis on screen:
(define-shader shape-vertex :vertex
((origin :vec2) (axis-x :vec2) (axis-y :vec2) (bounds :vec4)
(banding :vec4) (data :vec4) (color :vec4)
&uniform (target-size :vec2))
;; One instance per shape, four vertices: ORIGIN is where the em (0, 0)
;; lands, in pixels from the target's top left, and AXIS-X and AXIS-Y
;; where an em right and an em up go from there. BANDING and DATA are
;; what WRITE-SHAPE-DATA wrote.
(let* ((index (vertex-index))
(corner (vec2 (float (mod index 2)) (float (floor index 2))))
(pixels-per-em (vec2 (norm axis-x) (norm axis-y)))
(pad (/ (vec2 1) pixels-per-em))
(em (mix (- (swizzle bounds :xy) pad) (+ (swizzle bounds :zw) pad) corner))
(pixel (+ origin (* axis-x (x em)) (* axis-y (y em)))))
;; Pixels run down from the target's top left, as Vulkan's clip space
;; does.
(values (vec4 (- (* 2 (/ (x pixel) (x target-size))) 1)
(- (* 2 (/ (y pixel) (y target-size))) 1)
0 1)
color
em
banding
(truncate data))))
Its instance is 22 floats: origin, axes and bounds, ten, from the renderer; then the eight write-shape-data writes; then the colour. This is what Pyssel's Slug painter does, in pyssel/2d/slug.lisp.
Under a perspective transform, a fixed pad in ems is not a pixel everywhere. The reference vertex shader dilates each corner along its normal by the right amount for the full model-view-projection matrix; port its SlugDilate for 3D text.
The reference vertex format
The reference shaders take, per vertex, a tex attribute whose z and w hold the shape's data as two 32-bit words. shape-glyph-words packs them:
(cl-slug:shape-glyph-words shape) => tex-z, tex-w
tex-z:shape-data-xin bits 0–15,shape-data-yin bits 16–31.tex-w: the index of the last vertical band in bits 0–15, of the last horizontal band in bits 16–23, and bit 28 set for the even-odd rule.
A renderer using the reference HLSL shaders stores these words in the float attribute's bits. With slug-coverage, use write-shape-data instead.
Credit
The port keeps the reference shader's arithmetic, so its results match. The reference shaders are Copyright 2017 by Eric Lengyel and under the MIT License. Software that uses them, and is distributed, must give credit: the text is in cl-slug's NOTICE. The Slug patent was dedicated to the public domain on 2026-03-17.