fragsplainer Reaction-Diffusion
A Shader, Explained

Reaction-Diffusion

Two imaginary chemicals, feeding and starving each other into Turing patterns.

Nothing here is drawn. Two imaginary chemicals — call them A and B — sit on a grid, one number each per pixel. Every frame they follow a three-line rule, and out of that rule alone come spots, stripes, and branching coral. These are Turing patterns. Drag across the panel on the left to squirt more B into the soup and watch it bloom.


The whole simulation lives in one feedback buffer that reads its own previous frame — the grid’s memory. Each cell looks at itself and its eight neighbours and works out what it becomes next.

Pass · Buffer A — the chemistry

Three things happen to every cell, every frame: the chemicals diffuse (smear into their neighbours), they react (B eats A to make more B), and the world is topped up — A is fed in, B is killed off. Two dials decide which world you live in: the feed rate feed 0.055 and the kill rate kill 0.062. Tiny nudges flip the pattern between dots, worms, and mitosis-like splitting.

How a cell smears into its neighbours → technical explainer

Diffusion needs to know how a cell compares to those around it. The Laplacian is a weighted sum of the 3×3 block of neighbours: the centre cell counts as −1, its edge neighbours +0.2 each, the diagonals +0.05. Where a cell sits in a dip the total is positive (chemical flows in); on a bump it’s negative (flows out).

if (i==0||j==0) factor = 0.2;   // edge neighbours
if (i==0&&j==0) factor = -1.;   // the cell itself
sum += texture(iChannel0, xy+offset).ab * factor;

A spreads fast (A spread 1.00); B spreads slowly (B spread 0.25). That difference in pace is the engine of the whole pattern.

The reaction: A + 2B → 3B

The reaction itself is the term a*b*b — wherever A and B meet, a little A is converted into B. A is then replenished toward 1, and B decays away. Balance the two and the front neither floods nor dies, but settles into a standing pattern.

a + dA*lapAB.x - a*b*b + feed*(1.0 - a),   // A
b + dB*lapAB.y + a*b*b - (k + feed)*b      // B
The slow swirl → technical explainer

Before reading itself back, the buffer rotates its sampling coordinates by a thousandth of a radian. Over many frames that imperceptible turn drags the whole pattern into a lazy spiral — a free flourish that costs one mat2 multiply.

Pass · Image — the developer

The buffer’s chemical A is a soft 0 → 1 gradient. The display pass does one thing: a single smoothstep snaps that haze into crisp black and white, like developing a photograph.

Reading just the A channel
vec3 col = vec3(smoothstep(0.4, 0.6, texture(iChannel0, uv).a));

Everything below 0.4 goes black, everything above 0.6 white, with a thin antialiased seam between. Try wiring the .b channel here instead and you’d see B’s mirror image.

↗ Shadertoy
1 // Two virtual chemicals live in this buffer: A in the alpha channel, B in blue.
2 // Each frame they diffuse, react, and feed back into themselves — the recipe
3 // known as the Gray-Scott reaction-diffusion model.
4
5 vec2 rotate(vec2 v, float a) {
6 float s = sin(a);
7 float c = cos(a);
8 mat2 m = mat2(c, -s, s, c);
9 return m * v;
10 }
11
12 // A weighted average of each pixel's neighbours minus itself — a discrete
13 // Laplacian. Positive where a cell sits in a dip, negative on a bump; this is
14 // what makes a chemical spread out from where it's concentrated.
15 vec2 laplacianAB(in vec2 xy) {
16 vec2 sum = vec2(0.);
17 vec2 px = 1./iResolution.xy;
18 int range = 1;
19
20 for (int i=-range;i<range+1;i++){
21 for (int j=-range;j<range+1;j++){
22 vec2 offset = vec2(float(i),float(j))*px;
23 float factor = 0.05; // diagonal neighbours
24 if (i==0||j==0) factor = 0.2; // edge neighbours
25 if (i==0&&j==0) factor = -1.; // the cell itself
26 sum += texture(iChannel0,xy+offset).ab*factor;
27 }
28 }
29
30 return sum;
31 }
32
33 vec2 setAB(in vec2 xy) {
34 float a = texture(iChannel0,xy).a;
35 float b = texture(iChannel0,xy).b;
36 vec2 lapAB = laplacianAB(xy);
37
38 return vec2(
39 a + 1.00 * lapAB.x - a*b*b + 0.055 * (1. - a), // A: diffuses, gets eaten, is replenished
40 b + 0.25 * lapAB.y + a*b*b - (0.062 + 0.055) * b // B: diffuses, is created, decays
41 );
42
43 }
44
45 void mainImage( out vec4 fragColor, in vec2 fragCoord )
46 {
47 vec2 uv = fragCoord/iResolution.xy;
48 vec2 uvc = (fragCoord*2. - iResolution.xy)/iResolution.y;
49 if (iFrame < 10) {
50
51 fragColor.b = step(0.5,length(uvc));
52 fragColor.b = mix(fragColor.b,1.,step(0.1,sin(50.*length(uvc.x))));
53 fragColor.b = mix(fragColor.b,1.,step(0.1,sin(50.*length(uvc.y))));
54 fragColor.a = 1.-fragColor.b;
55
56 return;
57 }
58
59 float mouse_dist = distance(iMouse.xy/iResolution.xy,uv);
60
61
62 uv -= 0.5;
63 uv.x *= iResolution.x/iResolution.y;
64 uv = rotate(uv,0.001); // a hair of rotation each frame: a slow swirl
65 uv.x /= iResolution.x/iResolution.y;
66 uv -= 0.5;
67
68 fragColor.ab = setAB(uv);
69
70 if (iMouse.z > 0.5) {
71 if (mouse_dist<0.03) { fragColor.b = 1.; fragColor.a = 0.; } // paint B under the cursor
72 }
73 }
74