Appearance
The Dot Product — Measuring Alignment
The dot product takes two vectors and hands you back a single number. That sounds modest, but that one number tells you the angle between the vectors, whether they are perpendicular, how much one "casts a shadow" onto the other, and how directly a surface faces a light source. By the end of this chapter you will be able to implement a realistic lighting model, write an enemy field-of-view check, and understand the math underlying both. All of it comes down to multiplying, summing, and knowing what that sum means.
How Does a Game Know Light Is Shining on a Surface?
Picture a cave in an action game. A torch hangs on the wall and casts warm light on a stone floor. Tiles directly below the torch are bright. Tiles on the ground at a shallow angle are dim. Tiles on the ceiling behind the torch are dark. The game does not trace thousands of photon paths in real time — it does one tiny arithmetic operation per surface: it measures how closely the surface's facing direction aligns with the direction toward the light.
If the surface faces directly toward the light, it catches every photon that arrives. If it faces sideways, it catches fewer. If it faces away, it catches none. "How much do two directions agree?" — that is exactly the question the dot product answers.
The same question appears everywhere: Is the enemy in the player's field of view? Is a character's face toward the camera? Is a moving object approaching a wall, or moving along it? Every time you need to measure the angle of agreement between two directions, the dot product is your tool.
The Algebraic Formula
The dot product (also called the scalar product or inner product) of two vectors and is the sum of the products of their corresponding components.[^1]
In two dimensions:
In three dimensions:
The dot between the two vectors is the standard notation. The result is a plain number — a scalar — not a vector.
Worked example. Let and :
Three-dimensional example: and :
In code, this is a handful of multiplications and additions:
ts
function dot(a: number[], b: number[]): number {
return a.reduce((sum, ai, i) => sum + ai * b[i], 0);
}
dot([3, 1], [2, 4]); // 10
dot([1, -2, 3], [4, 0, -1]); // 1That is the full computation. The interesting part is what the number means.
INFO
The dot product is commutative: . Swapping the two vectors doesn't change the result, because multiplication is commutative and addition is commutative. This makes the dot product symmetric in a way that will come in handy when writing checks — it doesn't matter which vector you call "first."
The Geometric Meaning — Cosine and Angles
The algebraic formula is easy to compute, but the geometric interpretation is what makes the dot product powerful. It turns out the dot product is related to the cosine of the angle between the two vectors:[^1][^2]
where is the angle between and (measured between 0° and 180°). Solving for :
This is the formula for the angle between any two nonzero vectors.[^2]
Worked example. Find the angle between and .
The vector points northeast at a 45° angle from the east-pointing . The math agrees with geometry.
Reading the Sign
Cosine is positive for angles less than 90°, zero at exactly 90°, and negative for angles between 90° and 180°. The dot product inherits that behavior:
Same direction Perpendicular Opposite directions
(θ = 0°) (θ = 90°) (θ = 180°)
a ------> a ------> a ------>
b ------> | <------ b
| b
v
a · b > 0 a · b = 0 a · b < 0
(agree) (neither agree (disagree)
nor disagree)The sign alone tells you whether two vectors are broadly pointing the same way, perpendicular, or opposing. Many game checks only need the sign — no arccos required.
Unit Vectors Make It Simple
When both vectors are unit vectors (), the formula simplifies beautifully:
The dot product of two unit vectors is the cosine of the angle between them, with no division needed. This is why normalizing vectors before dotting them is such a common pattern — the result is directly interpretable as a cosine value in the range .
TIP
In game code, keep your direction vectors normalized. A dot product between two unit vectors gives a cosine in with no division. A dot product between un-normalized vectors gives a number whose scale depends on both magnitudes — useful when you need it, but easy to misinterpret.
Perpendicularity — When the Dot Product Is Zero
If and neither vector is zero, then , which means . The vectors are perpendicular (also called orthogonal).[^2]
This is not just a special case to note and move on — it is one of the most useful facts in applied linear algebra. Every time a game or ML system needs to know whether two directions are independent, it checks whether their dot product is zero.
Examples:
- and : dot product . The x-axis and y-axis are perpendicular. ✓
- A surface normal and any vector lying in the surface: their dot product is always zero, which is how graphics engines verify that a normal is correct.
- Two walls meeting at a right angle: their outward-facing normals have a dot product of zero.
WARNING
The converse only holds for nonzero vectors. The zero vector dotted with anything gives 0, but the zero vector has no direction, so the result is meaningless. Always confirm that neither vector is zero before using a zero dot product to conclude perpendicularity.
Scalar Projection — How Much of One Vector Lands on Another?
Return to the torch-on-the-wall scenario. The question "how much light does this surface catch?" is really asking: "how much of the light-direction vector points toward the surface?"
That is the idea of scalar projection: given two vectors, how much of one vector runs along the direction of the other? Geometrically, imagine the sun shining straight down. The shadow of vector cast onto vector is the scalar projection of onto .[^3]
^ a
| \
| \ (shadow of a onto b)
| \
---------+-----*----------> b
|<-- comp_b(a) -->|The signed length of that shadow is:[^3]
When is already a unit vector, the denominator is 1 and the scalar projection is just the dot product itself:
The vector projection takes the scalar projection and turns it back into a vector pointing along :[^3]
Or equivalently, when is a unit vector:
The sign matters: a positive projection means has a component in the same direction as ; a negative projection means it has a component pointing opposite.
Why this formula works
From the geometric definition, . The scalar projection is — the adjacent side of the right triangle formed by and its shadow. Factor out :
To convert the scalar back to a vector, multiply by the unit vector in the direction of : that is . Combined:
Application 1 — Lighting a Surface (Lambertian Shading)
Now the torch question has a rigorous answer.
In 3D graphics, every surface has a normal vector — a unit vector pointing perpendicularly away from the surface. When a light source illuminates that surface, the light travels in a direction (a unit vector pointing toward the light). The fraction of light the surface receives is governed by Lambert's cosine law:[^4]
Because both vectors are unit vectors, the dot product equals — the cosine of the angle between the surface normal and the light direction.
- : the light hits the surface head-on. . Full brightness.
- : the light hits at an angle. . Half brightness.
- : the light grazes the surface. . No light.
- : the light comes from behind. Dot product is negative, clamped to . Dark.
The clamp is essential: without it, surfaces facing away from the light would receive negative illumination, which is physically meaningless and would subtract light rather than add it.[^5]
Worked example. A floor tile has normal (pointing straight up). A torch is positioned such that the direction from the tile to the torch is (normalized). What is the diffuse factor?
The tile receives 80% of maximum light intensity. Apply the surface color:
In a GLSL fragment shader, the calculation looks exactly like this:[^5]
glsl
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;Four lines. The dot product is the entire mathematical core — everything else is normalization and clamping.
INFO
Why "Lambertian"? The law is named after Johann Heinrich Lambert, an 18th-century Swiss mathematician who described it in his 1760 work Photometria. His insight was that a matte surface reflects light equally in all directions (no gloss), with brightness proportional only to the cosine of the incoming angle. Modern real-time rendering still uses his law as the standard diffuse model.
Application 2 — Field-of-View and Line-of-Sight Checks
A guard NPC stands in a corridor. The player sneaks up. Should the guard spot the player? That depends on whether the player is inside the guard's field of view — and the dot product tells you instantly.
Here is the setup. The guard has a facing direction (a unit vector pointing forward). The player is at some position, and the vector from the guard to the player is (normalized). The guard's field of view has a half-angle of (half of the total cone, measured from center to edge).
player (in FOV)
*
/
----<phi>------/---------> facing direction (f)
/
/
* player (outside FOV)The player is inside the field of view when the angle between and is less than . Because cosine is a decreasing function in , "angle less than " is equivalent to "cosine greater than ":
No arccos needed — precompute cos(phi) once and compare directly.[^6] This makes the check extremely cheap: two dot products (one to get , one to test the angle) and one comparison.
Worked example. The guard faces east: . The field of view is 90° total, so and .
Guard is at , player is at .
Step 1 — displacement vector:
Step 2 — normalize:
Step 3 — dot product with facing direction:
Step 4 — compare to threshold:
The player is right at the edge of the field of view. Change the player's position one step north (more to the side) and the dot product drops below the threshold — the guard sees nothing.
In TypeScript:
ts
function isInFOV(
guardPos: [number, number],
guardFacing: [number, number], // unit vector
playerPos: [number, number],
halfFovAngleDeg: number
): boolean {
const dx = playerPos[0] - guardPos[0];
const dy = playerPos[1] - guardPos[1];
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) return false;
const dirX = dx / dist;
const dirY = dy / dist;
const dotProduct = guardFacing[0] * dirX + guardFacing[1] * dirY;
const threshold = Math.cos((halfFovAngleDeg * Math.PI) / 180);
return dotProduct >= threshold;
}Add a maximum detection range check (dist <= maxRange) before the dot product test and you have a complete, performant line-of-sight cone.[^6]
TIP
The same pattern appears in many other game systems:
- Camera frustum culling — is an object inside the view cone?
- Headlight cone — is an obstacle ahead of a vehicle?
- Spotlight attenuation — how much does the light fade toward the cone edge?
- Stealth systems — is the player in shadow and in front of the enemy?
In every case, the check is: compute and compare to .
Chapter Recap
| Concept | Formula | Meaning |
|---|---|---|
| Dot product (2D/3D) | Sum of component products | |
| Geometric identity | Ties dot product to angle | |
| Angle between vectors | Derived from geometric identity | |
| Unit-vector dot product | Direct cosine, no division | |
| Perpendicularity | Vectors are at 90° | |
| Scalar projection | Signed shadow length | |
| Lambertian shading | Diffuse light factor | |
| FOV check | Is target inside cone? |
Key ideas to carry forward:
- The dot product measures how much two vectors agree in direction. Positive means they broadly point the same way; zero means perpendicular; negative means they oppose each other.
- When both vectors are unit vectors, the dot product equals the cosine of the angle between them — no extra computation required.
- Scalar projection answers "how much of lies along ?" — it is the signed length of the shadow.
- In practice, avoid computing
arccosunless you actually need the angle in degrees. For comparisons, work directly with cosine values.
The next chapter introduces matrices — tables of numbers that encode entire transformations of space. The dot product will reappear immediately: matrix-vector multiplication is nothing more than a sequence of dot products applied row by row.
References
[^1]: "Dot product." Wikipedia. https://en.wikipedia.org/wiki/Dot_product
[^2]: "The Dot Product." A First Course in Linear Algebra (Kuttler), LibreTexts, §4.7. https://math.libretexts.org/Bookshelves/Linear_Algebra/A_First_Course_in_Linear_Algebra_(Kuttler)/04:_R/4.07:_The_Dot_Product
[^3]: "Dot Product." Paul's Online Math Notes: Calculus II, Lamar University. https://tutorial.math.lamar.edu/classes/calcii/dotproduct.aspx
[^4]: "Diffuse and Lambertian Shading." Scratchapixel. https://www.scratchapixel.com/lessons/3d-basic-rendering/introduction-to-shading/diffuse-lambertian-shading.html
[^5]: "Basic Lighting." LearnOpenGL. https://learnopengl.com/Lighting/Basic-Lighting
[^6]: "Dot Product-based Object Detection in Field of View: A Step-by-Step Tutorial." Freakout Studios. https://freakoutstudios.com/dot-product-fov.html