Physics Morphogenesis WebGL GLSL

How the Leopard Got Its Spots at 60 FPS: Gray–Scott Reaction–Diffusion in WebGL


Live GPU Turing Morphogenesis Solver. Drag anywhere on the canvas to seed fresh autocatalyst $V$, or open ⚙ Controls to switch between Coral Labyrinth, Mitosis Cells (self-replicating dividing spots), Spiral Waves, Soliton Worms, and the Spatial $(F, k)$ Map that renders the entire Pearson phase diagram across the screen at once.

After building a multi-pass incompressible Navier–Stokes solver on ping-pong WebGL Framebuffer Objects (FBOs), a natural question arises: what happens when you strip away the velocity field and let two diffusing chemicals react directly on the grid?

Common sense says diffusion is a great equalizer – drop ink into still water and the gradients smooth out into a uniform blur. In 1952, Alan Turing proved that common sense is wrong. In his seminal paper "The Chemical Basis of Morphogenesis," Turing showed that if an autocatalytic activator diffuses more slowly than its substrate or inhibitor ($D_v \lt D_u$), diffusion itself destabilizes the uniform state and spontaneously carves stripes, spots, labyrinths, and dividing cells out of nothing.

This post builds a real-time WebGL solver for the Gray–Scott reaction–diffusion system (Pearson, 1993), executing 14 PDE substeps per animation frame (~840 chemical steps per second) on an isotropic 9-point Laplacian stencil and shading the concentration field as a 3D Blinn–Phong normal-mapped coral reef.


0. Anatomy of the Gray–Scott System

Every texel in the simulation above stores two dimensionless chemical concentrations in its red and green channels, $(u, v) \in [0, 1]^2$, governed by two irreversible reactions inside an open Continuously Stirred Tank Reactor (CSTR):

Autocatalysis ($U + 2V \to 3V$)
Two molecules of catalyst $V$ convert one molecule of substrate $U$ into a third molecule of $V$ at nonlinear rate $R(u, v) = u v^2$.
Feed Rate ($F$)
Fresh substrate $U$ is continuously replenished toward $u = 1$ at rate $F(1 - u)$, while both species are washed out at rate $F$.
Kill Rate ($k$)
Catalyst $V$ decays into an inert product $P$ ($V \to P$) at rate $k v$, giving a total removal rate $(F + k)v$.
Diffusion Ratio ($D_u / D_v \approx 2$)
With $D_u = 0.2097$ and $D_v = 0.1050$, substrate $U$ diffuses twice as fast as catalyst $V$, creating local lateral depletion zones around each growing $V$ peak.
Ping-Pong Substepping
Because each step is a single fullscreen pass with no Poisson solve, the GPU runs 14 ping-pong substeps per frame at 60 fps.

1. The Gray–Scott PDEs and Diffusion-Driven Instability

Combining Fickian diffusion ($D \nabla^2$) with the cubic autocatalytic reaction $u v^2$, substrate replenishment $F(1 - u)$, and catalyst decay $(F + k)v$ yields the coupled Gray–Scott partial differential equations:

Coupled Gray–Scott Reaction–Diffusion PDEs
$$\frac{\partial u}{\partial t} = D_u \nabla^2 u - u v^2 + F(1 - u)$$ $$\frac{\partial v}{\partial t} = D_v \nabla^2 v + u v^2 - (F + k)v$$

Notice the exact opposite signs on $\mp u v^2$: every unit of substrate $u$ consumed by the reaction creates an equal unit of catalyst $v$.

Why does $D_u \gt D_v$ create intricate self-organizing structures instead of a uniform soup? Consider a tiny island where $v$ is locally high:

  1. Local self-amplification: Inside the island, the cubic reaction $+u v^2$ rapidly converts $u$ into more $v$, trying to make the peak taller.
  2. Fast lateral refueling ($D_u \gg D_v$): As $u$ is depleted at the center of the island, fast-diffusing substrate $U$ rushes in from the surrounding sea ($u \approx 1$) to feed the outer rim of the island, while slow-diffusing $V$ remains trapped in a sharp ridge.
  3. Core starvation & bifurcation: Once the island grows wider than the diffusion length $\ell_u \sim \sqrt{D_u / F}$, fresh substrate $U$ can no longer reach the center before being consumed by the rim. The center starves and collapses back to $v \approx 0$, splitting a single spot into a dividing dumbbell (Mitosis) or a hollow branching ring (Coral Labyrinth)!
Homogeneous Steady States & Saddle-Node Bifurcation Condition
$$(u_0, v_0) = (1, 0), \qquad u_\pm = \frac{1 \pm \sqrt{1 - 4(F + k)^2 / F}}{2}, \qquad v_\pm = \frac{F(1 - u_\pm)}{F + k}$$

Non-trivial uniform equilibria $(u_\pm, v_\pm)$ exist only inside the parabola $4(F + k)^2 \le F$. Near the boundary of this region (John Pearson's 1993 phase diagram), finite-wavelength Turing modes ($|\vec q| \neq 0$) destabilize the homogeneous states into spots, stripes, and waves.


2. The Isotropic 9-Point Discrete Laplacian

If you discretize $\nabla^2 u$ with the standard 5-point cross stencil ($N + S + E + W - 4C$), its leading truncation error $\frac{h^2}{12}(\partial_x^4 + \partial_y^4)$ is strongly anisotropic: waves travel at different speeds along the grid axes ($0^\circ, 90^\circ$) than along the diagonals ($45^\circ$), turning circular Turing spots into diamond-shaped squares.

Blending the 5-point cardinal cross with the 4 diagonal corners ($NE, NW, SE, SW$) in a $4:1$ ratio eliminates the $\mathcal{O}(h^2)$ directional anisotropy, making the discrete Laplacian rotationally invariant to fourth order:

Isotropic 9-Point Laplacian Stencil Used in the Shader
$$\nabla^2_{\text{9pt}} \phi_{i,j} = \begin{bmatrix} 0.05 & 0.20 & 0.05 \\ 0.20 & -1.00 & 0.20 \\ 0.05 & 0.20 & 0.05 \end{bmatrix} * \phi_{i,j} = 0.20\big(\phi_{i\pm 1,j} + \phi_{i,j\pm 1}\big) + 0.05\big(\phi_{i\pm 1,j\pm 1}\big) - \phi_{i,j}$$

Because the eight neighbor weights sum to $4(0.20) + 4(0.05) = 1.00$, a constant field has $\nabla^2_{\text{9pt}} \phi \equiv 0$ to machine precision, and dividing cells pinch off at arbitrary angles without locking to the pixel grid.

💻 GLSL: Isotropic 9-Point Gray–Scott Reaction–Diffusion StepGLSL
void main() {
  vec2 C  = texture2D(uState, vUv).rg;
  vec2 N  = texture2D(uState, vUv + vec2( 0.0,       uTexel.y)).rg;
  vec2 S  = texture2D(uState, vUv + vec2( 0.0,      -uTexel.y)).rg;
  vec2 E  = texture2D(uState, vUv + vec2( uTexel.x,  0.0     )).rg;
  vec2 W  = texture2D(uState, vUv + vec2(-uTexel.x,  0.0     )).rg;
  vec2 NE = texture2D(uState, vUv + vec2( uTexel.x,  uTexel.y)).rg;
  vec2 NW = texture2D(uState, vUv + vec2(-uTexel.x,  uTexel.y)).rg;
  vec2 SE = texture2D(uState, vUv + vec2( uTexel.x, -uTexel.y)).rg;
  vec2 SW = texture2D(uState, vUv + vec2(-uTexel.x, -uTexel.y)).rg;

  // Isotropic 9-point Laplacian (0.20 cardinal, 0.05 diagonal, -1.0 center)
  vec2 lap = 0.20 * (N + S + E + W) + 0.05 * (NE + NW + SE + SW) - C;

  float u   = C.r;
  float v   = C.g;
  float uvv = u * v * v;

  // Optional spatial (F, k) parameter map across the canvas
  float F = (uSpatialMap > 0.5) ? mix(0.018, 0.058, vUv.y) : uF;
  float k = (uSpatialMap > 0.5) ? mix(0.048, 0.065, vUv.x) : uK;

  float du = uDu * lap.r - uvv + F * (1.0 - u);
  float dv = uDv * lap.g + uvv - (F + k) * v;

  u = clamp(u + du * uDt, 0.0, 1.0);
  v = clamp(v + dv * uDt, 0.0, 1.0);

  gl_FragColor = vec4(u, v, 0.0, 1.0);
}

3. Turning a 2D Chemical Field into 3D Coral Relief

A raw grayscale plot of $v(\vec x)$ looks flat. Instead, the display shader treats the catalyst concentration $v(x, y)$ as a 3D heightfield $z = \alpha\,v(x, y)$ and reconstructs its unit surface normal $\hat n$ per pixel using central finite differences:

Heightfield Surface Normal & Blinn–Phong + Fresnel Shading
$$\hat n(x, y) = \frac{\big(-\alpha\,\partial_x v,\; -\alpha\,\partial_y v,\; 1\big)}{\sqrt{1 + \alpha^2|\nabla v|^2}}, \qquad I = I_{\text{albedo}}(v)\big(k_a + k_d(\hat n \cdot \hat L)\big) + k_s(\hat n \cdot \hat H)^p + k_{\text{rim}}(1 - \hat n_z)^{2.2}$$

Evaluating $\partial_x v$ and $\partial_y v$ on the fly in the display pass gives every chemical ridge a wet specular highlight along its crest and a bioluminescent cyan Fresnel rim along its steep flanks.

Explore the whole phase space at once: toggle the gold Spatial (F, k) Map pill in Controls. Instead of using a single constant $(F, k)$, the shader maps Feed rate $F \in [0.018, 0.058]$ along the vertical axis and Kill rate $k \in [0.048, 0.065]$ along the horizontal axis – so turbulent spiral waves, dividing mitosis cells, coral labyrinths, and soliton worms all coexist in different regions of the same screen!

4. The Multi-Substep Ping-Pong Pipeline

Because the Gray–Scott step requires only a single 9-tap texture read, each animation frame loops the reaction–diffusion shader 14 times across a single RGBA16F double FBO before computing normals and lighting in the display pass:

Seed + Brush u = 1, v droplets 9-Point Laplacian 0.20 cardinal + 0.05 diag Gray–Scott ×14 ±uv² + F(1−u) − (F+k)v Normal map n̂ (−α∂ₓv, −α∂ᵧv, 1) Blinn Phong
Figure 1: Each animation frame runs 14 ping-pong PDE substeps on an RGBA16F torus grid (gl.REPEAT), then reconstructs 3D surface normals from $\nabla v$ on the fly.

5. A Tour of Pearson's $(F, k)$ Morphogenesis Zoo

All four preset buttons in the simulation run the exact same 30-line fragment shader – only the two scalar uniforms $F$ and $k$ change:

Preset Regime Parameters $(F, k)$ Emergent Dynamical Behavior
Coral Labyrinth $F = 0.0370,\; k = 0.0600$ Tips elongate and bifurcate while avoiding neighbors, filling space with a space-filling labyrinth
Mitosis Cells $F = 0.0280,\; k = 0.0620$ Circular spots grow until their cores starve, pinch into dumbbells, and divide like biological cells
Spiral Waves $F = 0.0140,\; k = 0.0450$ Excitable medium supporting self-sustaining Belousov–Zhabotinsky spiral wave rotors
Soliton Worms $F = 0.0540,\; k = 0.0630$ High feed rate stabilizes narrow vermicular ridges that slither and bud from their tips

Click Mitosis Cells in the controls above and paint a single dot on an empty patch of the canvas – watching one droplet of $V$ swell, pinch its waist, and divide exponentially into a colony of hundreds of cells is the purest demonstration of how two lines of partial differential equations can encode biological form.