TouchDesigner Expert Article 6

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.

⏱ 60 min glsl generative simulation feedback physarum

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.