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 color 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 layout order | strips and chains |
vec4 render2D(vec2 uv) | uv — 0 to 1 across the surface | everything |
vec4 render3D(vec3 pos) | pos — real position, in meters | 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 color wash, a strobe.
render1D is position along the output's layout order: 0 at one end of
the output, 1 at the other, in the order its pixels are laid out along the
install. That is the order a viewer reads the structure in, not the order the
cable happens to run, so a chase written against t travels smoothly along
the install even where strips were wired out of sequence, and it does not
care about shape. It needs an output that has a layout order, which a strip
has and a screen does not.
render1D is not handed the pixel's number. Every entry point can read it as
Pointindex (see below), but it is for looking up data stored per pixel, not
for placing content: it follows the order the output's geometry holds its
pixels, which is not necessarily the layout order t follows, so a chase
written against Pointindex can jump between strips.
render2D is position across the surface, 0 to 1 — and nothing else.
It is not square. On an 8 × 4 meter 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.
uv comes from the output's texture coordinates. An output can carry more
than one set of them, and the operator chooses which set each layer uses
with UV Map in the look editor, so the same source can wrap around a
structure on one layer and run flat across its front on another. Your
source needs nothing for this: it is handed whichever set was chosen. See
Making an Output → UV sets.
render3D is where the pixel physically is, in meters. 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 meters. 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 |
Pointcount | how many pixels this output has, as a float |
Pointindex | which pixel this is, from 0 up to Pointcount - 1, as an int |
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 |
| the output's attributes | every extra attribute the output carries per pixel, by its own name |
The output's own attributes
An output can carry its own data on every pixel: which rib of a structure the pixel sits on, which sector, how far along a fixture it is. Each of these point attributes is a name your source can read directly, in any entry point, with the value for the pixel being drawn:
vec4 render0D()
{
// Rib is a point attribute the output carries
float v = fract(Rib * 0.1 + Time * Speed);
return vec4(ApplyPalette(v, vec3(1.0)), 1.0);
}An attribute arrives as the type its size implies — float, vec2, vec3
or vec4 — and an integer attribute as an int. Attributes whose names
start with Tex are UV sets
rather than names to read, and the
attributes Lightpath draws with itself (the point's position, normal and
color, Worldpos, Layout, Pointindex and Pointcount) are not exposed
this way.
A source that reads an attribute is offered only where it can run. The source picker hides it from a look when any of the look's outputs lacks the attribute, and the look editor won't add such an output to a look that uses it. A name that no output in the project carries is not defined at all, so the source fails to build with an error naming it.
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
color 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 color 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 color 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.
In any source, Name(Pointindex) is the value for the pixel being drawn,
and Name(Pointindex + 1) is the next pixel's. For the pixel being drawn,
an attribute the output already carries is simpler to
read by name. A POP parameter is for data
the output doesn't carry, or for reading a pixel other than the one being
drawn:
vec4 render1D(float t)
{
// a POP parameter named Height: how much higher the next pixel sits
float rise = Height(Pointindex + 1).x - Height(Pointindex).x;
return vec4(ApplyPalette(clamp(rise * 10.0, 0.0, 1.0), vec3(1.0)), 1.0);
}If the POP has no attribute with the parameter's name, the output's whole look stops rendering and the error names the missing attribute.
Reserved names
Lightpath supplies these names itself, so a source parameter cannot use them:
Pointcount, Pointindex, Resolution, Time
A source with one of these parameters does not build. The source COMP shows
an error naming the parameter, and looks using it stay on what they last
showed. Rename the parameter (for example Time to Rate) and the
error clears on the next build.
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.
Call your shader's main rate Speed and its main size Scale, and a Panel's
Speed and Scale macros will drive them — the uniform simply arrives with the
macro applied. A Shape float does the same for the Shape macro. Higher Speed
must mean faster and higher Scale bigger; see
Macro 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.
Changing a source that is already in use
Outputs and the look editor's preview pick up a changed source within about a second, without reactivating the look. That covers editing the GLSL, adding or renaming a parameter, and reloading the plugin's .tox after rebuilding it. The source's thumbnail on the Plugins page regenerates too.
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 color 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.
A source that reads one of the output's own attributes is offered only on outputs that carry it.
Both rules apply to every output a look targets. While you edit a look, the output picker grays out any output the look's sources can't run on, with the reason underneath: "The source name is not supported on this output". Change the source first, or remove it, to add that output.
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

