Minkowski Sum of Convex Polygons — Junior Level¶
One-line summary: The Minkowski sum of two shapes
AandBis the set of all pairwise sumsa + b— geometrically, sweep one shape over the other and keep every point either of them ever touches. For two convex polygons it is again a convex polygon, and you can build it inO(n + m)by taking the edge vectors of both polygons and chaining them in order of angle.
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Geometry Primitives
- Core Concepts
- Big-O Summary
- Real-World Analogies
- Pros & Cons
- Step-by-Step Walkthrough
- Code Examples
- Coding Patterns
- Error Handling
- Performance Tips
- Best Practices
- Edge Cases & Pitfalls
- Common Mistakes
- Cheat Sheet
- Visual Animation
- Summary
- Further Reading
Introduction¶
Imagine you have a small coin and a flat metal plate with some shape cut out of it. Now glue the center of the coin to a pencil and drag the pencil along the entire plate — every point inside and on its border. The region swept out by the whole coin (not just its center) is bigger than the plate: it bulges outward by the radius of the coin in every direction. That swept region is the Minkowski sum of the plate and the coin.
Formally, given two sets of points A and B in the plane, their Minkowski sum is
where a + b is ordinary vector addition: (a.x + b.x, a.y + b.y). You take every point of A, add to it every point of B, and collect all the results. The symbol ⊕ (a plus inside a circle) is the standard notation, sometimes written just +.
That definition looks like it would produce an enormous, messy cloud of points. The beautiful fact — and the reason this topic exists — is:
If
AandBare both convex polygons, thenA ⊕ Bis again a convex polygon, and its boundary is built simply by merging the edge vectors ofAandBin angular order.
So instead of forming all n × m pairwise sums and taking a convex hull (slow), you walk around the two polygons once each and interleave their edges by angle, giving an O(n + m) algorithm. This is the workhorse you will learn here.
Why care? The Minkowski sum is the mathematical engine behind:
- Robot motion planning — "inflate" obstacles by the shape of the robot so the robot shrinks to a single point that must avoid the inflated obstacles (the configuration space trick).
- Collision detection — the GJK algorithm tests whether the Minkowski difference of two shapes contains the origin.
- CAD / CAM offsetting — growing or shrinking a part by a tool radius.
- Games — fattening level geometry so a character of nonzero size can be treated as a point.
A mental anchor for the whole topic:
Minkowski sum = "place a copy of
Bat every point ofA" (or vice versa) and take the union. For convex polygons that union has a boundary made ofA's edges andB's edges, sorted by direction.
Prerequisites¶
Before reading this file you should be comfortable with:
- Points and vectors — a point is a pair
(x, y); a vector is the difference of two points. Adding vectors adds components. - Convex polygons — a polygon with no inward dents; given as a list of vertices in counter-clockwise (CCW) order. (See sibling topic 01-convex-hull.)
- The cross product / orientation test —
cross(O, A, B)tells you whetherO→A→Bturns left, right, or goes straight. We use it to find a starting vertex and to sort by angle. (See 01-convex-hull, "Geometry Primitives".) - Sorting and merging — the construction is essentially the merge step of merge sort applied to two angle-sorted edge lists.
- Big-O notation —
O(n),O(n + m),O((n + m) log(n + m)),O(n²m²).
Optional but helpful:
- A feel for polar angle (the direction a vector points, measured as an angle from the positive x-axis).
- The sibling topic 06-rotating-calipers, which also walks two convex chains in lockstep.
Glossary¶
| Term | Meaning |
|---|---|
Minkowski sum A ⊕ B | The set { a + b : a ∈ A, b ∈ B }. |
Minkowski difference A ⊖ B | Erosion: { c : c + B ⊆ A }. Closely related but not the same as A ⊕ (−B). |
Reflection −B | B flipped through the origin: { −b : b ∈ B }. Used by GJK. |
| Edge vector | For a polygon edge from vertex Pᵢ to Pᵢ₊₁, the vector Pᵢ₊₁ − Pᵢ. |
| Polar angle | The direction a vector points, an angle in [0, 2π) from the +x axis. |
| CCW order | Vertices listed counter-clockwise; the standard convention here. |
| Bottom-most vertex | The starting vertex: lowest y, breaking ties by lowest x. Its outgoing edge has the smallest polar angle. |
| Convex | No inward dents; every interior angle ≤ 180°. |
| Configuration space (C-space) | The space of all robot positions; obstacles are inflated by the robot shape so the robot becomes a point. |
| Offsetting | Growing (positive offset) or shrinking (negative offset) a shape — a Minkowski sum/difference with a disk. |
| GJK | Gilbert–Johnson–Keerthi: a collision test that asks whether the Minkowski difference contains the origin. |
Geometry Primitives¶
Everything here rests on two tiny operations on 2-D points. Get these right once and reuse them.
A point/vector is just (x, y). The two primitives:
add(a, b) = (a.x + b.x, a.y + b.y) # vector addition — the heart of the sum
sub(a, b) = (a.x - b.x, a.y - b.y) # vector from b to a (an edge vector)
cross(a, b) = a.x*b.y - a.y*b.x # signed area of the parallelogram a,b
The cross product cross(u, v) is the single most important sign in 2-D geometry:
cross(u, v) > 0→vis counter-clockwise fromu(a left turn).cross(u, v) < 0→vis clockwise fromu(a right turn).cross(u, v) = 0→uandvare parallel (collinear directions).
We use the cross product to compare the angles of two edge vectors without ever computing an actual angle (atan2) — comparing by cross product is exact for integer coordinates and avoids floating-point trig. When we say "the edge with the smaller polar angle," we decide it with a cross-product comparison.
# "Does edge u come before edge v in CCW angular order (starting from +x)?"
# half(w) = 0 if w points into the upper half-plane (or +x), else 1
# compare halves first, then break ties with cross product
For convex polygons given in CCW order and started at the bottom-most vertex, the edge vectors already come out in increasing polar angle as you walk the boundary. That single fact is what makes the merge so cheap — no sorting needed if the inputs are well-formed.
Core Concepts¶
1. The sum is "B stamped at every point of A"¶
The cleanest way to picture A ⊕ B: take shape B, and stamp a translated copy of it centered at every point a of A. The union of all those stamps is A ⊕ B. Equivalently, slide B around so its reference point traces all of A; the total region covered is the sum. Because the operation is symmetric (A ⊕ B = B ⊕ A), you may stamp A over B instead — same result.
2. For convex polygons, only the edges matter¶
Here is the key structural insight. Walk around convex polygon A and list its edge vectors a₀, a₁, … in CCW order; do the same for B to get b₀, b₁, …. The boundary of A ⊕ B is made of exactly these edges, all of them, reordered by angle. No new edge directions appear, and none disappear. The sum's boundary is the two edge lists merged by polar angle, chained tip-to-tail.
Intuition: an edge of A ⊕ B points in some direction d. The vertex of the sum "furthest in direction d" is (vertex of A furthest in d) + (vertex of B furthest in d). As d rotates, the supporting vertex of A changes exactly when you cross one of A's edges, and the supporting vertex of B changes when you cross one of B's edges. So the sum's edges are A's edges and B's edges, in the order their directions occur.
3. The O(n + m) edge-merge algorithm¶
Concretely:
- Reorder each polygon so it starts at its bottom-most vertex (lowest
y, then lowestx) and is in CCW order. Now each polygon's edge vectors are in increasing polar angle. - The first vertex of the sum is
A[0] + B[0](bottom-most ofAplus bottom-most ofB— itself the bottom-most vertex of the sum). - Merge the two edge lists like merge sort: at each step append whichever next edge has the smaller polar angle (cross-product comparison); on a tie, append both (their sum direction). Each appended edge extends the current boundary vertex tip-to-tail.
- Stop when both edge lists are exhausted. The accumulated vertices are the convex polygon
A ⊕ B.
That is it: two pointers walking two edge lists, O(n + m) total.
4. Minkowski difference (erosion) and reflection¶
The Minkowski difference A ⊖ B = { c : c + B ⊆ A } shrinks A by B — useful for "where can the center of B go while B stays inside A?" It is not simply A ⊕ (−B); that confusion is a classic trap. However, the reflection −B (flip B through the origin) is what collision detection uses: two convex shapes overlap iff the origin lies inside A ⊕ (−B). That A ⊕ (−B) set is often (loosely) called "the Minkowski difference" in collision-detection literature, including GJK. Keep the two meanings separate; we will be careful.
5. The motion-planning payoff (configuration space)¶
Suppose a robot shaped like polygon R must move among obstacles. Treat the robot as a single reference point. Then the robot collides with obstacle O exactly when the reference point enters the inflated obstacle O ⊕ (−R). Inflate every obstacle this way once, and path planning becomes "move a point through free space" — far simpler. This is the configuration-space (C-space) transformation, and the Minkowski sum is how you build it.
Big-O Summary¶
| Operation | Complexity | Notes |
|---|---|---|
Sum of two convex polygons (n, m vertices) | O(n + m) | Edge merge; inputs already CCW from bottom vertex. |
| Sum if inputs not pre-sorted by angle | O(n log n + m log m) | One sort each, then merge. |
Add a single point to a polygon (A ⊕ {p}) | O(n) | Pure translation — shift every vertex by p. |
| Naive all-pairs + convex hull | O(nm log(nm)) | Form nm points, hull them. Avoid this. |
| Sum of two non-convex polygons | O(n²m²) | Decompose into convex pieces, sum pairwise, union. |
| Point-in-sum test (e.g. GJK origin test) | O(n + m) | Without ever materializing the sum. |
n,m= vertex counts ofA,B. The sum of two convex polygons has at mostn + mvertices, which is why the merge is linear.
Real-World Analogies¶
| Concept | Analogy |
|---|---|
A ⊕ B | Stamping a cookie cutter (B) at every point of a region (A) and unioning the cookies. |
| Inflating an obstacle by the robot | A Roomba near a wall: the center of the Roomba can't get within one radius of the wall, so the "no-go" wall is fattened by the Roomba's radius. |
| Edge-merge by angle | Zipping two zippers whose teeth are sorted by angle into one combined zipper. |
Reflection −B | Flipping a stencil over before stamping. |
| Minkowski difference (erosion) | Shaving a layer off the inside of a shape — how far in can a ball roll? |
| Sum is convex | Two smooth bulges add up to a smooth bulge; no dent appears from adding two dentless shapes. |
Where the analogies break: the cookie-cutter picture suggests overlap matters, but the union automatically dissolves overlaps; and the Roomba's true inflated region uses a disk, giving rounded corners that a polygon approximates with many edges.
Pros & Cons¶
| Pros | Cons |
|---|---|
O(n + m) for convex polygons — optimal and simple. | Linear-time merge only works for convex inputs. |
| Output is convex, so downstream tests (point-in-polygon, GJK) stay easy. | Non-convex inputs blow up to O(n²m²) after decomposition. |
| Turns motion planning into "move a point" — huge conceptual simplification. | True circular offset needs infinitely many edges; polygons only approximate it. |
| Same idea powers collision detection (GJK) and CAD offsetting. | Floating-point angle comparisons need care; prefer exact cross products. |
| Symmetric and associative — easy to reason about and compose. | Minkowski difference (erosion) is subtler than the sum; easy to confuse with A ⊕ (−B). |
When to use: inflating obstacles for robot/character motion, collision detection between convex shapes, offsetting convex parts in CAD, computing reachable regions.
When NOT to use: when shapes are wildly non-convex with many pieces (decomposition cost dominates — consider grid/sampling methods), or when an exact rounded offset is required (use arcs, not polygons).
Step-by-Step Walkthrough¶
Let A be a triangle and B be a square, both CCW.
Step 1 — start each at its bottom-most vertex. Both already start at (0,0) (lowest y, then lowest x). Good.
Step 2 — list edge vectors in CCW order.
A edges: a0 = P1-P0 = ( 2, 0) angle 0°
a1 = P2-P1 = (-2, 2) angle 135°
a2 = P0-P2 = ( 0,-2) angle 270°
B edges: b0 = Q1-Q0 = ( 1, 0) angle 0°
b1 = Q2-Q1 = ( 0, 1) angle 90°
b2 = Q3-Q2 = (-1, 0) angle 180°
b3 = Q0-Q3 = ( 0,-1) angle 270°
Step 3 — first vertex of the sum = A[0] + B[0] = (0,0)+(0,0) = (0,0).
Step 4 — merge edges by increasing angle, chaining tip-to-tail from (0,0):
angle 0°: a0=(2,0) and b0=(1,0) tie → take both: (0,0) →(2,0)→ (3,0)
angle 90°: b1=(0,1): (3,0) → (3,1)
angle135°: a1=(-2,2): (3,1) → (1,3)
angle180°: b2=(-1,0): (1,3) → (0,3)
angle270°: a2=(0,-2) and b3=(0,-1) tie → both: (0,3) →(0,1)→ (0,0)
The accumulated boundary (dropping the final return to start) is:
A 6-vertex convex polygon — exactly 3 + 4 − 1 distinct edge directions because two pairs of edges were collinear and merged. Notice every edge of A ⊕ B is parallel to an edge of A or of B. That is the whole algorithm: interleave the edges by angle.
Code Examples¶
Example: Minkowski sum of two convex polygons in O(n + m)¶
The helper reorder rotates a CCW polygon so it starts at the bottom-most vertex; then we merge edge vectors by polar angle using a cross-product comparison.
Go¶
package main
import "fmt"
type Pt struct{ X, Y int64 }
func add(a, b Pt) Pt { return Pt{a.X + b.X, a.Y + b.Y} }
func sub(a, b Pt) Pt { return Pt{a.X - b.X, a.Y - b.Y} }
func cross(a, b Pt) int64 { return a.X*b.Y - a.Y*b.X }
// reorder rotates a CCW polygon to start at its bottom-most (then left-most) vertex.
func reorder(p []Pt) []Pt {
k := 0
for i := 1; i < len(p); i++ {
if p[i].Y < p[k].Y || (p[i].Y == p[k].Y && p[i].X < p[k].X) {
k = i
}
}
out := make([]Pt, 0, len(p))
for i := 0; i < len(p); i++ {
out = append(out, p[(k+i)%len(p)])
}
return out
}
// half returns 0 for the lower half-plane sort key trick: edges of a CCW polygon
// started at the bottom vertex are already in increasing polar angle, so we only
// need the cross product to break ties.
func minkowski(a, b []Pt) []Pt {
a = reorder(a)
b = reorder(b)
// Repeat first edge so the wrap-around edge is included.
a = append(a, a[0], a[1])
b = append(b, b[0], b[1])
var res []Pt
i, j := 0, 0
for i < len(a)-2 || j < len(b)-2 {
res = append(res, add(a[i], b[j]))
c := cross(sub(a[i+1], a[i]), sub(b[j+1], b[j]))
if c > 0 { // edge of A turns first (smaller angle)
i++
} else if c < 0 {
j++
} else { // collinear edges — advance both
i++
j++
}
}
return res
}
func main() {
a := []Pt{{0, 0}, {2, 0}, {0, 2}}
b := []Pt{{0, 0}, {1, 0}, {1, 1}, {0, 1}}
fmt.Println(minkowski(a, b))
// [{0 0} {2 0} {3 0} {3 1} {1 3} {0 3} {0 1}] (collinear vertices may appear)
}
Java¶
import java.util.*;
public class Minkowski {
record Pt(long x, long y) {}
static Pt add(Pt a, Pt b) { return new Pt(a.x + b.x, a.y + b.y); }
static Pt sub(Pt a, Pt b) { return new Pt(a.x - b.x, a.y - b.y); }
static long cross(Pt a, Pt b) { return a.x * b.y - a.y * b.x; }
static List<Pt> reorder(List<Pt> p) {
int k = 0;
for (int i = 1; i < p.size(); i++) {
Pt a = p.get(i), b = p.get(k);
if (a.y() < b.y() || (a.y() == b.y() && a.x() < b.x())) k = i;
}
List<Pt> out = new ArrayList<>();
for (int i = 0; i < p.size(); i++) out.add(p.get((k + i) % p.size()));
return out;
}
static List<Pt> minkowski(List<Pt> a, List<Pt> b) {
a = reorder(a);
b = reorder(b);
a = new ArrayList<>(a); a.add(a.get(0)); a.add(a.get(1));
b = new ArrayList<>(b); b.add(b.get(0)); b.add(b.get(1));
List<Pt> res = new ArrayList<>();
int i = 0, j = 0;
while (i < a.size() - 2 || j < b.size() - 2) {
res.add(add(a.get(i), b.get(j)));
long c = cross(sub(a.get(i + 1), a.get(i)), sub(b.get(j + 1), b.get(j)));
if (c > 0) i++;
else if (c < 0) j++;
else { i++; j++; }
}
return res;
}
public static void main(String[] args) {
List<Pt> a = List.of(new Pt(0,0), new Pt(2,0), new Pt(0,2));
List<Pt> b = List.of(new Pt(0,0), new Pt(1,0), new Pt(1,1), new Pt(0,1));
System.out.println(minkowski(a, b));
}
}
Python¶
def add(a, b): return (a[0] + b[0], a[1] + b[1])
def sub(a, b): return (a[0] - b[0], a[1] - b[1])
def cross(a, b): return a[0] * b[1] - a[1] * b[0]
def reorder(p):
"""Rotate a CCW polygon to start at its bottom-most (then left-most) vertex."""
k = min(range(len(p)), key=lambda i: (p[i][1], p[i][0]))
return p[k:] + p[:k]
def minkowski(a, b):
a = reorder(a)
b = reorder(b)
a = a + [a[0], a[1]] # wrap so the closing edge is included
b = b + [b[0], b[1]]
res = []
i = j = 0
while i < len(a) - 2 or j < len(b) - 2:
res.append(add(a[i], b[j]))
c = cross(sub(a[i + 1], a[i]), sub(b[j + 1], b[j]))
if c > 0:
i += 1
elif c < 0:
j += 1
else:
i += 1
j += 1
return res
if __name__ == "__main__":
a = [(0, 0), (2, 0), (0, 2)]
b = [(0, 0), (1, 0), (1, 1), (0, 1)]
print(minkowski(a, b))
# [(0, 0), (2, 0), (3, 0), (3, 1), (1, 3), (0, 3), (0, 1)]
What it does: merges the edge vectors of two CCW convex polygons by polar angle, producing their Minkowski sum in linear time. Run: go run main.go | javac Minkowski.java && java Minkowski | python minkowski.py
Coding Patterns¶
Pattern 1: Translate-only sum (polygon ⊕ point)¶
Intent: A ⊕ {p} is just A shifted by p. No merge needed.
This is the simplest Minkowski sum and a sanity check: summing with a single-point set must equal a translation.
Pattern 2: Reflect a polygon (for collision / GJK)¶
Intent: GJK needs −B. Reflecting through the origin negates every vertex; to keep CCW order you must also reverse the list.
Pattern 3: Inflate an obstacle by a robot shape (C-space)¶
Intent: the configuration-space obstacle for robot R and obstacle O is O ⊕ (−R).
After inflating every obstacle, the robot is treated as a single point that must stay out of all inflated obstacles.
Error Handling¶
| Error | Cause | Fix |
|---|---|---|
| Garbage / self-intersecting output | Input polygon was clockwise, not CCW. | Normalize orientation first (check signed area; reverse if negative). |
| Missing the wrap-around edge | Forgot to append the first edge again before merging. | Append p[0] (and p[1]) so the closing edge is processed. |
| Wrong start vertex | Did not rotate to the bottom-most vertex. | reorder to lowest y, then lowest x, before merging. |
| Collinear vertices in output | Two edges shared a direction. | Usually harmless; optionally strip points where cross == 0. |
| Off-by-one in pointer loop | Loop bound doesn't account for the duplicated wrap vertices. | Loop while i < len(a)-2 || j < len(b)-2. |
Overflow on cross | Large integer coordinates. | Use 64-bit (Go int64, Java long); for huge values use big integers. |
Performance Tips¶
- Keep inputs convex and CCW. Then the merge is a clean
O(n + m)with no sorting. - Compare angles with cross products, not
atan2. Exact for integers, faster, and avoids trig rounding. - Avoid the all-pairs construction. Forming
nmpoints and hulling them isO(nm log(nm))— orders of magnitude worse. - Reuse the bottom-vertex normalization if you sum the same polygon repeatedly (precompute its reordered form).
- Strip collinear points lazily. Only clean the output if a later stage (e.g. point-in-polygon) needs strictly convex vertices.
Best Practices¶
- Always document and enforce the orientation convention (CCW) at function boundaries.
- Validate that both inputs are actually convex before calling the linear-time routine; fall back to hull-of-all-pairs only when you must.
- Treat
A ⊕ {p}(translation) andA ⊕ (−B)(reflection-sum) as named helpers — they appear constantly in robotics code. - Test the sum against the brute-force
convexHull(all a+b pairs)on random small convex polygons; the vertex sets must match. - Keep coordinates integer when possible for exact cross-product comparisons.
Edge Cases & Pitfalls¶
- One input is a single point — the sum is a pure translation of the other polygon.
- One input is a segment — the sum is the other polygon "smeared" along the segment (a valid degenerate convex shape).
- Collinear edges between the two polygons — they merge into one edge; advance both pointers on a tie.
- Clockwise input — silently produces nonsense; always normalize to CCW.
- Duplicate vertices in the input — clean them first; zero-length edges have undefined angle.
- Confusing
A ⊖ BwithA ⊕ (−B)— erosion is not reflection-sum; keep the two operations distinct.
Common Mistakes¶
- Using
atan2to sort edges — slower and introduces floating-point ties; use cross-product comparison. - Forgetting to reflect
Bfor collision tests — GJK needsA ⊕ (−B), notA ⊕ B. - Not reordering to the bottom vertex — the edges then aren't in clean angular order and the merge misbehaves.
- Applying the linear merge to non-convex polygons — only convex inputs guarantee the
O(n + m)result. - Dropping the wrap-around edge — leaves a gap in the boundary.
- Assuming
A ⊖ B = A ⊕ (−B)— false in general; the difference (erosion) is a different set. - Integer overflow in
cross— use 64-bit accumulators for large coordinates.
Cheat Sheet¶
| Step | What to do |
|---|---|
| Normalize | Make both polygons CCW; rotate each to its bottom-most vertex. |
| Seed | First sum vertex = A[0] + B[0]. |
| Merge | Walk both edge lists; append the edge with smaller polar angle (cross-product test); on tie, advance both. |
| Output | Accumulated vertices form the convex A ⊕ B. |
| Translate | A ⊕ {p} = shift all vertices by p. |
| Reflect | −B = negate all vertices and reverse order (keep CCW). |
| Collision | A and B overlap ⇔ origin ∈ A ⊕ (−B). |
Complexity:
convex ⊕ convex : O(n + m) # edge merge
not pre-sorted : O(n log n + m log m)
non-convex : O(n^2 m^2) # decompose + sum + union
Hard rule: inputs must be convex and CCW for the linear merge.
Visual Animation¶
See
animation.htmlfor an interactive visual animation of the Minkowski sum.The animation demonstrates: - Two convex polygons
AandBand their edge vectors - The edge vectors being merged by polar angle, one at a time - The sum polygonA ⊕ Bgrowing edge by edge - The configuration-space inflation idea (obstacle ⊕ reflected robot) - Step / Run / Reset controls, an info panel, a Big-O table, and an operation log
Summary¶
The Minkowski sum A ⊕ B = { a + b } is the union of B stamped at every point of A. For two convex polygons it is again convex, and you build it in O(n + m) by merging the two polygons' edge vectors in polar-angle order — essentially the merge step of merge sort applied to angle-sorted edges. The same operation underlies robot motion planning (inflate obstacles into configuration-space obstacles so the robot becomes a point), collision detection (GJK's origin-in-Minkowski-difference test), and CAD offsetting. Master three things: the a + b definition, the edge-merge construction, and the CCW/bottom-vertex normalization that makes the merge clean.
Further Reading¶
- de Berg, Cheong, van Kreveld, Overmars — Computational Geometry: Algorithms and Applications, Chapter 13 ("Robot Motion Planning").
- Latombe, J.-C. — Robot Motion Planning — the configuration-space framework in depth.
- Ericson, C. — Real-Time Collision Detection — GJK and Minkowski-difference collision tests.
- cp-algorithms.com — "Minkowski sum of convex polygons."
- Sibling topics: 01-convex-hull (preprocessing non-convex inputs), 06-rotating-calipers (also walks two convex chains), 03-point-in-polygon (testing the resulting sum).
Next step: middle.md — why the sum is convex, the Minkowski difference, configuration-space motion planning, and why the edge-merge beats the naive all-pairs construction.
In this topic
- junior
- middle
- senior
- professional