3 Conventions
dxtr edited this page 2026-10-14 19:30:32 +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.

Conventions

The examples on this page use the package from Getting Started.

Storage

Every value is a specialised array of single floats, or of doubles for the types of Doubles:

Type Array Holds
vec2, vec3, vec4 2, 3 or 4 floats X, Y, Z and W
quat 4 floats X, Y and Z, the vector part, then W, the real part
mat3, mat4 9 or 16 floats The elements column by column, as GLSL stores them
mat3x2 6 floats A 2D affine transform's elements column by column: the X axis, the Y axis and the origin

A vector or quaternion is also a typed structure over its array, so its components have accessors, such as vec3-x; see Vectors.

A quat and a vec4 are the same type of array, so a function cannot tell them apart: give each function the kind it names. Their accessors cannot either: quat-w reads a vec4's W.

Matrices are stored column by column, so a translation is in the last four floats, as a GPU expects it:

(gm:mat4-translation (gm:make-vec3 :x 1 :y 2 :z 3))
;; => #(1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 1.0 2.0 3.0 1.0)
(gm:mref (gm:mat4-translation (gm:make-vec3 :x 1 :y 2 :z 3)) 0 3)   ; row 0, column 3
;; => 1.0

mref reads the element of a mat3, mat3x2 or mat4 at a row and a column, whatever the storage order; setf writes it. gm:make-mat3 and gm:make-mat4 take their elements column by column, as GLSL's constructors do.

Multiplication order

Matrices multiply column vectors: (gm:mat4*vec4 m v) is M v. A product applies its right-hand matrix first: (gm:mat4* a b) applies B, then A.

(let ((move (gm:mat4-translation (gm:make-vec3 :x 10)))
      (turn (gm:mat4-rotation-z (gm:radians 90))))
  (list (gm:mat4-transform-point (gm:mat4* move turn) (gm:make-vec3 :x 1))     ; turn, then move
        (gm:mat4-transform-point (gm:mat4* turn move) (gm:make-vec3 :x 1))))   ; move, then turn
;; => (#(10.0 1.0 0.0) #(-4.808253e-7 11.0 0.0))

So a model-view-projection matrix is (gm:mat4* projection (gm:mat4* view model)), and a child's world matrix is (gm:mat4* parent-world child-local). Quaternions follow the same rule: (gm:quat* a b) rotates by B, then by A.

Space

Right-handed, with +Y up, and a camera looks down −Z, as in Godot, Bevy, glTF and Blender. So +X is right, and +Z points out of the screen, toward the viewer.

A positive angle turns counterclockwise when its axis points at you: about +Z, from +X toward +Y; about +X, from +Y toward +Z; about +Y, from +Z toward +X.

(gm:mat4-transform-point (gm:mat4-rotation-z (gm:radians 90)) (gm:make-vec3 :x 1))
;; => #(-4.371139e-8 1.0 0.0)

The tiny X, rather than 0, is rounding: single floats hold π/2 only approximately.

Clip space

Projections map into Vulkan's clip space, which cl-gfx uses on every backend: after the division by W, X and Y run from −1 to 1 with +Y down, so the top of the view is at Y −1, and depth runs from 0 to 1. World and view space keep +Y up; the projection turns it over.

Reversed depth

mat4-perspective, mat4-perspective-infinite and mat4-orthographic put the near plane at depth 1 and the far plane at 0:

(let ((projection (gm:mat4-perspective (gm:radians 60) 1 0.1 100))
      (near (gm:make-vec3 :z -0.1))   ; a point on the near plane
      (far (gm:make-vec3 :z -100)))   ; and one on the far plane
  (list (gm:vec3-z (gm:mat4-project-point projection near))
        (gm:vec3-z (gm:mat4-project-point projection far))))
;; => (1.0 0.0)

Floating-point numbers are densest near 0, which reversed depth gives to far away, where perspective packs distances closest together. Draw with the depth test :greater, and clear depth to 0.

Angles

Angles are in radians. radians and degrees convert, giving a double for a double, such as Lisp's pi, and otherwise a single float:

(gm:radians 180)        ; => 3.1415927
(gm:degrees (/ pi 2))   ; => 90.0d0