GLSL Shaders From Zero — Writing Your First Fragment Shader

Every vivid visual on this site — ocean waves, reaction-diffusion patterns, aurora glows — runs inside a GLSL fragment shader executed on the GPU. Here is how they work, step by step.

What is a GLSL Shader?

A shader is a small program that runs in parallel on the GPU — once per pixel (fragment shader) or once per vertex (vertex shader). GLSL (OpenGL Shading Language) is the C-like language used to write them. In WebGL and Three.js, both types are required, but the fragment shader is where most visual magic happens.

The GPU runs the same fragment shader for every pixel simultaneously. A 1920×1080 canvas runs the shader ~2 million times in each frame, all in parallel — that is why shaders are so fast.

The Minimal Fragment Shader

The only required output is gl_FragColor — a vec4 of (red, green, blue, alpha), each in the range 0.0–1.0.

// Solid red — the "hello world" of shaders precision mediump float; void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // RGB = red, A = opaque }

Using Coordinates: the Built-in gl_FragCoord

The uniform gl_FragCoord.xy gives the pixel position in window pixels. Normalise it to get a position in [0,1] and use it as a colour gradient:

precision mediump float; uniform vec2 u_resolution; // width, height — passed from JS void main() { vec2 uv = gl_FragCoord.xy / u_resolution; // normalise to [0,1]² gl_FragColor = vec4(uv.x, uv.y, 0.5, 1.0); // red rises left→right, green bottom→top }

This renders a red-to-yellow gradient left-to-right, green-to-cyan bottom-to-top — classic introductory GLSL output.

Essential Built-in Functions

Function What it does Common use
sin(x) / cos(x) Trigonometric wave, period 2π Waves, oscillations, rotations
fract(x) Fractional part of x — alias x mod 1.0 Repeating tiles, stripes, grids
length(v) Euclidean distance of a vector from origin Radial gradients, circles, distance fields
smoothstep(a, b, x) S-curve from 0→1 as x goes from a→b Anti-aliased edges, vignettes
mix(a, b, t) Linear interpolation: a*(1-t) + b*t Color blending, transitions
abs(x) Absolute value Symmetry, folding space
floor(x) Round down to nearest integer Cell/grid ID extraction
clamp(x, lo, hi) Clamp x to [lo, hi] Safe colour values, depth
dot(a, b) Dot product of two vectors Lighting, projections, diffuse shading
normalize(v) Unit vector in same direction Normals, directions

Drawing a Circle with SDF

A Signed Distance Field (SDF) is a function that returns negative values inside a shape and positive values outside. Use length + smoothstep for a crisp anti-aliased circle:

precision mediump float; uniform vec2 u_resolution; void main() { vec2 uv = (gl_FragCoord.xy / u_resolution) * 2.0 - 1.0; // [-1, 1]² uv.x *= u_resolution.x / u_resolution.y; // correct aspect ratio float d = length(uv); // distance from centre float circle = smoothstep(0.5, 0.49, d); // 1 inside, 0 outside gl_FragColor = vec4(vec3(circle), 1.0); }

Noise: The Secret Ingredient

GLSL has no random function, but many shaders fake it with a hash function, then smooth it into Perlin-style noise. This is the compact pseudo-random hash used across the site:

// Pseudo-random from a vec2 seed (0–1) float hash(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 19.19); return fract(p.x * p.y); } // Value noise: bi-linear interpolation of 4 hash values float noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f); // smoothstep curve return mix(mix(hash(i), hash(i + vec2(1,0)), u.x), mix(hash(i + vec2(0,1)), hash(i + vec2(1,1)), u.x), u.y); }

Layer multiple noise octaves at increasing frequency and decreasing amplitude to get fractional Brownian motion (fBm) — the turbulent, cloud-like texture used in the ocean and atmosphere simulations.

Using Uniforms: Passing Data from JavaScript

Uniforms are read-only values passed from the CPU (JavaScript) to the shader each frame. In Three.js they are declared in the uniforms object and declared in GLSL with the uniform keyword:

// JavaScript side (Three.js) const material = new THREE.ShaderMaterial({ uniforms: { u_time: { value: 0.0 }, u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) } }, vertexShader: `...`, fragmentShader: ` precision mediump float; uniform float u_time; uniform vec2 u_resolution; void main() { vec2 uv = gl_FragCoord.xy / u_resolution; float wave = sin(uv.x * 10.0 + u_time) * 0.5 + 0.5; gl_FragColor = vec4(wave, uv.y, 1.0 - wave, 1.0); } ` }); // In animate loop: material.uniforms.u_time.value += 0.016; // advance time each frame

Where Shaders Appear on This Site

Fragment shader blends deep/shallow blue tones from a height map uniform. Fresnel rim glow added with a dot(normal, viewDir) calculation.
4-octave fBm noise drives cloud density. The threshold smoothstep creates the sharp volumetric boundary between clear sky and cloud.
Gray-Scott simulation writes to a floating-point render target each step. A fragment shader maps the u/v chemical concentrations to colour via a lookup palette.
Intensity is computed analytically per pixel — no texture, just math. The fringe pattern is purely the output of a GLSL wave superposition function.
Layered noise with time-varying phase and a tall aspect ratio distortion produces the curtain-like shimmer. Entirely procedural — no texture files.
Standing wave mode shapes are evaluated per pixel. Threshold near zero-crossing is highlighted to show the nodal lines where sand settles.

Debugging GLSL

GLSL has no console.log. The standard technique is to visualise your data as colour:

Performance tip: On mobile, branching (if statements) inside shaders can be significantly slower than arithmetic, because both branches may execute in parallel on some GPUs. Prefer mix(a, b, step(...)) and clamp over conditionals wherever possible.

Next Steps

Once you are comfortable with fragment shaders, explore: