How Matter.Axes.fromVertices Works for SAT in Matter.js
This article explains the mathematical purpose of
Matter.Axes.fromVertices in Matter.js and its role in
collision detection via the Separating Axis Theorem (SAT). It covers how
the function transforms polygon boundary coordinates into normalized
perpendicular edge normals, filters out redundant parallel directions,
and provides the minimal set of one-dimensional projection axes required
to test whether two convex bodies intersect.
The Separating Axis Theorem (SAT) Requirement
The Separating Axis Theorem states that two convex polygons do not intersect if there exists a line—called a separating axis—along which the 1D projections of the two shapes do not overlap.
For two-dimensional convex polygons, the only candidate axes that
must be tested are the lines perpendicular to each edge of the polygons.
If any edge normal yields non-overlapping projections, the polygons are
fully separated. The purpose of Matter.Axes.fromVertices is
to construct these candidate axes directly from an ordered sequence of
polygonal vertices.
Edge Vector Calculation
A 2D polygon in Matter.js is defined by an array of ordered coordinate vertices:
\[V = [v_0, v_1, v_2, \dots, v_{n-1}]\]
where each vertex \(v_i = (x_i, y_i)\).
To evaluate each boundary segment,
Matter.Axes.fromVertices computes the directional vector
\(\vec{e}_i\) for every consecutive
pair of vertices:
\[\vec{e}_i = v_{i+1} - v_i = (x_{i+1} - x_i, \; y_{i+1} - y_i)\]
For the final edge connecting \(v_{n-1}\) back to \(v_0\), the index wraps around cyclically:
\[\vec{e}_{n-1} = v_0 - v_{n-1}\]
Orthogonal Normal Derivation
SAT requires axes that are perpendicular (orthogonal) to each boundary segment \(\vec{e}_i = (dx, dy)\). Two vectors are orthogonal if their dot product equals zero.
Matter.Axes.fromVertices derives the orthogonal vector
\(\vec{n}_i\) by swapping components
and negating one:
\[\vec{n}_i = (-dy, \; dx)\]
This satisfies the orthogonality condition:
\[\vec{e}_i \cdot \vec{n}_i = (dx)(-dy) + (dy)(dx) = 0\]
Geometrically, \(\vec{n}_i\) represents an outward (or inward) facing normal vector perpendicular to the edge \(\vec{e}_i\).
Vector Normalization
Projection along an arbitrary axis \(\vec{u}\) requires converting vertices into scalar values via the dot product:
\[s = v \cdot \vec{u}\]
If \(\vec{u}\) is not a unit vector, the resulting scalar values scale proportionally with the length of the edge, distorting minimum translation distance (penetration depth) calculations.
To ensure consistent geometric projections,
Matter.Axes.fromVertices normalizes each perpendicular
vector to unit length (\(\|\hat{n}\| =
1\)):
\[\|\vec{n}_i\| = \sqrt{(-dy)^2 + (dx)^2} = \sqrt{dx^2 + dy^2}\]
\[\hat{n}_i = \frac{\vec{n}_i}{\|\vec{n}_i\|} = \left(\frac{-dy}{\sqrt{dx^2 + dy^2}}, \; \frac{dx}{\sqrt{dx^2 + dy^2}}\right)\]
Redundancy Elimination
Testing opposite parallel edges is computationally redundant. For instance, a rectangle has four edges, but opposite edges produce normals that point in exactly opposite directions (\(\hat{n}\) and \(-\hat{n}\)). Projecting a body onto \(-\hat{n}\) yields the same overlap interval as projecting onto \(\hat{n}\), mirrored across the origin.
Matter.Axes.fromVertices compares newly generated unit
normals against previously stored ones. If a new normal vector is
parallel or anti-parallel to an existing axis:
\[|\hat{n}_a \cdot \hat{n}_b| \approx 1\]
the redundant axis is discarded. This reduces the number of projection axes:
- A rectangle is reduced from 4 candidate axes to 2.
- A regular hexagon is reduced from 6 candidate axes to 3.
Summary of the Output
The output of Matter.Axes.fromVertices is an array of
unique, normalized 2D vectors:
\[A = [\hat{n}_0, \hat{n}_1, \dots, \hat{n}_{k-1}]\]
where \(k \le n\). This minimal set of unit axes is cached on the rigid body, allowing the Matter.js collision engine to execute the SAT overlap tests with the fewest scalar projections possible during every physics frame.