Linear Algebra for GLSL Shaders
01 / Meet the vectors

Give every pixel
an address.

Let’s build a coordinate system that keeps a circle round — even when the canvas changes shape.

20–30 min with experimentsGLSL ES 3.00 / WebGL 2After the introduction
What we want to make

A circle that stays a circle.

Try the result ↓

Both images use the same distance test. Only the coordinates change.

An ellipse on a wide canvas: independently normalized X and Y stretch the shape horizontally.
01 / Separate scalesWide canvas. Wide “circle.”p = 2.0 * uv - 1.0;
A round circle on the same wide canvas, using equal units on both axes.
02 / One shared scaleThe proportions are correct.p = (2.0 * pixel - size) / size.y;

In the first book, we used coordinates to draw shapes and work with textures. Now we will slow down a little and look at those coordinates themselves.

A circle is a good place to start. It can look correct in a square preview and turn into an ellipse on a wide screen. The distance function has not changed. The numbers we give it have.

By the end of this chapter, you will know where those numbers come from, what one unit means, and how to keep that unit the same in both directions.

Start with an address

A fragment shader calculates a color for a fragment. For our full-screen examples, we can think of that as working on one pixel at a time.

GLSL gives us its position through gl_FragCoord. We only need the first two components:

vec2 pixel = gl_FragCoord.xy;

pixel.x tells us how far we are from the left edge. pixel.y tells us how far we are from the bottom. Both are measured in pixels of the drawing buffer: the image WebGL renders into.

Imagine a buffer that is 800 pixels wide and 400 pixels high. Its geometric center is (400, 200). Moving 100 pixels right adds 100 to x. Moving 100 pixels up adds 100 to y.

There is nothing wrong with pixel coordinates. A distance of 100 has the same scale on both axes. But a shape with a fixed radius of 100 pixels does not grow when the buffer gets larger. Sometimes that is what we want. Sometimes we want a shape to use a fixed part of the available space.

Put the canvas in 0–1

Let’s divide the position by the buffer size:

vec2 uv = gl_FragCoord.xy / uResolution;

uResolution is a vec2 uniform containing the actual buffer width and height. The page passes it to the shader. Every fragment in this draw receives the same resolution; its own gl_FragCoord is different. Division between two vectors happens component by component:

uv.x = pixel.x / uResolution.x;
uv.y = pixel.y / uResolution.y;

The left edge now has x = 0, the right edge has x = 1, and the center has x = 0.5. The same is true for the vertical direction. Pixel centers lie just inside these edge values.

We usually call this pair UV coordinates. Here, u means the horizontal component and v means the vertical component. They describe a relative position. They do not have to come from a texture.

We can see both values by using them as color channels. This is a complete fragment shader for our WebGL 2 setup:

#version 300 es
precision highp float;

uniform vec2 uResolution;
out vec4 fragColor;

void main() {
    vec2 uv = gl_FragCoord.xy / uResolution;
    fragColor = vec4(uv.x, uv.y, 0.0, 1.0);
}

Red increases to the right. Green increases upward. Near the bottom-left corner, both values are small, so the image is nearly black. Near the top-right corner, red and green are both close to 1, so we get yellow. Blue stays at 0; alpha stays at 1.

Select UV colors in the playground to see this map. The colors encode UV in every coordinate mode, so we always have the same reference.

Move zero to the center

For a centered shape, it is useful to make the middle of the canvas (0, 0). We can subtract 0.5 from both components. Multiplying by 2 then changes the edge range from [-0.5, 0.5] to [-1, 1]:

vec2 p = (uv - 0.5) * 2.0;
// Equivalent: vec2 p = 2.0 * uv - 1.0;

Both forms mean the same thing. In the rest of this chapter, p is the coordinate we use to draw the shape.

Now x is negative on the left and positive on the right. y is negative below the center and positive above it. We have moved the origin: the position called zero.

This is coordinate normalization: choosing a useful coordinate range. It is different from normalize(v), which changes a nonzero vector’s length to 1. We will study that operation in chapter 04.

Why is the circle stretched?

Let’s use our centered coordinates to draw a circle:

vec2 uv = gl_FragCoord.xy / uResolution;
vec2 p = 2.0 * uv - 1.0;

float distanceToCenter = length(p);
float circle = 1.0 - smoothstep(0.49, 0.51, distanceToCenter);

length(p) measures the distance from zero. We will look at the calculation in chapter 03. For now, use it as our distance tool. The last line fills the area inside radius 0.5 and adds a small soft edge between 0.49 and 0.51.

On a square canvas, this looks fine. On an 800 × 400 canvas, it becomes twice as wide as it is tall. Why?

Our horizontal range of 2 units covers 800 pixels. One X unit is therefore 400 pixels. The vertical range also has 2 units, but covers only 400 pixels. One Y unit is 200 pixels.

Radius 0.5 along X: 0.5 × 400 = 200 pixels

Radius 0.5 along Y: 0.5 × 200 = 100 pixels

The formula draws a circle in the coordinates we gave it. Those coordinates use different scales. Both axes need the same ruler.

Use the same unit on both axes

Go back to pixel coordinates. First, subtract half the resolution to put zero at the center:

vec2 centeredPixels = gl_FragCoord.xy - 0.5 * uResolution;

Then divide both components by the height. Multiply by 2 so the vertical edges become -1 and 1:

vec2 p = 2.0 * centeredPixels / uResolution.y;

Or put it into one line:

vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;

Now a one-pixel step changes either coordinate by 2.0 / uResolution.y. The scale is the same in both directions. That is the important part.

With an 800 × 400 buffer, one unit is 200 pixels on either axis. A radius of 0.5 is 100 pixels in both directions. Our circle is round again.

The vertical edge range stays [-1, 1]. The horizontal edge range is [-aspect, aspect], where aspect = width / height. For 800 × 400, that is [-2, 2]. We accept more horizontal space instead of stretching one unit.

You can also fix centered UV by multiplying its X component by the aspect ratio:

vec2 p = 2.0 * uv - 1.0;
p.x *= uResolution.x / uResolution.y;

Both versions give the same result. Choose one. Applying both corrections would stretch the coordinates again.

Height is a convention, not a special mathematical rule. Dividing both components by width would also keep equal units, but fix the horizontal range instead. We will use height throughout these chapters. The introduction used a wider overview of the scene; the idea of equal units was the same.

One position, three descriptions — an 800 × 400 buffer
PositionPixelsUVEqual units
Bottom-left edge(0, 0)(0, 0)(-2, -1)
Center(400, 200)(0.5, 0.5)(0, 0)
Upper-right quarter point(600, 300)(0.75, 0.75)(1, 0.5)
Top-right edge(800, 400)(1, 1)(2, 1)
Try it

Change the canvas. Keep the shape.

LIVE GLSL / WEBGL 2

Start with Equal units, then switch to Centered UV. The circle changes even though its radius stays at 0.5. Change the canvas shape and compare again.

Static example: a round circle with equal coordinate units on a wide canvas.
640 × 360 buffer pixelsMove, tap, or use arrow keys to inspect
Pixels(320.00, 180.00)
UV(0.50, 0.50)
Centered UV(0.00, 0.00)
Equal units(0.00, 0.00)

Choose your coordinates

p = (2.0 * pixel - uResolution) / uResolution.y;

Zero is at the center. Both axes use the same unit.

Canvas shape

The page scales the whole canvas to fit your screen and preserves its proportions. The numbers above describe its drawing buffer.

Loading shader…
Open the complete shader edit · run · reset

The page supplies the resolution, selected mode, view, and pointer as uniforms. The source below also draws the grid and marker. Change the coordinate formula first; leave those drawing helpers alone for now.

⌘ / Ctrl + Enter to run

The marker is a continuous position, so it can sit on an edge or exactly at the center. It does not snap to a fragment’s half-pixel center. In Pixels mode the radius is one quarter of the buffer height; in UV it is 0.25; in both centered modes it is 0.5.

The small version, without the helpers

The playground adds a grid and an inspector. This is all we need for the corrected circle itself. Paste it into the editor and run it. It only uses uResolution, so the coordinate-mode buttons will no longer change it. Width and height still work. Reset restores the full playground.

#version 300 es
precision highp float;

uniform vec2 uResolution;
out vec4 fragColor;

void main() {
    vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;

    float distanceToCenter = length(p);
    float circle = 1.0 - smoothstep(0.49, 0.51, distanceToCenter);

    vec3 background = vec3(0.04, 0.06, 0.09);
    vec3 fill = vec3(0.29, 0.88, 0.56);
    fragColor = vec4(mix(background, fill, circle), 1.0);
}

mix chooses the background where the mask is 0 and the fill where it is 1. Between them, it blends the colors. The edge width is measured in our chosen coordinate units. It is a simple soft edge, not a full lesson in antialiasing.

A note about the pointer

Browser pointer coordinates usually start at the top-left and are measured in CSS pixels. Our shader starts at the bottom-left and uses drawing-buffer pixels. The page converts between them before sending the pointer to the shader:

const rect = canvas.getBoundingClientRect();
const x = (event.clientX - rect.left) * gl.drawingBufferWidth / rect.width;
const y = (rect.bottom - event.clientY) * gl.drawingBufferHeight / rect.height;

Use the canvas content area here; in this demo the canvas has no border or padding. The Y calculation reverses the vertical direction. The size ratios handle a canvas displayed smaller than its drawing buffer. If the marker moves in the wrong direction or drifts away from the pointer, check this conversion before changing the shader.

Your turn

Set the buffer to 800 × 400. Use the center and edges as continuous positions, as in the table above.

  1. Before moving the marker, find where (0, 0) is in Pixels, UV, and Equal units. Do these names describe the same place?
  2. Now find (0.5, 0.5) in each space. In Pixels, this is the first pixel center. In UV, it is the canvas center. Where is it in Equal units?
  3. Compare Centered UV with Equal units, then switch between Wide, Square, and Tall. Explain why the first mode hides its problem on a square canvas.
  4. Paste the small shader above into the editor. Make the circle smaller by changing both soft-edge limits from 0.49 and 0.51 to values around a smaller radius. It must remain round on a wide and a tall canvas.
A hint

For Equal units, solve the formula for the pixel position: pixel = 0.5 * (p * height + resolution). For the circle, try replacing 0.49 and 0.51 with 0.24 and 0.26. Keep the lower edge value smaller than the upper one.

Check your result

Pixels and UV both place (0, 0) at the bottom-left edge. Equal units places it at the center: pixel position (400, 200).

Equal-units coordinate (0.5, 0.5) is pixel position (500, 300). Both moves from the center are 100 pixels. UV coordinate (0.5, 0.5) is (400, 200), while pixel coordinate (0.5, 0.5) is near the bottom-left edge.

When width equals height, both scale factors happen to match, so centered UV looks correct. On a rectangle they differ. With equal units, the smaller circle has radius 0.25, or 50 buffer pixels for a height of 400. Its horizontal and vertical radii match.

What you can do now

You can choose an origin, convert pixel positions to UV, and build a centered space with equal units. The useful check is simple: the same coordinate change along X and Y must cover the same number of pixels.

Equal units preserve shape, not a fixed size. Our radius is relative to height, so the circle grows when the buffer becomes taller. On a very narrow canvas, part of it may be clipped. Neither case means the circle is stretched.

Next, we will use two positions to describe a movement. That gives us our first useful vector: the difference between one point and another.