Docs · Custom effects

Custom effects

Write a fragment shader, get a real effect. It has its own layer slot, an inspector full of controls, and every one of those controls binds to the audio. Or build the same thing visually, without writing GLSL.

What you get

You write GLSL with a sprinkle of // @param comments. The editor turns that same source into a fully integrated effect: it appears in the Add gallery under Custom, gets an auto-generated parameter panel, saves inside your project file, and exports on its own as a .zestial-fx.json you can share.

Nothing about a custom effect is second-class. Its parameters bind to audio exactly like a built-in effect's, and it renders in the export the same way it renders in the preview.

The editor

  • Live preview. A canvas under the parameter list compiles what you're typing and runs it against real audio features, so you see the effect react as you write it.
  • Validation as you go. The editor compiles two seconds after you stop typing, or immediately when you press Validate. Errors flag their line in the gutter.
  • Nothing breaks mid-edit. While a shader is invalid, the composition keeps rendering the last version that compiled, and the badge tells you that's what you're looking at.
  • Scene or Post. Scene draws a full-screen quad; Post reads the scene so far from tDiffuse and transforms it.

The @param directive

Put a // @param line directly above a uniform and it becomes a control in the inspector:

// @param min=0 max=4 step=0.01 default=1.5 label="Speed"
uniform float uSpeed;

// @param default=#ff5fa2 label="Color A"
uniform vec3 uColorA;

// @param default=true label="Sharpen"
uniform bool uSharpen;

// @param options=soft,hard default=soft label="Mode"
uniform int uMode;

// @param min=0 max=2 default=0.5 bindable=false label="Aspect"
uniform float uAspect;

The control you get is inferred from the uniform's type:

float, intNumber slider, or a select if you give it options=
vec3Colour swatch
boolCheckbox
int with options=Dropdown

Number parameters are bindable to audio by default. Add bindable=false for the ones that shouldn't be. An aspect correction, say, that has a correct value rather than an expressive one.

Built-in uniforms

These are supplied for you every frame. Declare one and use it, but don't give it a @param, which the parser rejects:

uTimeSeconds since the effect started
uResolutionCanvas size in pixels
uRms, uPeakOverall level, smoothed and instantaneous
uBeat, uBeatPhase, uBpmBeat pulse, position within the beat, detected tempo
uProgressPosition through the track, 0 to 1
uBandSub … uBandAirSeven frequency bands, sub-bass through air
tDiffuseThe scene so far, post effects only

Spectrum textures

For anything that needs more than seven bands, two textures carry the full spectrum. Declare them only if you use them:

uniform sampler2D uSpectrum;        // 64x1, this frame
uniform sampler2D uSpectrumHistory; // 64x64 waterfall

// x in 0..1 reads low -> high frequency
float mag = texture2D(uSpectrum, vec2(x, 0.5)).r;

uSpectrum is 64 log-spaced magnitudes from 30 Hz to 16 kHz for the current frame. uSpectrumHistory stacks those rows into a 64×64 waterfall. v=0 is now, v=1 is about sixty-four frames ago. That's where scrolling spectrograms and trails come from. Both use linear filtering, so fractional coordinates interpolate for free. Both are shared too, so sampling them from ten effects costs the same as sampling them from one.

Building one without code

Every custom effect can also be built as a node graph, which compiles to exactly the same GLSL. Reach it from Add → Custom, either with the Code | Graph toggle or the New from graph starter… tile, which opens straight into a working graph you can take apart.

  • Press to search the node palette. It holds sources, maths, vector ops, procedural patterns and post nodes, grouped by category.
  • Click a socket, then another, to wire them. Zestial refuses connections that can't type-check or would make a cycle, and shows you why rather than failing silently. Esc cancels a half-drawn wire.
  • You edit node parameters inline, in the node body. Numbers, colours, and which audio band a source follows.
  • Ctrl+Z / Ctrl+Shift+Z undo and redo inside the editor, Ctrl+D duplicates, and F zooms to fit. Your pan and zoom are remembered with the graph.

Audio nodes follow the same features the shader runtime exposes: overall level, beat, tempo, and the seven bands.

One-way door worth knowing about. A graph can be converted to code. You get the generated GLSL and the graph is discarded, behind a confirmation. There's no route back: Zestial doesn't parse hand-written GLSL into a graph. So the Graph toggle is disabled once you've written your own code, rather than offering to destroy it.

Where custom effects live

Custom effects travel inside your .zestial.json project, so opening a project on another machine brings its effects with it. To move a single effect between projects, or send one to someone else, export it as a .zestial-fx.json, which is self-contained and needs no project context to import.