Vectors & Dot/Cross Products in Geometric Context

Geometry & Trigonometry

Chapter 4 · Vectors & Dot/Cross Products in Geometric Context

Linear Algebra Fundamentals defined the dot and cross products algebraically. This chapter puts them to work on genuinely geometric problems: decomposing a vector into useful directional components, finding the direction a surface faces, and computing how brightly a light illuminates it — three of the most common building blocks in real graphics and game code.

Vector Projection: Splitting a Vector Into Two Useful Parts

Given a vector a and a direction b, the projection of a onto b is the component of a that points along b: proj_b(a) = (a·b / b·b) · b. Whatever's left over, a − proj_b(a), is the component of a perpendicular to b.

Verified directly — a self-checking decomposition
For a=(5,3) projected onto b=(2,1): proj_b(a) = (5.2, 2.6), and the perpendicular remainder is (−0.2, 0.4). Two independent checks confirm this is correct: proj + perp recovers (5.0, 3.0) — exactly the original vector a — and the dot product of the perpendicular component with b is ≈−4.44×10⁻¹⁶, effectively zero, confirming the two really are perpendicular.

This decomposition is exactly how a game physics engine splits gravity into "the component pulling a character down a slope" (the projection onto the slope's own direction) and "the component pressing into the slope" (the perpendicular remainder) — two physically meaningful quantities recovered from one vector and one direction.

Surface Normals via the Cross Product

Every triangle in a 3D mesh has a direction it "faces" — its normal vector, perpendicular to the triangle's own surface. Given two of the triangle's edges as vectors, the cross product gives exactly that: normal = edge1 × edge2, normalized to unit length.

Verified directly — a real 3D triangle's normal, checked for correctness
For a triangle with vertices A=(0,0,0), B=(2,0,0), C=(0,3,1): the raw cross product of edge1=B−A and edge2=C−A gives (0, −2, 6), normalizing to the unit vector (0, −0.3162, 0.9487), confirmed to have length 0.9999999999999999≈1. Two independent checks confirm it's genuinely perpendicular to the triangle: the dot product of the (unnormalized) normal with both edges comes out to exactly 0.
Why the order of the cross product matters
edge1 × edge2 and edge2 × edge1 point in exactly opposite directions (the cross product anticommutes). Which order a mesh format uses determines whether a triangle's normal faces "outward" or "inward" — get it backward, and every triangle in a model appears to face the wrong way, a real and common source of "inside-out" looking 3D models.

Lighting: Why the Dot Product Needs a Clamp

The simplest realistic lighting model, Lambertian (diffuse) lighting, computes a surface's brightness as max(0, normal · light_direction) — the dot product between the surface normal and the direction toward the light.

Verified directly — light in front vs. light behind the same surface
Using the triangle normal computed above: with a light positioned generally in front of the surface, normal · light_direction ≈ 0.6069 — a sensible, positive brightness. With the exact same light instead positioned behind the surface (the mirror-image direction), the dot product comes out to ≈−0.6069 — a negative brightness, which is physically meaningless (a surface can't emit negative light). The max(0, ...) clamp exists exactly to catch this: it correctly reduces the negative result to 0, meaning "this surface receives no light from this direction at all."
A real, common bug: forgetting the clamp
Skipping max(0, ...) and using the raw dot product directly is a genuinely common graphics bug — a negative brightness value fed straight into a color channel either gets silently clamped somewhere else in the rendering pipeline (masking the bug) or produces visibly wrong, "negative-lit" dark patches on surfaces facing away from every light source. This is exactly the same defensive-clamping instinct as checking a value's valid range before using it — familiar territory from Numerical Methods & Floating-Point Computation's own emphasis on not trusting a raw computed value without checking it makes sense.

Bonus: Which Side of a Line Is a Point On?

The 2D cross product's sign (not magnitude) answers a genuinely useful question directly: for a line from P1 to P2, and a point Q, the sign of cross(P2−P1, Q−P1) tells you which side of the line Q is on.

Verified directly
For the line from (0,0) to (4,0) (the x-axis): a point (2,3) above the line gives a cross-product value of +12; a point (2,−3) below the line gives −12 — same magnitude, opposite sign, exactly tracking which side each point falls on.

Chapter 9 builds this exact sign test into a full point-in-polygon and line-intersection toolkit.

Where This Connects

This chapter's findingWhat it sets up
Vector projection splits a vector into parallel/perpendicular partsThe literal mechanism behind resolving a rotation into axis components in Chapter 5-6
Cross product order determines normal directionThe same ordering sensitivity reappears in Chapter 5's rotation composition, where order changes the result
The 2D cross-product sign test for "which side"Reused directly as the core mechanism behind Chapter 9's line-intersection and point-in-polygon tests

Hands-On Exercises

Exercise 1

Using this chapter's own projection formula, decompose a=(6,2) onto the direction b=(1,3). Compute the parallel and perpendicular components, and verify both that they sum back to a and that the perpendicular component is orthogonal to b.

📄 View solution
Exercise 2

A 3D model appears "inside-out" after being loaded — every surface that should be visible from outside is instead invisible (culled), and vice versa. Using this chapter's own explanation of cross-product order and normals, explain the most likely cause and how you would confirm it.

📄 View solution
Exercise 3

Using this chapter's own verified lighting example, explain why a renderer that skips the max(0, ...) clamp might still "look correct" in many scenes during testing, and describe a specific scene setup where the bug would become clearly visible.

📄 View solution

Chapter 4 Quick Reference

  • Vector projection: proj_b(a) = (a·b/b·b)·b; the perpendicular remainder is a − proj_b(a) — verified self-checking: the two parts sum back to a, and are mutually orthogonal (dot product ≈0)
  • Surface normal: edge1 × edge2, normalized — verified genuinely perpendicular to both triangle edges (dot products exactly 0) and unit length
  • Cross-product order matters: edge1×edge2 ≠ edge2×edge1 — reversing it flips which way a normal (and therefore a whole model's visible surfaces) faces
  • Lambertian lighting: max(0, normal·light_direction) — verified a light positioned behind a surface gives a negative dot product (≈−0.6069), which the clamp correctly reduces to 0
  • The 2D cross product's sign tells you which side of a line a point is on — verified +12 above vs. −12 below the same line
  • Next chapter: 2D rotations and rotation matrices — a deeper pass on Linear Algebra Fundamentals' own brief rotation-matrix introduction