3 Uniforms and Resources
dxtr edited this page 2026-10-14 03:35:08 +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.

Uniforms and Resources

Uniforms are values and resources that stay the same for every vertex, fragment or invocation of a draw or dispatch: a camera matrix, a tint, a texture, a storage buffer. Shader code reads them but cannot assign them, apart from writing into storage buffers and images.

There are two ways to declare one:

  • in a stage's parameter list, after &uniform;
  • as a shader variable, with define-variable, which any stage or function may read.

Declaring uniforms in a stage

After &uniform in a stage's parameter list, each uniform is (name type &key group storage format access):

(define-shader sprite-vertex :vertex ((position :vec2) (uv :vec2)
                                      &uniform (view :mat4 :group :frame))
  (values (* view (vec4 position 0 1)) uv))

(define-shader sprite-fragment :fragment ((uv :vec2)
                                          &uniform (tint :vec4)
                                                   (image :sampler2d :group :draw))
  (* (sample image uv) tint))
Key For Values
:group every uniform :frame, :material (the default) or :draw
:storage storage buffers :read or :read-write
:format storage images an image format, such as :rgba8-unorm
:access storage images :read, :write or :read-write (the default)

&uniform is cl-gsl's symbol. In a package that uses cl-gsl, as the examples here do, it needs no prefix; elsewhere write it through a nickname, as gsl:&uniform.

Two stages of a program that declare the same uniform share it, and must declare it alike. A shader function has no uniforms of its own: its stage passes them to it as arguments, textures included, or it reads shader variables.

Groups and bind groups

Uniforms fall into groups by how often they change, as Godot's uniform sets do. Each group is one bind group, or descriptor set:

Group Bind group For
:frame 0 What the engine sets once for a frame or pass, such as the camera or the target's size.
:material 1 What a material sets. The default.
:draw 2 What changes from draw to draw, such as a sprite's texture.

(uniform-group-index :draw) returns 2.

Within a group:

  • Binding 0 is the group's uniform block. It holds the group's uniforms of value types, laid out by the rules of std140: numbers, vectors and matrices of numbers, complex numbers, ratios, and structs and arrays of them. A :bool cannot be in a block; use an :int or :uint.
  • Bindings from 1 hold the group's resources: textures, storage buffers and storage images.

Both are in the order the stages first declare them, vertex before fragment.

For the two sprite shaders above:

(defvar *sprites* (compile-program :vertex 'sprite-vertex :fragment 'sprite-fragment))

(program-uniforms *sprites*)
;; => (#S(UNIFORM :NAME VIEW :TYPE :MAT4 :GROUP :FRAME :OFFSET 0)
;;     #S(UNIFORM :NAME TINT :TYPE :VEC4 :GROUP :MATERIAL :OFFSET 0))
(program-resources *sprites*)
;; => (#S(RESOURCE :NAME IMAGE :TYPE :SAMPLER2D :GROUP :DRAW :BINDING 1
;;                 :KIND :TEXTURE :ACCESS NIL :FORMAT NIL))
(program-bindings *sprites*)
;; => (#S(PROGRAM-BINDING :GROUP :FRAME :SET 0 :NUMBER 0 :KIND :UNIFORM-BLOCK :STAGES (:VERTEX))
;;     #S(PROGRAM-BINDING :GROUP :MATERIAL :SET 1 :NUMBER 0 :KIND :UNIFORM-BLOCK :STAGES (:FRAGMENT))
;;     #S(PROGRAM-BINDING :GROUP :DRAW :SET 2 :NUMBER 1 :KIND :SAMPLER2D :STAGES (:FRAGMENT)))
(program-uniform-block-size *sprites* :material)
;; => 16

program-bindings is what a GPU layer builds its bind group layouts from. A binding's kind is :uniform-block, :storage-buffer, :read-only-storage-buffer, or a texture or image type; texture-type-p and image-type-p tell those apart. See Programs for every query and the readers of what they return.

Shader variables

define-variable defines a Lisp special variable, as defvar does, and makes it a uniform that any shader code may read by name:

(define-variable *sun-direction* :vec3
  :value (vector 0.0 1.0 0.0) :group :frame
  :documentation "The direction light comes from.")

(define-function sunlit ((normal :vec3))
  (max (dot normal *sun-direction*) 0))

(define-shader sunlit-fragment :fragment ((normal :vec3) &uniform (tint :vec4))
  (* tint (sunlit normal)))

(program-uniforms (compile-program :fragment 'sunlit-fragment))
;; => (#S(UNIFORM :NAME *SUN-DIRECTION* :TYPE :VEC3 :GROUP :FRAME :OFFSET 0)
;;     #S(UNIFORM :NAME TINT :TYPE :VEC4 :GROUP :MATERIAL :OFFSET 0))
  • The syntax is (define-variable name type &key value group documentation). group defaults to :material.
  • Like defvar, it sets the value only if the variable has none.
  • Its type is a scalar, vector or matrix of numbers.
  • A stage that reads it, directly or through any function it calls, has it among its uniforms. You do not list it in the stage's &uniform.
  • Shader code cannot assign it.
  • pack-uniforms fills it from the Lisp variable's current dynamic value, so let can rebind it for one pass.
  • Redefining it makes programs that read it stale; see Programs. remove-definition forgets it as a shader variable, but the Lisp variable stays.

Packing uniform buffers

pack-uniforms writes a program's uniform block for one group into a byte vector, ready to upload:

(defvar *sunlit* (compile-program :fragment 'sunlit-fragment))

(defvar *frame-bytes*
  (make-array (program-uniform-block-size *sunlit* :frame) :element-type '(unsigned-byte 8)))
(defvar *material-bytes*
  (make-array (program-uniform-block-size *sunlit* :material) :element-type '(unsigned-byte 8)))

;; Each frame:
(pack-uniforms *sunlit* :frame :buffer *frame-bytes*)
(pack-uniforms *sunlit* :material :values (list :tint #(1.0 1.0 1.0 1.0))
                                  :buffer *material-bytes*)

(pack-uniforms program group &key values buffer):

  • Shader variables come from their Lisp variables.
  • Every other uniform comes from values, a plist of the uniforms' names, as keywords, and their values.
  • buffer is a vector of (unsigned-byte 8) at least program-uniform-block-size long. Given a buffer, pack-uniforms allocates nothing, so it is safe in a frame loop. Without one it returns a fresh vector.
  • A uniform missing from values is an error that names it, as is a group with no uniforms.

(pack-value type value buffer &key offset rules) writes one value at offset, by :std140 (the default) or :std430. It writes the bytes the value covers, not the padding, and allocates nothing. Values are written as Lisp holds them:

Shader type Lisp value
scalar a number
vector a sequence of components, such as #(1.0 0.5 0.5 1.0) or '(7 -1)
matrix a sequence of components, column by column, as cl-gmath keeps them
:complex, :dcomplex a Lisp complex, such as #c(1.0 -2.0)
:ratio a Lisp ratio, such as 2/3
array a sequence of elements
struct a plist of its slots, as its constructor takes them: (:position #(0 1 0) :color #(1 1 1))
(define-struct light (position :vec3) (color :vec3))

(let ((buffer (make-array 32 :element-type '(unsigned-byte 8) :initial-element 0)))
  (pack-value 'light '(:position #(1 2 3) :color #(1 1 1)) buffer))

A value of the wrong shape is a gsl-error that says what was expected, such as "is not a value of the shader type :VEC3: a sequence of 3 components".

Storage buffers

A storage buffer is a uniform with :storage :read or :storage :read-write. It holds any type a uniform block holds, laid out by std430, and may be or end in an array without a length, (:array type), whose length is known as the shader runs. A struct whose last slot is such an array may only be held by a storage buffer.

(define-struct particle
  (position :vec4)
  (velocity :vec4))

(define-shader advance (:compute :workgroup-size (64))
    (&uniform (particles (:array particle) :storage :read-write)
              (dt :float :group :frame))
  (let ((i (x (global-invocation-id))))
    (when (< i (uint (length particles)))
      (let ((p (aref particles i)))
        (incf (particle-position p) (* dt (particle-velocity p)))
        (setf (aref particles i) p)))))

Shader code reads elements and slots of a storage buffer with aref and accessors, writes them with setf if its access is :read-write (writing a :read buffer is an error), and changes :int and :uint places atomically with atomic-incf and its kin; see Compute Shaders.

(program-resources program :kind :storage-buffer) gives the storage buffers, each a resource with its resource-access. In program-bindings, a buffer that no stage writes is a :read-only-storage-buffer.

Vertex and fragment shaders may use storage buffers too.

Storage images

A storage image is a uniform of an image type, such as :image2d or :uimage3d, with a :format and an :access:

(&uniform (picture :image2d :format :rgba8-unorm :access :write))

The formats are cl-gfx's names:

Channels Formats
r, rg, rgba × 8-unorm, 16-unorm, 8-snorm, 16-snorm, 8-uint, 16-uint, 32-uint, 8-sint, 16-sint, 32-sint, 16-float, 32-float
packed :rgb10a2-unorm, :rgb10a2-uint, :rg11b10-ufloat

So :r32-uint, :rg16-float, :rgba8-snorm and so on. A format's texels must suit the image type: a -uint format with a :uimage..., -sint with an :iimage..., and the others with an :image.... cl-gsl says so when they do not:

The format :RGBA8-UNORM holds floats, but a :UIMAGE2D holds uints.

image-load, image-store, image-size and image-samples read, write and query it; see Textures and Images. (program-resources program :kind :image) gives the storage images, each a resource with its resource-format and resource-access.

Textures

A texture is a uniform of a texture type, such as :sampler2d: a texture together with its sampler. It takes no keys besides :group. See Textures and Images.