Course outline
Fragment Patterns
In one line
With a UV coordinate and five functions (step, smoothstep, mix, fract, mod) you can draw stripes, circles and gradients without any image. The edge case to know is banding, and dithering fixes it.
Think of it like this
A tailor cuts a pattern from a rule, not from a drawing: "every 5 cm, change colour". A fragment shader pattern is a rule from position to colour.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- uv
- A position on a surface from 0 to 1 in both directions, stored per vertex and blended into every pixel.
- smoothstep
- A function that goes from 0 to 1 between two edges along a smooth S-curve:
smoothstep(a, b, x). - banding
- Visible steps in a smooth gradient, because the screen has only 256 levels per channel.
- dithering
- Adding a little noise before rounding so steps become a fine speckle the eye blends.
Step by step
01A pattern is an expression
The first example keeps each pattern as one GLSL expression of uv that returns 0 to 1. The same function draws it on screen and into a 24 × 10 picture that is printed as text.
02Read the text pictures
Stripes: step(0.5, fract(uv.x * 4.0)) is white in the second half of each quarter. The circle looks oval only because the text picture is 24 × 10 characters. The gradient uses smoothstep so it starts and ends softly.
03Measure the banding
The second example draws a 0.10 to 0.20 gradient into 256 pixels. Without dithering it has 26 levels and flat runs of 11 pixels. With dithering the runs are at most 2 pixels, at the price of a little noise.
04See it
The page makes the canvas 4 times brighter with CSS so the steps in the top half are clear to see. The bottom half is dithered.
Try it yourself
- 1
Checkerboard
In the first example replace the gradient entry with
abs(mod(floor(uv.x * 4.0) + floor(uv.y * 4.0), 2.0)). The text picture shows alternating blocks: the two floors add to an even or odd number, andmod(..., 2.0)turns that into 0 or 1. - 2
Harden the circle
Replace the
smoothstepwithstep(length(uv - 0.5), 0.3). The rim is now a staircase of pixels: the soft edge hides it.
Code & diagrams
The three squares on screen use the same expressions as the text pictures.
Console
Run the scene to see its console.Expected console output
stripes:
### ### ### ###
### ### ### ###
### ### ### ###
### ### ### ###
### ### ### ###
### ### ### ###
### ### ### ###
### ### ### ###
### ### ### ###
### ### ### ###
circle:
.########.
:############:
##############
##############
:############:
.########.
gradient:
...::***########
...::***########
...::***########
...::***########
...::***########
...::***########
...::***########
...::***########
...::***########
...::***########Top half: banded. Bottom half: dithered. The CSS filter only makes the difference easy to see.
Console
Run the scene to see its console.Expected console output
no dither distinct levels: 26 | longest flat run: 11 px | steps: 25 | mean error: 0.25 levels
dither distinct levels: 28 | longest flat run: 2 px | steps: 224 | mean error: 0.54 levelsBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Switch dithering off
In the banding example change measure("dither", 1); to measure("dither", 0);.
Myth vs fact
Myth
More bits of precision in the shader removes banding.
Fact
The shader already computes in 32-bit floats. The banding happens when the result is rounded to the screen's 8 bits, so only dithering (or a 10-bit display) helps.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Dithering belongs at the very last step, after tone mapping and the sRGB conversion. three.js's
material.dithering = truedoes it for built-in materials (thedithering_fragmentchunk). - ▸
smoothstepwitha >= bis undefined in GLSL. Computesmoothstep(0.30, 0.32, x)with the smaller edge first, and write a rim as1.0 - smoothstep(...).
Remember this
- 1
The problem this solves: images cost memory and download time, and they blur when scaled. A pattern written as a rule is sharp at every size, costs no download and can animate. Stripes on a tyre, a checker on a floor or a glowing ring are all rules.
- 2
UV as colour. Before anything else, show
vec3(uv, 0.0): red grows to the right, green upwards. If you cannot see that, your UVs are wrong (Topic 4.5), not your pattern. - 3
The five tools.
fract(x * n)repeats 0 to 1ntimes: stripes.step(edge, x)makes a hard edge.smoothstep(a, b, x)makes a soft edge, so a circle1.0 - smoothstep(0.30, 0.32, length(uv - 0.5))has a smooth rim (hard edges flicker when they move).mix(a, b, t)blends two colours byt.mod(x, y)isx - y * floor(x / y), somod(-1.0, 3.0)is2.0, not-1.0. - 4
Banding. A screen shows 256 levels per channel. A dark gradient from 0.10 to 0.20 has only about 26 levels, so it shows steps (bands). The fix is dithering: add up to one level of noise before the colour is rounded, so neighbouring pixels flip between two levels and the eye averages them. The second example measures it: the longest flat run drops from 11 pixels to 2.
- 5
Edge cases. A pattern in UV space stretches when the UVs are stretched (a
PlaneGeometry(4, 1)makes circles into ovals); a pattern with afractrepeat shows a hard line where UVs jump (a seam, Topic 4.5); colour in aShaderMaterialis not converted to sRGB unless you includecolorspace_fragment, so mid greys look darker than expected.
Explain it without notes
Why is a smoothstep rim better than a step rim?
What causes banding and why does dithering hide it?
Practice
Draw 8 horizontal stripes using uv.y.
Make a ring (a circle outline) with smoothstep.
Done when you can
Draw stripes, a circle and a gradient from
uvExplain
mod(-1.0, 3.0)Say why banding appears and how dithering hides it
Show
vec3(uv, 0.0)to check your UVs