Custom shaders
Write your own shaders
Postmo runs your GLSL as a layer. The params and colours you declare become sliders and swatches, built-in helpers keep it looping seamlessly, and code from Shadertoy, ISF or GLSL Sandbox comes in as it is.
Start
Three ways in
- Write
- In the editor, Fill's shader menu → New shader, or a new design's Blank tab → Custom shader. A code window opens on a starter shader; it recompiles as you type.
- Import
- Drop a
.glsl,.frag,.fsor.fshfile onto the editor, open it with ⌘O, or paste the code. Shadertoy (mainImage), GLSL Sandbox and WebGL 1 code is rewritten for Postmo; ISF inputs become sliders, menus and colours. One pass only. - With AI
- The code window's Copy AI instructions puts these rules in the clipboard for any AI chat; paste its answer back. For coding agents, the open-source kit adds a skill and a check that compiles, renders and loop-tests a shader with Postmo's own renderer.
Shape
What a shader is
The body of a GLSL ES 3.00 fragment shader. Postmo puts its prefix first and calls your main(). Uniforms declared with a note become sliders and swatches in the inspector: at most 10 params and 8 colours. The layer around it is an ordinary layer: box, crop, shape, effects, seed and colour dice all work.
// @name Custom shader
// @postmo 1
// Params and colours: a uniform with a note. They show up in the inspector.
uniform float speed; // @param Speed 0 3 1 1
uniform float squares; // @param Squares 2 30 1 8
uniform float warp; // @param Warp 0 1 0.01 0.35
uniform vec3 shadows; // @color Shadows #111111
uniform vec3 glow; // @color Glow #58ff3e
uniform vec3 edge; // @color Edge #e9edec
// Time: uPhase goes 0 → 1 once per loop. Anything made from it loops
// seamlessly: sin(TAU * uPhase * k) with a whole k, noise through lp(p, r, z).
// For motion that runs on instead, use uTime (seconds) and uDuration.
// Place patterns with vUv (0–1) and uRes (pixels), not gl_FragCoord.
void main() {
vec2 p = aspectP(vUv);
// Bend the board with looping noise, then slide it a whole number of squares per loop.
vec2 w = vec2(snoise(lp(p * 1.3, 0.6, 0.0)), snoise(lp(p * 1.3 + 5.0, 0.6, 3.0)));
vec2 g = (p + warp * 0.25 * w) * squares + vec2(uPhase * floor(speed + 0.5) * 2.0, 0.0);
vec2 f = fract(g) - 0.5, aa = fwidth(g);
vec2 s = smoothstep(-aa, aa, f) * 2.0 - 1.0;
float tile = 0.5 - 0.5 * s.x * s.y;
vec3 c = mix(shadows, glow, tile);
float lip = 1.0 - smoothstep(0.0, 2.0 * aa.y, abs(f.y + 0.44));
c = mix(c, edge, lip * tile);
outColor = vec4(c, 1.0);
}
Rules
How to write one
- Write the body of a GLSL ES 3.00 fragment shader: no #version, no precision, no main() inputs of your own. Postmo puts its prefix first (noise, loop helpers, uniforms) and calls your main().
- Write the colour to outColor (vec4, straight alpha: write the colour and its opacity).
- Place patterns with vUv (0–1, origin bottom left) and uRes (the layer's size in px), never gl_FragCoord: the layer can be cropped, turned or drawn at preview quality. aspectP(vUv) gives centred coordinates scaled by the short side, so shapes stay round in any format.
- Params and colours are uniforms with a note on the same line (they become inspector sliders and swatches): @param is "Label min max step default", then optional [option, labels] (a menu; values min, min+step, …), "rate" (a speed: Keep speed scales it with the duration) or "pick" (picks a variant by number, with dice). @color is "Label #rrggbb". At most 10 params and 8 colours: choose the ones that change the picture most; make the rest constants. Name colours by their role (Shadows, Glow, Edge), not by hue.
uniform float speed; // @param Speed 0 2 0.01 1 rate uniform float mode; // @param Mode 0 2 1 0 [Soft, Hard, Glow] uniform vec3 shadows; // @color Shadows #101018 - Other notes: // @name Shader name, // @group Group name: key, key (params beyond six must be grouped, else they go to More), // @transparent (draws only a figure; the rest stays see-through), // @postmo 1.
- Only float and vec3 uniforms can be declared; never declare uniforms Postmo already has (below).
- Do not write your own noise or hash: use snoise / fbm / hash12 / hash13. They follow the layer's Seed, so changing the seed gives a new pattern for free.
- Time, two ways. To loop seamlessly (Postmo's speciality): use uPhase (0 → 1 once per loop) and only periodic functions of it: sin(TAU * uPhase * k) with a whole number k (floor(x + 0.5) for a param), noise that moves through lp(p, r, z) or loopv(r), never snoise(vec3(p, uTime)); discrete jumps with mod(floor(uPhase * N), N) as a hash key. uPhase may wrap several times in one loop (time scale), so every term must have period 1. To play over time instead (does not loop): use uTime (seconds) and uDuration.
- GLSL ES pitfalls: sample, filter, input, output, patch are reserved words; there is no PI (TAU * 0.5); call texture() before any early return; whole numbers are ints (write 1.0, not 1, in float maths).
- Keep it light: it runs per pixel at up to 4K, every frame. Avoid deep loops (more than ~64 steps) and nested fbm; no more than 64 KB of code.
- The layer's image or video (its Source, if the user loads one) is uTex: use coverUv(vUv, zoom) to fill the layer with it, and uHasTex to know whether one is loaded.
Reference
What your code gets
vec2 uRes- The layer's size in px.
float uPhase- 0 → 1 once per loop: the time to loop with.
float uTime, uDuration- Seconds, and the design's duration: to play over time instead.
float uFrame- The frame number, for grain only.
sampler2D uTex, bool uHasTex, vec2 uTexSize- The layer's Source, when an image or video is loaded.
vec3 uSeedOff- The layer's seed; the noise functions use it.
in vec2 vUv · out vec4 outColor- Where you are on the layer (0–1, origin bottom left), and what you write.
#define TAU- 6.28318530718 (there is no PI: write TAU * 0.5).
Functions
float snoise(vec3 v) | Simplex noise, about −1…1; follows the seed. |
float fbm(vec3 p) | Five octaves of snoise: rougher detail, about −1…1. |
float fbm3(vec3 p) | Three octaves of snoise: cheaper fbm. |
float hash12(vec2 p) | A random number 0…1 for a 2D point; follows the seed. |
float hash13(vec3 p3) | A random number 0…1 for a 3D point; follows the seed. |
vec2 loopv(float r) | A point going once round a circle of radius r per loop: add it to a noise coordinate to make it loop. |
vec3 lp(vec2 p, float r, float z) | A noise coordinate for p that sways on x and evolves on z along a closed loop: snoise(lp(p, r, z)) loops seamlessly; r is how far (how fast) it moves. |
vec2 designUv() | vUv with y running down from the top. |
vec2 seedJitter(float i) | Zero at seed 0, else a fixed nudge in [−1, 1]² for each i: moves hand-placed shapes with the seed. |
vec2 aspectP(vec2 uv) | Centred coordinates scaled by the short side: round stays round in any format. |
float px1080() | One pixel of a 1080 px tall layer, in this layer's pixels. |
vec3 ramp5(float t) | Blends the first five colours (uColors[0…4]) by t in 0…1. |
vec2 coverUv(vec2 uv, float zoom) | uv to read the Source (uTex) so it covers the layer; zoom 1 covers exactly. |
Kit
For AI coding tools
The kit is a skill (SKILL.md) and a check script, MIT licensed. Clone it, run npm install once, then:
node bin/check.mjs my-shader.glsl
It compiles the code with the renderer this site runs, says whether it loops, whether the seed changes it and what a frame costs, and writes PNG frames to look at. When it passes, drag the file onto the editor.