Геометрія · Алгоритми
📅 Березень 2026 ⏱ ≈ 11 хв читання 🎯 Середній

Діаграми Вороного — замітальна пряма Фортуна, Делоне та GLSL-поля відстаней

За заданим набором опорних точок діаграма Вороного розбиває площину на області — кожна область містить усі точки, ближчі до своєї опорної точки, ніж до будь-якої іншої. Просто сформулювати, на диво глибоко обчислювати й гарно споглядати.

1. Означення та властивості

Нехай P = {p₁, p₂, …, pₙ} — набір із n різних точок (опорних) у ℝ². Клітина Вороного опорної точки pᵢ — це:

V(pᵢ) = { x ∈ ℝ² | d(x, pᵢ) ≤ d(x, pⱼ) для всіх j ≠ i }

де d(·,·) — це евклідова відстань за замовчуванням (інші метрики дають екзотичні варіанти)

Межі між сусідніми клітинами — це серединні перпендикуляри: кожне ребро діаграми є серединним перпендикуляром до відрізка між двома опорними точками. Три клітини сходяться у вершині Вороного, яка є центром описаного кола трикутника, утвореного цими трьома опорними точками.

Ключові структурні факти:

3 опорні точкиОдин трикутник; 3 клітини, 1 вершина
n опорних точок (загальний випадок)Щонайбільше 2n−5 вершин, 3n−6 ребер
Розширення на ℝ³Кожна опорна точка отримує опуклу багатогранну клітину

2. Наївний алгоритм O(n²) — растеризація

Найпряміший підхід: для кожного пікселя знайти найближчу опорну точку й зафарбувати піксель її кольором. Це пошук найближчого сусіда повним перебором по сітці зображення.

// Діаграма Вороного повним перебором — O(W·H·n), проста, але повільна для великих 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;  // пропускаємо sqrt — лише порівнюємо
        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;
}

Добре для малих n (≤ 100) або зображень низької роздільності. Для діаграм високої роздільності з тисячами опорних точок це неприйнятно повільно — відповідь дає алгоритм Фортуна.

Трюк із GPU: підхід повним перебором тривіально розпаралелюється на GPU. Фрагментний шейдер WebGL, що виконує пошук найближчої опорної точки по всіх точках в uniform-масиві, працює за O(n) на піксель, але з величезною пропускною здатністю. Саме це по суті розглянуто в розділі про GLSL.

3. Алгоритм замітальної прямої Фортуна — O(n log n)

Алгоритм Стівена Фортуна 1987 року обчислює точну діаграму Вороного за час O(n log n) і простір O(n), проводячи горизонтальну пряму вниз по площині. Він підтримує дві структури даних:

Парабола для опорної точки pᵢ = (px, py) із замітальною прямою на y = ℓ:

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

Коли ℓ опускається нижче pᵢ, на береговій лінії народжується парабола для pᵢ.
Дві сусідні параболи перетинаються в точці, що окреслює ребро серединного перпендикуляра.

Коли спрацьовує подія точки (замітальна пряма досягає опорної точки pᵢ):

  1. Знайти дугу безпосередньо над pᵢ на береговій лінії.
  2. Розділити її та вставити нову параболічну дугу для pᵢ між двома половинами.
  3. Записати два напівребра для нового ребра Вороного, що окреслюється.
  4. Перевірити події кіл, що стосуються сусідів нової дуги.

Коли спрацьовує подія кола (три послідовні дуги сходяться в точку):

  1. Видалити середню дугу з берегової лінії.
  2. Записати вершину Вороного в центрі описаного кола трьох опорних точок.
  3. З'єднати два напівребра від видаленої дуги в завершені ребра.
  4. Повторно перевірити події кіл для нових сусідніх трійок дуг.
АлгоритмЧасПростірПримітки
Повний перебір (растр)O(W·H·n)O(W·H)Простий; паралельний на GPU
Jump flooding (JFA)O(n log n) кроків GPUO(W·H)Наближений; швидкий на GPU
Алгоритм ФортунаO(n log n)O(n)Точний; CPU; еталонна реалізація
Боуєра-Ватсона (Делоне + двоїстий)O(n log n)O(n)Через двоїстість Делоне
Складність реалізації: алгоритм Фортуна сумнозвісно складно реалізувати з нуля через проблеми числової точності (вироджені збіжні опорні точки, дуже близькі події кіл). Для практичного використання надавайте перевагу добре перевіреній бібліотеці на кшталт d3-delaunay (JavaScript) чи scipy.spatial.Voronoi (Python).

4. Тріангуляція Делоне — двоїстий граф

Тріангуляція Делоне DT(P) — це прямолінійна двоїста до діаграми Вороного: з'єднати дві опорні точки ребром щоразу, коли їхні клітини Вороного мають спільне ребро. Визначальна властивість — критерій порожнього описаного кола: жодна опорна точка не лежить строго всередині описаного кола будь-якого трикутника в DT(P).

Тріангуляція Делоне максимізує мінімальний кут серед усіх можливих тріангуляцій набору точок — що робить її ідеальною для скінченноелементних сіток і числових сіток симуляцій, де голчасті трикутники погіршують точність.

Інкрементне вставляння Боуєра-Ватсона

Легка для реалізації альтернатива алгоритму Фортуна для обчислення тріангуляцій Делоне:

// Інкрементна тріангуляція Делоне Боуєра-Ватсона
// (спрощено — без видалення межі («супертрикутника»))
function bowyer(points) {
  const triangles = [superTriangle(points)];  // спочатку один величезний трикутник

  for (const p of points) {
    // Знайти всі трикутники, описане коло яких містить p
    const bad = triangles.filter(t => inCircumcircle(t, p));

    // Знайти межу багатокутного отвору
    const boundary = uniqueEdges(bad);  // ребра, не спільні для двох поганих трикутників

    // Видалити погані трикутники
    for (const t of bad) triangles.splice(triangles.indexOf(t), 1);

    // Перетріангулювати отвір: з'єднати кожне ребро межі з p
    for (const [a, b] of boundary) {
      triangles.push({ a, b, c: p });
    }
  }
  return triangles.filter(t => !sharesSuperVertex(t));
}

// Перевірка порожнього описаного кола
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;
}

Алгоритм Боуєра-Ватсона має складність O(n²) у найгіршому випадку (хоча O(n log n) на практиці з рандомізованим вставлянням). Щойно ви маєте тріангуляцію Делоне, діаграма Вороного є її геометричною двоїстою — з'єднайте центри описаних кіл сусідніх трикутників.

5. Релаксація Ллойда — центроїдна тесселяція Вороного

Центроїдна тесселяція Вороного (CVT) — це особлива діаграма Вороного, де кожна опорна точка збігається з центроїдом своєї власної клітини. Алгоритм Ллойда ітерує до CVT, чергуючи два кроки:

  1. Обчислити діаграму Вороного поточних опорних точок.
  2. Перемістити кожну опорну точку в центроїд (зважене середнє положень пікселів у тій клітині).

Після 10–30 ітерацій опорні точки рівномірно розподіляються по області, утворюючи характерний пінистий гексагональний візерунок рівної площі, що трапляється в біологічних тканинах, географічному районуванні та растровому переведенні зображень.

// Растрова релаксація Ллойда — одна ітерація
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
  );
}
Стипплінг / дизеринг: якщо зважувати кожен піксель за темнотою зображення (значенням у відтінках сірого), релаксація Ллойда дає зважену CVT. Отриманий розподіл опорних точок пропорційно відповідає яскравості зображення — техніка під назвою стипплінг Вороного, популяризована Адріаном Секордом (2002).

6. GLSL-діаграма Вороного на полі відстаней

Найефектніші візуалізації Вороного виконуються у фрагментних шейдерах, де кожен піксель паралельно на GPU обчислює найближчу опорну точку. Компактний варіант inigo quilez додатково обчислює відстань до другої найближчої точки, уможливлюючи плавні межі клітин і внутрішні візерунки.

// GLSL Вороной — передає опорні точки через текстуру або uniform-масив
// Тут: процедурні опорні точки, згенеровані з хешу сітки
// На основі техніки voronoise Іньїго Кілеса

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);
}

// Повертає (minDist, secondMinDist, індекс клітини)
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);
      // Зміщуємо опорну точку всередині клітини
      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);
}

// Використання у фрагментному шейдері
void main() {
  vec2 uv = vUv;
  vec3 v = voronoi(uv, 8.0);

  float minD  = v.x;   // відстань до найближчої опорної точки
  float minD2 = v.y;   // відстань до другої найближчої
  float border = v.z;  // ≈ відстань до ребра (minD2 - minD)

  // Стиль: клітини з чіткими межами
  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);
}

Анімуючи зміщення опорних точок hash22 у часі (додайте зсув uTime до положення опорної точки), ви отримаєте плавно трансформівні рідкі клітини Вороного — популярний ефект у шейдерних демо та фонах інтерфейсів.

7. Застосування в науці, іграх і візуалізації

📐 Категорія «Математика»

Досліджуйте спірографи, фрактали та інші математичні симуляції — усі працюють наживо у WebGL.

Перейти до математики →

8. Розширення: 3D, зважені та степеневі діаграми