Стаття Фізика та механіка · ≈ ⏱ 11 хв читання

Event-driven simulation: точні зіткнення без покрокового просування часу

Більярдна куля, що рухається зі швидкістю 40 м/с, може пройти наскрізь через тонкий бортик між двома дискретними кроками часу — баг, відомий як «тунелювання» (tunneling). Event-driven simulation повністю усуває його, обчислюючи точний час кожного майбутнього зіткнення аналітично.

Коротко: Event-driven симуляція уникає тунелювання, властивого покроковим методам, розв'язуючи квадратне рівняння для точного часу зіткнення між об'єктами. Події обробляються по черзі за пріоритетом, а застарілі позначаються через lazy invalidation. Це дає точні, фізично коректні зіткнення (збережено імпульс і енергію) при складності O(N² log N) — ідеально для більярду й молекулярної динаміки твердих сфер.

1. Проблема тунелювання

У покроковій (time-stepped) симуляції позиція частинки оновлюється дискретно: x(t + dt) = x(t) + v·dt. Якщо частинка рухається досить швидко відносно dt і товщини перешкоди, вона може «перестрибнути» перешкоду цілком — на кроці t вона перед стінкою, на кроці t+dt — вже за нею, і жодна перевірка перетину не спрацьовує. Це явище називають тунелюванням (tunneling).

Умова тунелювання v · dt > wall_thickness    ⟹   зіткнення пропущено

Зменшення dt «вирішує» проблему ціною квадратичного зростання вартості (менший dt → більше кроків на ту саму тривалість симуляції). Для більярду, ігор з високою швидкістю снарядів чи молекулярної динаміки з жорсткими зіткненнями це неприйнятно.

2. Покрокова vs event-driven симуляція

Event-driven simulation інвертує підхід: замість «просувати всіх на dt і перевіряти перетини» вона обчислює точний аналітичний момент часу наступної колізії для кожної пари об'єктів, кладе всі майбутні події в чергу з пріоритетом за часом і просуває симуляцію рівно до наступної події — не менше, не більше.

Покрокова (time-stepped)

  • Фіксований або адаптивний dt
  • Може пропустити швидкі зіткнення (tunneling)
  • Вартість O(кроків · N²) для перевірки перетинів
  • Простіше реалізувати для м'яких тіл

Event-driven

  • Час зіткнення — точне аналітичне рішення
  • Тунелювання неможливе за конструкцією
  • Вартість O(N² log N) — залежить від N подій, не dt
  • Ідеально для жорстких тіл, більярду, MD hard-spheres

3. Аналітичний вивід часу удару

Для двох куль радіусів r₁, r₂ з позиціями p₁, p₂ і швидкостями v₁, v₂ момент зіткнення — це найменше t > 0, за якого відстань між центрами дорівнює сумі радіусів. Позначимо відносну позицію Δp = p₂ − p₁ і відносну швидкість Δv = v₂ − v₁:

Умова зіткнення |Δp + Δv·t|² = (r₁ + r₂)²

Розкриваючи квадрат, отримуємо квадратне рівняння відносно t:

Квадратне рівняння відносно t a·t² + b·t + c = 0
a = Δv · Δv
b = 2 · (Δp · Δv)
c = Δp · Δp − (r₁ + r₂)²

Розв'язок: t = (−b − √(b² − 4ac)) / (2a) — беремо менший корінь (перший момент дотику, а не другий, коли кулі вже пройшли одна одну). Колізія реальна лише якщо:

  • Дискримінант ≥ 0 — траєкторії взагалі перетинаються.
  • t > 0 — зіткнення в майбутньому, не в минулому.
  • Δp · Δv < 0 — кулі зближуються, а не віддаляються.

4. Алгоритм черги з пріоритетом

Алгоритм (класична схема, описана Sedgewick & Wayne для симуляції молекулярної динаміки твердих сфер) підтримує мін-купу подій, впорядковану за часом зіткнення:

Головний цикл event-driven 1. Обчислити всі майбутні зіткнення (пара-пара, пара-стіна)
2. Покласти у пріоритетну чергу за часом
3. Витягти найближчу подію (t_min, i, j)
4. Просунути ВСІ частинки на t_min (прямолінійний рух)
5. Розв'язати зіткнення (i, j) — оновити швидкості
6. Перерахувати нові можливі зіткнення для i, j
7. Повторити з кроку 3

Складність O(N² log N) для початкового заповнення черги (усі пари) плюс O(N log N) на кожну подію (перерахунок і вставка нових зіткнень для двох змінених частинок). Для N ~ 100–1000 куль це швидше і завжди точніше за time-stepped підхід.

5. Анулювання подій

Найтонший момент реалізації: коли частинка i зіштовхується з j, усі раніше заплановані події за участю i чи j стають недійсними — швидкість i чи j змінилась, і старий прогноз зіткнення більше не вірний. Наївний підхід — видаляти застарілі події з черги (дорого для бінарної купи). Практичний підхід — залишати їх у черзі, але зберігати лічильник зіткнень кожної частинки на момент створення події; при витягуванні події з черги перевіряти, чи лічильники досі співпадають.

Прийом lazy invalidation

event.valid = (particle.collisionCount === event.countAtCreation). Якщо ні — подія застаріла, просто пропускаємо її без перерахунку черги. Це уникає дорогого видалення довільного елемента з бінарної купи (O(N)) ціною зберігання «мертвих» подій, які згодом просто ігноруються (O(log N) для pop).

6. Пружна реакція зіткнення

Для пружного (elastic) зіткнення двох куль мас m₁, m₂ у момент дотику, імпульс передається вздовж лінії центрів (нормаль n̂ = Δp / |Δp|):

Пружне зіткнення на основі імпульсу J = 2 · m₁ · m₂ / (m₁ + m₂) · (Δv · n̂)
v₁' = v₁ + (J / m₁) · n̂
v₂' = v₂ − (J / m₂) · n̂

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

7. Порівняння методів

Метод Тунелювання Час зіткнення Модель вартості Використовується для
Дискретний покроковий Можливе Округлений до dt O(кроків · N²) М'які тіла, звичайні ігри
Continuous CCD (swept) Запобігається за крок Точний в межах кроку O(кроків · N² · розв'язок) Швидкі снаряди в ігрових рушіях
Event-driven Неможливе за конструкцією Точний, аналітичний O(N² log N) Більярд, hard-sphere MD

8. Псевдокод

Час удару + черга з пріоритетом (JS)

// Час до зіткнення двох куль (найменший додатний корінь)
function timeToCollision(a, b) {
  const dp = sub(b.pos, a.pos);
  const dv = sub(b.vel, a.vel);
  const R = a.radius + b.radius;

  const A = dot(dv, dv);
  if (A === 0) return Infinity;   // однакова швидкість — ніколи не зіткнуться

  const B = 2 * dot(dp, dv);
  if (B >= 0) return Infinity;    // віддаляються

  const C = dot(dp, dp) − R * R;
  const disc = B * B − 4 * A * C;
  if (disc < 0) return Infinity;    // траєкторії не перетинаються

  return (−B − Math.sqrt(disc)) / (2 * A);  // перший момент дотику
}

// Головний цикл: витягти найближчу подію, просунути час, розв'язати
function runEventDriven(particles, pq, tEnd) {
  let tNow = 0;
  while (tNow < tEnd) {
    const event = pq.popMin();
    if (!event.isValid()) continue;    // lazy invalidation

    const dt = event.time − tNow;
    for (const p of particles) p.advance(dt);  // прямолінійний рух
    tNow = event.time;

    resolveCollision(event.a, event.b);          // оновити швидкості
    predictNewCollisions(event.a, event.b, pq); // нові події в чергу
  }
}
▶ Демо наживо

Побачити точні зіткнення в дії

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

🎱 Більярд 🔵 Молекулярна динаміка

🔗 Пов'язані симуляції

🎱Більярд 🔵Молек. динаміка N-тіла