Command Palette

Search for a command to run...

Hectal
Course outline
Topic 12.1Advanced ~20 min · step 1 of 7

What a Shader Is

In one line

A shader is a tiny program the GPU runs thousands of times at once: a vertex shader places each corner of a triangle, a fragment shader colours each pixel. You write them in GLSL and try them in a Shadertoy-style sandbox.

0/7 · 0%

Think of it like this

Picture a printing press with a stamp for every pixel. You do not tell it where each colour goes. You hand every worker the same rule card, "look at your own position and decide your colour", and all of them follow it at the same moment. The rule card is a shader.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

shader
A small program that runs on the GPU. Vertex shaders place corners, fragment shaders colour pixels.
GLSL
The language shaders are written in. WebGL 2 uses GLSL ES 3.00.
fragment
One pixel-sized piece of a triangle, before it is finally written to the screen. It becomes a pixel if it passes the depth test (Topic 1.4).
swizzle
Reading a vector's parts in any order: v.zyx, v.xy, v.rgb.
gl_FragCoord
The pixel's own position in the picture, counted in pixels, with a half-pixel offset: the first pixel is (0.5, 0.5).
render target
A picture the GPU draws into instead of the screen. Reading it back gives exact pixel numbers.

Step by step

01Cover the screen

The vertex shader writes gl_Position = vec4(position.xy, 0.0, 1.0). A PlaneGeometry(2, 2) has corners at -1 and 1, which are already the screen's edges in clip space (Topic 1.4), so no matrices are needed.

02Colour each pixel

The fragment shader turns gl_FragCoord.xy / iResolution into uv, then 0.5 + 0.5 * cos(...) turns a cosine (-1 to 1) into a colour channel (0 to 1), with three phases for red, green and blue.

03Feed it numbers

uniforms.iTime.value is set from a frame counter, frame / 60, so a second always has 60 frames, in any browser.

04Read it back

probe(time) draws into an 8 × 4 render target and reads bytes with readRenderTargetPixels. The numbers it prints are exactly what the GPU computed.

Try it yourself

  1. 1

    Change the colour rule

    In the sandbox replace uv.xyx + vec3(0.0, 2.0, 4.0) with vec3(uv.x). All three channels now follow the same cosine, so the picture is grey and every printed pixel has equal r, g and b: 255,255,255, 190,190,190, 67,67,67 at the bottom left. (Changing only the offsets to vec3(0.0) is not enough: uv.xyx still gives green the y value, so red and blue match but green differs a little.)

  2. 2

    Make it the other way up

    Replace gl_FragCoord.xy / iResolution with vec2(gl_FragCoord.x, iResolution.y - gl_FragCoord.y) / iResolution. The gradient flips top to bottom: GLSL's y grows upwards, the page's y grows downwards. The first printed line becomes iTime 0 | bottom-left 255,5,50 | top-right 203,60,156: the green values of the two corners have swapped places.

Code & diagrams

Try it: interactive

Where the shaders run

Step through the stages. Vertex shaders do the first steps (model to clip space), fragment shaders the colour step. Turn and move the triangle and watch the numbers change.

A shader sandbox with iTime and iResolution New tab

The colours drift because `iTime` grows. The printed pixels are three fixed times drawn off-screen.

idle

Console

Run the scene to see its console.

Expected console output

iTime 0 | bottom-left 255,60,50 | top-right 203,5,156
iTime 1 | bottom-left 190,0,171 | top-right 82,33,247
iTime 2 | bottom-left 67,57,252 | top-right 3,148,229
A GLSL calculator, and what the GPU does with bad maths New tab

Each row is one GLSL expression run on the GPU for one pixel. The last three rows are undefined behaviour: other GPUs may print other numbers.

idle

Console

Run the scene to see its console.

Expected console output

swizzle .zyx reverses a vec3         3, 2, 1, 0
maths works on every component       0.5, 1, 11, 12
mod, fract, floor, abs               1.5, 0.5, -1, 3
clamp, smoothstep, mix, step         1, 0.1563, 12.5, 0
3 / 2 is 1 (integers), mod(-1, 3)    1, 1.5, 2, 0
1/0, -1/0, 0/0                       Infinity, -Infinity, NaN, 0
normalize of a zero vector           NaN, NaN, NaN, 0
sqrt(-1), pow(-2, 2): undefined      0, 0, 0, 0

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Division by zero

In the calculator add the row ["1.0 / zero, 1.0, 1.0, 1.0", "test"].

terminal
$ Run
── what you'll see ──
test Infinity, 1, 1, 1

Myth vs fact

Myth

Shaders are only for fancy effects.

Fact

Every pixel you have drawn so far went through a vertex and a fragment shader. three.js wrote them for you. Custom shaders only replace that default code.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    GPUs run fragment shaders in blocks of pixels in lockstep. Code that branches differently inside one block runs both paths, so if costs more than it looks; mix and step often avoid it.

  • ▸

    gl_FragCoord is in drawing-buffer pixels. On a phone with pixel ratio 3, a 390 CSS-pixel canvas is 1170 pixels wide: divide by a resolution uniform that comes from renderer.domElement.width, never from innerWidth.

Remember this

  1. 1

    The problem this solves: MeshStandardMaterial gives you the looks its authors thought of. A metal-flake paint, a wave, a hologram or a dissolve is not on that list. A shader lets you decide the maths of every vertex and every pixel yourself.

  2. 2

    Two programs. A vertex shader runs once per vertex (a corner of a triangle, Topic 1.2) and must set gl_Position, where the corner lands on screen. A fragment shader runs once per pixel the triangle covers and sets the colour (gl_FragColor in three.js). The GPU runs thousands of copies side by side. Each copy knows only its own inputs and cannot ask its neighbour a question. That limit is what makes it fast.

  3. 3

    GLSL (OpenGL Shading Language) looks like C. WebGL 2 uses version GLSL ES 3.00. There are no implicit conversions: float x = 1; is an error, you write 1.0. The types are float, int, bool, vectors vec2, vec3, vec4 and matrices mat3, mat4. A vector can be read in any order and any length with swizzles: v.zyx, v.xy, v.rgb (rgba and xyzw mean the same thing).

  4. 4

    Maths acts on every component. vec2(1.0, 2.0) * 2.0 is vec2(2.0, 4.0). The functions you will use all course are mix(a, b, t) (blend), clamp, step(edge, x) (0 below the edge, 1 above), smoothstep(a, b, x) (a soft step), fract(x) (the part after the point) and mod.

  5. 5

    The sandbox. The first example is a Shadertoy-style page: one quad that covers the screen, a vertex shader that does almost nothing, and a fragment shader that reads two uniforms, iTime and iResolution (names borrowed from Shadertoy so its code pastes in with few changes). gl_FragCoord.xy / iResolution gives each pixel a position from 0 to 1. The numbers it prints come from a small off-screen picture at a fixed time, so they never depend on the window or the clock.

Explain it without notes

01

What is the difference between a vertex shader and a fragment shader, and how many times does each run for one triangle?

02

Why does float x = 1; fail in GLSL?

Practice

01

Make the sandbox show a vertical white-to-black gradient (white at the top).

02

Write the GLSL expression for a vec3 whose parts are the reverse of colour.

Done when you can

  • Say what a vertex and a fragment shader each do

  • Write 1.0, not 1, in GLSL

  • Read a pixel back from a render target

  • Explain what gl_FragCoord / resolution gives you