If you write a lot of fragment shaders, you end up reusing the same small building blocks: coordinate setup, rotations, smooth edges, palette helpers, and looping time tricks. This page collects practical GLSL snippets you can copy, adapt, and combine in ShaderGif (or any WebGL-style shader environment) to move faster and avoid common gotchas.
Quick start templates
GLSL ES 3.00 (WebGL 2 style)
Use this as a clean baseline when your environment provides a UV coordinate and a time uniform. ShaderGif commonly exposes UV, time, and a ratio (aspect) uniform.
#version 300 es
precision highp float;
in vec2 UV; // 0..1
uniform float time; // seconds
uniform float ratio; // width/height
out vec4 out_color;
void main() {
vec2 uv = UV;
// Centre coordinates around (0,0) and correct aspect ratio
vec2 p = uv * 2.0 - 1.0;
p.x *= ratio;
// Example: simple gradient
vec3 col = vec3(0.5 + 0.5 * p.x, 0.5 + 0.5 * p.y, 0.7);
out_color = vec4(col, 1.0);
}
GLSL ES 1.00 (WebGL 1 style)
If you’re using a WebGL 1 compatible header, the output is typically gl_FragColor and UV may be provided as a varying. (Exact names vary by host.)
precision highp float;
varying vec2 vUv; // 0..1
uniform float time;
uniform vec2 resolution; // pixels
void main() {
vec2 uv = vUv;
vec2 p = (uv * 2.0 - 1.0);
p.x *= resolution.x / resolution.y;
vec3 col = vec3(0.5 + 0.5 * p.x, 0.5 + 0.5 * p.y, 0.7);
gl_FragColor = vec4(col, 1.0);
}
Useful constants and micro-helpers
#define PI 3.141592653589793
#define TAU 6.283185307179586
float saturate(float x) { return clamp(x, 0.0, 1.0); }
vec2 saturate(vec2 x) { return clamp(x, 0.0, 1.0); }
vec3 saturate(vec3 x) { return clamp(x, 0.0, 1.0); }
// Map x from [a,b] to [0,1]
float unlerp(float a, float b, float x) { return (x - a) / (b - a); }
// Map x from [a,b] to [c,d]
float remap(float a, float b, float c, float d, float x) {
return mix(c, d, saturate(unlerp(a, b, x)));
}
Coordinates and aspect ratio
Most “why is this stretched?” issues come from mixing coordinate spaces. A dependable pattern is: start with uv in 0..1, convert to centred p in -1..1, then fix aspect on x.
vec2 centredUv(vec2 uv, float ratio) {
vec2 p = uv * 2.0 - 1.0;
p.x *= ratio;
return p;
}
If you want pixel-sized features (lines that stay ~1px wide), work in pixel space and convert back only when needed:
// Given UV 0..1 and resolution in pixels
vec2 pixelSpace(vec2 uv, vec2 resolution) {
// Centre in pixels: (0,0) is the middle of the screen
return (uv * resolution) - 0.5 * resolution;
}
Angles and rotation
Angle of a point
float angleOf(vec2 p) {
return atan(p.y, p.x); // radians
}
2D rotation matrix
mat2 rot(float a) {
float s = sin(a), c = cos(a);
return mat2(c, -s, s, c);
}
// Usage:
// p = rot(time) * p;
Polar coordinates
Polar is handy for radial gradients, rings, and “spin” effects.
vec2 toPolar(vec2 p) {
float r = length(p);
float a = atan(p.y, p.x); // -PI..PI
return vec2(r, a);
}
Shaping functions and clean edges
Hard step() edges alias easily. For smoother results (especially when recording GIFs), use derivatives via fwidth when available.
Anti-aliased step
float aaStep(float edge, float x) {
float w = fwidth(x);
return smoothstep(edge - w, edge + w, x);
}
Anti-aliased stroke
A stroke is just “a band” around a distance value.
float aaStroke(float dist, float centre, float width) {
float w = fwidth(dist);
float a = smoothstep(centre - width - w, centre - width + w, dist);
float b = smoothstep(centre + width - w, centre + width + w, dist);
return a - b;
}
Signed Distance Field (SDF) building blocks
SDFs are one of the fastest ways to build crisp 2D shapes. You compute a distance-like value and then shade it with smoothstep or aaStep.
Circle and box
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
float sdBox(vec2 p, vec2 b) {
// b = half-size (width/2, height/2)
vec2 d = abs(p) - b;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
Combining shapes
float opUnion(float a, float b) { return min(a, b); }
float opIntersect(float a, float b) { return max(a, b); }
float opSubtract(float a, float b) { return max(a, -b); }
// Smooth union (k controls blending amount)
float opSmoothUnion(float a, float b, float k) {
float h = saturate(0.5 + 0.5 * (b - a) / k);
return mix(b, a, h) - k * h * (1.0 - h);
}
Rendering an SDF
vec3 shadeSdf(float d, vec3 inside, vec3 outside) {
// Inside: d < 0, Outside: d > 0
float t = aaStep(0.0, -d); // 1 inside, 0 outside (anti-aliased)
return mix(outside, inside, t);
}
Repetition and tiling
Repeating the domain gives you patterns “for free”. Start simple with fract, then experiment with mirrored tiles and polar repeats.
Basic tile repeat
vec2 repeat(vec2 p, float s) {
return fract(p / s) * s - 0.5 * s;
}
Mirrored repeat (reduces seams)
vec2 mirrorRepeat(vec2 p, float s) {
vec2 q = p / s;
q = abs(fract(q) - 0.5) * 2.0; // 0..1 mirrored
return (q - 0.5) * s;
}
Polar repetition (k slices)
vec2 polarRepeat(vec2 p, float k) {
float a = atan(p.y, p.x);
float r = length(p);
float sector = TAU / k;
a = mod(a + 0.5 * sector, sector) - 0.5 * sector;
return vec2(cos(a), sin(a)) * r;
}
Colour helpers (palettes, gradients, and HSL)
When output looks “muddy”, it’s often because colour is unconstrained. Palette functions give you predictable, art-directable colour variation.
Cosine palette (compact and flexible)
vec3 palette(float t, vec3 a, vec3 b, vec3 c, vec3 d) {
return a + b * cos(TAU * (c * t + d));
}
// Example:
// vec3 col = palette(t, vec3(0.5), vec3(0.5), vec3(1.0), vec3(0.0, 0.1, 0.2));
HSL to RGB (handy for hue ramps)
vec3 hsl2rgb(vec3 hsl) {
vec3 rgb = clamp(abs(mod(hsl.x * 6.0 + vec3(0.0,4.0,2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
return hsl.z + hsl.y * (rgb - 0.5) * (1.0 - abs(2.0 * hsl.z - 1.0));
}
Noise: tiny hash + value noise
Full-featured noise libraries are great, but for many GIF-scale sketches you can get far with a small hash and a simple interpolated noise.
Hash (2D → 1D)
float hash21(vec2 p) {
// Cheap, decent distribution for visuals
p = fract(p * vec2(123.34, 345.45));
p += dot(p, p + 34.345);
return fract(p.x * p.y);
}
Value noise (2D)
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
// Smooth interpolation
vec2 u = f * f * (3.0 - 2.0 * f);
float a = hash21(i + vec2(0.0, 0.0));
float b = hash21(i + vec2(1.0, 0.0));
float c = hash21(i + vec2(0.0, 1.0));
float d = hash21(i + vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
Fractal Brownian Motion (FBM)
float fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
for (int i = 0; i < 5; i++) {
v += a * noise(p);
p *= 2.0;
a *= 0.5;
}
return v;
}
Time tricks for seamless loops
GIFs look best when they loop cleanly. Instead of relying on “time just keeps going”, consider mapping time into a circle using sine and cosine. That gives you a 2D loop parameter that always returns to the start.
Normalised loop phase
float loopPhase(float time, float secondsPerLoop) {
return fract(time / secondsPerLoop); // 0..1
}
2D looping driver
vec2 loop2(float time, float secondsPerLoop) {
float t = TAU * loopPhase(time, secondsPerLoop);
return vec2(cos(t), sin(t));
}
// Example usage:
// vec2 L = loop2(time, 3.0);
// float wobble = 0.1 * L.x; // perfectly looping
Ping-pong motion
float pingpong(float t) {
// 0..1..0..1
return 1.0 - abs(fract(t) * 2.0 - 1.0);
}
Performance and precision notes
- Prefer fixed-size loops where possible. WebGL compilers handle
for (int i = 0; i < N; i++)reliably; dynamic loop bounds can be problematic on some devices. - Avoid branching inside tight loops if a math alternative exists (e.g., using masks from
step). - Use the right precision. If you see banding or shimmering, try
highp. If you’re targeting slower devices, keep the shader simple before you micro-optimise. - Derivatives (
fwidth) are extremely useful for anti-aliasing, but require standard derivative support (most modern WebGL contexts have it).
Debugging patterns
When the output is a flat colour (or completely black), reduce the shader to the smallest thing that should work, then add pieces back. A few quick “visual debug” tricks help you see what’s going on:
Show UVs and centred coordinates
vec3 debugUv(vec2 uv) {
return vec3(uv, 0.0); // red = x, green = y
}
vec3 debugP(vec2 p) {
return 0.5 + 0.5 * vec3(p, 0.0); // map -1..1 to 0..1
}
Visualise a scalar
vec3 debugScalar(float v) {
return vec3(v); // greyscale
}
If nothing renders, it’s usually (a) a compile error, (b) precision/type mismatch, or (c) coordinate/aspect mistakes. The ShaderGif guide “Troubleshooting Black Canvas” is also worth keeping handy when you’re stuck.
Last updated: 30 December 2025
