2D transforms
Translate, rotate and scale UV space with vectors and a mat2, and why you move space instead of the shape.
How it works
A shape in a fragment shader is a function of the pixel’s coordinate: a circle is “the pixels where length(p) is
below 0.3”. There is no circle object to move. To move, turn or resize it, you change the coordinate before the shape
function sees it. Every pattern on this site, from a checkerboard to a noise field, moves the same way.
Translate
Subtracting an offset from the coordinate moves the shape by that offset:
vec2 p = uv * 2.0 - 1.0; // centered, -1..1
vec2 q = p - vec2(0.4, 0.2); // the circle's center is now at (0.4, 0.2)
float circle = step(length(q), 0.3);
It feels backward at first. The shape is drawn where q is near zero, and q is zero at the pixel p = (0.4, 0.2).
Subtracting moves the shape forward. That reversal is the heart of this fundamental: you are transforming the space the
shape is drawn in, so every operation runs as the inverse of the one you want the shape to do.
Scale
Dividing the coordinate makes the shape bigger: p / 2.0 lets the coordinate grow half as fast, so everything
appears twice the size. Multiplying makes it smaller, and repeats more of a pattern: uv * 8.0 fits eight checker
cells across. Dividing x and y by different amounts stretches the shape, as uncorrected aspect ratio does. A distance
measured in the scaled space is scaled too; that matters for the distance fields later in the order.
Rotate
Rotation by an angle Matrix A grid of numbers (mat2, mat3, mat4) that maps one vector to another in a single multiplication. Rotation, scale and, with homogeneous coordinates, translation and projection are all matrices.a around the origin sends (x, y) to (x cos a − y sin a, x sin a + y cos a). That is a
matrixA grid of numbers (mat2, mat3, mat4) that maps one vector to another in a single multiplication. Rotation, scale and, with homogeneous coordinates, translation and projection are all matrices.rotate(a) = [cos a −sin a; sin a cos a]
mat2 rotate2d(float a) {
float c = cos(a);
float s = sin(a);
return mat2(c, s, -s, c); // columns: (c, s) and (-s, c)
}
vec2 q = rotate2d(-angle) * p; // turns the shape by +angle, counterclockwise
The minus sign is the same inverse as before: to turn the shape counterclockwise, turn the space clockwise. angle is
in radians: a full turn is 2π, about 6.283, and 90° is π / 2.
GLSL matrices are column-major: the constructor fills the first column, then the second. mat2(c, s, -s, c) has
(c, s) as its first column, so it is the matrix written in math books as [c −s; s c]. Reading the four numbers as
rows instead gives the transposed matrix, which for a rotation is the inverse: it turns the other way. That is the
most common rotation bug. The same fact gives a shortcut: p * m multiplies by the transpose, so
p * rotate2d(angle) is the same as rotate2d(-angle) * p.
Order and pivots
Translate, rotate and scale don’t commute: rotating then moving is not the same as moving then rotating. To spin a shape around its own center instead of the origin, move the space so the center is at the origin, rotate, and draw there:
vec2 center = vec2(0.4, 0.2);
// the shape, at center, turned by angle around its own center
vec2 q = rotate2d(-angle) * (p - center);
For a full transform of a shape (scale s, then rotate by a, then move to t), the space undoes those steps in the
opposite order: subtract t, rotate by -a, divide by s.
vec2 q = (rotate2d(-a) * (p - t)) / s;
Fix the aspect ratio before any rotation. Rotating in stretched UVs mixes x and y while they have different units, and the shape shears as it turns.
Transforming patterns and textures
The same operations work on anything that reads a coordinate. Rotate uv before fract and the whole grid turns.
Scale it over time and a pattern zooms. Add time * speed and it scrolls. For a texture, rotate around its middle by
centering first: rotate2d(-a) * (uv - 0.5) + 0.5, then sample with the result. Applying a different transform at
every pixel, from a wave or from noise, is UV warping and domain warping: the same idea, with the offset varying
across the screen.
A mat2 holds rotation and scale, but not translation, which is why the offset is added separately. Folding
translation into the matrix too takes one more dimension, a mat3 on vec3(p, 1.0); in 3D that becomes the mat4
of the model, view and projection matrices.