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):
| Uniform | Type | Meaning |
|---|---|---|
u_time | float | Seconds since the effect started (also iTime, TIME). |
u_res | vec2 | Matrix size in pixels (also iResolution.xy, RENDERSIZE, u_resolution). |
u_level | float | Overall audio loudness, 0..1. |
u_bass u_mid u_treble | float | Per-band audio energy, 0..1. |
u_beat | float | Beat envelope, 0..1 — spikes on kicks with a moderate decay. |
u_onset | float | Short 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_scale | float | Generic sliders (only shown when your shader reads them). |
u_p1 … u_p4 | float | Generic 0..1 sliders (only shown when read). |
u_colA u_colB | vec3 | Two colour pickers (only shown when read). |
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.
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):
- Attack / Release — how fast levels rise / fall. Lower them to smooth jittery motion.
- Smoothing — extra jitter filter on the raw bands.
- Beat sensitivity — how readily
u_beat/u_onsetfire (raise it for quiet kicks). - Noise gate — silences everything below a threshold. Raise it to kill room-noise / mic-hum twitch.
Coordinates & output
gl_FragCoord.xyis the pixel position (origin bottom-left). Divide byu_resfor 0..1 coordinates.- LMS flips the output so a bottom-left-origin (Shadertoy / ISF) shader lands the right way up on the matrix.
- Colours are
vec4(r, g, b, a), each 0..1. Alpha is ignored (LED output is opaque).
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:
- Brightness — overall level.
- Gamma — the fix for "dark shades look too bright on the wall": above 1 it crushes the low/mid tones so darks go properly dark while highlights stay. (Shaders are made for backlit screens; LEDs are near-linear, so their darks read brighter than intended.)
- Contrast — spreads light vs dark around mid-grey; a blunter version of the same idea.
- Saturation — richer / washed-out / greyscale. LEDs often want a small boost.
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 type | Becomes |
|---|---|
float | Slider (MIN / MAX / DEFAULT) |
bool / event | Checkbox |
long | Dropdown (VALUES / LABELS) |
color | Colour picker |
point2D | Two sliders (X / Y) |
image | Load-an-image control → sampled as a texture. An input named inputImage is special — see Filters below. |
audio / audioFFT | Auto-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
"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
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
- ISF library ↗ — pro VJ generators with ready-made controls; LMS reads the ISF format directly, params and all.
- The Book of Shaders ↗ — simple single-pass shaders, great for learning; import cleanly.
Huge library (mind the caveat above)
- Shadertoy ↗ — by far the biggest. Best bets: shaders with a single Image tab and no
iChannel. Paste the code into the LMS editor. - GLSL Sandbox ↗ — older, simpler, big back-catalogue.
Write your own
- KodeLife ↗ (by Hexler) — a real-time live-coding shader editor.
- Book of Shaders editor ↗ — write in the browser with a live preview.
Tips & gotchas
- Looks steppy? The Designer preview is capped (default 30fps — there's a FPS slider). Fast, sharp-edged patterns strobe at low frame rates; softer gradients read smoother.
- Aliasing: LED matrices are low-resolution. High-frequency detail (tight rings, fine lines) will shimmer — keep spatial frequencies moderate.
- Compile errors show the driver's line number — the count includes LMS's small preamble, so it may be a few lines higher than in your source.
- Textures: Shadertoy shaders that sample
iChannel0…won't work yet (no texture channels outside ISF image inputs). - Everything you write is saved inside the project (
.lms), and travels with your scenes and chases.