Reaction Diffusion
In 1952, Alan Turing (the father of modern computing) wrote a paper explaining how leopards get their spots and zebras get their stripes. He proposed that two invisible chemicals spread through the skin and react with each other to create the patterns.
Chemical A and Chemical B:
1# Reaction-Diffusion Chemistry2def gray_scott_equation(A, B, feed, kill):3 """How the two chemicals interact."""45 # 1. Chemical A is added to the system at a constant 'feed' rate.6 # 2. Chemical B is removed from the system at a constant 'kill' rate.78 # 3. The Reaction:9 # 2 parts of B react with 1 part of A to create 3 parts of B!10 # (A + 2B -> 3B)1112 reaction_rate = A * B * B1314 # 4. Change in A = Feed - Reaction15 delta_A = feed * (1 - A) - reaction_rate1617 # 5. Change in B = Reaction - Kill18 delta_B = reaction_rate - (kill + feed) * B1920 return delta_A, delta_B
Cellular Automata (like the Game of Life) are digital: a cell is either 1 or 0. Reaction-Diffusion is analog: it simulates continuous fluid dynamics using floating-point numbers.
Decimals vs Integers:
1# Floats, not Booleans2def create_concentration_grid(width, height):3 """Setting up the petri dish."""45 grid = []67 for y in range(height):8 row = []9 for x in range(width):1011 # Instead of a boolean (Dead/Alive)12 # A cell stores two decimal numbers!13 cell_state = {14 "A": 1.0, # 100% full of A15 "B": 0.0 # 0% full of B16 }1718 row.append(cell_state)1920 grid.append(row)2122 return grid
If we drop ink into a glass of water, it slowly bleeds outward until the entire glass is tinted. This is called Diffusion. In our grid, chemicals bleed into their neighbors using a Laplacian Convolution Matrix.
Weighted Neighborhoods:
-1.0). 4. The 4 direct neighbors (Top/Bottom/Left/Right) contribute 0.2 each. 5. The 4 diagonal neighbors are slightly further away, so they only contribute 0.05 each. 6. This mathematical matrix (a 3x3 Convolution Kernel) perfectly simulates liquid diffusion!1# 2D Spatial Diffusion2def calculate_laplacian(grid, x, y, chemical):3 """How the chemical bleeds into neighbors."""45 # 1. Start with the center cell (multiplied by -1)6 sum_val = grid[y][x][chemical] * -1.078 # 2. Add the direct neighbors (Top, Bottom, Left, Right)9 # They get a weight of 0.210 sum_val += grid[y-1][x][chemical] * 0.211 sum_val += grid[y+1][x][chemical] * 0.212 sum_val += grid[y][x-1][chemical] * 0.213 sum_val += grid[y][x+1][chemical] * 0.21415 # 3. Add the diagonal neighbors16 # They get a smaller weight of 0.05 because they are further away17 sum_val += grid[y-1][x-1][chemical] * 0.0518 sum_val += grid[y-1][x+1][chemical] * 0.0519 sum_val += grid[y+1][x-1][chemical] * 0.0520 sum_val += grid[y+1][x+1][chemical] * 0.052122 return sum_val
We now have the two halves of our physics simulation: The Reaction (how the chemicals eat each other) and the Diffusion (how the chemicals bleed across the grid).
The Full Equation:
1# Calculating the Next Frame2def update_cell_concentration(grid, x, y, feed, kill, diff_A, diff_B, time_step):34 # 1. Look at the current amount of chemicals in this specific cell5 A = grid[y][x]["A"]6 B = grid[y][x]["B"]78 # 2. How much liquid is bleeding in/out from the neighbors?9 laplace_A = calculate_laplacian(grid, x, y, "A")10 laplace_B = calculate_laplacian(grid, x, y, "B")1112 # 3. How much are the chemicals reacting with each other?13 delta_A, delta_B = gray_scott_equation(A, B, feed, kill)1415 # 4. Add it all together to get the new state!16 # Formula: New = Old + (Diffusion + Reaction) * TimeStep1718 new_A = A + (diff_A * laplace_A + delta_A) * time_step19 new_B = B + (diff_B * laplace_B + delta_B) * time_step2021 return new_A, new_B
When calculating the Laplacian Matrix, a cell needs to look at its Top, Bottom, Left, and Right neighbors. But what happens if the cell is on the absolute edge of the grid? It doesn't have a Left neighbor!
Toroidal Wrapping:
1# Wrapping the Edges2def wrap_coordinate(value, max_size):3 """Making the grid infinite (Toroidal)."""45 # If we go off the right edge (value >= max_size)6 # The modulo operator (%) wraps us back to 0!78 # If we go off the left edge (value < 0)9 # Modulo wraps us back to max_size - 1!1011 return value % max_size1213# Usage inside the Laplacian function:14# ny = wrap_coordinate(y - 1, grid.height)15# nx = wrap_coordinate(x + 1, grid.width)16# neighbor_value = grid[ny][nx]
Our Reaction-Diffusion simulation is just a giant grid of floating-point numbers. To make it useful for Computational Geometry, we must map those numbers onto a physical 3D mesh.
Displacement Mapping:
1# Data Visualization2def render_grid_to_mesh(grid, mesh):3 """Converting chemical data into physical shape."""45 # 1. We have a flat 3D Plane Mesh with lots of vertices6 for vertex in mesh.vertices:78 # 2. Get the chemical concentration at this exact coordinate9 A = grid[vertex.y][vertex.x]["A"]10 B = grid[vertex.y][vertex.x]["B"]1112 # 3. We calculate a "Difference" score13 # If A is high and B is low, the number is positive.14 # If B is high and A is low, the number is negative.15 score = A - B1617 # 4. We push the vertex UP or DOWN based on the score!18 vertex.z = score * 5.01920 return mesh
By simulating Gray-Scott equations inside a hardware-accelerated WebGL GPGPU shader, we solve chemical diffusion in real-time. Varying the Feed and Kill rates yields self-organizing organic patterns resembling cell division, coral branching, or leopard spots.
Simulation Rules:
1def step_gray_scott(A, B, dA, dB, feed, kill):2 nextA = copy(A)3 nextB = copy(B)4 for y in range(1, height-1):5 for x in range(1, width-1):6 # 1. Calculate laplacians7 lapA = laplacian(A, x, y)8 lapB = laplacian(B, x, y)910 # 2. Apply Gray-Scott update rules11 a, b = A[y][x], B[y][x]12 nextA[y][x] = a + dA * lapA - a*b*b + feed * (1.0 - a)13 nextB[y][x] = b + dB * lapB + a*b*b - (kill + feed) * b14 return nextA, nextB