A fragment shader is the GPU program that decides the final colour (and often transparency) of each rendered sample on screen. If you are learning GLSL for creative coding, understanding fragment shaders is the fastest way to start drawing shapes, patterns, lighting, and full-screen effects – even without complex 3D geometry.
Where fragment shaders sit in the GPU pipeline
Modern real-time rendering is a pipeline: data goes in, pixels come out. Fragment shaders live near the end of that pipeline, after your geometry has been projected to the screen.
- Vertex shader runs once per vertex and outputs positions (and any values you want to interpolate).
- Rasterisation converts triangles into a grid of fragment candidates across the framebuffer.
- Fragment shader runs for each fragment candidate and outputs a final colour (and sometimes extra buffers).
- Tests and blending (depth, stencil, and alpha) determine what is finally written to the framebuffer.
One common source of confusion: a fragment is not always the same as a pixel. A pixel is the final stored value in the framebuffer, while a fragment is a candidate sample that may be discarded, overwritten by depth tests, or combined via blending. With multisampling (MSAA), a single pixel can have multiple samples, which means your fragment shader can effectively contribute to more than one sample per pixel.
What a fragment shader actually does
At a high level, a fragment shader answers: “Given this screen location and the interpolated data from the triangle, what colour should I output?” In creative coding, you often skip textures and 3D entirely and treat the fragment shader like a tiny function that maps coordinates to colour.
- Inputs: screen position, interpolated values from the vertex shader, uniforms (time, resolution, mouse), and optional textures.
- Work: maths – gradients, noise, distance fields, lighting, shading models, palette mapping.
- Outputs: typically a single
vec4colour (RGBA).
If you want the formal language details for WebGL 2 / GLSL ES 3.00, the authoritative reference is the GLSL ES 3.00 specification (PDF).
Fragment shader inputs: coordinates, interpolation, and built-ins
Fragment shaders are run per fragment generated by rasterisation. The GPU provides useful built-in values and interpolated data:
1) Screen-space coordinates
The most common built-in is gl_FragCoord, which contains the fragment’s window coordinates (usually pixel coordinates) plus depth. In WebGL, the origin is typically the lower-left for gl_FragCoord (but you will often normalise it anyway).
// Normalise pixel coordinates into 0..1 UV space
vec2 uv = gl_FragCoord.xy / iResolution.xy;
2) Interpolated values from the vertex shader
Anything you output from a vertex shader (for example UVs, colours, normals) can be smoothly interpolated across the triangle and received by the fragment shader. This is how texture mapping and per-pixel lighting work. In GLSL ES 3.00 (WebGL 2), you use out in the vertex shader and in in the fragment shader with matching names/types.
3) Front-facing and depth
For 3D, fragment shaders can read whether a fragment belongs to a front-facing triangle via gl_FrontFacing. They can also read and (optionally) write depth with gl_FragDepth, though writing depth can disable early depth optimisations and impact performance.
Writing your first fragment shader (WebGL 2 / GLSL ES 3.00)
Here is a minimal fragment shader that draws a time-animated grayscale wave. It uses two typical uniforms: resolution and time. In ShaderGif, you will usually have comparable uniforms available (see common uniforms).
#version 300 es
precision highp float;
uniform vec2 iResolution;
uniform float iTime;
out vec4 outColor;
void main() {
// UV in 0..1
vec2 uv = gl_FragCoord.xy / iResolution.xy;
// Simple animation: wave across X
float v = 0.5 + 0.5 * sin(iTime + uv.x * 6.2831853);
outColor = vec4(vec3(v), 1.0);
}
Two details matter a lot in practice:
- Precision: In OpenGL ES / WebGL, precision qualifiers are required in fragment shaders. Use
highpwhen you do detailed maths (especially on large canvases or when zooming), but remember some mobile GPUs may treathighpdifferently for performance. - Output variable: In GLSL ES 3.00 you define an output like
out vec4 outColor;. In GLSL ES 1.00 (WebGL 1), you typically write togl_FragColor.
Coordinate systems: aspect ratio, centred space, and “shader-friendly” UVs
Most shader art starts by converting screen coordinates into a more convenient coordinate system. A common approach is to centre the space and correct for aspect ratio so circles stay circular:
vec2 p = (gl_FragCoord.xy - 0.5 * iResolution.xy) / iResolution.y;
// p is centred, with consistent scale on X and Y
Why divide by iResolution.y? Because it makes one unit in X roughly equal to one unit in Y regardless of the canvas shape. This is especially useful when you build shapes from distances (circles, lines, signed distance fields) and want them to look stable across different output sizes.
Colours, alpha, and what “vec4” really means
Fragment shaders usually output a vec4: (red, green, blue, alpha), each component typically in the 0..1 range. Some practical points:
- Clamping: Values outside 0..1 are usually clamped when written to an 8-bit framebuffer (unless you are using HDR formats).
- Alpha: Alpha does nothing by itself. It becomes meaningful when blending is enabled by the render pipeline (outside the shader). If blending is off, alpha is just another stored channel.
- Gamma / sRGB: Many “washed out” or “too dark” results come from mixing linear maths with sRGB display assumptions. If you are doing physically-inspired lighting, you often want to do maths in linear space, then convert at the end (depending on your pipeline).
In generative shader art, you can treat colour as a design tool: create a grayscale “mask” or “field” first, then map it through a palette. If you are exploring palettes, see colour palettes in shaders.
Textures and sampling (when you do need images)
Even though many fragment shader demos are “pure maths”, textures are still important for real projects: image mapping, normal maps, noise lookups, LUTs, and feedback effects. In GLSL ES 3.00 you typically sample with texture():
uniform sampler2D uTex;
vec3 sampleColour(vec2 uv) {
return texture(uTex, uv).rgb;
}
Sampling is affected by texture parameters set in JavaScript (wrap mode, filtering, mipmaps). If you see blocky results, it is often filtering. If you see seams, it is often wrap mode or UVs leaving the 0..1 range.
Discarding fragments and drawing crisp shapes
Fragment shaders can choose not to output anything for a fragment by calling discard. This can be useful for cut-outs, but it can also be slower than writing alpha, and it interacts with depth and blending.
A more common technique for sharp procedural shapes is to compute a distance to a shape boundary and then use smoothstep to anti-alias the edge. This gives you clean results without jagged stair-stepping. If you want a deeper explanation, see anti-aliasing basics.
// Circle SDF: negative inside, positive outside
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
float aaEdge(float d) {
// Small smoothing width; you can also use fwidth(d) for scale-aware AA
return 1.0 - smoothstep(0.0, 0.01, d);
}
For resolution-independent smoothing, many shaders use derivatives (dFdx, dFdy, fwidth) to estimate how quickly a value changes across pixels. That helps edges remain stable when you resize the canvas or zoom.
Performance: what makes fragment shaders slow?
Because fragment shaders can run for every pixel (and possibly multiple samples per pixel), they are often the performance bottleneck. Some practical rules of thumb:
- Prefer simple maths over complex loops. Small loops can be fine, but unbounded loops or heavy iteration will scale poorly with resolution.
- Be careful with branching (
ifstatements) inside tight per-pixel code, especially when neighbouring pixels take different paths. - Minimise expensive ops: trigonometric functions and many texture samples can add up fast.
- Use the right precision:
mediumpcan be faster on some mobile hardware, but can introduce artefacts in detailed scenes.
If you are optimising a shader for real-time preview and GIF export, the best place to start is the fundamentals in performance optimisation.
Common gotchas (and why you might see a black canvas)
A “black canvas” almost always means the shader failed to compile/link, or it compiled but outputs nothing visible. Typical causes include:
- Missing precision qualifiers in a fragment shader.
- Using GLSL ES 3.00 syntax (like
in/out) in a WebGL 1 context. - Forgetting to write to the fragment output variable.
- NaNs creeping into your maths (division by zero, invalid normalisation).
GLSL compiler errors can be terse, particularly when you are learning vector maths, coordinate transformations and version-specific syntax. This best AI tools for coding article can help explain errors and suggest corrections, although you should always test the resulting shader in your actual WebGL renderer.
If your output suddenly disappears after a small change, work backwards: comment out blocks, output a constant colour, then reintroduce your code step by step. For a practical checklist, see troubleshooting a black canvas.
How fragment shaders relate to ShaderGif exports
ShaderGif treats your fragment shader as the “final image generator” for each frame. Once your shader renders correctly in real time, exporting to a GIF is mostly about maintaining consistent timing, stable resolution, and avoiding effects that rely on an unpredictable frame rate. If you are trying to make a seamless loop, start with how to make a perfect loop, and when you are ready to record, follow export WebGL to GIF.
