GLSL Shaders for Music Visualization: A Beginner's Guide
The most visually striking music videos on YouTube right now — Star Nest, Octave Meatballs, audio-reactive fractals, tunnel zooms — are all built from a single technique: a GLSL fragment shader driven by audio data. Shaders run on the GPU at 60+ frames per second, can produce effects no canvas API can match, and are surprisingly accessible once you understand the structure. This guide is a from-zero introduction to writing custom GLSL shaders for music visualization.
What a Fragment Shader Actually Does
A fragment shader is a tiny program that runs once per pixel on the screen, every frame. It receives the pixel's coordinates, returns a color. That's it. With 1080p × 30fps you're running this little program 62 million times per second. The GPU eats that for breakfast.
The program has access to "uniforms" — variables the host application sets each frame, identical for every pixel. For a music visualizer, the uniforms are things like the current time, the screen resolution, and audio analysis data.
Shadertoy Convention (And Why Shimga Follows It)
Shadertoy.com is the de-facto standard for GLSL shader sharing. It defines a fixed set of uniforms every shader has access to. Shimga's custom shader layer uses the same names, so you can paste Shadertoy code in with minimal edits.
The standard uniforms:
iTime— seconds since the song started, as a floatiFrame— frame number, as a floatiResolution— vec3 of canvas size (width, height, 1.0)iMouse— vec4, mouse position (we always set this to 0 in shimga; canvas doesn't have hover)iChannel0— sampler2D containing audio dataiChannelResolution0— vec3 of the audio texture size (512, 2, 1)
Plus shimga-specific helpers:
iAudioBass, iAudioMid, iAudioHigh, iAudioAmp— float band energies, 0..1 each
The Audio Texture Trick
The big unlock for audio-reactive shaders is iChannel0 — a 2-row texture where:
- Top row (y = 0.25): current waveform sample at x = 0..1 across the screen
- Bottom row (y = 0.75): current spectrum (FFT) at the same x coordinates
So to draw a frequency-spectrum bar at pixel x:
float h = texture2D(iChannel0, vec2(uv.x, 0.75)).x;
That returns 0..1, the volume at frequency bin x. Map it to a bar height and you've got a spectrum analyzer in three lines.
Your First Shader: Bass-Reactive Radial Pulse
The shimga custom-shader starter is this:
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec2 ce = uv - 0.5;
ce.x *= iResolution.x / iResolution.y;
// Spectrum bars (bottom-up)
float spec = texture2D(iChannel0, vec2(uv.x, 0.75)).x;
float bar = step(1.0 - 0.05 - spec * 0.65, uv.y);
// Waveform line through the middle
float wave = texture2D(iChannel0, vec2(uv.x, 0.25)).x - 0.5;
float waveLine = smoothstep(0.005, 0.0, abs(wave - (uv.y - 0.5)));
// Bass-driven radial pulse
float r = length(ce);
float pulse = smoothstep(0.4 + iAudioBass * 0.4, 0.38 + iAudioBass * 0.4, r);
// Color blend
vec3 c1 = 0.5 + 0.5 * cos(iTime * 0.5 + vec3(0.0, 2.094, 4.189) + iAudioMid * 3.0);
vec3 col = mix(vec3(0.02), c1, bar);
col = mix(col, vec3(1.0), waveLine);
col += (vec3(1.0) - c1) * pulse * 0.3;
fragColor = vec4(col, 1.0);
}
Three independent effects composed in one pass: a spectrum bar chart, a center waveform line, and a bass-driven radial pulse. The hue shifts with mid-band energy, so the whole frame breathes with the song.
The Mental Model: UV Coordinates and Effects
Every fragment shader starts with the same dance: convert fragCoord (raw pixel) to uv (0..1 across the screen). From there:
uv.y > 0.5= bottom half. Use this for spectrum bars.length(uv - 0.5)= distance from center. Use this for radial effects.atan(uv.y - 0.5, uv.x - 0.5)= angle from center. Use this for radial spectrum (like circle-spectrum).mod(uv.x * N, 1.0)= repeat pattern N times across screen.
Audio Reactivity Patterns That Work
Pattern 1: "Punch on the bass"
Use iAudioBass as a scale factor on something that already moves. Don't start a motion on bass — that looks twitchy. Amplify an existing slow motion.
float radius = 0.3 + 0.05 * iAudioBass; // base radius, bass boosts
Pattern 2: "Hue rotation with mid"
Mid frequencies are where vocals + melodic content live. Rotating hue with mid gives the strongest "feels alive" reaction.
vec3 hueColor = 0.5 + 0.5 * cos(iTime + iAudioMid * 3.0 + vec3(0.0, 2.094, 4.189));
Pattern 3: "Shimmer with highs"
High frequencies are hi-hats and cymbals. Use them on tiny accents — sparkle, noise grain, edge glints — not on large shapes.
float sparkle = fract(sin(uv.x * 999.0 + iTime * 5.0) * 43758.5453); sparkle = step(0.985 - iAudioHigh * 0.05, sparkle); // brighter sparkle on high energy
Borrowing From Shadertoy
Most Shadertoy audio-reactive shaders work in shimga with two edits:
- Replace any
iChannelcode that fetches video frames or images — shimga's iChannel0 is audio only. - Replace any uniforms not in the standard list (some authors define their own).
Beyond that, the GLSL ES 1.0 dialect is identical between Shadertoy and shimga, so the math just works. More on how FFT data is structured.
Performance Tips
- Avoid loops with non-constant bounds. WebGL 1 unrolls loops; you can't use
for (int i = 0; i < someUniform; i++). Use a constant max and break early. - Minimize
length()calls. It's ansqrt. If you only need to compare to a threshold, compare squared distances instead. - Don't bake high-detail noise per-frame. A 1080p frame is 2 million pixels; a 50-octave noise function on every pixel will tank your frame rate. Cap iterations.
Try the custom shader layer now
Add a "Custom GLSL Shader" element in the studio, paste any shader, hit Apply.
Open Shimga Studio →