a new kind of drawing
A mark on a page – drawn from nothing, by you – is one of our earliest experiences of creation. You think of a line and your hand makes it. The translation from thought to mark is almost nothing. Immediate. Physical. Yours.
The computer introduced a different bargain. To make a mark, you describe it. A line becomes two points. A circle becomes a center and a radius. You lose something of the body, but you gain something else entirely: the ability to describe a thing once and have it happen a thousand times, each instance slightly different, each one obeying the same rule. The computer is not a faster hand. It is a different kind of tool, with a different kind of fluency.
For most of computing history, drawing on a screen meant giving instructions in order. Go right. Draw a line. Make a circle here. The computer followed along, one step at a time, like a very obedient and very fast hand.
But in the 1980s, as rendering images grew more ambitious – lighting, shadows, the way glass catches the sun – a different idea emerged in film studios. What if each pixel decided for itself?
Instead of one long list of instructions, you write a single small program. And then every pixel on the screen runs that program simultaneously, each one asking: given where I am, what should I look like? A million tiny questions, answered all at once.
This is what a shader is.
The word comes from its original purpose – shading surfaces, simulating light – but shaders have grown far beyond that. It took another twenty years for that idea to become fast enough to escape the render farm and reach the screen in your hand. Today they generate the skies in video games, the transitions between scenes in films, the animated interfaces on your phone. Almost every image rendered by a modern screen passes through one.
And yet shaders have stayed strangely hidden. The tools for writing them are made by and for engineers. The learning curve is steep and arcane. This is a loss, because the idea at the heart of a shader is not an engineering idea. It is a design idea. An art idea. The question what should this pixel look like? is the same question a painter asks, a photographer asks, a designer asks – just asked very, very fast, and all at once.
This guide is an attempt to bring shaders into the light. We start with a single pixel. We ask it one question. And we go from there.
one answer
A screen is made of pixels. Each pixel is a tiny point of light, and each one can be told how bright to shine, anywhere from 0 (completely dark) to 100 (completely bright).
In this guide, you will write small programs that answer the pixels’ questions. Each program is a set of instructions, written as plain words and numbers. There is no punctuation to memorize. No strange symbols. Just instructions, one per line.
The first instruction you will learn is answer. It is the answer to the pixel’s question: what should I look like? It takes a single number – the brightness level you want – and sets the pixel to that value. Written like this:
answer 50
That is a complete program. One instruction. One pixel. Try it. Change the number in the editor below and press run – the square on the right is your pixel, scaled up so you can see it clearly.
What does 0 look like? What about 100? What about 42?
where are you?
In the last chapter, every pixel would get the same answer. One number, one brightness, one result: the program didn’t need to think about which pixel it was talking to, because it treated them all identically.
But pixels are not identical. Each one occupies a specific place on the screen, and that place is part of what it is.
We describe position in two directions. Left to right, we call <x>. Bottom to top, we call <y>. Every pixel has both, a unique address, a location that belongs to it alone. We’ll start with looking at <x>.
Every pixel comes with an <x> already given. You don’t set it, it comes with the position. On a grid five pixels wide, <x> runs from 0 on the left to 100 on the right. The pixel in the middle has an <x> of 50.
This means you can use <x> in your answer. When you run the program, every pixel substitutes its own <x> into the program: the pixel on the left edge uses 0, the one in the middle uses 50, the one on the right uses 100.
Try it below on a single pixel. Its <x> is fixed at 50, sitting in the middle. Nothing very surprising yet.
But that program is no longer just a number. It is a rule. And rules only show their nature when applied to more than one thing.
Everything that follows in this guide is that one idea, growing.
Let’s run the same program on a 5×5 grid. Each pixel gets its own answer.
But a pixel knows more than just its left-right position. It also knows how far up or down it is. This is <y>.
<y>
A pixel at the very bottom has a <y> of 0. A pixel at the very top has a <y> of 100. The middle is 50. It works exactly like <x>, just in the vertical direction.
Try it, and notice how the gradient now runs bottom to top instead of left to right. By the way, if you move your mouse over the output canvas, it will show you the coordinate of the pixel you’re hovering over.
Now you have two coordinates, two things every pixel knows about itself. Between <x> and <y>, a pixel knows exactly where it sits in the grid. No two pixels share the same pair.
Try switching between answer <x> and answer <y> to see the difference. Then try answer 50 again, where every pixel gives the same answer, because the rule ignores position entirely.
combining things
So far, each program has been a single value: a number, <x>, or <y>. But you can combine values using arithmetic: +, -, *, and /.
This is where things get interesting. When you write answer <x> + <y>, each pixel adds its own <x> to its own <y>. A pixel in the bottom-left corner gets 0 + 0 = 0. A pixel in the top-right gets 100 + 100 = 200 (which is clamped to 100, because brightness can’t exceed that).
The result is a diagonal gradient. Light gathers in one corner, darkness in the other.
Try it. Then try answer <x> - <y>. Or answer <x> * <y> / 100. Each expression produces a different pattern, the direct visual consequence of a rule.
Note that you can use parentheses to control the order of operations, just as you would on paper: answer (<x> + <y>) / 2.
Programs can also contain comments, little notes to yourself. Anything after // on a line is ignored by the pixel.
answer <x> + <y> // diagonal gradient
Comments change nothing about the output. They are for you, to remember what you were thinking, or to explain something to someone else.
when are you?
Everything so far has been still. You write a program, you run it, and the canvas shows you a single, frozen moment. But a screen is not a photograph. It can change.
There is a new value available in your programs: <time>. Unlike <x> and <y>, which stay fixed for each pixel, <time> increases continuously, counting upward from the moment you press run. It rises quickly: about 50 units per second.
The canvas will flash from black to white in about two seconds, then stay white, because brightness is clamped at 100 and time keeps climbing past it. A single flash, then nothing.
Time runs away. That is its nature. To make something repeat, you need a way to wrap a number around: 101 becomes 1 again, 201 becomes 1 again, forever.
This is what % does. It is called the remainder operator, also called modulo. It gives you the leftover after division.
137 % 100 is 37, because 137 divided by 100 leaves a remainder of 37.100 % 100 is 0.250 % 100 is 50.
When you write <time> % 100, the result counts from 0 up to 100, then jumps back to 0 and starts again. A cycle. A pulse. About once every two seconds.
This works for negative numbers too: -30 % 100 is 70 – the value wraps around from the other end. That wrap is what keeps a leftward-moving wave seamless.
Now the canvas breathes, from darkness to light, over and over. Every pixel gives the same answer at the same moment, so they all pulse together in unison.
But you know how to break that unison. You have <x> and <y>. What happens when you add position to time?
Try answer (<x> - <time>) % 100. The gradient begins to scroll, each pixel’s answer offset by where it sits, so the wave moves across the canvas.
Try answer (<x> + <y> - <time>) % 100. A diagonal wave.
Change the number after % to change the speed and scale of the pattern. A smaller number means tighter, faster cycles. A larger number means slower, broader ones.
naming things
You’ve been tuning programs by changing numbers directly. But a number on its own says nothing about what it does. Give it a name and it starts to explain itself.
Use set to give a number or an expression a name:
set speed 2
After that line, speed carries the value 2. You can use it anywhere you would use a number.
When you read <time> * speed, you know
what that multiplication is doing.
The name speed is yours to choose – you could call it s, rate, or anything else you like.
A faster wave. The name speed makes it easy to adjust:
change it in one place and the whole pattern follows.
Notice that speed holds the same value for every pixel – a constant, just with a label.
The next program uses set differently. dx and dy depend on <x> and <y>,
so each pixel gets its own values for them.
That’s what lets two named ideas compose into a shape.
A soft dome. dx and dy take a different value at every pixel:
large at the edges, zero at the center.
Squaring them and adding turns that pair into a single number:
how far each pixel sits from the center.
Try changing the / 50. A smaller number darkens the edges faster;
a larger one spreads the dome wider.
Replace <x> - 50 with 0 and the dome flattens into a horizontal band, brightest across the middle
and fading toward the top and bottom.
This is what naming things unlocks. Instead of one long formula, you build up in steps. Each step has a name that says what it means, and the final line reads almost like a sentence.
tools of the trade
Some operations come up again and again: making a negative number positive, finding the larger of two values, creating a smooth wave. These are built in as functions.
A function takes a value and gives back a new one. You use it by writing the function name followed by the value, or an expression in parentheses.
The first function: positive. It gives the absolute value,
the distance from zero. positive -30 is 30,
and positive 30 is also 30.
In the editor below, positive measures each pixel’s distance
from the center along two axes. The result is a diamond shape,
with pixels near the center bright and those at the edges dark.
One thing to notice: positive <x> - 50 means
“absolute value of <x>, then subtract 50”: a function only takes the single value immediately after it,
so this is read as (positive <x>) - 50.
If you want to apply positive to a whole expression, use parentheses: positive (<x> - 50).
Now for something smooth. wave produces a wave,
a value that rises and falls in a continuous curve. wave takes an input from 0 to 100
(one full cycle) and gives back a value from 0 to 100.
At 0 the output is 50, at 25 it peaks at 100, at 50 it returns to 50,
and at 75 it dips to 0.
Try answer wave <time> below, for a slow, gentle pulse.
Then try answer wave <x> for a spatial wave,
or answer wave (<x> + <time>) for a wave that scrolls.
And then there are some functions that take two values. lowest gives the smaller of two values, highest the larger.
lowest a b means “the lower of a and b”, so lowest 20 80 is 20.
highest a b means “the higher of a and b”, so highest 20 80 is 80.
If you need to pass a longer expression, wrap it in parentheses: highest (<x> - 50) (<y> - 50).
In the editor below, highest turns the diamond into a square.
The diamond used dx + dy, adding both distances.
Replacing the sum with highest measures only the greater distance.
The result is a square boundary instead of a diamond.
One more function: distance. It takes two values – an x coordinate
and a y coordinate – and returns the straight-line distance from the current pixel
to that point.
distance 50 50 gives how far each pixel sits from the center.
Unlike positive (<x> - 50), which only measures horizontal separation, distance accounts for both axes at once. The result is a true radial gradient.
Feed that into wave together with <time>,
and the rings ripple outward from the center.
A few more functions will quietly serve you for the rest of this guide –
worth meeting now, even though their moment comes later. floor and round snap a value to a whole number: floor always rounds down, round rounds to the nearest integer. flip x inverts by returning 100 - x, so bright becomes dark and vice versa.
And sqrt is the square root: sqrt 25 is 5, sqrt 100 is 10.
on the edge
So far every program has produced a smooth, continuous result. Every pixel’s answer has been a function of its position or time, varying gradually across the canvas. But most images contain edges: places where something stops and something else begins. How do you make an edge?
In most languages you would reach for an if/else: if x is less than 50, return black; otherwise, return white. But this language has no if/else. Every pixel runs the same program; there is no branching.
Instead, you express the idea as arithmetic. cut takes a value
and a threshold. If the value is below the threshold, it returns 0.
If the value is at or above the threshold, it returns 100.
The result snaps, but it is still just a number.
A hard vertical line, expressed as a value, not a branch. And because it is a value, you can use it in arithmetic.
The real power of cut is that it gives you a mask:
a number that is either 0 or 100. Multiply your answer by a mask
and you have masked it. A circle can be drawn this way: compute
the distance from the center, cut it at the desired radius, multiply.
The shape is the multiplication.
One detail: because a mask is 100 rather than 1, divide by 100 after
multiplying – answer value * mask / 100. (For simple on/off
images the division can be skipped, since brightness is clamped at 100
anyway – that’s what the rectangle below does.)
A dark disc on a bright background. d is the distance from
the center, the value being tested. 30 is the threshold.
Every pixel within 30 units of the center falls below it and answers 0.
Every pixel beyond answers 100.
Now combine two cuts. cut d 20 snaps at radius 20; cut d 30 snaps at radius 30.
Both are 100 beyond their threshold, so their difference is 100
only in the ring between them.
A rectangle works the same way: cut in one axis, then the other, and multiply. Each cut carves a boundary; their product is the region where both are active.
So far every edge has been perfectly sharp, a sudden jump from 0 to 100. But many shapes need softer boundaries: a glow, a shadow, a smooth edge.
fade is like cut but with a transition zone.
It takes a value and two thresholds: fade value threshold1 threshold2.
Below threshold1 it returns 0.
Above threshold2 it returns 100.
Between the two thresholds, it glides smoothly from 0 to 100
along a gentle S-curve.
If you give the higher threshold first, the ramp reverses: 100 below, 0 above.
That is what the soft circle below does – pixels close to the center (small d)
get 100, pixels beyond 35 get 0.
Try the soft circle below. Adjust the two threshold values and feel how they control the hardness of the edge.
A circle with a soft, glowing edge.
When the two edges are far apart, the transition is wide and gentle.
When they are close together, the edge is nearly sharp.
When they are equal, it behaves like cut.
You can animate the edges too.
Below, fade makes a circle breathe,
its rhythm driven by <time>.
In conventional programming, shape and logic are separate concerns. Here they are the same thing. A shape is a mask is a number. You do not draw a circle. You compute whether each pixel is inside one. This is the shader way of thinking about form.
Now that you can compute shapes, the next question is: what do you do with them?
The simplest use of a mask is to switch between two values; switching
sharply at a threshold is just cut. What if you want to mix two things together, in any proportion?
blend a b t interpolates between two values.
When t is 0, it returns a.
When t is 100, it returns b.
In between, it returns a proportional mix.
Think of it as a crossfader: at one extreme you hear only the first signal, at the other extreme only the second, and anywhere in between you hear both.
The left side of the canvas shows the slow wave; the right side shows the fast one.
In between, they blend smoothly. The mixing amount is just <x>,
a number that goes from 0 to 100 across the canvas.
your own questions
You have used built-in tools like wave, positive,
and distance, each one taking an input and giving back a result.
But what happens when you need a tool that does not exist yet?
You can define your own. In this language they are called questions: you ask one, and it gives you an answer.
The word question starts a definition.
After it comes a name (here, halve), and then
any inputs it needs, separated by spaces.
The body goes inside { }, and the answer inside returns the result.
Notice that answer means something different inside a question:
it is the question’s private result, not the pixel’s brightness.
The answer at the bottom, outside any question,
still sets the pixel.
A question can take more than one input. Try this one that averages two values.
Questions can use everything you have learned:
variables, masks, and even other questions.
And they have their own private workspace: a set inside a question stays inside; it cannot leak out and affect
the rest of your program.
The real power comes when one question uses another. spot draws a filled disc. ring calls it twice –
once at the outer radius, once at the inner – and subtracts.
One disc punched through another.
Once you can package a calculation into a question, you can reuse it as many times as you like, with different inputs each time.
One more question that can help you make decisions:
choose value threshold a b returns a when value is below the threshold,
and b when it is at or above it.
It is exactly like an if/else, expressed as a number.
This pattern is particularly useful in programs that carry information from one frame to the next, where you need to decide which of two values a pixel should hold based on a condition. You will see it again in later chapters.
reach in
Until now, every program you have written runs on its own. You set it going and watch. What if you could reach into the canvas and change things while the program runs?
Two new variables: <mousex> and <mousey>.
They track the pointer’s position over the output canvas,
using the same 0–100 coordinate system as <x> and <y>.
Before you interact, both start at 50, centered on the canvas.
Once you move over the canvas, they remember the last position.
Run the program below, then move your pointer over the output.
A bright band that follows the cursor left and right.
Now use distance to measure from the pointer
instead of a fixed point.
A spotlight that follows your pointer.
Try wave on the distance to get interactive ripples.
Add <time> and the ripples radiate outward from wherever you point.
Try changing the multiplier or subtracting <time> to reverse direction.
There is one more mouse variable: <mousedown>.
It is 100 when you are holding the mouse button, and 0 when you are not.
Use it to switch between two different states – press and hold to see the difference.
A spotlight when you’re not pressing, ripples when you are. <mousedown> is just a number – it can go anywhere a number goes.
nature’s brush
Everything you have made so far is precise. Circles are perfect. Waves repeat forever. Gradients flow smoothly from one value to another.
But look at the world outside the screen: tree bark, coastlines, clouds, the surface of a stone. There is structure, but it is never exactly the same twice. Nature’s textures are organic: smooth up close, rough from a distance, and full of surprises at every scale.
We need a new kind of function. noise takes three values
and gives back a smooth, unpredictable number, mostly between 0 and 100.
The same inputs always give the same output, but nearby inputs give
nearby outputs, creating a landscape of gentle hills and valleys.
Where wave repeats the same curve forever, noise wanders, never repeating, never sharp.
An organic texture, nothing like wave’s perfect ripples.
No two regions look the same, yet the whole surface is smooth and continuous.
Try changing the scale of the detail by multiplying the inputs, like this: noise (<x> * 2) (<y> * 2) 0.
A higher multiplier means finer, more rapid changes.
But noise takes three values.
The third dimension can make the noise change its shape gradually, and therefore it’s perfect for <time> to make the noise
evolve smoothly, like clouds drifting across the sky.
Noise gives one scale of detail, a gentle rolling landscape. But nature has texture at every scale. A mountain range has peaks, which have boulders, which have rocks, which have pebbles. Each scale adds finer detail on top of the last.
You can build this yourself: layer the same noise at different scales, making each layer smaller and quieter. Multiply the input to zoom in, and divide the result to make it subtler.
This layering technique is called fractal noise (or fractal Brownian motion). Shader artists use it all the time.
Now combine everything you know.
Use fractalnoise (<x> + <time>) 0 0 as a horizon line and cut to draw sky above and land below,
a scrolling mountain landscape.
There is so much to explore from here.
Try wave (2 * noise <x> <y> <time>) for plasma.
Try cut (fractalnoise <x> (<y> / 2 - <time> * 2) 0) <y> for rising fire.
Try thresholding noise at different heights to build a
layered terrain map.
Or feed <mousex> and <mousey> into the noise to sculpt
textures by hand.
making memories
Every chapter so far, the pixel has answered its question fresh. Each pixel answers from scratch every frame. It knows its position, the time, the mouse – but nothing about what it was on the previous frame. However many frames have run, each one is computed as if it were the first.
But there is something new the pixel can know: what it was on the last frame.
remember <x> <y> reads the brightness at position <x>, <y> from the previous frame, the image that was on the canvas one step ago.
You can read your own position, or any other pixel’s position.
The simplest use: mix the current answer with the previous one.
The canvas is no longer stateless. It accumulates. It remembers.
Try the program below. Move your pointer over the canvas. You are drawing, and the marks linger, fading slowly into darkness.
The weight of the mix controls the length of the memory. When you multiply the previous value by something close to 100 (like 99), the trail fades slowly, a long and lingering memory. When you multiply by something smaller (like 90), it fades quickly. A short, snappy response.
Add <mousedown> and the trail becomes a brush:
paint only applies when the button is held.
But remember does not have to read its own position.
A pixel can look at what its neighbors were on the last frame.
When it averages itself with its four neighbors
((<x> + 1) <y>, (<x> - 1) <y>, <x> (<y> + 1), <x> (<y> - 1)),
heat spreads. Ink bleeds.
This is diffusion. Try it below: move your pointer to paint brightness onto the canvas, then watch it dissolve outward.
Diffusion smooths. A nonlinear reaction sharpens, pushing values toward black or white. When these two forces compete, neither wins, and organic boundaries emerge. Press run below and watch the noise sort itself into blobs.
The patterns on animal skins, the branching of corals, the whorls of a fingerprint – all of them grow from this same principle. It has a name: reaction-diffusion.
Now try reading the previous frame at warped coordinates.
If you offset the lookup with wave, the image folds
into itself, a feedback loop that creates endlessly evolving patterns.
Reading at a different position doesn’t just blend or diffuse: it transports. Each frame, content moves a fraction of a pixel, then the shifted content moves again. Small offsets compound into large, coherent drifts. The canvas is now a medium, carrying patterns through space as well as through time.
look out
Everything so far has been self-contained. The pixel’s world has been mathematics, position, time, the mouse. Nothing from outside the screen has entered the picture.
eye <x> <y> opens a window to the world.
Whatever the device’s camera sees becomes available as a value,
sampled at any coordinate. Just like remember,
but reading from the live camera feed instead of the last frame.
The simplest use: answer eye <x> <y>
A live grayscale feed. Underwhelming for about one second, then you realize you are writing a program that processes real light in real time.
Your browser will ask for camera permission when you press run. If you decline, the examples use a short looping video instead – everything still works.
Everything you have learned now applies to a real image.
Threshold it with cut for hard black and white,
like a photocopy left in the sun:
The camera does not have to sample straight.
Offset the sample coordinates with wave and
the image wobbles, a wave displacement animated with time:
Compare each pixel to its neighbor and brighten or darken where they differ. That is edge detection, the same principle used in computer vision:
The pixel is still just asking the same question it always has – what should I look like? – but now the answer can contain the world.
Try combining the camera with everything else: remember for trails, noise for texture, wave for animation.
The camera is just another input, another thing each pixel knows.
the wider world
You have come a long way.
From answer 50 to a program that processes the world
through a camera lens.
Pause and notice what actually happened: you learned to think in a completely new way. Not sequentially, not imperatively, but in parallel, as rules rather than instructions. Every program you wrote was a single description applied simultaneously to every pixel on the canvas. That shift in thinking is the real achievement of this guide.
You are not alone in this pursuit. There is a remarkable subculture called the demoscene that has been doing exactly this since the 1980s. Groups of programmers and artists compete to produce the most extraordinary real-time visuals under extreme constraints, sometimes an entire animated scene with music in 64 kilobytes. This is where much of the craft of shader programming was developed, passed down not through textbooks but through released executables and shared wonder.
There are artists who have dedicated their lives to this medium. Inigo Quilez has spent decades making extraordinary images from pure mathematics: signed distance fields, implicit surfaces, entire worlds built from equations. His writing has taught a generation of shader artists. The creative coding community more broadly (Processing, p5.js, the lineage that runs from John Maeda through Casey Reas and Ben Fry to thousands of artists working today) is part of a much larger conversation about computation as a creative medium.
Real-time is a specific condition. The work exists in time, responds to the viewer, cannot be fully experienced as a static object. This connects it to performance, to installation art, to music. A shader running on a screen is closer to a musical score being performed than to a painting hanging on a wall – it is a set of rules that produces an experience, and the experience is never exactly the same twice.
This guide began with the observation that drawing by hand is one of the first things we learn to do. The computer offered a different bargain: describe, don’t draw. You have now spent time on the other side of that bargain, and, I hope, found that something was gained as well as lost. The fluency is different. But it is fluency.
Here is where to go next:
- The playground – all features unlocked, nothing in the way
- The Book of Shaders – learn more about shader programming
- Shadertoy – see what others have made, share your own work
- Inigo Quilez’s website – a lifetime of shader art and writing
- pouët.net – the demoscene archive
Thank you for reading. Every pixel is still asking. You know how to answer.