In chapter 13, we reversed known steps by hand. Now we will express the return trip with an inverse matrix. Keep the same column-vector convention and the distinction between local drawing coordinates and world coordinates.
Start with a fixed pixel
Imagine a local circle of radius 0.6. With uniform scale 2, its visible world radius should be 1.2. A world pixel at p=(1,0.5) belongs to the original drawing at local=(0.5,0.25).
vec2 local = p / 2.0;
float field = length(local) - 0.6;
Dividing the lookup coordinate makes the visible shape bigger. Why? The boundary now satisfies length(p/2)=0.6, or length(p)=1.2. A pixel twice as far from the origin retrieves the same original coordinate.
Multiplying p by 2 instead makes the boundary satisfy length(p)=0.3. This is the common mistake: using a forward point transformation as a sampling transformation. The shader is evaluating the color of a pixel that already has a position; it is not moving that pixel.
Add a translation t and the forward rule becomes world=2*local+t. Recover local by subtracting t first, then dividing by 2. Subtracting t after division would treat a world-space offset as though it were already local.
Name the return map
For a reversible matrix M, its inverse undoes M. We write it mathematically as M⁻¹ and in GLSL as inverse(M). Their products in either order give the identity matrix I:
M⁻¹ * M = I
M * M⁻¹ = I
These are mathematical identities. They mean that applying an action and its inverse restores any input vector. For a transformed drawing with world-space translation t:
vec2 world = M * local + t;
vec2 recovered = inverse(M) * (world - t);
Translation remains separate from mat2 because mat2 alone leaves the origin fixed. The inverse acts on the displacement world-t, measured from the transformed origin. We will give translation a place inside a larger matrix later.
GLSL ES 3.00 provides inverse for square matrices. It cannot recover a coordinate that a transformation has destroyed. Our lab keeps scale factors in [0.5,2], shear in [-1,1], and all parameters finite. Each individual operation has a defined reverse, and these bounds avoid extreme stretching ratios. Zero scale and collapse belong to the next chapter.
Do not substitute transpose(M) for inverse(M) in general. Transposing scale=(2,1) leaves that diagonal matrix unchanged, while its inverse needs scale=(0.5,1). Rotation has a special transpose relationship; scale and shear require the actual inverse.
Undo the last action first
Build one fixed chain from familiar pieces. Scale with S, apply horizontal shear H, rotate with R, then add the translation:
mat2 S = mat2(scale.x, 0.0, 0.0, scale.y);
mat2 H = mat2(1.0, 0.0, shear, 1.0);
mat2 M = R * H * S;
vec2 world = M * local + t;
To return, subtract t, rotate backward, undo the shear, then divide by scale. The inverse of the product therefore has the reversed order S⁻¹*H⁻¹*R⁻¹.
For two reversible matrices, inverse(A*B)=inverse(B)*inverse(A). We can see why by letting the neighboring actions cancel:
B⁻¹ * A⁻¹ * (A * B * local)
= B⁻¹ * (A⁻¹ * A) * B * local
= B⁻¹ * B * local
= local
Regrouping is allowed; exchanging actions is not. Writing inverse(A)*inverse(B) would generally undo them in the wrong order.
Use the Combined preset: 90°, scale=(2,1), shear=0.5, and t=(0.5,-0.25). P=(0.4,0.3) passes through (0.8,0.3), then (0.95,0.3), then (-0.3,0.95), and finally world=(0.2,0.7).
Follow the same numbers backward. Subtract t to get (-0.3,0.95). Rotate by -90° to recover (0.95,0.3). Subtract shear*y from X to recover (0.8,0.3). Divide by (2,1), and P returns.
Keep the pattern attached
A pattern stays attached when its colors and its shape mask use the same local coordinate. Our checkerboard uses squares 0.2 local units wide:
vec2 cell = floor(local / 0.2);
float checker = mod(cell.x + cell.y, 2.0);
float field = length(local) - 0.6;
floor selects an integer cell coordinate. The even or odd sum chooses one of two colors. This procedural pattern needs no image file. Its hard cell edges make the coordinate changes easy to inspect.
If you evaluate the checker with p while evaluating the mask with local, the figure moves across a pattern fixed in world space. Both choices can be useful, but only the shared local coordinate makes the pattern part of this drawing.
The small filled dot is also sampled in local space around P=(0.4,0.3). A separate gold ring uses the forward position M*P+t. With correct sampling, their centers agree as the controls change. This gives us a visible check that does not depend on the circle's symmetry.
Check the round trip
Move the white probe to a world position p. Recover its local coordinate, then send that coordinate forward again:
vec2 local = inverse(M) * (p - t);
vec2 roundTrip = M * local + t;
float error = length(roundTrip - p);
The returned point should meet the probe. Floating-point arithmetic leaves small numerical errors; it does not promise bit-for-bit identity. For this bounded lab, an error below 0.0001 world units is a useful reference check.
The Direct mistake mode deliberately uses M*(p-t) instead. At the Combined settings, the probe (0.2,0.7) retrieves (-0.95,-0.125). Sending that coordinate forward reaches (0.625,-2.2125), far from the probe.
The readouts are host float32 reference calculations, not GPU readback. They describe the built-in formulas even after shader edits. The probe may also be outside the figure: a valid lookup still exists there, but the mask returns background.
The circle field remains a local distance after deformation. Its sign and zero contour still define the mask; its magnitude is generally not a world-space distance. We use fwidth to smooth the rendered boundary approximately in screen space, as in chapter 12.