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.
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:
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:
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:
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:
Where Shaders Appear on This Site
Debugging GLSL
GLSL has no console.log. The standard technique is to
visualise your data as colour:
-
Visualise a float:
gl_FragColor = vec4(myValue, 0.0, 0.0, 1.0);— red intensity shows the value. -
Visualise a vec2:
gl_FragColor = vec4(myVec, 0.0, 1.0); -
Check range: wrap suspicious values in
fract()to make overflows visible as discontinuities. - Isolate steps: comment out logic and return intermediate values as colour until you find the broken line.
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:
- Vertex shaders — displace geometry on the GPU (ocean surface mesh).
- Render targets — write to a texture and read it back in the next frame (reaction-diffusion ping-pong).
- Instanced mesh — one geometry, thousands of positions set via vertex attributes.
- Compute shaders (WebGPU) — arbitrary GPU programs, not just graphics pipelines.