TouchDesigner

Shader Sources

Write a source as a single GLSL function and let Lightpath run it on LEDs, screens, and anything else in the venue.

There are two kinds of source in Lightpath.

A texture source produces an image. It renders its own chain, at its own resolution, and the output samples the result. That is the kind described in Making Plugins.

A shader source produces a colour for a point. You write one GLSL function; Lightpath composes it into the material each output renders with, and calls it once per LED or once per pixel. There is no texture in between, so there is no resolution to pick and nothing to unwrap.

That difference is what lets one source run on a strip of 40 LEDs and a 4K screen at the same time, and look like one continuous thing across both.


The one function you write

A shader source defines exactly one of these:

You writeYou are handedRuns on
vec4 render0D()nothingeverything
vec4 render1D(float t)t — 0 to 1 along the wiring orderstrips and chains
vec4 render2D(vec2 uv)uv — 0 to 1 across the surfaceeverything
vec4 render3D(vec3 pos)pos — real position, in metreseverything

Which one you pick is the declaration of what your source is. There is no separate setting, and that is deliberate: a setting can drift out of step with the code, a function signature cannot.

Lightpath builds the rest of the shader around you — no main(), no uniform declarations, no varyings to match.

Choosing

render0D assumes nothing at all and therefore runs anywhere. Reach for it when the idea is "the room does this" rather than "something moves across the room" — a breath, a colour wash, a strobe.

render1D is position along the order the cable was run, not position in space. That is the whole point: a chase written against t travels the way the installer expects even on a strip that doubles back on itself, and it does not care about shape. It needs an output that has a wiring order, which a strip has and a screen does not.

render2D is position across the surface, 0 to 1 — and nothing else. It is not square. On an 8 × 4 metre wall, one unit of u covers twice the distance one unit of v does, so a circle drawn in uv arrives as an ellipse unless you say otherwise. Correct it yourself with Resolution:

vec2 p = uv - 0.5;
p.x *= Resolution.x / Resolution.y;   // now a circle is a circle
return vec4(vec3(step(length(p), 0.3)), 1.0);

Doing it here rather than in the coordinates is deliberate. An output's resolution is chosen to match its surface, so a texture source authored at that resolution lands on it exactly — which it could not if the coordinates had been pre-stretched to suit a square source.

render3D is where the pixel physically is, in metres. This is the only entry point that makes one look agree across several outputs at once: a plane sweeping the venue crosses a floor strip and a wall screen at the same instant, because both are answering in the same metres. It needs the output's world space set to mean anything.

The coordinate only ever reaches you as the argument. Writing pos inside a render1D function is a compile error, not a silent zero — the name does not exist there.


What every source can read

Available in all four entry points, no declaration needed:

NameWhat it is
Timethe show clock in seconds, identical on every machine
Pixelcounthow many pixels this output has
Resolutionthis output's render size in pixels, as a vec2
ApplyPalette(v, c)the operator's palette, applied to your value
your parametersevery custom parameter, by its own name

Working with the palette

A texture source samples the palette with a Lookup TOP. A shader source calls ApplyPalette instead. Hand it the scalar you computed and the colour you would have used on your own:

float v = 0.5 + 0.5 * sin(6.28318530718 * Time * Speed);
vec3 mine = vec3(1.0, 0.88, 0.72);
return vec4(ApplyPalette(v, mine), 1.0);

When the operator has picked a palette, it wins. When they have not, your colour is used. You do not have to check which.

Parameters become uniforms

Add a custom parameter on the Look page and it appears in your GLSL under the same name. A Speed float is Speed. No wiring, no binding. It arrives as the type its size implies: one number is a float, a two-component position a vec2, a three-component colour a vec3.

A parameter can also point at an operator, which hands your shader data rather than a number. Below, Name stands for your parameter's name:

ParameterIn your GLSLRead it with
TOPa sampler2Dtexture(Name, uv)
CHOPa texture buffer, one sample per index, channels in .x .y .z .wName(i) returns a vec4; NameCount is its length
POPa buffer of one point attribute — the one with the parameter's nameName(i) returns a vec4; NameCount is its length

A CHOP can change length while the look runs; NameCount follows it with no recompile. A POP parameter named Layout reads that POP's Layout attribute, so name the parameter after the attribute you want. Reading past the end returns zero from a CHOP and repeats the last point of a POP, so keep i below NameCount.

Use Look for what an operator dials in per look, and Config for install-level settings. The full rules are in TouchDesigner Overview → Custom parameters.


Writing one

Create a source plugin from the Plugins page. The template that arrives is a shader source carrying all four entry points as worked examples, with a menu to switch between them. Delete the three you do not want and write over the one you keep.

Previewing as you go

The template contains a Shader Debugger. It compiles what you have written, shows it on geometry that suits the entry point you declared — a flat quad for 0D and 2D, a spiral for 1D, a cube for 3D — and lists any compile errors with the line they came from.

Turn Show Debugger off, or delete the component, once the source works. It carries its own composition and costs real frame time in a show.


Where a source shows up

An output decides which entry points it is willing to be offered, on the Outputs page under Source Types. A 1D source renders as one flat colour on a screen, so a screen can be set to stop offering them.

Sources that declare no entry point — texture sources, and older shaders written against the earlier color() signature — are always offered, because they run anywhere.


Where to go next

On this page