Command Palette

Search for a command to run...

Hectal
Course outline
Topic 12.3Advanced ~20 min · step 3 of 7

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.

0/7 · 0%

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. 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, and mod(..., 2.0) turns that into 0 or 1.

  2. 2

    Harden the circle

    Replace the smoothstep with step(length(uv - 0.5), 0.3). The rim is now a staircase of pixels: the soft edge hides it.

Code & diagrams

Stripes, a circle and a gradient, as text and as pixels New tab

The three squares on screen use the same expressions as the text pictures.

idle

Console

Run the scene to see its console.

Expected console output

stripes:
     ###   ###   ###   ###
     ###   ###   ###   ###
     ###   ###   ###   ###
     ###   ###   ###   ###
     ###   ###   ###   ###
     ###   ###   ###   ###
     ###   ###   ###   ###
     ###   ###   ###   ###
     ###   ###   ###   ###
     ###   ###   ###   ###
circle:
                          
                          
         .########.       
       :############:     
       ##############     
       ##############     
       :############:     
         .########.       
                          
                          
gradient:
          ...::***########
          ...::***########
          ...::***########
          ...::***########
          ...::***########
          ...::***########
          ...::***########
          ...::***########
          ...::***########
          ...::***########
Banding and dithering, measured New tab

Top half: banded. Bottom half: dithered. The CSS filter only makes the difference easy to see.

idle

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 levels

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

Switch dithering off

In the banding example change measure("dither", 1); to measure("dither", 0);.

terminal
$ Run
── what you'll see ──
no dither distinct levels: 26 | longest flat run: 11 px | steps: 25 | mean error: 0.25 levels
dither distinct levels: 26 | longest flat run: 11 px | steps: 25 | mean error: 0.25 levels
Both rows are identical: the second row is no longer dithered.

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 = true does it for built-in materials (the dithering_fragment chunk).

  • ▸

    smoothstep with a >= b is undefined in GLSL. Compute smoothstep(0.30, 0.32, x) with the smaller edge first, and write a rim as 1.0 - smoothstep(...).

Remember this

  1. 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. 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. 3

    The five tools. fract(x * n) repeats 0 to 1 n times: stripes. step(edge, x) makes a hard edge. smoothstep(a, b, x) makes a soft edge, so a circle 1.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 by t. mod(x, y) is x - y * floor(x / y), so mod(-1.0, 3.0) is 2.0, not -1.0.

  4. 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. 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 a fract repeat shows a hard line where UVs jump (a seam, Topic 4.5); colour in a ShaderMaterial is not converted to sRGB unless you include colorspace_fragment, so mid greys look darker than expected.

Explain it without notes

01

Why is a smoothstep rim better than a step rim?

02

What causes banding and why does dithering hide it?

Practice

01

Draw 8 horizontal stripes using uv.y.

02

Make a ring (a circle outline) with smoothstep.

Done when you can

  • Draw stripes, a circle and a gradient from uv

  • Explain mod(-1.0, 3.0)

  • Say why banding appears and how dithering hides it

  • Show vec3(uv, 0.0) to check your UVs