← Smartshow LMS Online manual · v1.2.6 · Not in English? Right-click anywhere and choose your browser's Translate option. ⬇ PDF

Custom Shader writing guide

A Custom Shader is a small GPU program (a GLSL fragment shader) that decides the colour of every pixel on your matrix, in parallel. Write one, paste one from Shadertoy, or import an ISF file — it becomes a normal LMS effect with scenes, chases, regions, Show Mode and Art-Net output for free.

Quick start

Add a Custom Shader effect (under Other), open its options, and either pick a built-in example or click ✎ Edit / New…. The simplest possible shader paints everything red:

// every pixel is red
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);  // r, g, b, alpha (0..1)
}

Make the colour depend on position and time and you get animation. Press Compile for an instant ✓/✗, then Save.

void main() {
  vec2 uv = gl_FragCoord.xy / u_res;          // 0..1 across the matrix
  vec3 col = 0.5 + 0.5 * cos(u_time + uv.xyx + vec3(0.0, 2.0, 4.0));
  gl_FragColor = vec4(col, 1.0);
}

Three dialects

LMS accepts shaders written three ways and adapts them automatically:

1 · Native (LMS)

Write your own void main() and set gl_FragColor. You get all the LMS uniforms below. This is what the examples above use.

2 · Shadertoy

Paste a shader that defines void mainImage(out vec4 fragColor, in vec2 fragCoord) and uses iTime / iResolution / iMouse. LMS wraps it and maps those names on. Most Shadertoy shaders that don't sample textures (iChannel) just work.

3 · ISF

Import an .fs file with a JSON header (the format Resolume / VDMX / MadMapper use). LMS reads the header and auto-generates the controls — see The ISF format. Book of Shaders / glslCanvas shaders (using u_resolution / u_mouse) also import.

TouchDesigner

Shaders exported from a TouchDesigner GLSL TOP import too: uTime, vUV and TDOutputSwizzle() are handled automatically. One thing to change by hand: TD's uAudio is a texture, which LMS doesn't provide — swap audio lookups for the u_bass / u_mid / u_treble / u_beat floats instead.

Uniforms you get

These are supplied every frame — just read them, don't declare them (LMS declares them for you):

UniformTypeMeaning
u_timefloatSeconds since the effect started (also iTime, TIME).
u_resvec2Matrix size in pixels (also iResolution.xy, RENDERSIZE, u_resolution).
u_levelfloatOverall audio loudness, 0..1.
u_bass u_mid u_treblefloatPer-band audio energy, 0..1.
u_beatfloatBeat envelope, 0..1 — spikes on kicks with a moderate decay.
u_onsetfloatShort kick pulse, 0..1 — snaps back to zero within a few frames. Use it for tight strobes / impacts that must lock to the kick.
u_speed u_scalefloatGeneric sliders (only shown when your shader reads them).
u_p1 … u_p4floatGeneric 0..1 sliders (only shown when read).
u_colA u_colBvec3Two colour pickers (only shown when read).
LMS only shows a control if your shader actually references its uniform. A slider the shader never uses would do nothing, so it's hidden.

Audio reactivity

Any shader can react to sound just by reading u_bass, u_beat, etc. Audio comes from your chosen input in Settings › Audio (mic or system loopback). Example — pulse brightness on the beat:

col *= 1.0 + u_beat * 0.5;   // brighter on each beat

When a shader reads audio, an Audio amount slider appears in its options: 0 = ignore audio (smooth), 1 = normal, up to 3 = exaggerated. Use it to tame a shader that looks twitchy on a live mic.

Tip — don't drive motion with audio. A shader that multiplies u_time by an audio value lurches on every beat. Keep the motion clock steady and let audio drive the look — brightness, contrast, warp depth. Use u_onset (not u_beat) for anything that should snap, like a strobe.

Audio monitor & envelope shaping

Settings › Audio shows a live 0..1 read-out of every audio uniform (u_level, u_bass, u_mid, u_treble, u_beat, u_onset). If a shader misbehaves, glance here first — a value stuck high is the input gain, not your code.

Below it are envelope controls that shape the analysis before it reaches any shader (all neutral by default, so nothing changes until you touch them):

Coordinates & output

Output adjustments (per shader)

Under 🎨 Output in the shader's options, four post-process controls tune the finished pixels without editing the shader — they apply to any shader:

All four are neutral (1) by default, so nothing changes until you touch them, and each is per-effect — dial in a gamma for a washed-out shader without affecting the others.

The ISF format

An ISF shader starts with a JSON comment describing its adjustable inputs. LMS reads it and builds the matching sliders / colour pickers / dropdowns automatically — no hand-wiring.

/*{
  "DESCRIPTION": "My generator",
  "ISFVSN": "2",
  "INPUTS": [
    { "NAME": "speed",   "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 4.0, "LABEL": "Speed" },
    { "NAME": "tint",    "TYPE": "color", "DEFAULT": [0.2, 0.8, 1.0, 1.0] }
  ]
}*/
void main() {
  vec2 uv = isf_FragNormCoord;      // 0..1, provided by ISF
  float v = sin(length(uv - 0.5) * 20.0 - TIME * speed);
  gl_FragColor = vec4(tint.rgb * (0.5 + 0.5 * v), 1.0);
}

Each INPUT becomes a uniform of the same name, and a control in the effect's options. ISF also gives you TIME, RENDERSIZE, isf_FragNormCoord, and the IMG_NORM_PIXEL() / IMG_PIXEL() sampling macros.

Input types

ISF typeBecomes
floatSlider (MIN / MAX / DEFAULT)
bool / eventCheckbox
longDropdown (VALUES / LABELS)
colorColour picker
point2DTwo sliders (X / Y)
imageLoad-an-image control → sampled as a texture. An input named inputImage is special — see Filters below.
audio / audioFFTAuto-fed from live audio (waveform / spectrum) — no control

Multi-pass & feedback

Multi-pass ISF shaders work. Declare PASSES with a TARGET buffer; a PERSISTENT target keeps its contents between frames, so you get feedback / trails. Your shader runs once per pass — branch on PASSINDEX, and sample a target buffer by its name.

"PASSES": [ { "TARGET": "bufA", "PERSISTENT": true }, {} ]
void main(){
  if (PASSINDEX == 0) { // draw new content over the faded last frame
    vec3 prev = texture2D(bufA, isf_FragNormCoord).rgb * 0.94;
    gl_FragColor = vec4(max(prev, /* new stuff */ vec3(0.0)), 1.0);
  } else {
    gl_FragColor = texture2D(bufA, isf_FragNormCoord);   // show the buffer
  }
}

Filters — process the layer below

An ISF whose image input is named inputImage is a filter: it transforms an incoming frame instead of generating one. In LMS the incoming frame is the layer below in the same channel — so put the filter in a channel's fx2 slot over any effect in fx1, and it filters that effect live. (Set the channel mix to Normal so the filtered result shows through.) The inputImage control is hidden — it's fed automatically.

What's not supported yet

Passes with an expression-sized buffer (e.g. "WIDTH": "$WIDTH/2.0") aren't evaluated yet — those are detected and refused rather than misrendered. Fixed / full-size passes are fine.

Where to get shaders

Will it work in LMS? For Shadertoy, pick shaders with a single Image tab that don't use iChannel or extra Buffer tabs (that style of multi-pass isn't supported). ISF multi-pass is different and is supported — see Multi-pass & feedback. Either dialect works: use mainImage() or write gl_FragColor. LMS tells you if a shader needs something it can't do.

Most likely to just work

Huge library (mind the caveat above)

Write your own

Tips & gotchas