Стаття · Геймдев · Фізика · Алгоритми
📅 Липень 2026 ⏱ ≈ 20 хв 🎯 Середній – Просунутий рівень · Останнє оновлення: 5 липня 2026 р.

XPBD: розширена позиційна динаміка

Position-Based Dynamics (PBD) зробила симуляцію тканин, мотузок і м'яких тіл простою — але її жорсткість непомітно залежала від кількості ітерацій розв'язувача та кроку часу, тож "сталевий трос" у грі на 60 fps міг перетворитися на гуму, щойно комп'ютер гравця втрачав кадри. XPBD (Extended Position-Based Dynamics), представлена Macklin, Müller і Chentanez у 2016 році, виправляє це, прив'язуючи кожне обмеження до реальної фізичної величини — комплаєнсу (податливості). Тепер це алгоритм, що лежить в основі солвера тканин Unity, NVIDIA PhysX 5 та симуляції тканин/м'яких тіл у Blender.

1. Чому звичайного PBD було недостатньо

Класична Position-Based Dynamics (Müller та ін., 2007) симулює обмеження — відстань, згин, об'єм — шляхом прямого проєктування позицій частинок так, щоб функція обмеження C(x) = 0 виконувалася, а потім повторює це проєктування фіксовану кількість разів за кадр (у стилі Gauss-Seidel). Це швидко, безумовно стабільно і тривіально реалізується. Але є проблема: у самому кроці проєктування немає нічого, що відповідало б реальному значенню жорсткості в Н/м. "Жорсткість", яку ви отримуєте, — це артефакт двох непов'язаних чисел: кількості ітерацій розв'язувача та кроку часу, помножених одне на одне.

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

Суть проблеми: проєктування обмежень у звичайному PBD еквівалентне запуску неявного розв'язувача з нескінченно жорстким обмеженням, яке потім штучно пом'якшується за рахунок недорелаксації протягом ітерацій. Немає способу задати "алюміній" проти "гуми" — лише "більше ітерацій" проти "менше".

2. Комплаєнс і множники Лагранжа

XPBD переформульовує проєктування як розв'язання задачі умовної оптимізації прямо з лагранжевої механіки. Кожне обмеження C_j(x) = 0 отримує значення комплаєнсу α_j — обернену величину фізичної жорсткості, виміряну в м/Н (метрах на Ньютон, тобто наскільки обмеження "поступається" на одиницю сили):

α = 1 / k (k = жорсткість у Н/м, α = комплаєнс у м/Н) α = 0 → абсолютно жорстке обмеження (поведінка як у звичайного PBD) α > 0 → м'яке / пружне обмеження з реальними фізичними одиницями

Повна потенційна енергія обмеження дорівнює U(x) = ½ · α⁻¹ · C(x)². Мінімізація цієї енергії за умови рівнянь руху вводить множник Лагранжа λ для кожного обмеження — фізично λ є накопиченим імпульсом сили обмеження. Ключовий трюк XPBD полягає в тому, щоб оновлювати λ інкрементально разом з оновленням позиції, замість розв'язання повної лінійної системи:

Δλ = ( −C(x) − α̃·λ ) / ( Σᵢ wᵢ·|∇Cᵢ|² + α̃ ) де α̃ = α / dt² (комплаєнс, перерахований на крок субкроку) і wᵢ = 1 / mᵢ (обернена маса частинки i) Δxᵢ = wᵢ · Δλ · ∇Cᵢ(x) λ ← λ + Δλ

Задайте α = 0 — і це точно зводиться до формули проєктування класичного PBD: XPBD є строгою надмножиною. Задайте α > 0 — і ви отримаєте пружину з фізично осмисленою жорсткістю, незалежною від кількості ітерацій.

3. Цикл розв'язувача XPBD

Повний алгоритм на кадр чергує субкроки, кожен з яких прогнозує позиції, розв'язує всі обмеження один раз (з обнуленням λ на початку субкроку, а не кадру) та виводить швидкість зі зміни позиції:

function xpbdStep(particles, constraints, dt, substeps) {
  const h = dt / substeps; // крок субкроку

  for (let s = 0; s < substeps; s++) {
    // 1. Прогноз позицій із зовнішніми силами (гравітація, вітер...)
    for (const p of particles) {
      if (p.invMass === 0) continue; // закріплена/статична
      p.vel.y += -9.8 * h;
      p.prevPos.x = p.pos.x; p.prevPos.y = p.pos.y;
      p.pos.x += p.vel.x * h;
      p.pos.y += p.vel.y * h;
    }

    // 2. Скидання множників Лагранжа для цього субкроку
    for (const c of constraints) c.lambda = 0;

    // 3. Розв'язати кожне обмеження один раз (прохід Gauss-Seidel)
    for (const c of constraints) solveConstraint(c, h);

    // 4. Вивести швидкість зі зміни позиції (основна ідея PBD)
    for (const p of particles) {
      if (p.invMass === 0) continue;
      p.vel.x = (p.pos.x - p.prevPos.x) / h;
      p.vel.y = (p.pos.y - p.prevPos.y) / h;
    }
  }
}

Зверніть увагу — тут немає окремого проходу "розв'язання швидкості", як у рушіях на основі імпульсів: швидкість завжди є побічним продуктом зміни позиції, поділеної на h. Саме це робить PBD і XPBD безумовно стабільними: ви ніколи не можете розійтися до нескінченної швидкості, бо швидкість обмежена тим, наскільки далеко позиція реально може змінитись за один субкрок.

4. Виведення обмеження відстані

Робоче обмеження для тканин і мотузок — це обмеження відстані, яке утримує дві частинки на відстані спокою L₀:

C(x₁, x₂) = |x₁ − x₂| − L₀ ∇C₁ = n̂ де n̂ = (x₁ − x₂) / |x₁ − x₂| ∇C₂ = −n̂ denom = w₁ + w₂ + α̃ Δλ = ( −C − α̃·λ ) / denom Δx₁ = w₁ · Δλ · n̂ Δx₂ = −w₂ · Δλ · n̂
function solveDistanceConstraint(c, h) {
  const { p1, p2, restLength, compliance } = c;
  const dx = p1.pos.x - p2.pos.x;
  const dy = p1.pos.y - p2.pos.y;
  const dist = Math.hypot(dx, dy);
  if (dist < 1e-9) return;

  const C = dist - restLength;
  const nx = dx / dist, ny = dy / dist;

  const alphaTilde = compliance / (h * h);
  const denom = p1.invMass + p2.invMass + alphaTilde;
  if (denom < 1e-9) return;

  const dLambda = (-C - alphaTilde * c.lambda) / denom;
  c.lambda += dLambda;

  p1.pos.x +=  p1.invMass * dLambda * nx;
  p1.pos.y +=  p1.invMass * dLambda * ny;
  p2.pos.x -=  p2.invMass * dLambda * nx;
  p2.pos.y -=  p2.invMass * dLambda * ny;
}

Той самий скелет — обчислити C, обчислити ∇C для кожної частинки, розв'язати відносно Δλ, застосувати зважені зміщення позицій — працює і для обмежень згину (двогранний кут між двома трикутниками), обмежень об'єму (знаковий об'єм тетраедра) та обмежень площі. Змінюється лише C та її градієнт.

5. Чому субкроки, а не ітерації розв'язувача

Оригінальна стаття PBD фіксувала крок часу на рівні частоти кадрів (наприклад, 1/60 с) і збільшувала кількість ітерацій для покращення збіжності та жорсткості. Автори XPBD виявили, що це погано збігається — прохід Gauss-Seidel по багатьох обмеженнях поширює корекцію лише на одну ланку за ітерацію, тож довгій смузі тканини потрібні десятки ітерацій, щоб відчуватись натягнутою.

Натомість XPBD надає перевагу багатьом малим субкрокам з одним розв'язанням обмежень кожен, замість одного великого кадру з багатьма ітераціями. За той самий обчислювальний бюджет субкрокінг збігається значно швидше, бо член комплаєнсу α̃ = α / h² зменшується разом з h — менші субкроки автоматично роблять кожне обмеження жорсткішим і точнішим, без переналаштування жодного параметра.

Практичне правило: 20–30 субкроків з однією ітерацією кожен збігаються краще, ніж 1 субкрок зі 100 ітераціями, за приблизно ту саму вартість, і — що критично — результуюча жорсткість залишається коректною незалежно від кількості обраних субкроків, бо α — це фізична константа, а не регулятор для "виглядає достатньо жорстко".

6. Демпфування без багів втрати енергії

Наївне демпфування швидкості після розв'язання (v *= 0.98) видаляє енергію нерівномірно і може знову зробити симуляцію залежною від частоти кадрів. Натомість XPBD рекомендує демпфування на основі обмежень: додати невелику додаткову корекцію позиції, пропорційну відносній швидкості вздовж градієнта обмеження, масштабовану комплаєнсом демпфування β та субкроком h:

Δx_damp = −β · h · (∇C · v_rel) · ∇C / (Σ wᵢ|∇C|² ) застосовується в тому ж субкроці, одразу після пружної корекції

Це утримує демпфування пропорційним фактичній відносній швидкості на обмеженні (як фізичний демпфер/амортизатор), тож симуляція тканини, запущена на 20 субкроках, і та, що на 30, осідають до однакової форми спокою з однаковою швидкістю.

7. Повна реалізація: підвісний ланцюг

Зберемо все разом — ланцюг частинок, з'єднаних обмеженнями відстані, з першою частинкою закріпленою (invMass = 0):

class Particle {
  constructor(x, y, invMass = 1) {
    this.pos = { x, y };
    this.prevPos = { x, y };
    this.vel = { x: 0, y: 0 };
    this.invMass = invMass;
  }
}

class DistanceConstraint {
  constructor(p1, p2, compliance = 0) {
    this.p1 = p1; this.p2 = p2;
    this.restLength = Math.hypot(p1.pos.x - p2.pos.x, p1.pos.y - p2.pos.y);
    this.compliance = compliance; // 0 = жорстке, більше = розтяжніше
    this.lambda = 0;
  }
}

// Побудова ланцюга з 12 ланок, перша ланка закріплена на місці
const chain = [];
for (let i = 0; i < 12; i++)
  chain.push(new Particle(i * 0.2, 2, i === 0 ? 0 : 1));

const links = [];
for (let i = 0; i < chain.length - 1; i++)
  links.push(new DistanceConstraint(chain[i], chain[i + 1], 0.0001)); // злегка податлива сталь

// Головний цикл (викликати раз на відрендерений кадр, dt ≈ 1/60)
function simulate(dt) {
  xpbdStep(chain, links, dt, 24); // 24 субкроки
}
Налаштування комплаєнсу: сталевий трос ≈ 1e-8 – 1e-7 м/Н, нейлонова мотузка ≈ 1e-6 – 1e-5, м'яка гумова стрічка ≈ 1e-4 – 1e-3. Задайте α = 0 для нерозтяжного обмеження, ідентичного класичному PBD.

8. Застосування: тканини, стержні та м'які тіла

9. Типові пастки

Для подальшого читання: Macklin, Müller, Chentanez, "XPBD: Position-Based Simulation of Compliant Constrained Dynamics" (MIG 2016), та подальша робота "Small Steps in Physics Simulation" (SCA 2019), яка формалізує аргумент про субкроки проти ітерацій із розділу 5 вище.