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.
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 floatdeclaration 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:
glsl_rd— the simulation step shader. Input 0:feedback1(current state).feedback1(Feedback TOP) — its “Target TOP” points back toglsl_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.