TouchDesigner Intermediate Article 2

GLSL Shaders in TouchDesigner

Write custom GLSL fragment and vertex shaders inside TouchDesigner using the GLSL TOP and GLSL MAT, passing uniforms from CHOPs and sampling texture inputs.

⏱ 25 min glsl shaders gpu

TouchDesigner exposes the GPU directly through two operators: the GLSL TOP, which runs a fragment shader on a fullscreen quad and outputs the result as a texture, and the GLSL MAT, which takes separate vertex and fragment shaders to shade 3D geometry in a Render TOP pipeline. Knowing both unlocks generative visuals, image-processing effects, and feedback loops that run entirely on the GPU at full frame rate.

GLSL TOP Boilerplate

Create a GLSL TOP and open its Pixel Shader tab. The minimum working fragment shader looks like this:

// Pixel shader for GLSL TOP
uniform float uTime;

out vec4 fragColor;

void main()
{
    // vUV is provided automatically — normalized [0,1] UV coordinates
    vec2 uv = vUV.st;
    
    float r = uv.x;
    float g = uv.y;
    float b = abs(sin(uTime));
    
    fragColor = vec4(r, g, b, 1.0);
}

vUV is a built-in varying that TD passes to every GLSL TOP fragment shader — it gives you normalized UV coordinates across the output texture. uResolution is also available as a built-in vec2 containing the output width and height in pixels.

uTime is not automatic — you must pass it manually (covered below). For the shader above to animate, wire a Math CHOP set to output absTime.seconds into the GLSL TOP’s CHOP input, then reference it in the GLSL TOP’s Vectors 1 page using uniform name uTime.

Passing Uniforms from CHOPs

On the GLSL TOP’s parameters, go to the Vectors 1 page. Each row lets you bind a CHOP channel to a named uniform:

  • Set “CHOP” to the path of your CHOP, e.g. constant1
  • Set “Uniform Name” to match the uniform float declaration in your shader

For a vec2 uniform, pick two consecutive channels from the same CHOP. For a vec4, pick four. TD handles the CHOP→uniform upload every cook.

Sampling Texture Inputs

Add a TOP to the GLSL TOP’s first input by wiring it. Inside the shader, sample it with the built-in array sTD2DInputs:

uniform sampler2D sTD2DInputs[1]; // declared automatically by TD

out vec4 fragColor;

void main()
{
    vec2 uv = vUV.st;
    
    // Sample the first TOP input
    vec4 src = texture(sTD2DInputs[0], uv);
    
    // Invert colours
    fragColor = vec4(1.0 - src.rgb, src.a);
}

Wire multiple TOPs into the GLSL TOP and access them as sTD2DInputs[1], sTD2DInputs[2], and so on. The GLSL TOP Info DAT shows you exactly how many inputs are declared.

Kaleidoscope Shader

A classic effect that folds UV space into symmetrical segments:

uniform float uSegments; // pass from a CHOP, e.g. value 6.0
uniform float uTime;

out vec4 fragColor;

void main()
{
    vec2 uv = vUV.st * 2.0 - 1.0; // remap to [-1, 1]
    
    float angle = atan(uv.y, uv.x);
    float radius = length(uv);
    
    float seg = 3.14159265 / uSegments;
    angle = mod(angle, seg * 2.0);
    angle = abs(angle - seg);
    
    vec2 folded = vec2(cos(angle), sin(angle)) * radius;
    folded = folded * 0.5 + 0.5; // back to [0, 1]
    
    vec4 col = texture(sTD2DInputs[0], folded + uTime * 0.01);
    fragColor = col;
}

Reaction-Diffusion Ping-Pong

GPU reaction-diffusion requires two render targets that alternate roles each frame — one holds the current state, the other receives the next state. In TD you build this with two GLSL TOPs and a Feedback TOP.

Setup:

  1. glsl_rd — the simulation step shader. Input 0: feedback1 (current state).
  2. feedback1 (Feedback TOP) — its “Target TOP” points back to glsl_rd. This creates the ping-pong loop.
// glsl_rd pixel shader — Gray-Scott model
uniform float uFeed;    // feed rate, e.g. 0.055
uniform float uKill;    // kill rate, e.g. 0.062
uniform float uDiffA;   // diffusion A, e.g. 1.0
uniform float uDiffB;   // diffusion B, e.g. 0.5
uniform float uDt;      // time step, e.g. 1.0

out vec4 fragColor;

vec4 sampleState(vec2 uv) {
    return texture(sTD2DInputs[0], uv);
}

void main()
{
    vec2 uv = vUV.st;
    vec2 texel = 1.0 / uResolution.xy;
    
    vec4 center = sampleState(uv);
    float A = center.r;
    float B = center.g;
    
    // 5-point Laplacian
    float lapA = sampleState(uv + vec2(texel.x, 0)).r
               + sampleState(uv - vec2(texel.x, 0)).r
               + sampleState(uv + vec2(0, texel.y)).r
               + sampleState(uv - vec2(0, texel.y)).r
               - 4.0 * A;
    float lapB = sampleState(uv + vec2(texel.x, 0)).g
               + sampleState(uv - vec2(texel.x, 0)).g
               + sampleState(uv + vec2(0, texel.y)).g
               + sampleState(uv - vec2(0, texel.y)).g
               - 4.0 * B;
    
    float reaction = A * B * B;
    float newA = A + uDt * (uDiffA * lapA - reaction + uFeed * (1.0 - A));
    float newB = B + uDt * (uDiffB * lapB + reaction - (uKill + uFeed) * B);
    
    newA = clamp(newA, 0.0, 1.0);
    newB = clamp(newB, 0.0, 1.0);
    
    fragColor = vec4(newA, newB, 0.0, 1.0);
}

Seed the simulation by pulsing the Feedback TOP’s Reset parameter, or by adding a second input TOP with random seeds and mixing it in for the first frame.

GLSL MAT for 3D Geometry

A GLSL MAT takes both a vertex shader (transforms geometry) and a pixel shader (determines surface colour). Apply it to a Geo COMP’s material parameter.

// Vertex shader (Vertex Shader tab of GLSL MAT)
uniform float uWaveAmp;
uniform float uTime;

// TDDeform() applies TD's built-in instancing and skinning transforms
void main()
{
    vec4 worldPos = TDDeform(P);
    
    // Displace along normal by a wave
    float wave = sin(worldPos.x * 0.1 + uTime) * uWaveAmp;
    worldPos.xyz += N * wave;
    
    gl_Position = TDWorldToProj(worldPos);
}
// Pixel shader (Pixel Shader tab of GLSL MAT)
uniform float uTime;

out vec4 fragColor;

void main()
{
    // iUV is the interpolated UV from the vertex shader
    float brightness = abs(sin(iUV.x * 10.0 + uTime));
    fragColor = vec4(brightness, brightness * 0.3, 1.0 - brightness, 1.0);
}

TDDeform() and TDWorldToProj() are TD-specific GLSL functions that handle the model-view-projection matrix and built-in instancing data automatically. Always use them instead of writing the MVP multiply manually.

Feedback Distortion Loop

A self-distorting feedback effect: the GLSL TOP reads its own previous frame via a Feedback TOP and pushes pixels according to a displacement field.

// Input 0: feedback (previous frame), Input 1: displacement TOP
uniform float uStrength;

out vec4 fragColor;

void main()
{
    vec2 uv = vUV.st;
    
    vec4 disp = texture(sTD2DInputs[1], uv);
    vec2 offset = (disp.rg - 0.5) * uStrength;
    
    // Sample previous frame at displaced UV
    vec4 prev = texture(sTD2DInputs[0], uv + offset);
    
    // Slight decay to prevent saturation
    fragColor = prev * 0.98;
}

Set uStrength to around 0.005 to start — small offsets accumulate into large, fluid-looking distortions over many frames. Adjust the 0.98 decay factor to control how long the trails persist.

The GLSL Multi TOP

When a single shader needs to write to more than one texture simultaneously — for example writing both position and velocity in a particle simulation — use the GLSL Multi TOP. It supports up to 8 simultaneous outputs, each written to a separate layout(location = N) out vec4 variable. Each output becomes a separate output connection on the node.