Procedural generationPRC-02
Reaction-Diffusion
Two substances, two rules, two numbers — and out of them corals, stripes and leopard spots.
- Running time
- O(cells · steps)
- Model
- Gray-Scott
- Knobs
- feed and kill
- Idea by
- Alan Turing, 1952
01What it is about
In 1952 Alan Turing published a paper that had nothing to do with computers: The Chemical Basis of Morphogenesis. The question in it was how a uniform lump of cells can end up as a leopard — where the pattern comes from when everything is the same everywhere to begin with.
His answer: two substances that spread at different speeds and react with each other produce spots and stripes all by themselves. An even state turns into an uneven one without anyone prescribing the pattern. That is counter-intuitive enough that it took thirty years before chemists could show it in a test tube.
What is implemented here is the Gray-Scott model, the best known variant. It consists of two lines of arithmetic — and delivers corals, zebra stripes, brain folds and fingerprints, depending on which two numbers are turned.



02How it works
The two substances
A is fed in from outside. B eats A and turns into more B in the process. And B is drained off continuously. Both spread, but A twice as fast as B — that difference is the real cause of the pattern.
A' = A + dA · spread(A) − A·B·B + feed · (1 − A) B' = B + dB · spread(B) + A·B·B − (kill + feed) · B
The A·B·B is the reaction: it takes two B to convert one A — hence the square. Exactly this feedback makes the system unstable: where there is some B already, more B appears faster.
The spreading
spread is the Laplacian — the same one that finds edges over on edge detection. Here it has a more vivid meaning: how much flows in from the neighbours? It is the average of the surroundings minus the cell's own value. Diagonal neighbours count less, because they are further away.
The borders are glued together: what runs out on the right comes back in on the left. That is why the pattern has no edge.
Feed and kill
Everything hangs on two numbers, and both lie between 0.02 and 0.08:
- feed — how much A is supplied
- kill — how much B is drained
A hundredth in one direction and stripes turn into dots. A hundredth in the other and everything dies off or grows shut. The map of these two values is itself a well known picture in the literature; the three recipes in the example are points on it.
The first drop
Without a disturbance nothing ever happens: A everywhere, B nowhere — that state stays forever. So a few drops of B are put in. From there the pattern grows outwards, and where two growing fronts meet, exactly those branchings appear that make the whole thing look like something grown.
03Try it
04Implementation
using MagicBook.Algorithms.Randomness;
// Alan Turing asked in 1952 how a uniform blob of cells can end up as a leopard, and
// answered it with two substances that spread and react with each other. The model
// used here is Gray-Scott: A is fed in from outside, B eats A and turns into more B,
// and B is drained off. Both spread, but A twice as fast as B.
//
// Everything depends on two numbers. feed is how much A is fed in, kill how much B is
// drained. A hundredth in either direction turns stripes into dots or lets the whole
// pattern die.
public static class ReactionDiffusion
{
private const float SpreadA = 1.0f;
private const float SpreadB = 0.5f;
public static float[] Simulate(int width, int height, int steps, float feed, float kill, ulong seed)
{
var a = new float[width * height];
var b = new float[width * height];
Array.Fill(a, 1f);
Splash(b, width, height, seed);
var nextA = new float[a.Length];
var nextB = new float[b.Length];
for (var step = 0; step < steps; step++)
{
for (var cell = 0; cell < a.Length; cell++)
{
// How much A and B meet here. Two B are needed to turn one A into
// another B, which is why the square is in there.
var reaction = a[cell] * b[cell] * b[cell];
nextA[cell] = a[cell] + SpreadA * Spread(a, width, height, cell) - reaction + feed * (1 - a[cell]);
nextB[cell] = b[cell] + SpreadB * Spread(b, width, height, cell) + reaction - (kill + feed) * b[cell];
nextA[cell] = Math.Clamp(nextA[cell], 0f, 1f);
nextB[cell] = Math.Clamp(nextB[cell], 0f, 1f);
}
(a, nextA) = (nextA, a);
(b, nextB) = (nextB, b);
}
return b;
}
// How much flows in from the neighbours: the average around the cell minus the
// cell itself. Diagonal neighbours count less, because they are further away.
// This is the Laplacian, the same operator the edge filters use.
private static float Spread(float[] values, int width, int height, int cell)
{
var x = cell % width;
var y = cell / width;
var sum = -values[cell];
for (var dy = -1; dy <= 1; dy++)
{
for (var dx = -1; dx <= 1; dx++)
{
if (dx == 0 && dy == 0)
continue;
// The edges are glued together, so the pattern has no border.
var neighbour = ((y + dy + height) % height) * width + (x + dx + width) % width;
sum += values[neighbour] * (dx == 0 || dy == 0 ? 0.2f : 0.05f);
}
}
return sum;
}
// Without a disturbance nothing would ever happen: A is everywhere, B nowhere,
// and that state stays as it is. So a few drops of B are put in.
private static void Splash(float[] b, int width, int height, ulong seed)
{
var random = new Pcg(seed);
for (var drop = 0; drop < 14; drop++)
{
var centreX = random.Next(width);
var centreY = random.Next(height);
for (var y = -4; y <= 4; y++)
{
for (var x = -4; x <= 4; x++)
b[((centreY + y + height) % height) * width + (centreX + x + width) % width] = 1f;
}
}
}
}
05Three recipes
const int width = 200;
const int height = 150;
const int steps = 6000;
// The same two rules, the same starting drops, only feed and kill differ -
// and out comes something completely different every time.
(string Name, float Feed, float Kill)[] recipes =
[
("coral", 0.0545f, 0.0620f),
("stripes", 0.0580f, 0.0650f),
("spots", 0.0300f, 0.0620f),
];
Console.WriteLine("pattern feed kill covered by B");
foreach (var (name, feed, kill) in recipes)
{
var b = ReactionDiffusion.Simulate(width, height, steps, feed, kill, seed: 20240921);
// B never gets anywhere near 1, so for the picture the values are pulled
// apart until the strongest spot is white.
var strongest = b.Max();
var shown = new float[b.Length];
for (var i = 0; i < b.Length; i++)
shown[i] = b[i] / strongest;
Channels.ToImage(shown, width, height).Save(Path.Combine(folder, $"reaction-{name}.png"));
Console.WriteLine($"{name,-8} {feed:0.0000} {kill:0.0000} " +
$"{b.Count(value => value > 0.25f) * 100.0 / b.Length,10:0.0} %");
}
pattern feed kill covered by B
coral 0.0545 0.0620 44.9 %
stripes 0.0580 0.0650 17.0 %
spots 0.0300 0.0620 17.5 %
06Good to know
- The method is an explicit simulation: small steps, many of them. 6000 steps on 200 × 150 cells are 180 million cell updates — and still done in seconds, because each of them is only a few multiplications. Steps that are too large make the arithmetic explode; that is the reason for the many small ones.
- Every cell depends only on its eight neighbours. That makes the method perfectly parallel — on a graphics card it runs in real time, and that is exactly how the live demos one finds on the web are built.
- Turing predicted the pattern without ever seeing it. It took the Belousov-Zhabotinsky reaction to show that chemistry really does this, and only in the nineties could biologists trace the stripes of certain fish back to such systems.
- Related in effect but far simpler in its rule: Conway's Game of Life and cellular automata in general. The difference is that the arithmetic here runs on floating point numbers instead of on and off — which is why the pattern is soft rather than pixelated.