Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

shader

Runs a fragment shader from the config over the node’s box: a glow, an animated gradient, a procedural pattern. It reads only the images it names and takes no input. To run a shader between two pictures, use an image transition; to run one over a node’s painted subtree, use effect.shader, which shares this page’s .frag contract.

A band that glows in over 400 ms when pulse_on turns true:

local pulse_on = state("pulse_on", false)

local glow = shader {
    width = 200,
    height = 40,
    source = mantle.config_dir .. "/shaders/glow.frag",
    progress = pulse_on:map(function(on) return on and 1 or 0 end),
    params = { tint = { 0.54, 0.71, 0.98 } },
    animate = { progress = 400 },
}

return glow

shaders/glow.frag in the config directory:

uniform vec3 tint;

void main() {
    // Distance from the horizontal centre line, 0 at the middle, 1 at the edges.
    float edge = abs(v_uv.y - 0.5) * 2.0;
    float alpha = (1.0 - edge) * u_progress;
    fragColor = vec4(tint * alpha, alpha); // premultiplied
}

Properties

shader takes the common properties, plus:

PropertyTypeDefaultBehaviour
sourcestring|Bound""Absolute, or ~/ under $HOME, .frag path; relative is refused, "" draws nothing. Compiling, errors and reloads: the .frag file
progressnumber|Bound, [-8192, 8192]0Becomes u_progress. There is no clock uniform: animate this for motion; the wide range lets a spring overshoot
paramstable<string, number|number[]>|Bound{}Uniforms by name: a finite number for float, a list for vec2-vec4 or an array of either, flattened to at most 4096 numbers. Missing ones are 0. Not tweened
imagestable<string, string>|Bound{}Up to 8 PNG, JPEG or WebP paths, absolute or ~/ under $HOME, by sampler name: images. Missing ones sample transparent black. Not tweened

It has no intrinsic size: without width and height it draws nothing. opacity, transforms, shadows and effect.blur apply to it. Place animated boxes with compositor_blur = true beneath a shader when the desktop behind it should blur (blurs).

The .frag file

GLSL ES 3.00 without the header. The engine prepends #version 300 es, precision highp float and the declarations below, then compiles the file as written. Error line numbers count from the file’s first line.

NameTypeWhat
v_uvin vec2Box coordinate, 0..1, top-left origin, y down
fragColorout vec4Premultiplied RGBA. The engine multiplies it by the node’s opacity afterwards
u_progressfloatThe node’s progress
u_sizevec2The node’s size in logical px
uniform float, vec2, vec3, vec4 of your own, or arrays of themSet from params by name, 0 when params leaves one out. An array takes one flat list, element after element. A params name with no uniform is ignored; a wrong component count is padded or truncated and logged once

mantle_sdf and mantle_input exist only for effect.shader. Write void main(). params never sets a uniform named u_* or mantle_*. A uniform the shader reads of any other type, such as an int or a sampler2D, refuses the whole shader.

EventResult
Compile or link failsLogged once, draws nothing until the file changes
A .frag under the config directory is savedThe config reloads, which recompiles it. A file elsewhere recompiles at the surface’s next pass
mantle checkPasses: it has no GPU and compiles no GLSL. The first compile is in the running shell
The shader hangs the GPUThe session hangs. It is config code, as trusted as process.run

Images

images binds raster files for the shader to sample: a normal or displacement map, a noise texture, a gradient LUT.

local glass = shader {
    width = 200,
    height = 120,
    source = mantle.config_dir .. "/shaders/lens.frag",
    images = { lens = mantle.config_dir .. "/lens.png" },
}
void main() {
    vec2 offset = (texture(lens, v_uv).rg - 0.5) * 0.1;
    fragColor = vec4(offset + 0.5, 0.0, 1.0);
    // lens_size is the file's size in pixels, so a texel is 1.0 / lens_size.
}
RuleDetail
Declared for youuniform sampler2D <name>; and uniform vec2 <name>_size; (pixels). Do not declare them
NamesA GLSL identifier of up to 64 characters, not u_*, mantle_*, gl_*, v_uv, fragColor, main or a GLSL keyword or builtin such as texture, float, vec2 or mix, with no __ and not ending in _size. Others are refused
Files.png, .jpg, .jpeg or .webp (any case), absolute or ~/ under $HOME; at most 8 entries. GIF, SVG and other extensions are refused. An animated WebP or APNG draws its first frame. A side over 2048 px is downscaled to 2048; each image is decoded in the frame that first draws it and held for the node’s life, at most 16 MiB, so 128 MiB for eight
Samplingtexture(name, uv) with uv in 0..1, top-left origin like v_uv. Linear filter, no mipmaps. Clamped to the edge, so use fract(uv) to tile. Texels are premultiplied, as fragColor is, and in sRGB as stored: no linearising, so a normal map reads as written
Missing or undecodable fileSamples vec4(0.0) with name_size of vec2(1.0), logged once per path. The node still draws
Changed fileA new modification time or length reloads it, as an image does
Elsewhereeffect.shader takes the same key, on input = "content" and "backdrop"
SharingTwo nodes naming one path share one texture, held by the image cache and freed with it

How do I…

TaskAnswer
Fade an effect in and outBind progress to 0 or 1 and tween it with animate, as above
Loop an animationanimate = { progress = { keyframes = { 0, 1 }, duration = 2000, loops = "infinite" } } (keyframes)
Pass a colourA vec3 or vec4 uniform, params = { tint = { r, g, b } } in 0..1
Pass many values, like a visualizer’s barsuniform vec4 bars[64] takes 256 numbers, params = { bars = levels }; read bar i as bars[i / 4][i % 4]. Drivers may count each float array element as a whole vec4 against the uniform limit, so pack into vec4s
Work in pixelsv_uv * u_size is the fragment’s position in logical px
Click a shaderGive it on_click; without a handler it is transparent to the pointer
Round its cornersWrap it in a rect with radius and clip = "rounded" (clip)

Gotchas

TrapFix
Draws nothing, and check passedRead mantle log for the compile error. Check the node has a size and an absolute or ~/ source
The shader is staticThere is no time uniform. Animate progress
A uniform int refuses the shaderDeclare it float and pass the integer as a number
Colours glow too bright where alpha is lowfragColor is premultiplied: multiply RGB by alpha
A params change jumpsparams is not tweened. Drive the change through progress

See also: image transitions, animation, paint.

Source: vocabulary, content parsers, params, shader stage, .frag reloads.