Skip to content

Your First Shader

On this page you write a fragment shader — a small program that the GPU runs for every pixel — and make it move with your music. At the end, you change it while it plays.

You need VSCode with the resonon extension, as in Patterns, Tracks & Effects. Write each step into a .non file, select it, and press Cmd+Enter. Every step adds to what the previous one built.

Finish Signals and Modulation. Open your project’s src/main.non, evaluate RESET; on its own, then remove that line. The session should be silent with no earlier visuals routed to screen. You need a working audio output and graphics display; the drums use a bundled kit.

Visuals are drawn in a window that resonon’s server opens. Run any line first, so that VSCode starts the server and connects to it:

print("Hello, screen!");

Then open the resonon sidebar and click Visuals in the Views section — or run the command Open Visuals. An empty window opens.

A shader lives in a file of its own. Make a folder shaders beside main.non, and in it a file plasma.glsl:

shaders/plasma.glsl
#version 300 es
precision highp float;
uniform vec2 iResolution;
out vec4 fragColor;
void main() {
vec2 uv = gl_FragCoord.xy / iResolution;
fragColor = vec4(uv.x, uv.y, 0.5, 1.0);
}

Save it, and run this in main.non:

use "std/visuals" as gfx;
let v = gfx.load_shader("plasma", "shaders/plasma.glsl");
v >> screen;

The window fills with a gradient: red to the right, green to the top.

  • The file is the shader, in GLSL. main runs once for every pixel and writes its colour to fragColor.
  • iResolution is the size of the window in pixels. resonon gives it to every shader, so uv goes from 0 to 1 across the window.
  • gfx.load_shader("plasma", "shaders/plasma.glsl") makes a visual from the file, with the name plasma. The path is relative to main.non.
  • v >> screen shows it in the window, the way >> master sends a track to the master.

In VSCode, a GLSL extension such as Shader languages support highlights the file.

iTime is the playhead’s position in seconds. Add it to the shader:

shaders/plasma.glsl
#version 300 es
precision highp float;
uniform float iTime;
uniform vec2 iResolution;
out vec4 fragColor;
void main() {
vec2 uv = gl_FragCoord.xy / iResolution;
float wave = 0.5 + 0.5 * sin(iTime + uv.x * 6.2831);
fragColor = vec4(uv.x, uv.y, wave, 1.0);
}

Save the file. Nothing watches it, so run the load_shader line again, and start the transport:

let v = gfx.load_shader("plasma", "shaders/plasma.glsl");
PLAY;

A blue wave moves across the gradient. You did not route it again: the visual with the name plasma is already on the screen, and it took the new shader.

iTime moves with the playhead, as the music does: PAUSE; holds the wave where it is, and STOP; puts it back where it started.

Any other uniform that you declare is a knob that you set from resonon. Add a brightness:

shaders/plasma.glsl
#version 300 es
precision highp float;
uniform float iTime;
uniform vec2 iResolution;
uniform float brightness;
out vec4 fragColor;
void main() {
vec2 uv = gl_FragCoord.xy / iResolution;
float wave = 0.5 + 0.5 * sin(iTime + uv.x * 6.2831);
fragColor = vec4(vec3(uv.x, uv.y, wave) * brightness, 1.0);
}

Save it, load it again, and set the knob:

let v = gfx.load_shader("plasma", "shaders/plasma.glsl");
v.parameter("brightness") << 0.4;

The picture is darker. As with an effect’s knob, << also takes a signal:

use "std/signals" { Sine };
v.parameter("brightness") << Sine(4).range(0.2, 1.0);

The picture now fades up and down four times every cycle.

Every track tells you how loud it is, as .rms. Add a beat, and send its level to the knob:

use "std/instruments" { Sampler, Kit };
let drums = AudioTrack("drums");
drums.set_instrument(Sampler(Kit("CR-78")));
drums << [bd [sd _ _ bd] bd sd];
v.parameter("brightness") << drums.rms.smooth(20, 300).range(0.2, 3.0);

The picture flashes on every hit and fades out after it.

  • drums.rms goes from 0 (silent) to 1 (full scale). On its own, it jumps from one frame to the next, so the picture flickers.
  • .smooth(20, 300) makes it rise in 20 ms and fall over 300 ms.
  • .range(0.2, 3.0) maps 0–1 to 0.2–3.0, because a drum track is seldom near full scale.

Change the shader: make the wave move faster and draw rings instead of stripes.

shaders/plasma.glsl
#version 300 es
precision highp float;
uniform float iTime;
uniform vec2 iResolution;
uniform float brightness;
out vec4 fragColor;
void main() {
vec2 uv = (gl_FragCoord.xy * 2.0 - iResolution) / min(iResolution.x, iResolution.y);
float rings = 0.5 + 0.5 * sin(length(uv) * 10.0 - iTime * 4.0);
fragColor = vec4(vec3(rings, rings * 0.5, 1.0 - rings) * brightness, 1.0);
}

Save it, and run the load_shader line again:

let v = gfx.load_shader("plasma", "shaders/plasma.glsl");

The rings replace the stripes, and they still flash with the drums: the new shader kept the knob and what it is connected to. The music did not stop.

Now make a mistake: delete a ; in plasma.glsl, save it and run the line again. The last good picture stays on the screen, the music plays on, and VSCode marks the error in plasma.glsl, on the line you broke. Put the ; back, save, and run the line again.

You wrote a shader, gave it knobs, connected it to the drums, and changed it while it played.