3 Shaders
dxtr edited this page 2026-10-15 09:00:37 +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.

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:

  1. Draws a quad over the shape's bounds, shape-xmin to shape-xmax and shape-ymin to shape-ymax, a pixel larger on every side, so the anti-aliased edge has room.
  2. Passes each corner's position in ems, which the GPU interpolates to render.
  3. Passes banding and data, 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-x in bits 0–15, shape-data-y in 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.