3 Reading Fields
dxtr edited this page 2026-10-18 16:23:24 +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.

Reading Fields

The generators return a field: the samples, and everything needed to place and decode them.

The field object

Reader Returns
(field-width field) Columns, a positive integer.
(field-height field) Rows, a positive integer.
(field-channels field) 1 (SDF), 3 (MSDF) or 4 (MTSDF).
(field-data field) The samples, a (simple-array single-float (*)); see Layout.
(field-bounds field) The list (xmin ymin xmax ymax) of double-floats the texels cover, in shape units, after padding: a rectangle, which :bounds takes back, unless padding took it beyond ±1e36.
(field-spread field) The distance, in shape units, that maps to 0 and 1.
(field-y-axis field) :up if row zero is at ymin, :down if it is at ymax.
(field-normalized-p field) True if samples are encoded 0..1; false if they are raw distances.

All of these are fixed when the field is made. field-bounds returns the field's own list: do not modify it. Only the samples in field-data may change, through (setf field-ref) or the vector itself.

With *square*, the unit square from Home:

(defparameter *field* (sdf:generate-mtsdf *square* 64 64 :padding 0.25))

(list (sdf:field-width *field*) (sdf:field-height *field*) (sdf:field-channels *field*)
      (sdf:field-y-axis *field*) (sdf:field-normalized-p *field*))
;; => (64 64 4 :UP T)
(sdf:field-bounds *field*)  ; => (-0.25d0 -0.25d0 1.25d0 1.25d0)
(sdf:field-spread *field*)  ; => 0.09375d0

A field prints briefly, without its samples:

*field*  ; => #<CL-SDF:FIELD 64 by 64, 4 channels {1004A1B2C3}>

A raw field adds , raw.

Layout

field-data is row-major with the channels of a texel together:

index = (row × width + column) × channels + channel

Columns run from xmin to xmax. Row zero is at ymin when field-y-axis is :up, and at ymax when it is :down. Texel (column, row) holds the distance at its center:

x = xmin + (column + 1/2) × (xmax − xmin) / width
y = ymin + (row + 1/2) × (ymax − ymin) / height     ; :up
y = ymax − (row + 1/2) × (ymax − ymin) / height     ; :down

This is the layout of a texture uploaded row by row, so the first row of the data is the texture's first row, at texture coordinate v = 0. With :up, v = 0 is the bottom of the bounds; with :down, the top.

Reading samples

(sdf:field-ref *field* 32 32 3)       ; => 1.0, the stored sample
(sdf:field-distance *field* 32 32 3)  ; => 0.09375d0, decoded to shape units
(sdf:field-distance *field* 0 0 3)    ; => -0.09375d0

(field-ref field column row &optional (channel 0)) returns the stored single-float. field-distance takes the same arguments and returns a double-float distance in shape units: for a normalized field it decodes the sample, so the result lies within ±spread; for a raw field it is the sample itself. A column, row or channel out of range signals an sdf-error that gives the valid ranges.

(setf (field-ref field column row channel) value) stores any real within the single-float range, as a single-float; a larger one is a type-error. Nothing checks that it stays in 0..1.

Bytes for a texture

(sdf:field-octets field)  ; => (simple-array (unsigned-byte 8) (*))

field-octets returns a fresh vector of bytes in the same layout as field-data, one byte per channel. Each sample is clamped to 0..1 and mapped to the nearest of 0..255, so the edge, 0.5, becomes 128. The field must be normalized; a raw field signals an sdf-error.

To view the bytes as an image of rows, displace a 2D array onto them, as cl-textrender/sdf does for cl-atlas:

(let ((octets (sdf:field-octets field)))
  (make-array (list (sdf:field-height field)
                    (* (sdf:field-channels field) (sdf:field-width field)))
              :element-type '(unsigned-byte 8)
              :displaced-to octets))

Uploading to a GPU

Field Texture format (Vulkan name) Notes
SDF R8_UNORM
MSDF R8G8B8A8_UNORM Pad to four channels first: few GPUs can sample R8G8B8_UNORM. See Recipes.
MTSDF R8G8B8A8_UNORM
  • Use a UNORM format, never sRGB: the samples are distances, not colors, and sRGB decoding would bend them.
  • Sample with linear filtering. The whole method depends on bilinear interpolation.
  • Clamp texture coordinates to the edge, or keep a gutter of texels around each field in an atlas, so the filter does not read a neighbor's texels.
  • Avoid mipmaps for MSDF and MTSDF: averaging the channels of distant texels breaks their median. Fields are usually drawn from their full-resolution level anyway; the spread does the job mipmaps would.

For distances finer than 8 bits allow, upload field-data itself as a 32-bit float texture, or convert it to 16-bit floats yourself; the samples are normalized the same way.

Threads

A field may be read from any number of threads at once. Writing with (setf field-ref) while another thread reads or writes the same field needs your own synchronization. Different fields are independent.