Geometry · Algorithms
📅 March 2026 ⏱ ≈ 11 min read 🎯 Intermediate

Voronoi Diagrams — Fortune's sweep line, Delaunay and GLSL distance fields

Given a set of seed points, a Voronoi diagram partitions the plane into regions — each region containing every point closer to its seed than to any other. Simple to state, surprisingly deep to compute, and beautiful to look at.

1. Definition and properties

Let P = {p₁, p₂, …, pₙ} be a set of n distinct points (seeds) in ℝ². The Voronoi cell of seed pᵢ is:

V(pᵢ) = { x ∈ ℝ² | d(x, pᵢ) ≤ d(x, pⱼ) for all j ≠ i }

where d(·,·) is Euclidean distance by default (other metrics produce exotic variants)

The boundaries between adjacent cells are bisectors — each edge of the diagram is the perpendicular bisector of the segment between two seeds. Three cells meet at a Voronoi vertex, which is the circumcentre of the triangle formed by those three seeds.

Key structural facts:

3 seedsA single triangle; 3 cells, 1 vertex
n seeds (general)At most 2n−5 vertices, 3n−6 edges
ℝ³ extensionEach seed gets a convex polyhedral cell

2. Naïve O(n²) algorithm — rasterisation

The most direct approach: for every pixel, find the nearest seed and colour the pixel with that seed's colour. This is brute-force nearest-neighbour over the image grid.

// Brute-force Voronoi — O(W·H·n), simple but slow for large n
function voronoiBrute(seeds, width, height) {
  const out = new Uint8ClampedArray(width * height * 4);
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      let minDist = Infinity, nearest = 0;
      for (let i = 0; i < seeds.length; i++) {
        const dx = x - seeds[i].x, dy = y - seeds[i].y;
        const d = dx*dx + dy*dy;  // skip sqrt — only comparing
        if (d < minDist) { minDist = d; nearest = i; }
      }
      const c = seeds[nearest].color;
      const idx = (y * width + x) * 4;
      out[idx] = c[0]; out[idx+1] = c[1]; out[idx+2] = c[2]; out[idx+3] = 255;
    }
  }
  return out;
}

Fine for small n (≤ 100) or low-res outputs. For high-resolution diagrams with thousands of seeds, this is prohibitively slow — the answer is Fortune's algorithm.

GPU trick: the brute-force approach parallelises trivially on the GPU. A WebGL fragment shader performing the nearest-seed search over all seeds in a uniform array runs in O(n) per pixel but with enormous throughput. This is essentially what the GLSL section covers.

3. Fortune's sweep-line algorithm — O(n log n)

Steven Fortune's 1987 algorithm computes the exact Voronoi diagram in O(n log n) time and O(n) space by sweeping a horizontal line downward across the plane. It maintains two data structures:

Parabola for seed pᵢ = (px, py) with sweep line at y = ℓ:

f(x) = (x − px)² / (2(py − ℓ)) + (py + ℓ) / 2

As ℓ descends past pᵢ, the parabola for pᵢ is born on the beach line.
Two adjacent parabolas intersect at a point tracing a bisector edge.

When a site event fires (sweep line reaches seed pᵢ):

  1. Locate the arc directly above pᵢ on the beach line.
  2. Split it and insert a new parabola arc for pᵢ between the two halves.
  3. Record two half-edges for the new Voronoi edge being traced.
  4. Check for circle events involving the new arc's neighbours.

When a circle event fires (three consecutive arcs converge to a point):

  1. Remove the middle arc from the beach line.
  2. Record the Voronoi vertex at the circumcentre of the three seeds.
  3. Connect the two half-edges from the removed arc into finished edges.
  4. Re-check circle events for the new neighbouring arc triplets.
AlgorithmTimeSpaceNotes
Brute force (raster)O(W·H·n)O(W·H)Simple; GPU-parallel
Jump flooding (JFA)O(n log n) GPU stepsO(W·H)Approximate; fast on GPU
Fortune's algorithmO(n log n)O(n)Exact; CPU; reference impl
Bowyer-Watson (Delaunay + dual)O(n log n)O(n)Via Delaunay dual
Implementation complexity: Fortune's algorithm is notoriously tricky to implement from scratch due to numerical precision issues (degenerate coincident seeds, circle events very close together). For practical use, prefer a well-tested library such as d3-delaunay (JavaScript) or scipy.spatial.Voronoi (Python).

4. Delaunay triangulation — the dual graph

The Delaunay triangulation DT(P) is the straight-line dual of the Voronoi diagram: connect two seeds with an edge whenever their Voronoi cells share an edge. The defining property is the empty-circumcircle criterion: no seed lies strictly inside the circumcircle of any triangle in DT(P).

The Delaunay triangulation maximises the minimum angle over all possible triangulations of a point set — making it ideal for finite-element meshes and numerical simulation grids where sliver triangles degrade accuracy.

Bowyer-Watson incremental insertion

An easy-to-implement alternative to Fortune's algorithm for computing Delaunay triangulations:

// Bowyer-Watson incremental Delaunay triangulation
// (simplified — omits boundary ("super-triangle") removal)
function bowyer(points) {
  const triangles = [superTriangle(points)];  // initially one huge triangle

  for (const p of points) {
    // Find all triangles whose circumcircle contains p
    const bad = triangles.filter(t => inCircumcircle(t, p));

    // Find the boundary of the polygonal hole
    const boundary = uniqueEdges(bad);  // edges not shared by two bad triangles

    // Remove bad triangles
    for (const t of bad) triangles.splice(triangles.indexOf(t), 1);

    // Re-triangulate hole: connect each boundary edge to p
    for (const [a, b] of boundary) {
      triangles.push({ a, b, c: p });
    }
  }
  return triangles.filter(t => !sharesSuperVertex(t));
}

// Empty-circumcircle test
function inCircumcircle({ a, b, c }, p) {
  const ax = a.x - p.x, ay = a.y - p.y;
  const bx = b.x - p.x, by = b.y - p.y;
  const cx = c.x - p.x, cy = c.y - p.y;
  return (
    ax*(by*(cx*cx+cy*cy) - cy*(bx*bx+by*by)) -
    ay*(bx*(cx*cx+cy*cy) - cx*(bx*bx+by*by)) +
    (ax*ax+ay*ay)*(bx*cy - by*cx)
  ) > 0;
}

The Bowyer-Watson algorithm is O(n²) in the worst case (though O(n log n) in practice with randomised insertion). Once you have the Delaunay triangulation, the Voronoi diagram is its geometric dual — connect the circumcentres of adjacent triangles.

5. Lloyd relaxation — centroidal Voronoi tessellation

A centroidal Voronoi tessellation (CVT) is a special Voronoi diagram where each seed coincides with the centroid of its own cell. Lloyd's algorithm iterates toward a CVT by alternating two steps:

  1. Compute the Voronoi diagram of the current seeds.
  2. Move each seed to the centroid (weighted average of pixel positions in that cell).

After 10–30 iterations the seeds distribute evenly across the domain, producing the characteristic foam-like, equal-area hexagonal pattern found in biological tissues, geographic districting, and halftoning.

// Raster Lloyd relaxation — one iteration
function lloydStep(seeds, width, height) {
  const sums = seeds.map(() => ({ sx: 0, sy: 0, count: 0 }));

  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      let best = 0, bestD = Infinity;
      for (let i = 0; i < seeds.length; i++) {
        const dx = x - seeds[i].x, dy = y - seeds[i].y;
        const d = dx*dx + dy*dy;
        if (d < bestD) { bestD = d; best = i; }
      }
      sums[best].sx += x;
      sums[best].sy += y;
      sums[best].count++;
    }
  }
  return seeds.map((s, i) => sums[i].count > 0
    ? { ...s, x: sums[i].sx / sums[i].count,
               y: sums[i].sy / sums[i].count }
    : s
  );
}
Stippling / dithering: if you weight each pixel by its image darkness (greyscale value), Lloyd relaxation produces weighted CVT. The resulting seed distribution proportionally follows the image brightness — a technique called Voronoi stippling, popularised by Adrian Secord (2002).

6. GLSL distance-field Voronoi

The most visually compelling Voronoi renders are done in fragment shaders, where each pixel computes the nearest seed in parallel on the GPU. The inigo quilez compact variant additionally computes the second-nearest distance, enabling smooth cell borders and interior patterns.

// GLSL Voronoi — passes seeds via texture or uniform array
// Here: procedural seeds generated from grid hash
// Based on Inigo Quilez's voronoise technique

vec2 hash22(vec2 p) {
  p = vec2(dot(p, vec2(127.1, 311.7)),
            dot(p, vec2(269.5, 183.3)));
  return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
}

// Returns (minDist, secondMinDist, cell index)
vec3 voronoi(vec2 uv, float scale) {
  vec2 p = uv * scale;
  vec2 gv = floor(p);
  float md = 8.0, md2 = 8.0;
  vec2 closest;

  for (int y = -2; y <= 2; y++) {
    for (int x = -2; x <= 2; x++) {
      vec2 cell = gv + vec2(x, y);
      // Jitter seed within cell
      vec2 seed = cell + 0.5 + 0.5 * hash22(cell);
      float d = length(p - seed);
      if (d < md) { md2 = md; md = d; closest = seed; }
      else if (d < md2) { md2 = d; }
    }
  }
  return vec3(md, md2, md2 - md);
}

// Fragment shader usage
void main() {
  vec2 uv = vUv;
  vec3 v = voronoi(uv, 8.0);

  float minD  = v.x;   // distance to nearest seed
  float minD2 = v.y;   // distance to second nearest
  float border = v.z;  // ≈ distance to edge (minD2 - minD)

  // Style: cells with sharp borders
  float edge = smoothstep(0.0, 0.04, border);
  vec3 col = mix(vec3(1.0), vec3(minD * 1.5), edge);
  gl_FragColor = vec4(col, 1.0);
}

By animating the hash22 seed jitter over time (add a uTime offset to the seed position) you get smoothly morphing liquid Voronoi cells — a popular effect in shader demos and UI backgrounds.

7. Applications in science, games and visualisation

📐 Mathematics Category

Explore spirographs, fractals and more mathematical simulations — all running live in WebGL.

Browse Math →

8. Extensions: 3D, weighted and power diagrams