Shader Code
Shader code is Lisp forms that cl-gsl type-checks and translates to GLSL. It looks and behaves like Common Lisp wherever the GPU allows it. This page covers how to write it. Stages covers what each kind of shader takes and returns, and Operator Reference lists every operator.
Defining shader functions
define-function looks like defun with typed parameters:
(define-function lighten ((color :vec3) (amount :float))
"COLOR blended toward white by AMOUNT, from 0 to 1."
(let ((white (vec3 1)))
(mix color white (clamp amount 0 1))))
- Each parameter is
(name type), where the type is a shader type. - The body may start with a docstring, which
shader-documentationreturns. - The last form gives the value. Its type is inferred. Use
valuesto return several. - Any stage, or any other shader function, may call the function.
define-shader defines a stage in the same way, with a stage designator after the name. See Stages.
Checked when evaluated
A definition is checked as soon as it is evaluated. If the check fails, cl-gsl signals a shader-error that names the definition and the form and says what would fit. The previous definition stays, so a mistake at the REPL never breaks programs that already work.
(define-function broken ((v :vec3))
(+ v (vec2 1)))
;; error: In BROKEN: + cannot take arguments of the types :VEC3, :VEC2. It takes
;; two scalars or two vectors of the same type, which work per component;
;; a scalar and a vector, or a vector and a scalar, of the same component type;
;; two matrices of the same type, or a matrix and a :FLOAT;
;; for *, also a matrix and a vector, or a vector and a matrix, or two matrices,
;; whose sizes agree, for the linear algebra product.
;; in: (+ V (VEC2 1))
Forward references. A function may call one that is not defined yet. That gives a forward-reference-warning, a style warning, not an error. The definition is kept, and the call is checked again when a program that uses it is compiled.
(define-function shade ((n :vec3)) (lambert-term n))
;; WARNING: SHADE calls LAMBERT-TERM, which is not a defined shader function yet.
;; Define it before compiling a program that uses SHADE.
Removing a definition. (remove-definition 'name) forgets a shader function, stage, struct or shader variable, and returns true if there was one.
Variables and inference
let and let* bind variables. A variable's type is the type of its initial value, and it keeps that type: assigning a value of another type is an error.
(let* ((n (normalize normal)) ; :vec3
(d (max (dot n light) 0)) ; :float
(i 0)) ; :int
...)
Literals.
| Literal | Type |
|---|---|
1.5, 1.0 |
:float |
1d0, 1.5d0 |
:double |
3, -1 |
:int, but it also fits where a :float, :double or :uint is expected |
2/3 |
:ratio |
#c(1 2) |
:complex, or :dcomplex with double parts |
t, nil |
:bool |
So (* 2 v), (max x 0) and (vec4 position 1) need no decimal point. To give a literal a specific type, use (the :uint 3) or a conversion such as (uint 3).
Conversions are explicit, apart from those literals: (float i), (int f) (which truncates), (uint i), (bool x), (double f), (float x 1d0), and vector and matrix constructors such as (vec3 ivec) and (mat3 m4).
Assignment and places
setf, setq, incf and decf assign to places:
- variables;
- components:
(x v),(y v),(z v),(w v),(aref v i); - swizzles without repeated components:
(swizzle v :xy); - array elements and matrix columns:
(aref a i); - struct slots:
(light-color l),(slot-value l 'color), or slot names insidewith-slots.
(let ((u v))
(setf (swizzle u :xy) (swizzle v :yx))
(incf (z u) 1)
(setf (aref u 3) 0.5)
u)
Parameters are values that the function owns, as in Lisp: assigning one changes the function's copy only. Uniforms and shader variables cannot be assigned.
Control flow
| Form | Notes |
|---|---|
(if test then [else]) |
The test is a :bool. It gives a value if both branches give one of the same type. |
when, unless |
For effect. |
(cond (test form...) ... [(t form...)]) |
Gives a value if it ends with a t clause and every clause gives one of the same type. |
(case key ((k...) form...) ... [(otherwise form...)]) |
Keys are values written in the code, compared with =. A clause may name a single key without a list. |
and, or, not |
Short-circuiting, on :bools. not also negates a vector of :bools per component. |
progn |
Forms in order; the last gives the value. |
(the type value) |
Asserts a type, and makes an integer literal a :float or :uint. |
(define-function pick ((i :int))
(case i
(0 (vec3 1 0 0))
((1 2) (vec3 0 1 0))
(otherwise (vec3 0 0 1))))
(discard) in a fragment shader drops the fragment and returns no further.
Loops
Loops are dotimes and the simple loop. return leaves the innermost loop and gives the loop its value, as in Lisp.
(define-function lowest-set-bit ((bits :uint))
"The index of the lowest bit set in BITS, or -1 if none is."
(dotimes (i 32 -1)
(when (logtest bits (ash (uint 1) i))
(return i))))
(define-function first-square-above ((limit :int))
(let ((n 0))
(loop (when (> (* n n) limit) (return n))
(incf n))))
dotimesevaluates its count once. The count is an:intor:uint, and the variable has the count's type.- A loop gives no value unless a
returngives one. If anyreturngives a value, everyreturngives a value of the same type, and adotimesresult form does too. - The extended
loop, such as(loop for i below n ...), is not shader code. Count withdotimes.
Several values
Several values work as in Lisp. A function returns them with values, multiple-value-bind receives them, and where one value is wanted, the first is used.
(define-function polar ((v :vec2))
(values (length v) (atan (y v) (x v))))
(define-function spiral ((v :vec2))
(multiple-value-bind (radius angle) (polar v)
(multiple-value-bind (turns rest) (floor angle 6.2831855)
(+ radius turns rest))))
nth-value, multiple-value-setq, multiple-value-prog1 and multiple-value-call work too. floor, ceiling, truncate, round and their f kinds give the quotient and the remainder; decode-float, add-carry, subtract-borrow and multiply-extended also give several values.
In the generated GLSL, the values after the first become out parameters.
A stage's values are its outputs: a vertex shader returns the position and then the values for the next stage, and a fragment shader returns one colour per render target.
Structs
define-struct defines a struct, as defstruct does, with a constructor that takes every slot by keyword and an accessor per slot, which setf assigns:
(define-struct light
"A point light."
(position :vec3)
(color :vec3))
(define-function lit ((l light) (p :vec3))
(/ (light-color l) (+ 1 (distance (light-position l) p))))
(define-function white-light-at ((p :vec3))
(make-light :position p :color (vec3 1)))
Structs are values, as vectors are: binding or passing one copies it. Structs covers them in full: slot-value and with-slots, nesting, structs as uniforms and in storage buffers, their memory layout, and packing them from Lisp.
Arrays
An array type is (:array element-type length).
(define-shader lights-fragment :fragment ((p :vec3)
&uniform (lights (:array light 4)))
(let ((sum (vec3 0)))
(dotimes (i (length lights))
(incf sum (lit (aref lights i) p)))
(vec4 sum 1)))
(vector a b c)makes an array of its elements, all of one type. Integer literals fit the type of the others.(make-array n :initial-element x)makes an array ofncopies ofx, wherenis written in the code.arefreads and writes elements, andlengthcounts them, as an:int.- Arrays are values: binding or passing one copies it.
(:array type), without a length, appears only in storage buffers; itslengthis known as the shader runs.
Local functions and macros
flet and labels define local functions, with typed parameters as define-function has. They see the variables around them, and assigning one of those changes it outside too, as in Lisp. labels functions also see each other.
(define-function weighted-sum ((v :vec3) (k :float))
(let ((total 0.0))
(flet ((add ((x :float)) (incf total (* k x))))
(add (x v)) (add (y v)) (add (z v)))
(labels ((twice ((x :float)) (* 2 x))
(quad ((x :float)) (twice (twice x))))
(quad total))))
(funcall #'name argument ...) calls a shader function by name, as (name argument ...) does.
macrolet and symbol-macrolet define local macros. A macrolet body is Lisp, evaluated as the shader code is checked. For global shader macros, see Extending Shader Code.
Where shader code is not Lisp
Shader code differs from Lisp where GPUs do:
- No recursion. A function may not call itself, directly or through others. This includes
labels. - Static types. A variable keeps the type of its initial value.
- Fixed-size integers.
:intand:uintare 32 bits and wrap around.ashby 32 bits or more is undefined. A constant or:uintshift count shifts one way; any other count chooses the direction by its sign as the shader runs. - Float division.
floorand its kin give float quotients for float arguments, and integer ones for integers.(/ a b)of integers truncates, as in GLSL. - No closures or first-class functions.
funcallnames the function it calls. - Opaque values stay put. A texture or image can be passed to a shader function as a parameter, but cannot be stored in a variable or returned.
- Derivatives (
dfdx,fwidth, implicit-levelsample) are undefined where neighbouring fragments take different branches. - Undefined results where GLSL leaves them undefined, such as
normalizeof a zero vector,inverseof a singular matrix,exptof a negative base with a non-integer power, andacoshbelow 1.shader-documentationsays so for each.