Vectors and matrices
For the pure-maths intuition — cos/sin, what a dot is, cross, SDFs — see ../math/trig-and-vector-heuristics.md. This note is the same few operations seen from inside a renderer or a physics step: which one answers which question, and how each goes wrong.
A vector is a point or a direction, never both
Points move when the world moves; directions don't.
The same three numbers mean "here" or "that way", and transforms carry translation — but a normal pointing up still points up after the object slides sideways. A fourth component tags which you meant: w=1 says point, transform me fully; w=0 says direction, skip the translation column.
Normalize: keep the direction, forget the distance
Normalize when the length would corrupt the answer, not as a reflex.
Unit vectors make dot products come out as pure cosines — normals, light and view directions and camera axes all must be unit. It costs a square root, so skip it where length cancels, and never take a root just to compare distances: distSq < r*r is exact, and sorting by squared distance gives the same order.
Dot: how much do these agree
One number for "same direction" — most lighting and visibility tests are built from it.
max(0, dot(N, L))— diffuse lighting: how square-on the surface faces the light.dot(toTarget, forward) > 0— is it in front of me? Sign only; no normalize needed.dot(P, n) - d— which side of a plane, and how far. Frustum culling is six of these.dot(a, b̂) * b̂— the component ofaalongb. Collision response splits velocity into this normal part (which bounces) and the leftover tangential part (which friction eats).dot(L, spotDir) > cos(θ)— inside the spotlight cone? Compare cosines, never angles.
Cross: the perpendicular you asked for
Square to both inputs, with length equal to the parallelogram they span.
A triangle's normal is normalize(cross(b-a, c-a)); winding picks which side is "out". A camera basis is the same trick: forward, then right = normalize(cross(worldUp, forward)), then up = cross(forward, right) — three arrows that are a rotation matrix. In 2D it collapses to a scalar, a.x*b.y - a.y*b.x, whose sign says whether B is left or right of A and whose value is the signed area that decides winding, hence backface direction.
A matrix is where the axes land
Read the columns as arrows: where x̂ lands, where ŷ lands, where ẑ lands, and where the origin lands.
The last column is translation because it is the origin's new home — and that is what a 4×4 buys over a 3×3: rotation, scale and translation in one multiply. Order matters: T*R*S scales, rotates, then moves (read right to left); S*R*T scales the translation too.
Normals transform by the inverse transpose. Non-uniform scale shears angles — squash a sphere and its surface tilts one way while a naively scaled normal tilts the other, so lighting breaks on exactly the stretched objects.
Orientation
Euler angles are three sequential rotations — readable, order-dependent, and at some poses two axes align and a degree of freedom vanishes (gimbal lock: the camera that can't pitch past vertical). Quaternions are the thing you use instead: four numbers that rotate without that failure, converted to a matrix on the way to the GPU. slerp blends two at constant angular speed — rotations interpolated without the wobble.
Demo
A vector playground — drag A and B: the agreement bar in Dot, the parallelogram in Cross, the determinant collapsing to a line in Basis, the same dot as shading in Lighting.
Gotchas
| Symptom | Cause |
|---|---|
| Lighting shifts when an object only translates | A direction was transformed as a point — it picked up the translation column |
| Normals wrong only on squashed or stretched meshes | Non-uniform scale; transform normals by the inverse transpose |
| Surface too dark or blown out after a transform | Normal or light direction no longer unit length — renormalize |
| Object orbits the origin instead of spinning in place | Rotation applied after translation; rotate first, then translate |
| Backfaces culled on the wrong side | Winding flipped — a mirror or negative scale inverts the cross product |
| Camera jams or flips when looking straight up | Gimbal lock, or cross(worldUp, forward) degenerating as forward ≈ worldUp |
| Rotation tween swings the long way round or stutters | Lerping Euler angles or raw quaternion components; slerp, and normalize after |
| Distance checks show up in the profile | A sqrt inside a comparison — compare squared distances |
| Transform looks right alone, wrong when parented | Multiplication doesn't commute; check the order the matrices are concatenated |