fragsplainer
Reference

Techniques

The recurring moves that show up across these shaders — each one explained once, and linked from every explainer that leans on it.

The 2×2 Rotation Matrix

Spin a 2-D vector about the origin.

Almost every shader needs to rotate something — a coordinate frame, a texture lookup, a ray. In two dimensions that is one small matrix built from the sine and cosine of the angle.

Multiplying a vector by this matrix turns it counter-clockwise by a radians while preserving its length. Rotating *coordinates* by −a is the same as rotating the *content* by +a, which is why you often see it applied to uv.

mat2 rot(float a) {
    float s = sin(a), c = cos(a);
    return mat2(c, -s, s, c);
}
// uv *= rot(angle);  // rotate the whole plane

Signed Distance Fields

Describe a shape by its distance, not its pixels.

A signed distance field (SDF) is a function that returns, for any point, the distance to the nearest surface of a shape — negative inside, zero on the edge, positive outside. The shape is never stored; it is computed wherever you ask.

SDFs compose beautifully: min(a, b) is union, max(a, b) is intersection, max(a, -b) is subtraction. Soft-thresholding the distance with smoothstep gives a crisp, resolution-independent edge. The same field can be drawn in 2-D or marched in 3-D.

// rounded box, radius r
float sdBox(vec2 p, vec2 s, float r) {
    vec2 q = abs(p) - s + r;
    return min(max(q.x, q.y), 0.) + length(max(q, 0.)) - r;
}

Raymarching

Render 3-D surfaces by stepping a ray through a distance field.

Raymarching renders solid 3-D scenes with no meshes. For each pixel it fires a ray and walks a point forward in steps. At each step it evaluates a distance function — "how far to the nearest surface?" — and advances by that amount, so it leaps through empty space and slows to a crawl near a surface.

When the step size drops below a small threshold the ray has hit something. The surface normal then comes from finite differences: sample the field a few times around the hit point and take the gradient. Lighting, reflections and fog all build from there.

float d = 0.0;
for (int i = 0; i < 100; i++) {
    vec3 p = ro + rd * d;       // point along the ray
    float h = map(p);           // distance to nearest surface
    if (h < 0.001) break;       // close enough = a hit
    d += h;                     // safe to leap this far
}

Refraction & Total Internal Reflection

Bend a ray as it crosses into glass, and trap it inside.

When light passes between two media it bends, by an amount set by their index-of-refraction ratio (Snell's law). GLSL has refract(rd, n, eta) built in: give it the incoming ray, the surface normal, and the ratio of indices, and it returns the bent ray. To render glass you raymarch *into* the surface, refract, flip the sign of the distance field so you are now marching through the inside, and continue.

Past a critical angle there is no valid exit ray — the light cannot escape and reflects back inward. refract signals this by returning a zero vector; the shader checks dot(r, r) < epsilon and falls back to reflect. Looping this refract-march-refract a few times gives the layered, lensy look of a thick bubble, each bounce contributing a little less.

vec3 r = refract(rd, n, eta);
if (dot(r, r) < 1e-4)      // total internal reflection
    rd = reflect(rd, n);
else { rd = r; eta = 1.0/eta; inside = -inside; }
Seen in Bubblin

The Gyroid

An infinite, smooth lattice from one line of trig.

The gyroid is a triply-periodic minimal surface — a smooth lattice that repeats in all three directions with no flat spots. In a shader it collapses to a single expression, which makes it a favourite source of organic 3-D structure.

Scaling the input changes the lattice frequency; nesting one gyroid inside another, or adding sine modulation, warps it into endlessly varied tunnels and membranes. Note it is not a true distance field, so raymarching it needs cautious, scaled-down steps.

#define gyr(p) dot(sin(p), cos(p.zxy))
// gyr(p*scale) - thickness;  // a gyroid surface
Seen in Gyroid Marcher

Feedback Buffers

Give a shader a memory by reading its own last frame.

Shaders are normally stateless — the same picture every frame. A feedback buffer breaks that: an off-screen buffer samples its *own* output from the previous frame, so state can copy itself forward through time, living entirely inside a texture.

In practice this is a ping-pong of two textures: you read from last frame and write to a fresh one, then swap. It is the foundation of simulations, trails, growth, and any shader that "remembers" — from window positions to chemical concentrations.

// Buffer A, channel 0 = Buffer A (itself, last frame)
vec4 prev = texture(iChannel0, uv);
// ...evolve prev...
fragColor = next;   // becomes next frame's input

The Laplacian Kernel

Measure how a cell differs from its neighbours.

Many grid simulations need to know whether a pixel sits in a dip or on a bump relative to its surroundings. The discrete Laplacian answers that with a small convolution: a weighted sum of the 3×3 neighbourhood where the centre is strongly negative and the neighbours positive.

The result drives diffusion — quantity flows from high concentration to low. Tuning the neighbour weights (edges vs. diagonals) changes how isotropic the spread is. The same convolution idea underlies blur, sharpen, and edge-detect kernels.

// 3x3 weights: center -1, edges +0.2, corners +0.05
float sum = -1.0 * here;
sum += 0.2  * (up + down + left + right);
sum += 0.05 * (corners...);

Symmetry Folds

Draw a slice once, mirror it into many.

Rotational symmetry is cheap if you fold the plane before drawing. Convert a point to polar coordinates, wrap its angle into a single wedge with mod, and convert back. Whatever you draw in that one wedge now appears repeated around the circle — kaleidoscope for free.

The wedge size sets the symmetry order: mod(angle, PI/2.) gives four-fold, PI/3. gives six-fold, and so on. Reflecting the wedge as well (with abs) adds mirror symmetry on top of rotational.

// 4-fold rotational symmetry
float a = mod(atan(p.y, p.x), PI/2.);
p = length(p) * vec2(cos(a), sin(a));