Skip to content

Slug filled paths

Slug (Eric Lengyel's GPU curve-rendering algorithm, JCGT 2017) renders a filled FLine as analytic curve coverage on the GPU. The path can contain straight edges, cubic curves and holes. Its six-vertex bounding quad moves with the camera; the fragment shader evaluates the actual outline within it.

Slug is experimental. Use it when you want a filled planar shape whose boundary stays smooth as its size changes. It does not replace FLine strokes or triangulation.

Start with the example

Build the optional helper from the repository root. This needs Git, a C++20 compiler and network access on the first build. The script fetches the pinned Slughorn revision; it is not part of ordinary Field startup.

bash native/field-slug/build.sh

For a small self-contained demo, drag slug_starter.edn into Field, launch Slug renderer, then 5e. Slug filled paths. It embeds the code and both shaders, so importing this single file needs no source sidecars.

For the full curriculum, open Welcome → Starting points → canonical_graphics, launch its window box, then 5e. Slug filled paths. The lesson draws a curved outline with a square hole. Focus the renderer window and use WASD/arrow keys to move, E/C to rise or fall, and R to reset the camera. There is no automatic orbit.

The renderer window: a filled shape with three straight sides and one bulging cubic side, a square hole cut through its middle, shaded with diagonal pastel rainbow bands, its curved edge smooth

The lesson uses "field-slug".file. FileManager registers the helper relative to Field's loaded installation/checkout; a document or project files/field-slug can override it. This is the compiler executable; SlugPath.compile creates and cleans up its own temporary input/output files.

The complete box code, vertex shader and fragment shader are a starting point for your own box. Keep it connected beneath the renderer.

Geometry and appearance

SlugPath.compile prepares immutable curve and band data using a small native subprocess. Its tolerance is measured in the FLine's own coordinate units. Smaller tolerance subdivides cubics more finely and increases preparation work. The fill rule is nonzero winding: reverse an inner contour to make a hole. Open contours are closed for filling. All coordinates, including cubic controls, must be finite and lie in the XY plane (z = 0).

val prepared = SlugPath.compile(
    path,
    "field-slug".file,
    tolerance = 1e-4f,
)
val data = SlugBuffer(renderer, prepared)

Camera motion and tint changes leave this data alone. To change geometry, prepare a fresh SlugPath, then call data.replace(newPath) on the render thread. The stable address supplier starts using the new allocation; Field retires the previous one after pending frames finish. Preparation starts a process, so keep it out of the frame loop. For frequent edits, prepare away from rendering and publish the finished result on the render thread.

Close the buffer when the box stops or runs again. The mesh can be removed before or after: a mesh still bound to a closed buffer draws nothing. close() can be called more than once, and a closed buffer cannot be replaced.

Shading and transparency

Edit shade() in the fragment shader to change the fill. Its input is the path's local XY coordinate. Coverage controls alpha independently of the procedural color. The demo uses straight-alpha RGB blending and ONE for the source alpha factor, so a half-transparent interior has alpha 0.5 rather than 0.25. Depth writes are disabled: transparent layers still need an appropriate drawing order.

The shared push block occupies 96 bytes: a combined model/view/projection matrix, buffer address, current framebuffer dimensions and tint. Framebuffer dimensions must follow the render target, particularly when resizing or rendering offscreen.

Limits

Each atlas holds one shape, in 512-wide packed textures. The native helper builds on Linux and macOS only, and Slug does not shape fonts. Strokes, glyph loading, multiview VR and incremental atlas packing are not supported. A plane that crosses the near plane or is seen nearly edge-on may not draw correctly, so keep the filled plane comfortably in front of the camera.