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 write | You are handed | Runs on |
|---|---|---|
vec4 render0D() | nothing | everything |
vec4 render1D(float t) | t — 0 to 1 along the wiring order | strips and chains |
vec4 render2D(vec2 uv) | uv — 0 to 1 across the surface | everything |
vec4 render3D(vec3 pos) | pos — real position, in metres | everything |
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:
| Name | What it is |
|---|---|
Time | the show clock in seconds, identical on every machine |
Pixelcount | how many pixels this output has |
Resolution | this output's render size in pixels, as a vec2 |
ApplyPalette(v, c) | the operator's palette, applied to your value |
| your parameters | every 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:
| Parameter | In your GLSL | Read it with |
|---|---|---|
| TOP | a sampler2D | texture(Name, uv) |
| CHOP | a texture buffer, one sample per index, channels in .x .y .z .w | Name(i) returns a vec4; NameCount is its length |
| POP | a buffer of one point attribute — the one with the parameter's name | Name(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
- Making Plugins — texture sources, effects, system plugins, and feeds
- Making an Output — the LED and Surface templates that render these sources
- Authoring Looks — how an operator combines sources into a look

