Basics, part 3

Vectors are little arrows made of numbers

A vector is a few numbers that travel together, like a position, a direction or a color. Add them, measure them, then let the dot product light a sphere.

  • Part 36 min dose
  • Beginner

In the coordinates lesson every pixel got a position, two numbers for how far across and how far up. Shaders keep numbers together like that all the time, and a group of them is a vector. Let’s add vectors, measure them, and then use one little trick, the dot product, to light a sphere.

A sphere and its little sun
Drag to turn the view, or use the arrow keys

Turn on JavaScript to run this demo.

The color of the sphere.
Fig. 1

Drag anywhere to swing the light around the sphere. By the end you’ll know the one line that shades it.

A few numbers that travel together

In a Unity shader, the type tells you how many numbers a vector holds. A float2 has two, a float3 three and a float4 four. They can stand for a position like (0.5, -0.2), the direction up (0, 1, 0) or the color orange (1, 0.5, 0.2). I like to picture a vector as an arrow that starts at (0, 0), so (2, 0.5) goes two units across and half a unit up.

Adding and scaling arrows

To add two vectors, add the matching numbers: (2,0.5)+(1,0.8)=(3,1.3)(2, 0.5) + (1, 0.8) = (3, 1.3). As arrows, the second one goes on the end of the first, and the sum runs from the start to where it ends.

Adding arrows

3: The sum runs from the start to where it ends.

(2, 0.5) + (1, 0.8) = (3, 1.3)
across: 2 + 1 = 3up: 0.5 + 0.8 = 1.3

To scale a vector, multiply each number by the same kk: 2⋅(1,0.8)=(2,1.6)2 \cdot (1, 0.8) = (2, 1.6), twice as long and the same direction. A negative kk turns it around, which is all subtracting is: a−ba - b is a+(−1) ba + (-1)\,b. And turning or stretching every vector at once is the job of a matrix.

Two arrows and their sum
Drag the tip of b, or press Tab to reach it and use the arrow keys

Turn on JavaScript to run this demo.

Multiplies b before it is added, so 2 doubles it and -1 turns it around.1.00
add.hlsl draws the demo above
Fig. 2

Drag the tip of b and watch its copy ride on the end of a, with the butter sum following along. Set Scale k to 2 and the copy doubles. At -1 it turns around, and the sum becomes a−ba - b.

How long is an arrow?

The length comes from Pythagoras. Think of the two numbers as the short sides of a triangle with a square corner, and the arrow is its long side.

∣v∣=x2+y2|v| = \sqrt{x^2 + y^2}

xx and yy are the numbers of vv, and ∣v∣|v| is its length. A float3 adds z2z^2 under the root. In code it’s length(v).

Often only the direction matters. Divide a vector by its length and you get a unit vector, with length 1 and the same direction. That’s normalize(v):

normalize⁡(v)=v∣v∣\operatorname{normalize}(v) = \frac{v}{|v|}

There’s one trap, though. The vector (0, 0) has no direction, and normalizing it divides by zero. What you get back is garbage.

Do two arrows agree?

The dot product multiplies the matching numbers and adds them up:

a⋅b=axbx+aybya \cdot b = a_x b_x + a_y b_y

axa_x is the across part of aa, byb_y the up part of bb. The answer is one number that says how much the arrows point the same way. It also comes from their lengths and the angle θ\theta between them, with the cosine from the sine and cosine lesson:

a⋅b=∣a∣ ∣b∣cos⁡θa \cdot b = |a|\,|b| \cos\theta

Take this one slowly. For two unit vectors both lengths are 1, which leaves the cosine alone. It’s 1 when they point the same way, 0 at a right angle and -1 when they point opposite ways. Now picture a light shining onto aa‘s line at a right angle. Then bb casts a shadow on the line, its projection. When aa has length 1, a⋅ba \cdot b is that shadow’s length.

The shadow of b on a
Drag the tip of b, or press Tab to reach it and use the arrow keys

Turn on JavaScript to run this demo.

Keeps b's direction but sets its length to 1, so the dot product becomes the cosine of the angle.
lib.hlsl and dot.hlsl draw the demo above
Fig. 3

Drag the tip of b and watch the numbers. The butter one, a⋅ba \cdot b, is the shadow’s length, negative when b points backwards. Turn on Normalize b to keep b on the circle, and a⋅ba \cdot b becomes the cosine.

Lighting with one dot

Here’s where the dot product earns its keep. Every point of a surface has a normal NN, a unit vector pointing straight out. On a sphere it’s the direction from the centre to the point. LL is a unit vector from the surface towards the light:

light=max⁡(0, N⋅L)\text{light} = \max(0,\ N \cdot L)

Where the surface faces the light head on, N⋅LN \cdot L is 1 and you get full brightness. It fades as the surface turns away, reaches 0 at a right angle and goes negative on the far side, where max holds it at 0. This is Lambert lighting, and it sits under nearly every lit shader. A toon shader cuts the same number into hard steps.

The dot product before max
Drag to turn the view, or use the arrow keys

Turn on JavaScript to run this demo.

Lit stops the dot product at 0. Raw keeps its sign, mint where it is positive and pink where it is negative.
The color of the sphere.
light.hlsl draws the demo above
Fig. 4

Drag to move the light. Show starts on Raw, so you see N⋅LN \cdot L before max. Mint means it’s positive and pink means it’s negative, with dark in between. Switch to Lit to see max at work.

The cross product

The dot product turns two vectors into a number. The cross product turns two 3D vectors, and only 3D ones, into a third at a right angle to both. Give it two edges of a triangle and it points straight out of the triangle.

The cross product

4: normalize makes it length 1, and that's the triangle's normal.

a = (1, 0, 2)b = (1, 0, 0)
cross(a, b) = (0, 2, 0)normalize → (0, 1, 0)

Here it is in a shader:

float3 edge1 = p1 - p0;
float3 edge2 = p2 - p0;
float3 n = normalize(cross(edge1, edge2));

p0, p1 and p2 are the corners, and normalize makes the result the triangle’s normal. Swap the two edges and it points the other way, which is why the order of a triangle’s corners decides which side is its front.

Picking parts with swizzles

You can read the parts of a vector by their letters: .x, .y, .z, .w, or .r, .g, .b, .a. Several letters make a new vector, in any order. That’s a swizzle:

float4 col = float4(1.0, 0.5, 0.2, 1.0); // orange, fully opaque
float3 rgb = col.rgb;          // (1.0, 0.5, 0.2): the color without alpha
float3 swapped = col.bgr;      // (0.2, 0.5, 1.0): now it's blue
float2 floorPos = worldPos.xz; // where a point is on the floor, seen from above

In practice

In a Unity shader, the lighting looks like this:

float3 N = normalize(i.worldNormal);
float3 L = normalize(_WorldSpaceLightPos0.xyz);
float light = saturate(dot(N, L));

_WorldSpaceLightPos0.xyz is the direction towards the main directional light, and saturate is the max from before that also stops at 1. Two traps:

  • Normalize again. The normal is blended across each triangle, and a blend of two unit vectors comes out a little shorter than 1.
  • No light at all. Many VRChat worlds have no realtime directional light, and then this direction can be all zeros. Hello again, normalize trap. Lit shaders check for that and fall back on a direction of their own.

Takeaways

  • A vector is a few numbers kept together. Add and scale them number by number.
  • length measures an arrow, normalize keeps only its direction and swizzles pick its parts.
  • The dot product says how much two directions agree, so N⋅LN \cdot L lights a surface. The cross product finds a direction at right angles to two others.
Search the lessons

Type any word from the lessons, like normalize or smoothstep. The start of a word is enough.