The Geometry of the Dot Product
The Dot Product is a fundamental building block for vector operations in video games and simulations. A solid understanding is crucial for applications involving view-related coordinate transformations and even physical modeling within a game world. For many practical use cases, the dot product offers an elegant alternative to constructing explicit visibility cones or relying on computationally expensive raytracing algorithms.
This article provides an introductory exploration of the theory of the dot product and its geometric interpretation.
An example involving field-of-view-calculations illustrates how the dot product can simplify visibility modeling and decision-making in games.
Additional proofs establish key lemmas that support further applications of the dot product and related operations in both 2D and 3D.
Geometric Interpretation
The dot-product takes two vectors and returns the sum of the products of their corresponding components. Given two vectors
the dot product yields a scalar value.
If , the two vectors are perpendicular to each other. We will derive this in the following.
Orthogonality of Unit Vectors
We will first have a look at a special case, namely when are both unit vectors.
In Figure 1, the radius of the unit circle is represented by the vectors and . Thus, for both and the following trivially holds:

Plot-Code (Python)
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Arc
fig, ax = plt.subplots(figsize=(6, 6))
# Plot setup
ax.set_xlim(-1.1, 1.1)
ax.set_ylim(-1.1, 1.1)
ax.set_xticks([-1, 0, 1])
ax.set_yticks([-1, 0, 1])
ax.set_aspect('equal')
ax.grid(True, linestyle=':', linewidth=0.5)
for spine in ax.spines.values():
spine.set_visible(False)
ax.spines['bottom'].set_position('zero')
ax.spines['left'].set_position('zero')
ax.spines['bottom'].set_visible(True)
ax.spines['left'].set_visible(True)
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
ax.text(1.05, 0.02, r"$x$", fontsize=14, va='bottom')
ax.text(0.02, 1.05, r"$y$", fontsize=14, ha='left')
# Vecors
origin = np.array([0, 0])
theta = np.radians(45)
cos_theta = np.cos(theta)
sin_theta = np.sin(theta)
opposite = np.array([0, np.sin(theta)])
v1 = np.array([np.cos(theta), np.sin(theta)])
v2 = np.array([1, 0])
ax.quiver(*origin, *v1, angles='xy', scale_units='xy', scale=1, color='r')
ax.quiver(*origin, *v2, angles='xy', scale_units='xy', scale=1, color='b')
offset = 0.01
ax.text(v1[0] - 0.5, v1[1] -0.3, r"$\vec{a}$", fontsize=12, color='r')
ax.text(v2[0] - 0.2, v2[1] -0.12, r"$\vec{b}$", fontsize=12, color='b')
# COSINE
ax.plot([0, cos_theta], [-0.02, -0.02], color='black', linestyle='--', linewidth=1)
ax.text(cos_theta / 2 - 0.05, -0.1, r"$\cos(\Theta)$", fontsize=10)
# SINE
start = np.array([cos_theta, 0])
end = start + opposite
ax.plot([start[0], end[0]], [start[1], end[1]], color='black', linewidth=1, linestyle='--')
ax.text(cos_theta + 0.05, sin_theta / 2 - 0.05, r"$\sin(\Theta)$", fontsize=10)
# Unit Circle
circle = plt.Circle((0, 0), 1, color='black', linewidth=0.5, fill=False, transform=ax.transData)
ax.add_patch(circle)
# Angle Arc
angle_deg = np.degrees(np.arccos(np.dot(v1, v2)))
arc = Arc(origin, 0.4, 0.4, angle=0, theta1=0, theta2=angle_deg, edgecolor='green')
ax.add_patch(arc)
ax.text(0.08, 0.02, r"$\Theta$", color='green')
plt.show()
Clearly, .
Additionally can easily be shown since
- the cosine represents the quotient of the adjacent side and the hypotenuse:
- the sine represents the quotient of the opposite side and the hypotenuse:
Solving for and respectively gives us
If , it follows directly that .
Orthogonality of Vectors with arbitrary length
Let's take a look at the common case when and are of arbitrary length and recap the Law of Cosines:
This relationship is shown in Figure 2

Plot-Code (Python)
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
a = np.array([4, 1])
b = np.array([1, 5])
c = b - a
a_len = np.linalg.norm(a)
b_len = np.linalg.norm(b)
dot_prod = np.dot(a, b)
cos_theta = dot_prod / (a_len * b_len)
theta_rad = np.arccos(cos_theta)
theta_deg = np.degrees(theta_rad)
angle_a = np.degrees(np.arctan2(a[1], a[0]))
theta1 = angle_a
theta2 = angle_a + theta_deg
fig, ax = plt.subplots(figsize=(6, 6))
ax.quiver(0, 0, a[0], a[1], angles='xy', scale_units='xy', scale=1, color='red')
ax.text(a[0] / 2, a[1] / 2 - 0.5, r'$\vec{a}$', color='red', fontsize=12, ha='right', va='bottom')
ax.quiver(0, 0, b[0], b[1], angles='xy', scale_units='xy', scale=1, color='blue')
ax.text(b[0] / 2 - 0.5, b[1] / 2, r'$\vec{b}$', color='blue', fontsize=12, ha='left', va='bottom')
ax.quiver(a[0], a[1], c[0], c[1], angles='xy', scale_units='xy', scale=1, color='black')
ax.text(a[0] + c[0] / 2 + 1, a[1] + c[1] / 2, r'$\vec{c} = \vec{b} - \vec{a}$', color='black', fontsize=12, ha='center', va='top')
arc = Arc((0, 0), 1.0, 1.0, angle=0, theta1=theta1, theta2=theta2, edgecolor='green')
ax.add_patch(arc)
ax.text(0.11, 0.11, r"$\Theta$", color='green', fontsize=14)
ax.set_xlim(-1, 5)
ax.set_ylim(-1, 6)
ax.set_aspect('equal')
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.grid(True)
plt.show()
First of all, let's substitute the variables with scalar values of our vectors and :
Note that , so we can substitute this, too. Additionally, :
We will start with simplifying the terms:
Observe that the resulting term on the right side represents the dot product (set in parentheses for clarity).
Finally, solve for :
Clearly, this holds for any vector except for .
We can now solve for the dot product:
The relation between the orthogonality of and and becomes more apparent when we consider
Since by definition, it follows that . We obtain the equivalence:
The dot product beautifully shows that is invariant under changes in the magnitude of the related vectors: Using the associativity of the dot product and scalar multiplication, we can derive:
Hence, the cosine of the angle between and
is simply the dot product of the corresponding unit vectors.
Referring back to the introductory example involving the unit circle, observe that . In this case, the dot product conveniently simplifies:
Using the usual mathematical notation for unit vectors, this can be written as
Application: Visibility Modeling
When a game needs to indicate an NPC's Field Of View and its visible range to the player, visibility cones are often used.
A prominent example is Commandos: Behind Enemy Lines1. Figure 3 shows a screenshot of its recent sequel, Commandos: Origins, where a visibility cone is rendered as a green wedge.

Without considering raytracing for collision detection2, the problem of determining whether a given object lies within the visibility cone (and is therefore detected by the game AI) can be simplified to a calculation based solely on the dot product.
In the following example (see Figure 4), the NPC has the following parameters:
- position
- view direction $v = (2, -3)
- field of view
- maximum view distance
There are two additional characters and , where is clearly outside the cone, and is directly within (similar to the scene shown in Figure 3).

Plot-Code (Python)
import numpy as np
import math
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
from matplotlib.ticker import MultipleLocator
from matplotlib.patches import Wedge
# plot layout
fig, ax = plt.subplots(figsize=(6, 6))
ax.set_xlim(-1, 8)
ax.set_ylim(-1, 8)
ax.set_aspect('equal')
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.grid(True)
ax.axhline(0, color='black', linewidth=1.5)
ax.axvline(0, color='black', linewidth=1.5)
ax.xaxis.set_major_locator(MultipleLocator(1))
ax.yaxis.set_major_locator(MultipleLocator(1))
# vectors
fov=80 #fov in degree
v_x = 2
v_y = -3
x = 3
y = 7
v_l = math.sqrt(v_x**2 + v_y**2)
npc = np.array([v_x, v_y])
ax.quiver(x, y, npc[0], npc[1], angles='xy', scale_units='xy', scale=1, color='red')
cos_npc = v_x/v_l
sin_npc = v_y/v_l
theta = np.degrees(np.arccos(cos_npc)) * (-1 if sin_npc < 0 else 1) # * 180 / math.pi
view_left = theta - (fov/2)
view_right = theta + (fov/2)
v1 = np.array([v_l * np.cos(np.radians(view_left)), v_l * np.sin(np.radians(view_left))])
v2 = np.array([v_l * np.cos(np.radians(view_right)), v_l * np.sin(np.radians(view_right))])
ax.quiver(x, y, v1[0], v1[1], angles='xy', scale_units='xy', scale=1, color='green')
ax.quiver(x, y, v2[0], v2[1], angles='xy', scale_units='xy', scale=1, color='green')
wedge = Wedge(
center=(3, 7),
r=v_l,
theta1=view_left,
theta2=view_right,
facecolor='green',
edgecolor='darkgreen',
alpha=0.2)
ax.add_patch(wedge)
# A, B
circle_green = plt.Circle((5, 5), 0.2, color='red', fill=True)
circle_red = plt.Circle((2, 4), 0.2, color='green', fill=True)
ax.add_patch(circle_green)
ax.add_patch(circle_red)
# texts
ax.text(2.5, 7 , 'p', color='black', fontsize=12)
ax.text(1.5, 4 , 'A', color='black', fontsize=12)
ax.text(5.2, 5 , 'B', color='black', fontsize=12)
plt.show()
We can now construct two vectors that point from to and :
Once we obtain , , , we can utilize the dot product and calculate the cosine of and , i.e. the angles between and and and (see Figure 5).
We solve for ;
We know that , so .
Since 3, we conclude that is within the angular limits defined by the FOV. However, since the length of must also be considered (i.e., the maximum view distance as shown in Figure 4), we have to verify whether . In this case, the condition holds, and thus is confirmed to be within the visibility cone.

Plot-Code (Python)
import numpy as np
import math
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
from matplotlib.ticker import MultipleLocator
from matplotlib.patches import Wedge
# plot layout
fig, ax = plt.subplots(figsize=(6, 6))
ax.set_xlim(-1, 8)
ax.set_ylim(-1, 8)
ax.set_aspect('equal')
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.grid(True)
ax.axhline(0, color='black', linewidth=1.5)
ax.axvline(0, color='black', linewidth=1.5)
ax.xaxis.set_major_locator(MultipleLocator(1))
ax.yaxis.set_major_locator(MultipleLocator(1))
# vectors
fov=80 #fov in degree
v_x = 2
v_y = -3
x = 3
y = 7
v_l = math.sqrt(v_x**2 + v_y**2)
npc = np.array([v_x, v_y])
ax.quiver(x, y, npc[0], npc[1], angles='xy', scale_units='xy', scale=1, color='red')
cos_npc = v_x/v_l
sin_npc = v_y/v_l
theta = np.degrees(np.arccos(cos_npc)) * (-1 if sin_npc < 0 else 1) # * 180 / math.pi
view_left = theta - (fov/2)
view_right = theta + (fov/2)
v1 = np.array([v_l * np.cos(np.radians(view_left)), v_l * np.sin(np.radians(view_left))])
v2 = np.array([v_l * np.cos(np.radians(view_right)), v_l * np.sin(np.radians(view_right))])
ax.quiver(x, y, v1[0], v1[1], angles='xy', scale_units='xy', scale=1, color='green')
ax.quiver(x, y, v2[0], v2[1], angles='xy', scale_units='xy', scale=1, color='green')
wedge = Wedge(
center=(3, 7),
r=v_l,
theta1=view_left,
theta2=view_right,
facecolor='green',
edgecolor='darkgreen',
alpha=0.2)
ax.add_patch(wedge)
# A, B
circle_green = plt.Circle((5, 5), 0.2, color='red', fill=True)
circle_red = plt.Circle((2, 4), 0.2, color='green', fill=True)
ax.add_patch(circle_green)
ax.add_patch(circle_red)
A = np.array([-1, -3])
ax.quiver(x, y, A[0], A[1], angles='xy', scale_units='xy', scale=1, color='black')
B = np.array([2, -2])
ax.quiver(x, y, B[0], B[1], angles='xy', scale_units='xy', scale=1, color='black')
cos_alpha = np.dot(A, npc) / (np.linalg.norm(A) * np.linalg.norm(npc))
alpha = np.arccos(cos_alpha)
alpha_deg = np.degrees(alpha)
arc_radius_alpha = 3.2
arc_alpha = Arc((3, 7), arc_radius_alpha, arc_radius_alpha,
angle=np.degrees(np.arctan2(npc[1], npc[0])),
theta1=-alpha_deg,
theta2=0,
edgecolor='black')
ax.add_patch(arc_alpha)
cos_beta = np.dot(B, npc) / (np.linalg.norm(B) * np.linalg.norm(npc))
beta = np.arccos(cos_beta)
beta_deg = np.degrees(beta)
arc_radius_beta = 4.2
arc_beta = Arc((3, 7), arc_radius_beta, arc_radius_beta,
angle=np.degrees(np.arctan2(npc[1], npc[0])),
theta1=0,
theta2=beta_deg,
edgecolor='black')
ax.add_patch(arc_beta)
# texts
ax.text(2.5, 7 , 'p', color='black', fontsize=12)
ax.text(4, 4.2 , r'$\vec{v}$', color='black', fontsize=12)
ax.text(1.8, 4.8 , r'$\vec{A}$', color='black', fontsize=12)
ax.text(4.3, 5.8 , r'$\vec{B}$', color='black', fontsize=12)
ax.text(1.5, 4 , 'A', color='black', fontsize=12)
ax.text(5.2, 5 , 'B', color='black', fontsize=12)
ax.text(3, 6, rf'$\alpha$', color='black', fontsize=12)
ax.text(4, 5.6, rf'$\beta$', color='black', fontsize=10)
ax.text(0.5, 6.5, rf'$\alpha = {alpha_deg:.2f}^\circ$', color='black', fontsize=12)
ax.text(0.5, 6, rf'$\beta = {beta_deg:.2f}^\circ$', color='black', fontsize=12)
plt.show()
Excursus: Constructing a second vector at a specific angle to an existing vector
In the previous example, the view direction of the NPC was given by the vector . By using the vector notation, it was also possible to specify the length of the visibility cone, i.e. the maximum range the NPC could see for detecting objects. By applying the dot product to the target's vector , we have seen that there is no need for calculating the visibility cone itself - but what if we would like to do so?
One way to obtain vectors at a given angle to a known vector is to take advantage of - the unit vector - in this case, the unit vector of :
In Figure 1, we have shown the relationship between sine and cosine as the ratios of the opposite and adjacent sides to the hypotenuse, which represents the radius of the unit circle and is therefore always equal to .
We can use this fact to our advantage by computing a second vector that has the angle to . All we need are the equations
-
We start with : Since we are using the unit vector , we can directly treat as the -component of , and as the -component of :
Since the FOV in the given example is , we divide by to obtain one half of the visibility cone and compute the corresponding direction as follows:
-
Compute analogously:
The values represent the and components of the desired vector , i.e. the ratio between adjacent side and hypotenuse (-direction) and opposite side and hypotenuse (-direction), whereas the hypotenuse is represented by . Since we have operated on a unit vector, we obtain a unit vector. Multiplying with any scalar will change the vector's length:

Plot-Code (Python)
import numpy as np
import math
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
from matplotlib.ticker import MultipleLocator
# plot layout
fig, ax = plt.subplots(figsize=(6, 6))
ax.set_xlim(-1, 4)
ax.set_ylim(-4, 1)
ax.set_aspect('equal')
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.grid(True)
ax.axhline(0, color='black', linewidth=1.5)
ax.axvline(0, color='black', linewidth=1.5)
ax.xaxis.set_major_locator(MultipleLocator(1))
ax.yaxis.set_major_locator(MultipleLocator(1))
# vectors
fov = 80 # field of view in degrees
v_x, v_y = 2, -3
v_l = math.sqrt(v_x**2 + v_y**2)
# base vectors
v = np.array([v_x, v_y])
v_u = v / v_l # unit vector of v
# rotated vector p (by +40°)
angle_offset_deg = 40
theta_v = math.atan2(v_y, v_x)
theta_p = theta_v + math.radians(angle_offset_deg)
p_u = np.array([math.cos(theta_p), math.sin(theta_p)])
p = p_u * v_l
# plot vectors
ax.quiver(0, 0, v[0], v[1], angles='xy', scale_units='xy', scale=1, color='red')
ax.text(v[0]/2 + 0.2, v[1]/2, r'$\vec{v}$', color='red', fontsize=12)
ax.quiver(0, 0, p[0], p[1], angles='xy', scale_units='xy', scale=1, color='green')
ax.text(p[0]/2 + 0.2, p[1]/2 +0.16, r'$\vec{p} = \hat{p} \cdot |\vec{v}|$', color='green', fontsize=12)
# unit vector directions
ax.quiver(0, 0, p_u[0], p_u[1], angles='xy', scale_units='xy', scale=1, color='orange')
ax.text(p_u[0]/2 + 0.4, p_u[1] + 0.12 , r'$\hat{p}$', color='orange', fontsize=12)
ax.quiver(0, 0, v_u[0], v_u[1], angles='xy', scale_units='xy', scale=1, color='blue')
ax.text(v_u[0]/2 - 0.2, v_u[1] , r'$\hat{v}$', color='blue', fontsize=12)
arc_radius = 1.2
arc = Arc((0, 0), arc_radius, arc_radius,
angle=0,
theta1=np.degrees(theta_v),
theta2=np.degrees(theta_p),
edgecolor='orange')
ax.add_patch(arc)
label_angle = theta_v + (theta_p - theta_v) / 2
label_x = arc_radius * math.cos(label_angle)
label_y = arc_radius * math.sin(label_angle)
arc_radius = 1.2
arc = Arc((0, 0), arc_radius * 2, arc_radius * 2,
angle=0,
theta1=-56,
theta2=0,
edgecolor='red')
ax.add_patch(arc)
ax.text(label_x - 0.3, label_y - 0.1, r'$\theta$', fontsize=14, color='red')
ax.text(label_x - 0.7, label_y + 0.4, r'$\alpha$', fontsize=14, color='orange')
plt.show()
For the other half of the visibility cone, we simply have to plug into the equation.
Rotations of points around a specific axis are performed with the help of rotation matrices. In our 2D-example, the matrix for rotation around the origin is:
By multiplying with 4, we obtain a new vector rotated ccw (counterclockwise) around the origin:
When applying the cosine/sine addition identity, we first constructed the unit vector , rotated it by , and obtained . We then scaled by : This step effectively cancelled out the denominator in the - and -components of . It is therefore easy to see that the following holds (in general):
Proofs
Uniqueness of Orthogonal Unit Vectors in 2D
Let , , , .
Claim: There exists no such that , and
Disproof by counterexample:
Choose
, , .
Clearly, and . Moreover,
and
This contradicts the assumption that no such vector exists with