1 2D Transforms
dxtr edited this page 2026-10-14 13:46:25 +02:00

2D Transforms

A mat3x2 is a 2D affine transform: a matrix of 3 columns and 2 rows, as GLSL names it, holding the X axis, the Y axis and the origin. Its six elements, in order, are the a b c d e f of a canvas transform: a point x, y goes to a x + c y + e, b x + d y + f. Functions that return a matrix or a vector take an optional last argument, result; see Results and Allocation. dmat3x2 is the same of doubles; see Doubles.

A node's transform

mat3x2-compose makes a 2D node's transform from a translation, an angle and a scale: scaled, then turned, then moved. mat3x2* puts a parent's transform before a child's, and mat3x2-inverse takes points back:

(let* ((parent (gm:mat3x2-compose (gm:make-vec2 :x 100 :y 50) 0 (gm:make-vec2 :x 2 :y 2)))
       (child (gm:mat3x2-compose (gm:make-vec2 :x 10) (/ pi 2) (gm:make-vec2 :x 1 :y 1)))
       (global (gm:mat3x2* parent child)))
  (values (gm:mat3x2-transform-point global (gm:make-vec2 :x 1))
          (gm:mat3x2-transform-point (gm:mat3x2-inverse global)
                                     (gm:make-vec2 :x 120 :y 52))))
;; => #(120.0 52.0), #(1.0 0.0)

The child turns its point (1, 0) a quarter turn to (0, 1) and moves it to (10, 1); the parent scales that to (20, 2) and moves it to (120, 52).

A positive angle turns +X toward +Y: counterclockwise where Y points up, and clockwise on a screen, where it points down. Whether +Y is up or down is the renderer's choice; the matrices only turn +X toward +Y.

Making them

Function Returns the mat3x2 that
(make-mat3x2 a b c d e f) holds the six elements, column by column. Another number of them signals gmath-error.
(mat3x2-identity) leaves points where they are.
(mat3x2-translation offset) moves points by the vec2 offset.
(mat3x2-rotation angle) turns points by angle, in radians, from +X toward +Y.
(mat3x2-scaling scale) scales points by the vec2 scale, along each axis.
(mat3x2-compose translation angle scale) scales, then turns, then moves: what (mat3x2* T (mat3x2* R S)) makes, in one step.
(gm:mat3x2-translation (gm:make-vec2 :x 5 :y 7))   ; => #(1.0 0.0 0.0 1.0 5.0 7.0)

Operations

Function Returns
(mat3x2* a b) The product A B: the transform that applies B, then A.
(mat3x2-transform-point matrix point) The vec2 point turned, scaled and moved.
(mat3x2-transform-direction matrix direction) The vec2 direction turned and scaled, not moved.
(mat3x2-inverse matrix) The inverse, which takes points back. Signals gmath-error for a singular matrix, which squashes the plane onto a line or a point.
(mat3x2-determinant matrix) The factor by which it scales areas, negative if it mirrors them.
(mat3x2-copy matrix) A copy.
(mat3x2-approx= a b) True if no element differs by more than an epsilon, 1e-5 by default.
(gm:mat3x2-determinant (gm:mat3x2-scaling (gm:make-vec2 :x 2 :y 3)))   ; => 6.0
(gm:mref (gm:make-mat3x2 1 2 3 4 5 6) 1 2)                             ; => 6.0, the origin's Y

mref reads an element by its row, 0 or 1, and its column, 0 to 2, and setf writes it.

As a 3x3

A mat3x2 is the mat3 of homogeneous 2D points without its bottom row, which is always 0 0 1. mat3-from-mat3x2 adds the row, for code that wants a mat3, and mat3x2-from-mat3 drops it:

(gm:mat3-from-mat3x2 (gm:mat3x2-translation (gm:make-vec2 :x 5 :y 7)))
;; => #(1.0 0.0 0.0 0.0 1.0 0.0 5.0 7.0 1.0)

The transforms have functions of their own as mat3s of homogeneous points too, which cl-gsl's cl-gsl/gmath also makes shader code:

Function Returns
(mat3-translation-2d offset) The mat3 that moves 2D points by the vec2 offset.
(mat3-rotation-2d angle) The mat3 that rotates 2D points by angle, from +X toward +Y.
(mat3-scaling-2d scale) The mat3 that scales 2D points by the vec2 scale.
(mat3-transform-point-2d matrix point) The vec2 point rotated, scaled and translated.
(mat3-transform-direction-2d matrix direction) The vec2 direction rotated and scaled, without the translation.

A mat3x2 computes exactly what its mat3 computes, bit for bit, so the two give the same results:

(let ((m (gm:mat3x2* (gm:mat3x2-translation (gm:make-vec2 :x 10))
                     (gm:mat3x2-rotation (gm:radians 90)))))
  (list (gm:mat3x2-transform-point m (gm:make-vec2 :x 1))
        (gm:mat3x2-transform-direction m (gm:make-vec2 :x 1))))
;; => (#(10.0 1.0) #(-4.371139e-8 1.0))

(let ((m (gm:mat3* (gm:mat3-translation-2d (gm:make-vec2 :x 10))
                   (gm:mat3-rotation-2d (gm:radians 90)))))
  (list (gm:mat3-transform-point-2d m (gm:make-vec2 :x 1))
        (gm:mat3-transform-direction-2d m (gm:make-vec2 :x 1))))
;; => (#(10.0 1.0) #(-4.371139e-8 1.0))

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