Стаття Алгоритми · ≈ 8 хв читання

Алгоритм Боїдів

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

1. Емерджентна поведінка

Емерджентність — це коли складна глобальна поведінка системи виникає з простих локальних правил взаємодії її частин. Жодна «пташка» у зграї не знає про траєкторію всієї зграї: вона реагує лише на найближчих сусідів.

Те саме відбувається у мурашиних колоніях (феромонні сліди), нейронних мережах, ринках і транспортних потоках. Боїди — найелегантніша ілюстрація цього принципу у комп'ютерній графіці.

Що таке «боїд»?

Назва «boid» (bird-oid object) придумав Крейг Рейнольдс. Перша публічна демонстрація — 1986 рік, SIGGRAPH. З тих пір алгоритм незмінно використовується у кіно, іграх і наукових симуляціях.

2. Три правила Рейнольдса

↔️

Розлучення

Уникай зіткнення з найближчими сусідами — відштовхуйся від занадто близьких боїдів.

↗️

Вирівнювання

Лети в тому ж напрямку, що й сусіди — вирівнюй свою швидкість до середньої у радіусі.

🎯

Зближення

Тримайся разом зі зграєю — рухайся до центра мас сусідньої групи.

Ці три правила можна трактувати як три різні сили тяжіння у полі швидкостей. Боїд обчислює вектор бажаної швидкості від кожного правила, а потім комбінує їх із вагами.

3. Вектори та вага правил

Розлучення (Separation)

Для кожного сусіда j, що занадто близько (відстань d < rsep), генеруємо вектор відштовхування, обернено пропорційний відстані:

Вектор розлучення vsep(i) = Σⱼ (pos[i] − pos[j]) / |pos[i] − pos[j]|²

де j — всі сусіди з |pos[i] − pos[j]| < rsep

Вирівнювання (Alignment)

Обчислюємо середню швидкість у групі та додаємо вектор, що «повертає» боїда до цього середнього:

Вектор вирівнювання valign(i) = (Σⱼ vel[j]) / Nneighbors − vel[i]

Зближення (Cohesion)

Знаходимо центр мас групи та генеруємо вектор у напрямі до нього:

Вектор зближення center = (Σⱼ pos[j]) / Nneighbors
vcoh(i) = center − pos[i]

Комбінація

Підсумкове прискорення a[i] = wsep · vsep + walign · valign + wcoh · vcoh

Типові ваги: wsep = 1.5, walign = 1.0, wcoh = 1.0. Зміна ваг кардинально міняє характер руху зграї: збільшення wsep дає «пухку» зграю, збільшення wcoh — щільні підгрупи.

4. Радіус сприйняття та кут зору

Реальні тварини бачать лише частину навколишнього простору. Для реалістичності додають обмеження:

  • Радіус сприйняття rview — боїд бачить лише сусідів у цьому радіусі.
  • Кут зору θ (наприклад, 270°) — боїд «сліпий» у мертвій зоні позаду. Перевіряємо: dot(forward, toNeighbor) > cos(θ/2).
  • Мінімальна відстань розлучення rsep < rview — зазвичай 0.3–0.5 від rview.
Перевірка кута зору toNeighbor = normalize(pos[j] − pos[i])
isVisible = dot(forward[i], toNeighbor) > cos(fieldOfView / 2)

5. Просторова оптимізація

Без оптимізації пошук сусідів — O(N²). При 5000 боїдів це 25 мільйонів перевірок на кадр. Вирішення — просторовий поділ простору:

  • Uniform grid — поділити зону на комірки розміром rview. Для кожного боїда перевіряти лише 9 (2D) або 27 (3D) сусідніх комірок.
  • Quadtree / Octree — адаптивне розбиття, краще для нерівномірно розподілених зграй.
  • Sort + Binary search — сортуємо боїдів за Morton code (Z-order curve), бінарно шукаємо діапазон.

У нашій WebGL-симуляції використовується uniform grid з розміром комірки rview. Це дає O(N) пошук сусідів і дозволяє симулювати 10 000+ боїдів у реальному часі у шейдерах.

GPU-паралелізм

На GPU кожен боїд — окремий потік. Uniform grid зберігається у texture buffer: боїди відсортовані за hash-значенням комірки через Radix Sort, а заголовки комірок — у окремому масиві. Кожен thread читає лише 27 комірок.

6. Розширення: хижак, перешкоди, лідер

Хижак (Predator / Flee)

Додаємо правило з великою вагою: якщо хижак у радіусі rflee, усі боїди в цьому радіусі отримують вектор утечі від хижака. Зграя «вибухає» на фрагменти і потім знову збирається.

Уникнення перешкод

Кидаємо промінь у напрямі руху боїда. Якщо знаходимо перешкоду на відстані d < ravoid, відхиляємо курс перпендикулярно поверхні перешкоди.

Лідер зграї

Один або кілька «лідерів» рухаються по окремій траєкторії (наприклад, по Bézier-кривій). Сусідні боїди додають вектор зближення з лідером — таким чином вся зграя слідує маршруту.

7. Псевдокод

function stepBoids(boids, dt):

  // 1. Оновити просторовий хеш
  updateGrid(boids, cellSize = VIEW_RADIUS)

  for each boid i:

    // 2. Знайти сусідів у радіусі VIEW_RADIUS
    neighbors = getNeighbors(i, VIEW_RADIUS)
    close     = neighbors.filter(j => dist(i,j) < SEP_RADIUS)

    // 3. Правило 1: Розлучення
    v_sep = vec2(0, 0)
    for j in close:
      d = dist(i, j)
      v_sep += (pos[i] - pos[j]) / (d * d)

    // 4. Правило 2: Вирівнювання
    avg_vel = average(vel[j] for j in neighbors)
    v_align = avg_vel - vel[i]

    // 5. Правило 3: Зближення
    center = average(pos[j] for j in neighbors)
    v_coh  = center - pos[i]

    // 6. Сумарне прискорення (з вагами)
    a = W_SEP * v_sep + W_ALIGN * v_align + W_COH * v_coh

    // 7. Обмеження максимальної сили
    a = clampLength(a, MAX_FORCE)

    // 8. Оновити швидкість і позицію
    vel[i] = clampLength(vel[i] + a * dt, MAX_SPEED)
    pos[i] += vel[i] * dt

    // 9. Обернені межі (тороїдальний простір)
    pos[i] = wrap(pos[i], bounds)

Константи для старту: VIEW_RADIUS = 50, SEP_RADIUS = 20, MAX_SPEED = 200 px/s, MAX_FORCE = 400 px/s², W_SEP = 1.5, W_ALIGN = 1.0, W_COH = 1.0.

🐦 Спробуйте симуляцію боїдів

Інтерактивна WebGL-симуляція з 5000+ агентів. Змінюйте ваги правил у реальному часі — спостерігайте, як зграя миттєво реагує.

Відкрити симуляцію →