Suppose we’re building a 2D pirate-themed combat game. Players can steer their ships through open waters and duel with other players they encounter. We want to implement an attack by firing a cannon from the port side — the ship’s left side when facing forward.
However, our game currently only defines the ship’s forward direction, represented as a 2D vector:
How can we compute a direction vector that points straight out from the ship’s port side?
A key observation is that the ship’s forward direction and the direction the cannon fires should be independent. A shot fired straight out the side of the ship shouldn’t have any forward or backward component. In mathematical terms, these two directions are orthogonal.
You can think of this as the ship having its own local coordinate system: a forward axis and a sideways axis, analogous to the world-space and axes, but rotated to match the ship’s orientation.
If you’ve seen my dot product article, you’ll know that the dot product measures how much one vector points in the direction of another. Since we want to find a vector that is completely independent of the ship’s forward direction , its projection onto must be zero. In other words, the two vectors must satisfy:
One simple way to satisfy this equation is to choose the components of so that the two terms cancel each other out. This can be achieved by swapping the components of and negating one of them, producing a family of solutions of the form:
where is any scalar value.
We can verify this directly:
Setting gives a direction pointing out from the ship’s port side. If we also want to support firing from starboard — the right side of the ship when facing forward — we can simply choose .
Now that we have the sideways firing direction, we can implement the cannon mechanic!
Now we realize our game is better off set in outer space and in 3D, giving our pirate ships more freedom of movement.
Let’s see how this affects our cannon firing mechanism. For a forward direction
we want to find a direction vector such that its dot product with is zero:
Unfortunately, this equation alone doesn’t narrow things down enough. Any vector orthogonal to satisfies this constraint. Geometrically, these vectors form an entire plane perpendicular to . But we don’t want our cannons to fire in an arbitrary direction within that plane. We want a single direction that points directly off the side of the ship, with no upward or downward component.
To narrow this down further, we need an additional constraint. We can get one from the ship’s “up” direction, represented by a vector , which we’ll store alongside its forward vector.
Intuitively, the firing direction off the side of the ship should be perpendicular to both the ship’s forward direction and its up direction. In other words, we’re looking for vectors that are orthogonal to both and .
There is a standard vector operation that produces a vector perpendicular to two given vectors in 3D. This operation is called the cross product. The cross product of the ship’s up direction and forward direction is defined as:
You can verify that this vector satisfies both of our original constraints:
As in the 2D case, this solution isn’t unique for our orthogonality constraints. Multiplying the result by any scalar produces another vector that remains orthogonal to both and :
Choosing gives a direction vector pointing off the ship’s port side, while flips the direction to the starboard side.
The magnitude of a cross product also carries useful meaning. For now, we’re only interested in its direction, but we’ll return to the significance of its length in a later example.
To understand which direction a cross product points visually, we need to know the handedness of the coordinate system we’re working in — that is, how its world-space axes are oriented relative to each other. A coordinate system can be either right-handed or left-handed, depending on that orientation.
This choice is a convention set by the game engine. For example, Godot uses a right-handed world space, while Unity uses a left-handed one.


In a right-handed system, the direction of a cross product follows the right-hand rule. For vectors and , point your right hand in the direction of and curl your fingers toward . Your thumb will then point in the direction of . In a left-handed system, the same visualization applies using the left-hand rule.


It’s worth noting that the algebraic definition of the cross product does not change between right- and left-handed systems. What changes is how that result is interpreted geometrically, based on the orientation of the coordinate system’s world-space axes.
One way to determine a system’s handedness is to check which hand satisfies the relation
where , , and are the engine’s world-space axes.
Another important property of the cross product is that it is anticommutative — swapping the operands negates the result.
We can see this effect directly in the pirate ship example. Knowing we’re in a right-handed world-space coordinate system, use the right-hand rule to determine the direction of
Now repeat the same process for . You should find that the resulting direction points the opposite way:
Before moving on, it’s worth noting that the ship’s forward, side, and up directions — represented by , , and — form what’s known as an orthogonal basis. These vectors are mutually perpendicular and can be treated as the ship’s local axes. Orthogonal bases show up all over game development, from constructing camera view matrices to transforming vectors between local and world space. The cross product is commonly used to compute such a basis.
There is another canonical equation that relates the magnitude of a cross product to its two operands. While it’s not often used explicitly in everyday game code, understanding it can still be very useful. We won’t derive it here, but you can find a derivation online if you’re interested.
Here, is the angle between and . In our space pirate example, the ship’s forward and up directions are perpendicular, so between and . In this case, the expression simplifies to:
If both and are kept at unit length, their cross product is also unit length. Keeping direction vectors normalized like this is a common practice and helps keep the math well-behaved.
This is a special case of the cross product that arises because the two vectors are orthogonal. In more general situations — where the angle between vectors differs from — the term becomes significant, and the angle between the vectors carries real physical meaning. One important example where both the magnitude of the cross product and the angle between its operands matter is computing torque, which we’ll look at next.
In physics, torque describes the tendency of a force to cause rotation. It’s defined as the cross product of a position vector from an object’s center of mass to a point where a force is applied, and the force itself:
Implementing a full torque-based physics system is outside the scope of this article, but the geometric meaning of this definition is easy to see in practice. Let’s look at a simple example. We’ll set up an object that is fixed in place but free to rotate when a force is applied.
When the object is clicked, a force is applied at the point of contact. The force vector points in the direction of the applied force, while the radius vector points from the object’s center of mass (which we’ve computed internally) to that contact point. The resulting torque vector represents the axis of rotation, and its magnitude determines how strongly the object rotates.
There are three ways to increase the torque applied to an object: increase the amount of the applied force, increase the distance from the center of mass at which the force is applied, or make the force and radius vectors as close to perpendicular as possible. Notice how the object barely rotates when the line of action of the force nearly passes through the center of mass.
The cross product is only formally defined in three dimensions (and, in a more abstract sense, in seven dimensions, which isn’t useful for our purposes here). However, the underlying idea of orthogonality applies in any number of dimensions, as we saw earlier in the 2D example.
In practice, when game developers refer to a “2D cross product”, they usually mean taking the cross product of two 2D vectors with an implicit third component of zero. This produces a 3D vector pointing straight out of the screen (or into the screen, depending on the vectors’ orientation and the coordinate system’s handedness). In 2D games, only the third (z) component as a scalar is typically used.

We’ll close with a more advanced example by implementing a ray-triangle intersection test in 3D. This example puts both the cross product and the dot product to work in a concrete, real-world game development scenario. If you’re not already comfortable with the dot product and its geometric meaning, feel free to check out my earlier article on it.
Suppose we want to fire a laser straight off the bow of the ship, in its forward direction. Let’s define a data type that represents the position and direction of the laser when it’s fired. We’ll assume the direction is normalized, which simplifies the math later on.
// Rust
pub struct Ray {
pub source: Vec3,
// Assumed to be unit length
pub direction: Vec3,
}
In most engines, complex geometry is represented as a mesh of triangles. We’ll assume each entity exposes its geometry as an iterator over triangles in world space.
pub struct Entity {
// ... private fields
}
impl Entity {
/// Returns an iterator over all triangles composing this entity
/// in world space.
///
/// Front faces use counterclockwise winding order.
pub fn triangles_iter(&self) -> impl Iterator<Item = (Vec3, Vec3, Vec3)> {
// ...
}
}
The triangle winding is important here, as we’ll see shortly, and typically follows a convention enforced by the game engine.


We can now loop over all triangles in the scene and test each one for intersection with the ray. For each hit, we record the intersection point and the index of the intersected entity, keeping only the one closest to the ray’s source. In a real game, you’ll probably want to reduce the number of triangles that need to be checked using a spatial data structure, but this approach is sufficient for illustrating the math.
pub fn ray_intersection(
entities: &[Entity],
ray: &Ray,
) -> Option<(usize, Vec3)> {
entities.iter()
.enumerate()
.flat_map(|(i, entity)| {
entity.triangles_iter().map(move |points| (i, points))
})
.filter_map(|(i, points)| {
cast_ray(points, ray).map(|q| (i, q))
})
.min_by(|(_, a), (_, b)| {
(a - ray.source)
.len_squared()
.total_cmp(&(b - ray.source).len_squared())
})
}
The core of our implementation lives in the cast_ray function:
pub fn cast_ray(
points: (Vec3, Vec3, Vec3),
ray: &Ray,
) -> Option<Vec3> {
let (v0, v1, v2) = points;
let n = (v1 - v0).cross(v2 - v0).normalize();
let ray_normal_alignment = (-ray.direction).dot(n);
if ray_normal_alignment <= 0.0 {
return None;
}
let signed_plane_distance = (ray.source - v0).dot(n);
let distance_along_ray = signed_plane_distance / ray_normal_alignment;
if distance_along_ray <= 0.0 {
return None;
}
let intersection = ray.source + ray.direction * distance_along_ray;
let is_inside_triangle =
(v1 - v0).cross(intersection - v0).dot(n) > 0.0 &&
(v2 - v1).cross(intersection - v1).dot(n) > 0.0 &&
(v0 - v2).cross(intersection - v2).dot(n) > 0.0;
if is_inside_triangle {
Some(intersection)
} else {
None
}
}
First, we compute the triangle’s normal — the vector perpendicular to its surface — as the cross product of two of its edges. This is where the triangle’s winding order becomes important: reversing the winding negates the normal, which would affect all subsequent calculations. The handedness of the coordinate system also matters. Throughout this example, we assume a right-handed coordinate system, so the cross product follows the right-hand rule. We want a unit-length normal that points outward from the triangle’s front face.
let (v0, v1, v2) = points;
let n = (v1 - v0).cross(v2 - v0).normalize();
Next, we check how the ray is aligned with the triangle’s normal. Since both vectors are unit length, their dot product gives the cosine of the angle between them. If this value is non-positive, the ray is either parallel to the triangle or pointing toward its back face, so we can early out.
let ray_normal_alignment = (-ray.direction).dot(n);
if ray_normal_alignment <= 0.0 {
return None;
}
We can now compute how far along the ray we need to travel to intersect the plane containing the triangle. The point v0
is chosen arbitrarily — any point on the triangle would yield the same result.
let signed_plane_distance = (ray.source - v0).dot(n);
let distance_along_ray = signed_plane_distance / ray_normal_alignment;
if distance_along_ray <= 0.0 {
return None;
}
let intersection = ray.source + ray.direction * distance_along_ray;

signed_plane_distance→
distance_along_rayThe distance_along_ray <= 0.0 check prevents an edge case where an intersection could occur behind the ray’s source.
Finally, we only return the intersection point if it actually lies within the bounds of the triangle. We do this by checking whether the intersection point lies on the “correct” side of each of the triangle’s edges. The triangle’s winding order matters here as well.
let is_inside_triangle =
(v1 - v0).cross(intersection - v0).dot(n) > 0.0 &&
(v2 - v1).cross(intersection - v1).dot(n) > 0.0 &&
(v0 - v2).cross(intersection - v2).dot(n) > 0.0;
if is_inside_triangle {
Some(intersection)
} else {
None
}




With the intersected entity and point of impact in hand, we can now fire our laser cannon!
Key points
- In 3D, the cross product can be used to compute a direction that is orthogonal to two given vectors. This is often the most direct way to derive a “sideways” or perpendicular direction in game code.
- The direction of the resulting vector depends on the handedness of the coordinate system. Knowing whether your engine uses a right- or left-handed convention is essential for interpreting the result correctly.
- The cross product also encodes how orthogonal two vectors are. Its magnitude is proportional to both input lengths and to how close the vectors are to being perpendicular, via the term.
- Orthogonality itself is a more general geometric concept that applies in any number of dimensions. The cross product is a 3D-specific operation that provides one concrete way of working with it.
- The dot product and cross product together form the “bread and butter” of game math. Much of the math used in game development builds on their related ideas of colinearity and orthogonality.
Asset Credits
- NASA/Goddard Space Flight Center Scientific Visualization Studio. Gaia DR2: ESA/Gaia/DPAC. Constellation figures based on those developed for the IAU by Alan MacRobert of Sky and Telescope magazine (Roger Sinnott and Rick Fienberg).
- Poly Haven. “Qwantani Moonrise (Pure Sky)” (CC0). https://polyhaven.com/a/qwantani_moonrise_puresky
— TheHurtDev