Advanced Generative Systems in TOPs
Implement reaction-diffusion, cellular automata, fluid simulation, and Physarum slime mould using GLSL feedback loops in TouchDesigner's texture pipeline.
The most computationally rich generative systems in TouchDesigner are implemented as iterative GLSL programs running over texture buffers — each frame reads the previous frame’s state and writes the next. This feedback architecture transforms the TOP pipeline into a general-purpose GPU compute environment, capable of simulating physical systems that would be intractable on the CPU.
The Feedback Loop Pattern
Every simulation in this article shares the same structural pattern:
GLSL TOP (simulation step)
└→ Feedback TOP
└─────────────────────┐
↑ (feeds back to GLSL TOP input 0)
The Feedback TOP in TD is what makes iterative computation possible — it provides the output of the last cook as an input to the current cook without creating a dependency cycle. Set the Feedback TOP’s Target OP to the GLSL TOP it’s feeding.
For multi-buffer simulations (where you need to read from multiple previous-state textures simultaneously), use multiple CHOP or TOP inputs alongside the single Feedback TOP, or use a pair of GLSL TOPs ping-ponging between two textures.
Reaction-Diffusion: Gray-Scott Model
The Gray-Scott model simulates two chemical species (U and V) reacting and diffusing across a 2D surface. The parameters feed (f) and kill (k) control the emergent pattern — different parameter combinations produce spots, stripes, solitons, and coral-like structures.
// GLSL TOP — Gray-Scott simulation step
// Input 0: previous state (R=U concentration, G=V concentration)
// Input 1: seed/mask texture (optional — inject V into selected pixels)
uniform float uFeed; // feed rate, typical range 0.01–0.08
uniform float uKill; // kill rate, typical range 0.04–0.07
uniform float uDiffuseU; // diffusion rate of U, typically 1.0
uniform float uDiffuseV; // diffusion rate of V, typically 0.5
uniform float uDeltaTime; // time step, typically 1.0
// 5-point Laplacian stencil with wrap-around addressing
vec2 laplacian(sampler2D tex, vec2 uv, vec2 texelSize)
{
vec2 center = texture(tex, uv).rg;
vec2 n = texture(tex, uv + vec2(0.0, texelSize.y)).rg;
vec2 s = texture(tex, uv + vec2(0.0, -texelSize.y)).rg;
vec2 e = texture(tex, uv + vec2( texelSize.x, 0.0)).rg;
vec2 w = texture(tex, uv + vec2(-texelSize.x, 0.0)).rg;
return (n + s + e + w) - 4.0 * center;
}
void main()
{
vec2 uv = vUV.st;
vec2 texelSize = 1.0 / uTD.res.zw;
vec2 state = texture(sTD2DInputs[0], uv).rg;
float U = state.r;
float V = state.g;
vec2 lap = laplacian(sTD2DInputs[0], uv, texelSize);
float reaction = U * V * V;
float dU = uDiffuseU * lap.r - reaction + uFeed * (1.0 - U);
float dV = uDiffuseV * lap.g + reaction - (uKill + uFeed) * V;
U += dU * uDeltaTime;
V += dV * uDeltaTime;
// Optional: inject V from mask texture (Input 1)
float mask = texture(sTD2DInputs[1], uv).r;
V = mix(V, 1.0, mask * 0.1);
fragColor = vec4(clamp(U, 0.0, 1.0), clamp(V, 0.0, 1.0), 0.0, 1.0);
}
For colourisation, pass the simulation output through a separate GLSL TOP that maps V concentration to a colour palette via a 1D lookup texture.
Parameter tuning for common patterns:
- Spots: f=0.035, k=0.065
- Stripes: f=0.060, k=0.062
- Coral: f=0.025, k=0.060
- Solitons: f=0.014, k=0.054
Run the simulation at 512×512 and upscale with a high-quality upsampler — the patterns are smooth enough that 4× upscaling is visually seamless at normal viewing distances.
Cellular Automata: Conway’s Game of Life and Beyond
Game of Life in a single GLSL TOP, running at 1920×1080, can sustain 60 fps without effort. The interesting variants are Larger Than Life (extended neighbourhood), Brian’s Brain (three-state), and Lenia (continuous CA).
// Game of Life — reads neighbourhood, applies B3/S23 rule
void main()
{
vec2 uv = vUV.st;
vec2 ts = 1.0 / uTD.res.zw;
float c = texture(sTD2DInputs[0], uv).r > 0.5 ? 1.0 : 0.0;
float n = 0.0;
// 8-neighbour sum
for (int dy = -1; dy <= 1; dy++)
for (int dx = -1; dx <= 1; dx++)
{
if (dx == 0 && dy == 0) continue;
vec2 nUV = uv + vec2(float(dx), float(dy)) * ts;
n += texture(sTD2DInputs[0], nUV).r > 0.5 ? 1.0 : 0.0;
}
float next = c;
if (c > 0.5)
next = (n == 2.0 || n == 3.0) ? 1.0 : 0.0; // Survival
else
next = (n == 3.0) ? 1.0 : 0.0; // Birth
fragColor = vec4(next, next, next, 1.0);
}
Physarum Slime Mould Simulation
Physarum polycephalum is a slime mould that forms efficient network patterns connecting food sources. The simulation is agent-based but runs entirely on GPU using a texture as the agent map. Each “agent” is a pixel in a separate direction texture; the diffusion and decay of the trail texture creates the characteristic vein patterns.
This simulation requires two textures:
- Trail map (512×512, R = chemoattractant concentration)
- Agent map (512×512, RG = agent position in UV space, B = agent heading in radians, A = alive flag)
// Pass 1: Agent update — reads trail map, updates agent position and heading
// Input 0: trail map (previous frame)
// Input 1: agent map (previous frame)
uniform float uSensorAngle; // radians, e.g. 0.4
uniform float uSensorDist; // UV units, e.g. 0.02
uniform float uTurnSpeed; // radians per frame, e.g. 0.15
uniform float uMoveSpeed; // UV units per frame, e.g. 0.001
float sampleTrail(vec2 pos, float angle)
{
vec2 sensorPos = pos + vec2(cos(angle), sin(angle)) * uSensorDist;
return texture(sTD2DInputs[0], fract(sensorPos)).r;
}
void main()
{
vec2 uv = vUV.st;
vec4 agent = texture(sTD2DInputs[1], uv);
if (agent.a < 0.5) { fragColor = agent; return; } // dead agent
vec2 pos = agent.rg;
float heading = agent.b;
// Three sensor readings: forward, left, right
float fwd = sampleTrail(pos, heading);
float left = sampleTrail(pos, heading + uSensorAngle);
float right = sampleTrail(pos, heading - uSensorAngle);
// Steering
if (fwd > left && fwd > right)
heading = heading; // continue forward
else if (left > right)
heading += uTurnSpeed;
else if (right > left)
heading -= uTurnSpeed;
else
heading += (fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.5453) - 0.5) * uTurnSpeed * 2.0;
// Move
pos = fract(pos + vec2(cos(heading), sin(heading)) * uMoveSpeed);
fragColor = vec4(pos, heading, 1.0);
}
// Pass 2: Trail deposit and decay
// Input 0: trail map (previous frame)
// Input 1: agent map (current frame, after Pass 1)
uniform float uDecay; // 0.98 = 2% decay per frame
uniform float uDeposit; // amount deposited per agent per frame
void main()
{
vec2 uv = vUV.st;
float trail = texture(sTD2DInputs[0], uv).r * uDecay;
// Diffuse trail: average with 3×3 neighbourhood (simple box blur step)
float blur = 0.0;
vec2 ts = 1.0 / uTD.res.zw;
for (int dy = -1; dy <= 1; dy++)
for (int dx = -1; dx <= 1; dx++)
blur += texture(sTD2DInputs[0], uv + vec2(float(dx), float(dy)) * ts).r;
blur /= 9.0;
trail = mix(trail, blur, 0.2);
// Deposit: check if any agent is near this pixel
// (simplified: agent map pixel at same UV — direct deposit)
vec4 agent = texture(sTD2DInputs[1], uv);
if (agent.a > 0.5)
trail = min(trail + uDeposit, 1.0);
fragColor = vec4(trail, trail, trail, 1.0);
}
Audio-Reactive Physarum
Modulate the Physarum simulation’s growth rate via audio analysis:
# In Perform DAT or Execute DAT, frameStart callback
def frameStart(frame):
bass = op('audio_bass')['chan1'][0]
rms = op('audio_rms')['chan1'][0]
# Bass controls deposit rate — more bass = faster network growth
op('physarum_pass2').par.Deposituniform = bass * 0.08 + 0.01
# RMS controls decay — louder = less decay (denser trails)
op('physarum_pass2').par.Decayuniform = 1.0 - (rms * 0.03)
# Move speed modulated by mid-frequency energy
mid = op('audio_mid')['chan1'][0]
op('physarum_pass1').par.Movespeeduniform = mid * 0.002 + 0.0005
For colour, map trail density through a GLSL coloriser using a palette inspired by bioluminescence:
// Colour mapping pass
void main()
{
float t = texture(sTD2DInputs[0], vUV.st).r;
// Deep blue background → cyan midtones → white-hot peaks
vec3 col = mix(vec3(0.02, 0.05, 0.2),
mix(vec3(0.1, 0.8, 0.9), vec3(1.0), smoothstep(0.7, 1.0, t)),
smoothstep(0.0, 0.4, t));
fragColor = vec4(col, 1.0);
}
Run the Physarum simulation at half the output resolution (e.g., 960×540 for a 1920×1080 output) and upscale with a Blur TOP at a very small radius (0.5–1 pixel) to smooth the pixelated agent positions. The biological authenticity of the pattern is retained even with this upscaling, and you recover roughly 4× the performance.