GLSL Tips and Snippets

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