Cannon-es: архітектура фізичного двигуна
Cannon-es — легкий JavaScript-двигун твердих тіл, на якому працюють кілька симуляцій цього сайту: підвіска автомобіля, руйнування, мотузки, шестерні. Розберемо, як насправді влаштовані його внутрішні частини — World, Body, Shape, Broadphase, Solver і Constraint.
1. Чому саме Cannon-es
Cannon-es — підтримуваний TypeScript-форк
оригінальної бібліотеки cannon.js Стефана Хедмана.
Це чистий JS-двигун фізики твердих тіл (без WASM), розрахований на
комфортну роботу у вкладці браузера поруч із рендерером Three.js.
Він жертвує частиною продуктивності й повноти функцій порівняно з
двигунами на кшталт Rapier чи Ammo.js (Bullet, скомпільований у
WASM) — натомість дає значно менший розмір і просту, зрозумілу
об'єктну модель. Саме тому він так часто зустрічається в освітніх
і невеликих інтерактивних симуляціях.
Розуміти цю архітектуру варто, навіть якщо ви ніколи не читаєте вихідний код: кожен двигун твердих тіл — Cannon-es, Rapier, Bullet, Havok — побудований з тих самих кількох блоків. Вивчіть їх один раз тут, і та сама модель мислення перенесеться будь-куди.
2. Об'єкт World
Усе в Cannon-es живе всередині одного екземпляра
World. Це контейнер верхнього рівня, який тримає
список тіл, вектор гравітації, реалізацію broadphase, solver і
список активних constraints та контактних матеріалів.
world.gravity.set(0, -9.82, 0)
world.broadphase = new CANNON.SAPBroadphase(world)
world.solver.iterations = 10
Виклик world.step(dt) раз за кадр анімації просуває
кожне тіло: запускає broadphase, генерацію контактів narrow-phase,
constraint solver і, нарешті, інтегрує позиції та швидкості на
dt вперед.
3. Body і Shape
Body — тверде тіло з масою, позицією, орієнтацією
(кватерніон), лінійною та кутовою швидкістю, демпфуванням і
матеріалом. Тіло володіє однією або кількома
Shape (box, sphere, cylinder, plane, опукла
поліедрична фігура, trimesh, або компаунд із кількох форм) —
вони визначають геометрію зіткнення, свідомо відокремлену від
будь-якого візуального меша, тож той самий невидимий бокс
зіткнення може приводити в рух значно детальнішу модель Three.js.
body.addShape(new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5)))
body.position.set(0, 5, 0)
world.addBody(body)
Маса 0 позначає тіло як
статичне — нескінченна ефективна інерція, ніколи
не рухається силами чи зіткненнями (площина землі, стіна). Кожне
динамічне тіло обчислює власний тензор інерції зі своїх форм, щоб
крутний момент давав правильне кутове прискорення.
4. Broadphase
При N тілах перевірка кожної пари на зіткнення — O(N²): нормально для десятка тіл, руйнівно для тисячі. Задача broadphase — дешево відкинути пари, що очевидно не торкаються (їхні bounding box не перетинаються), перш ніж запуститься дорога математика narrow-phase. Cannon-es постачається з трьома стратегіями broadphase:
| Клас | Підхід | Найкраще для |
|---|---|---|
| NaiveBroadphase | Груба сила O(N²) | < ~50 тіл, дебаг |
| GridBroadphase | Комірки рівномірної сітки | Рівномірно розподілені тіла |
| SAPBroadphase | Sweep-and-prune на відсортованих AABB | Загальне призначення, вибір за замовчуванням |
Повну картину того, як насправді працюють sweep-and-prune, сітки та BVH-дерева — і як алгоритми narrow-phase SAT/GJK/EPA переймають естафету звідти — дивіться у статті Broad-phase та narrow-phase виявлення зіткнень.
5. Solver
Коли пари контактів відомі, Cannon-es генерує
рівняння — по одному на точку контакту, плюс
рівняння тертя, плюс по одному на кожен активний constraint.
Стандартний GSSolver (Гаусс-Зейдель) — ітеративний
solver: він проходить список рівнянь повторно, потроху коригуючи
швидкість кожного тіла так, щоб усі обмеження задовольнялися
одночасно, замість того щоб точно розв'язувати одну величезну
лінійну систему.
Точний розв'язок сотень одночасних контактних обмежень — дорогий і валідний лише для однієї миті: набір контактів усе одно змінюється щокадру. Кілька ітерацій Гаусса-Зейделя (за замовчуванням — 10) дають візуально переконливий результат за частку вартості — це стандартний компроміс у кожному двигуні реального часу, не лише в Cannon-es.
Більше ітерацій означає жорсткіші, точніші стеки й шарніри за
вищу ціну CPU на кадр — прямий регулятор між якістю і
продуктивністю, відкритий як world.solver.iterations.
6. Constraints
Крім контактів, Cannon-es надає явні типи шарнірів, що з'єднують два тіла з обмеженим взаємним рухом:
- PointToPointConstraint — куля-в-гнізді, фіксує одну локальну точку кожного тіла разом (сегменти мотузки, стегна ragdoll).
- HingeConstraint — один обертальний ступінь свободи (двері, колеса, шестерні — див. симуляцію Механізми).
- DistanceConstraint — тримає два тіла на фіксованій відстані, дешевше за повний point-to-point.
- ConeTwistConstraint — шарнір з обмеженим конусом розмаху, для суглобів плеча/стегна в ragdoll.
- LockConstraint — жорстко зливає два тіла, корисно для тимчасового об'єднання фрагментів (див. конвеєр руйнування нижче).
Кожен constraint перетворюється на одне чи кілька рівнянь для того
самого GSSolver, описаного вище — архітектурно
шарнір — це просто контакт, який ніколи не розривається.
7. Цикл step()
Кожен виклик world.step(dt) запускає ті самі п'ять фаз по порядку:
// Спрощений конвеєр world.step() у Cannon-es
function step(dt) {
applyForces(bodies); // гравітація, сили користувача, пружини
const pairs = broadphase.collisionPairs(world);
const contacts = narrowphase(pairs); // точні контактні точки + нормалі
solver.solve(dt, contacts, constraints); // ітерації Гаусса-Зейделя
integrate(bodies, dt); // напів-неявний (симплектичний) Ейлер
}
Останній крок інтегрування використовує напів-неявний Ейлер: спершу з (щойно розв'язаних) сил оновлюється швидкість, потім позиція оновлюється за новою швидкістю. Це метод першого порядку точності, але симплектичний — він не «зливає» енергію так, як явний Ейлер. Чому ця різниця важлива для тривалих симуляцій, читайте у статті Верле, Leapfrog та RK4.
8. «Сплячі» тіла
Симулювати тіло, яке не рухалося двадцять кадрів, — марна трата
CPU. Cannon-es відстежує кінетичну енергію кожного тіла за
короткий проміжок часу; якщо вона залишається нижче
sleepSpeedLimit довше за
sleepTimeLimit, тіло позначається як
SLEEPING і повністю пропускається інтегруванням і
solver'ом — доки рухоме тіло знову його не торкнеться, що будить
його. Це критично важливо для сцен із десятками нерухомих
ящиків, купами уламків чи фрагментами руйнування, що зрештою
зупиняються.
9. Типові пастки
Стандартна narrow-phase у Cannon-es дискретна: вона перевіряє
позиції на початку й кінці кадру, але не між ними. Швидкий
снаряд (автомобіль на 40 м/с проти тонкої стіни) може пройти
наскрізь тонкий колайдер між двома кадрами. Збільшіть
iterations тіла, потовщіть тонкі колайдери, або
зменшіть dt за допомогою фіксованого
субкрокового кроку часу.
Колайдери trimesh підтримують генерацію контактів narrow-phase лише проти обмеженого набору форм і набагато дорожчі за компаунд опуклих примітивів (box, sphere, cylinder). Де можливо, декомпозуйте складну геометрію на кілька опуклих форм — саме так робить симуляція Solid Body Fracture, коли розділяє меш за методом Вороного на опуклі шматки.
Побачити Cannon-es у дії
Підвіска автомобіля, руйнування твердих тіл, ланцюжок і мотузка та колесо Максвелла на цьому сайті — усі працюють на Cannon-es.
🚗 Фізика автомобіля 🧱 Руйнування твердих тіл