Симуляція тканини методом інтегрування Верле
Тканина виглядає оманливо просто — тонкий гнучкий лист — проте переконлива її симуляція потребує одночасного опрацювання тисяч зв'язаних зв'язків, гравітації, опору повітря, зіткнень і самоперетину. Модель пружина-маса з інтегруванням Верле, популяризована Томасом Якобсеном у його доповіді на GDC 2001 року, дає стабільну, візуально переконливу тканину завдяки напрочуд простому алгоритму, що працює в реальному часі.
1. Модель пружина-маса
Тканина дискретизується як сітка частинок (точкових мас), з'єднаних пружинами. Три типи пружин відтворюють механіку тканини:
Структурні
З'єднують кожну частинку з 4 горизонтальними/вертикальними сусідами. Протидіють розтягу й стисненню — задають форму спокою тканини.
Зсувні
З'єднують кожну частинку з 4 діагональними сусідами. Протидіють зсувній деформації, що дозволяє квадратним коміркам ставати паралелограмами без розтягу.
Згинальні
З'єднують кожну частинку із сусідами через один крок (пропускаючи одного). Протидіють згинанню / складанню — надають тканині жорсткості проти заломів.
Розрив (опційно)
Прибирають пружини, коли розтяг перевищує поріг. Уможливлюють ефекти розриву тканини за невеликої додаткової вартості.
2. Інтегрування Верле
Стандартне інтегрування Ейлера накопичує похибку й може ставати нестійким для жорстких пружин. Інтегрування Верле симплектичне — добре зберігає енергію й безумовно стійке за достатньо малих кроків:
Ключова ідея в тому, що швидкість виводиться неявно з історії положень — немає окремої змінної швидкості, яка б накопичувала похибки. Це робить Верле унікально стійким для симуляцій на основі зв'язків.
3. Типи зв'язків
Зв'язок відстані між двома частинками p₁ та p₂ з довжиною спокою d₀ проєктує обидві частинки назовні/всередину, щоб відновити їхню відстань до d₀:
Зв'язки — це зв'язки нерозтяжності: вони забезпечують максимальну відстань (тканина не розтягується понад довжину спокою). Стиснення зазвичай дозволене — тканина може зморщуватися, але не може мати від'ємну довжину.
4. Релаксація зв'язків (метод Якобсена)
За багатьох переплетених зв'язків задоволення одного порушує інші. Підхід Якобсена: ітерувати по всіх зв'язках кілька разів за кадр. Кожен прохід наближає систему до одночасного задоволення всіх зв'язків:
Це еквівалентно ітеративному розв'язанню системи зв'язків методом Гаусса–Зейделя. Воно збігається квадратично поблизу розв'язку й стійке до перевизначених чи майже вироджених конфігурацій, які поклали б матричні розв'язувачі.
5. Зіткнення зі сферою та підлогою
Реакція на зіткнення частинки зі сферою з центром C та радіусом R: якщо частинка всередині сфери, спроєктувати її на поверхню:
У цьому витонченість Верле: реакція на зіткнення — це лише корекція положення. Жодної зміни швидкості не потрібно — інтегратор виводить нову швидкість зі скоригованих положень.
6. Самоперетин
Запобігання проходженню тканини крізь саму себе витратне. Наївний підхід (перевіряти кожну пару трикутників) має складність O(n² × m²). Промислові рішення:
- Просторове хешування: Зіставити частинки з хеш- сіткою; перевіряти лише близькі частинки й трикутники в межах тієї самої або сусідніх комірок. Очікувано O(n).
- Сили відштовхування: Додати м'яке відштовхування між близькими парами частинок — запобігає більшості самоперетинів без явного розв'язання.
- Неперервне виявлення зіткнень (CCD): Перевіряти заметені трикутники (за крок часу) на перетин. Витратно, але необхідно для швидкорухомої тканини (прапори на вітрі).
- Обмеження деформації (надійне): Обмежити покрокову зміну положення часткою діаметра частинки — тканина не може «телепортуватися» крізь себе.
Для інтерактивних демо сили відштовхування + збільшена кількість ітерацій зв'язків прийнятно опрацьовують 90% видимих артефактів самоперетину.
7. Повна реалізація на JavaScript
// Симуляція тканини — інтегрування Верле + релаксація зв'язків
class Particle {
constructor(x, y, z) {
this.pos = {x, y, z};
this.prev = {x, y, z};
this.acc = {x: 0, y: 0, z: 0};
this.pinned = false;
}
integrate(dt, damping = 0.99) {
if (this.pinned) return;
const {pos, prev, acc} = this;
const vx = (pos.x - prev.x) * damping;
const vy = (pos.y - prev.y) * damping;
const vz = (pos.z - prev.z) * damping;
prev.x = pos.x; prev.y = pos.y; prev.z = pos.z;
pos.x += vx + acc.x * dt * dt;
pos.y += vy + acc.y * dt * dt;
pos.z += vz + acc.z * dt * dt;
acc.x = 0; acc.y = 0; acc.z = 0;
}
}
class Constraint {
constructor(a, b) {
this.a = a; this.b = b;
const dx = b.pos.x-a.pos.x, dy = b.pos.y-a.pos.y, dz = b.pos.z-a.pos.z;
this.rest = Math.sqrt(dx*dx + dy*dy + dz*dz);
this.torn = false;
}
satisfy(tearThreshold = Infinity) {
if (this.torn) return;
const {a, b} = this;
const dx = b.pos.x - a.pos.x;
const dy = b.pos.y - a.pos.y;
const dz = b.pos.z - a.pos.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) || 0.0001;
if (dist > this.rest * tearThreshold) { this.torn = true; return; }
const diff = (dist - this.rest) / dist;
if (!a.pinned) { a.pos.x += dx * diff * 0.5; a.pos.y += dy * diff * 0.5; a.pos.z += dz * diff * 0.5; }
if (!b.pinned) { b.pos.x -= dx * diff * 0.5; b.pos.y -= dy * diff * 0.5; b.pos.z -= dz * diff * 0.5; }
}
}
class Cloth {
constructor(cols, rows, spacing) {
this.cols = cols; this.rows = rows;
this.particles = [];
this.constraints = [];
for (let r = 0; r < rows; r++)
for (let c = 0; c < cols; c++) {
const p = new Particle(c * spacing, -r * spacing, 0);
if (r === 0) p.pinned = true; // закріпити верхній ряд
this.particles.push(p);
}
const at = (r, c) => this.particles[r * cols + c];
for (let r = 0; r < rows; r++)
for (let c = 0; c < cols; c++) {
if (c < cols-1) this.constraints.push(new Constraint(at(r,c), at(r,c+1))); // структ. гориз.
if (r < rows-1) this.constraints.push(new Constraint(at(r,c), at(r+1,c))); // структ. верт.
if (c < cols-1 && r < rows-1) { // зсувні
this.constraints.push(new Constraint(at(r,c), at(r+1,c+1)));
this.constraints.push(new Constraint(at(r,c+1), at(r+1,c)));
}
if (c < cols-2) this.constraints.push(new Constraint(at(r,c), at(r,c+2))); // згинальні гориз.
if (r < rows-2) this.constraints.push(new Constraint(at(r,c), at(r+2,c))); // згинальні верт.
}
}
update(dt, gravity = -980, iters = 5, sphere = null) {
// Застосувати гравітацію
for (const p of this.particles) { if (!p.pinned) p.acc.y += gravity; }
// Проінтегрувати
for (const p of this.particles) p.integrate(dt);
// Релаксація зв'язків
for (let i = 0; i < iters; i++)
for (const c of this.constraints) c.satisfy();
// Зіткнення зі сферою
if (sphere) for (const p of this.particles) {
const dx = p.pos.x-sphere.x, dy = p.pos.y-sphere.y, dz = p.pos.z-sphere.z;
const len = Math.sqrt(dx*dx+dy*dy+dz*dz);
if (len < sphere.r) {
p.pos.x = sphere.x + dx/len*sphere.r;
p.pos.y = sphere.y + dy/len*sphere.r;
p.pos.z = sphere.z + dz/len*sphere.r;
}
}
}
}
// Використання: тканина 20×20, підвішена за верхній ряд
const cloth = new Cloth(20, 20, 15);
const sphere = {x: 140, y: -150, z: 0, r: 80};
function loop() {
cloth.update(0.016, -980, 5, sphere);
// ... рендеринг через canvas або Three.js
requestAnimationFrame(loop);
}
loop();
8. Розширення та подальше читання
- Сили вітру: Застосуйте силу, перпендикулярну до кожного трикутника тканини, пропорційну скалярному добутку нормалі поверхні на вектор вітру.
- Неявне інтегрування: Бараф і Віткін (1998) — розв'язує розріджену лінійну систему за крок, дозволяючи у 10 разів більші кроки за часом ціною одного лінійного розв'язання. Використовується у промисловій графіці (Houdini, Maya Nucleus).
- Динаміка на основі позицій (PBD): Мюллер та ін. (2007) — розширює каркас Якобсена на тверді тіла, рідини та деформовні об'єкти в єдиному проєкційному підході. Використовується у NVIDIA PhysX, Unreal Chaos.
- XPBD: Розширений PBD (2016) відокремлює жорсткість зв'язку від кількості ітерацій, уможливлюючи піддатливі зв'язки.
- Тканина на скінченних елементах: Оболонкові елементи з матеріальними моделями Сен-Венана–Кірхгофа або нео-Гукеана. Точно, але витратно — використовується для офлайн- симуляції одягу.