Розділ 6. Фізичні бібліотеки
Бібліотека означає віри акт
В якому покоління, що і досі темрявою оповиті
Засвідчують в пітьмі, що стануть свідками свого світанку.
— Віктор Гюго
В індійському штаті Мегхалая, люди племен Кхасі й Джайнті живуть у місцевостях з одним із найбільших рівнів опадів серед усього світу. Під час сезону мусонів повені часто унеможливлюють подорож між селами. У результаті виникла давня традиція будування живих кореневих мостів. Ці мости створюють, направляючи та пророщуючи коріння дерев через бамбук, пальмові стовбури або сталеві риштування. Вони ростуть і міцніють у міру взаємодії коріння з навколишнім середовищем, утворюючи адаптовані та пружні з’єднання.
Подумайте про те, чого ви вже досягли з цією книгою:
- Дізналися про концепції зі світу фізики (що таке вектор, сила, хвиля тощо)
- Зрозуміли математику й алгоритми, що стоять за цими концепціями
- Реалізували ці алгоритми в p5.js з об’єктно-орієнтованим підходом, що дозволило вам створювати симуляції автономних керованих агентів
Ці дії призвели до набору симуляцій руху, які дозволяють вам творчо визначати фізику світів, які ви будуєте (реалістичних чи фантастичних). Але, звісно, ми з вами не перші й не єди ні люди, хто це робить. Світ комп’ютерної графіки й програмування сповнений вже написаних бібліотек коду для фізичних симуляцій.
Просто спробуйте пошукати open-source physics engine і ви можете провести решту свого дня, переглядаючи безліч складних та багатих бібліотек коду. Напрошується запитання: якщо готові бібліотеки коду вже дбають про фізичні симуляції, навіщо вам самостійно вчитися писати будь-які алгоритми? Ось тут проявляється філософія цієї книги. Попри те, що багато бібліотек надають готові рішення для експериментування з фізичними симуляціями, є кілька вагомих причин для вивчення основи з нуля перед зануренням у такі бібліотеки.
По-перше, без розуміння векторів, сил і тригонометрії легко загубитися, просто читаючи документацію бібліотеки, не кажучи вже про її використання. По-друге, навіть якщо бібліотека може подбати про математику під капотом, це не обов’язково спростить ваш код. Вивчення того, як працює бібліотека і що вона очікуватиме від вас при програмуванні, може накладати додаткові ускладнення. Нарешті, немає значення наскільки чудовим буде готовий фізичний рушій, адже глибоко у своєму серці ви, швидше за все, прагнете створювати світи та візуалізації, які розширюють межі уяви. Тож, хоча бібліотека може бути хорошою, вона надає лише обмежений набір функціональності. При роботі над творчим програмним проєктом важливо розуміти, коли можна працювати у межах цих рамок, а коли вони можуть бути обмежувальними.
Цей розділ присвячений дослідженню двох фізичних бібліотек JavaScript з відкритим кодом: Matter.js і Toxiclibs.js. Я не маю на увазі, що для будь-яких творчих проєктів де корисним може бути фізичний рушій варто користуватись лише цими бібліотеками (щоб дізнатися про деякі альтернативи перегляньте секцію “Інші фізичні бібліотеки” і перевірте вебсайт книги щодо додаткових прикладів розділу, зроблених за допомогою інших бібліотек). Однак обидві вони чудово інтегруються з p5.js та дозволяють мені продемонструвати основні концепції, що лежать в основі фізичних рушіїв і те, як вони пов’язані й побудовані на основі матеріалу, який я вже розглянув до цього часу.
Зрештою, мета цього розділу полягає не у навчанні подробиць конкретної фізичної бібліотеки, а у наданні основи для роботи з будь-якою фізичною бібліотекою. Навички, які ви тут набудете, дозволять вам розбиратися і розуміти документацію, відкриваючи двері для розширення ваших можливостей з будь-якою обраною бібліотекою.
Навіщо використовувати фізичну бібліотеку?
Я вже виклав аргументи на користь написання власних фізичних симуляцій (які ви навчились робити в попередніх розділах), але які аргументи є на користь використання фізичної бібліотеки? Зрештою, щоразу, коли ви додаєте до проєкту ферймоворк чи бібліотеку, це вносить певне ускладнення і додатковий код. Чи ці додаткові ускладнення справді того варті? Наприклад, якщо вам просто потрібно змоделювати падіння кульки під дією сили тяжіння, чи справді вам потрібно імпортувати цілий фізичний рушій і вивчати його API? Сподіваюся, як показали перші розділи цієї книги, ймовірно, ні. Багато подібних сценаріїв, достатньо прості, щоб ви могли реалізувати їх самостійно.
Але розглянемо інший сценарій. Що, якщо ви хочете, щоб падало 100 кульок? І що, якщо замість кульок потрібні багатокутники неправильної форми? І що, якщо ви хочете, щоб ці багатокутники реалістично відштовхувались один від одного під час зіткнення?
Можливо, ви помітили, що хоча я докладно розглянув рух і сили, я поки що пропустив досить важливий аспект фізичного моделювання: зіткнення. Давайте на мить уявимо, що ви не читаєте розділ про фізичні бібліотеки і я вирішив прямо зараз пояснити, як справлятися із зіткненнями в системі частинок. Я мав би охопити два різних алгоритми, які відповідають на ці запитання:
- Як визначити, що дві фігури стикаються (або перетинаються)? Тут потрібен алгоритм виявлення зіткнень.
- Як визначити швидкості фігур після зіткнення? Тут потрібен алгоритм розв’язання зіткнень.
Якщо ви працюєте з простими геометричними фігурами, перше питання не дуже складне. Насправді ви стикалися з подібним раніше. Для двох кульок, наприклад, стан перетину відбувається, коли відстань між їх центрами менша за суму їхніх радіусів (див. малюнок 6.1).
Це досить легко, а як щодо обчислення швидкостей кульок після зіткнення? На цьому я збираюся завершити обговорення. Чому? Справа не в тому, що розуміння математики, яка стоїть за зіткненнями, не важлива або не цінна. (Насправді я включаю додаткові приклади на вебсайті, пов’язані із зіткненнями без використання фізичної бібліотеки.) Причина зупинки полягає в тому, що життя коротке! (Нехай це також буде причиною для вас, щоб вийти на вулицю і трохи прогулятися перш ніж сісти писати свою наступну програму.) Не слід очікувати, що ви опануєте кожний можливий аспект фізичного моделювання. І хоча ви можете насолоджуватися вивченням розв’язків зіткнень для кульок, це лише спонукатиме у вас бажання перейти далі до роботи з прямокутниками. А потім до багатокутників складнішої форми. А потім до фігур з криволінійними поверхнями. А потім до маятників, що стикаються з пружними пружинами. А потім, а потім, а потім...
Можливість включити у програму p5.js складніший функціонал, такий як зіткнення, і при цьому мати необхідний час на спілкування з друзями й родиною — ось для чого цей розділ. Люди витратили роки на розробку рішень такого роду проблем і чудові JavaScript-бібліотеки, такі як Matter.js та Toxiclibs.js є плодами цих зусиль. Тож немає потреби заново винаходити колесо, принаймні поки що.
На завершення, якщо ви описуєте ідею для програми p5.js і згадуєте слово зіткнення, то, ймовірно, настав час навчитися використовувати фізичний рушій.
Інші фізичні бібліотеки
Існує безліч інших фізичних бібліотек, вартих дослідження поруч із тими двома, що будуть розглянуті у цьому розділі, кожна з унікальними перевагами, які можуть надати вигоди в певних типах проєктів. Насправді коли я тільки почав писати цю книгу, бібліотеки Matter.js не існувало, тому фізичним рушієм, який я спочатку використовував для демонстрації прикладів, був Box2D. Це був (і, ймовірно, залишається) найвідоміший фізичний рушій з усіх.
Box2D розпочався як набір уроків з фізики, написаних Еріном Катто на C++ для конференції розробників ігор у 2006 році. З того часу Box2D перетворився на багатий і складний фізичний рушій з відкритим кодом. Його використовували у безлічі проєктах та іграх, особливо вдалими з яких є визнана нагородами Crayon Physics і неперевершений хіт Angry Birds.
Однією з важливих особливостей Box2D є те, що це справжній фізичний рушій: він нічого не знає про комп’ютерну графіку і світ пікселів, а натомість виконує всі вимірювання та обчислення в реальних одиницях, таких як метри, кілограми та секунди. Просто його “світ” (ключовий термін у Box2D) — це 2D площина з верхнім, нижнім, лівим і правим краями. Ви говорите йому щось на кшталт: “Гравітація світу становить 9.81 ньютона на кілограм, а коло з радіусом 4 метри й масою 50 кілограмів розташоване на висоті 10 метрів від нижнього краю землі”. Box2D потім повідомить вам такі речі, як: “Через секунду коло знаходиться на відстані 8 метрів від нижнього краю, через дві секунди на відстані 5 метрів...” і так далі.
Попри те, що це забезпечує надзвичайно точний і надійний фізичний рушій (дуже оптимізований і швидкий для проєктів на C++), він також вимагає багато складного коду для переведення взаємодії між фізичним світом і світом, який ви хочете намалювати — піксельним світом графічного полотна. Це створює величезне навантаження для кодера. Наскільки зможу, я продовжуватиму підтримувати для цієї книги набір прикладів, сумісних із Box2D (є кілька портів на JavaScript), але вважаю, що відносна простота роботи з бібліотеками типу Matter.js, яка є нативною для JavaScript і використовує пікселі як одиницю вимірювання, зробить мої приклади на p5.js більш інтуїтивно зрозумілими й дружніми.
Іншою відомою бібліотекою є p5play — проєкт ініційований Паоло Педерчіні та наразі очолюваний Квінтоном Ешлі, який був спеціально розроблений для розробки ігор. Бібліотека спрощує створення візуальних об’єктів, відомих як спрайти й керує їх взаємодією, зокрема зіткненнями та перекриттями. Як ви, можливо, здогадалися з назви, p5play створена для зручної роботи з p5.js. Для симуляції фізики вона під капотом викорис товує Box2D.
Імпорт бібліотеки Matter.js
За мить я перейду до роботи з Matter.js, створеної Ліамом Броммітом у 2014 році. Але перш ніж ви зможете використовувати зовнішню бібліотеку JavaScript, вам потрібно імпортувати її у свій проєкт. Як ви вже добре знаєте, для розробки та поширення прикладів коду цієї книги, я використовую офіційний вебредактор p5.js. Найпростіший спосіб додати бібліотеку — відредагувати файл index.html, який є частиною кожної нової програми p5.js, створеної у редакторі.
Для цього спочатку розгорніть панель навігації файлів у лівій частині редактора і виберіть index.html, як показано на малюнку 6.2.
Файл містить послідовність тегів <script>
усередині HTML тегів <head>
і </head>
. Саме так в проєкті p5.js підключаються JavaScript-бібліотеки. Це практично нічим не відрізняється від підключення файлів sketch.js
або particle.js
у <body>
, тільки тут, замість збереження і редагування копії самого JavaScript коду, на код бібліотеки відбувається посилання через URL-адресу мережі доправлення вмісту (CDN). Це тип серве ра для розміщення файлів. Для JavaScript-бібліотек, які використовуються на сотнях тисяч вебсторінок, до яких мають доступ мільйони користувачів, CDN-сервери повинні дуже добре справлятися зі своєю роботою передачі цих файлів.
Ви вже повинні були побачити тег <script>
із посиланням на CDN для самого p5.js (на час вашого читання це може бути новіша версія):
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js"></script>
Для підключення Matter.js додайте ще один тег <script>
із посиланням на його CDN прямо під підключенням p5.js:
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
На момент написання цього тексту останньою версією Matter.js була 0.19.0
, саме та версія, яку я використав у наведеному вище фрагменті. При оновленнях Matter.js і випуску нових версій часто доцільною ідеєю є оновити бібліотеку проєкту, але посилаючись на конкретну версію про яку ви знаєте, що вона працює з вашим кодом, ви не повинні турбуватися, що нові функції бібліотеки порушать ваш поточну програму.
Огляд Matter.js
Коли ви використовуєте Matter.js (або будь-який інший фізичний рушій) у p5.js, ваш код виглядає дещо інакше. Ось узагальнений псевдокод усіх прикладів у розділах з 1-го по 5-й:
setup()
- Створення всіх об’єктів світу.
draw()
- Розрахунок усіх сил світу.
- Застосування всіх сил до об’єктів ().
- Оновлення положення всіх об’єктів на основі їх прискорення.
- Малювання всіх об’єктів.
Для порівняння ось псевдокод для прикладу з Matter.js:
setup()
- Створення всіх об’єктів світу.
draw()
- Малювання всіх об’єктів.
Це, звісно, привабливість фізичного рушія. Я видалив усі ті болючі кроки обчислення того, як об’єкти рухаються відповідно до швидкості та прискорення. Matter.js подбає про це за мене!
Хоча буде ще багато деталей для розкриття, хороша новина полягає в тому, що простота цього псевдокоду точно відображає загальний процес. У цьому сенсі Matter.js трохи схожа на чарівну скриньку. У функції setup()
я скажу до Matter: “Привіт! Ось усі речі, які я хочу у своєму світі”. А потім, у функції draw()
, я ввічливо попрошу Matter: “О, привіт ще раз! Якщо це не дуже складно, я хотів би намалювати всі ці речі у своєму світі. Скажи мені, будь ласка, де вони знаходяться?”
Погана новина: це не так просто, як може здатися із псевдокоду. Насправді створення об’єктів, які потрапляють у світ Matter.js, включає кілька кроків, пов’язаних з тим, як будуються і налаштовуються різні типи фігур.
Також необхідно навчитися говорити мовою Matter.js щодо налаштувань різних сил та інших параметрів світу. Ось основні поняття:
- Рушій (двигун): сутність, яка керує самою фізичною симуляцією. Рушій зберігає “світ” симуляції, а також різні властивості про те, як світ оновлюється з часом.
- Тіла: слугують основними елементами світу, які відповідають фізичним об’єктам, що моделюються. Тіла мають положення і швидкість. Звучить знайомо? По суті, це ще одна версія класу, який я будував протягом усіх розділів з 1-го по 5-й. Тіла також мають геометрію для визначення їх форми. Важливо зауважити, що тіло — це загальний термін, який фізичні рушії використовують для опису речі у світі (подібно до терміну частинка), і це не має відношення до антропоморфного тіла.
- Композит: контейнер, який дозволяє створювати складні сутності, що складаються з кількох тіл. Сам світ є прикладом композиту і кожне створене тіло має бути додане до світу.
- Обмежувачі (обмеження): діють як з’єднання між тілами.
У наступних розділах я детально розгляну кожен з цих елементів, будуючи в процесі кілька прикладів. Але спочатку є ще один важливий елемент, який варто коротко обговорити:
- В ектор: описує об’єкт із магнітудою і напрямком за допомогою -компонентів, що визначають позиції, швидкості та сили у світі Matter.js.
Це підводить нас до важливого роздоріжжя. Будь-яка фізична бібліотека фундаментально побудована довкола концепції векторів, і залежно від того, як ви це сприймаєте, це може бути як плюсом, так і мінусом. Позитивна сторона полягає в тому, що ви щойно провели кілька розділів, знайомлячись із тим, як описувати рух і сили за допомогою векторів, тому концептуально нічого нового вчити не потрібно. Негативна сторона, яка змушує мене зронити сльозу, полягає в тому, що як тільки ви переступите цей поріг у чудовий новий світ фізичних бібліотек, ви більше не зможете використовувати p5.Vector
.
Чудово, що p5.js має вбудоване векторне представлення, але кожного разу, коли ви користуєте ся фізичною бібліотекою, ви, швидше за все, виявите, що вона включає власну реалізацію вектора, розроблену для особливої сумісності з рештою коду бібліотеки. Це має сенс. Зрештою, чому Matter.js повинен знати про об’єкти p5.Vector
?
У підсумку все зводиться до того, що вам не доведеться вивчати нові концепції, але доведеться звикнути до деяких нових конвенцій іменування і синтаксису. Для ілюстрації я покажу деякі вже знайомі вам операції класу p5.Vector
поруч з еквівалентним кодом для Matter.Vector
. Для початку поглянемо, як створити вектор:
p5.js | Matter.js |
---|---|
|
|
А як додати два вектори?
p5.js | Matter.js |
---|---|
|
|
Попередній приклад перезаписує вектор a
отриманим результатом. А ось приклад як помістити результат операції в окремий вектор:
p5.js | Matter.js |
---|---|
|
|
Як щодо масштабування вектора (множення на скалярне значення)?
p5.js | Matter.js |
---|---|
|
|
Як отримати магнітуду і нормалізацію?
p5.js | Matter.js |
---|---|
|
|
Як бачите, поняття ті самі, але специфіка коду відрізняється. По-перше, кожному імені методу тепер передує запис Matter.Vector
, що визначає простір імен вихідного коду. Це типово для JavaScript-бібліотек на відміну від p5.js. Наприклад, щоб намалювати круг у p5.js, ви просто викликаєте функцію circle()
, а не p5.circle()
. Функція circle()
знаходиться в глобальному просторі імен. Це, на мій погляд, одна з особливостей, яка робить p5.js особливим з точки зору простоти використання та зручності для по чатківців. Однак це також означає, що при роботі з p5.js, ви не можете використовувати для своїх змінних назви подібні до circle
. Простір імен бібліотеки захищає від такого роду помилок і конфліктів імен, тому ви побачите, що у Matter.js все викликається з префіксом Matter
.
Крім того, на відміну від статичних і нестатичних методів векторів p5.js, таких як add()
і mult()
, усі векторні методи в Matter.js статичні. Якщо під час використання Matter.Vector
ви хочете змінити вектор з яким працюєте, то можете додати його як необов’язковий третій аргумент: Matter.Vector.add(a, b, a)
додає a
і b
та поміщає результат у a
(третій аргумент). Ви також можете присвоїти результат, отриманий у результаті обчислення, вже наявній змінній: v = Matter.Vector.mult(v, 2)
. Однак це все одно створює новий вектор у пам’яті, а не оновлює старий.
У цьому розділі я розгляну деякі основні аспекти роботи з Matter.Vector
, але для отримання більшої інформації, можна звернутися до повної документації на вебсайті Matter.js.
Рушій
Багато фізичних бібліотек включають особливий об’єкт для керування усією симуляцією, що називається світом. Світ, як правило, відповідає за координатний простір, веде список усіх тіл симуляції, контролює час тощо. У Matter.js світ створюється всередині об’єкта Engine
— головному контролері симуляції вашого фізичного світу:
let Engine = Matter.Engine;
Посилання на Matter.Engine.
let engine;
Змінна для фізичного рушія Matter.js.
function setup() {
createCanvas(640, 360);
engine = Engine.create();
Створення рушія Matter.js.
}
Зверніть увагу, що перший рядок коду створює змінну Engine
і призначає їй значення Matter.Engine
. Тут я вирішив використати назву Engine
, що посилатиметься на відповідний клас Engine
у просторі імен Matter.js, щоб зробити свій код менш багатослівним. Це нормально, оскільки я знаю, що не використовуватиму слово Engine
для інших змінних і воно не конфліктує з чимось із p5.js. У прикладах я буду робити це і для класів Vector
, Bodies
, Composite
та інших. (Але хоча прикріплений вихідний код завжди включатиме усі потрібні посилання, я не завжди показуватиму їх у тексті книги.)
Коли ви викликаєте метод create()
через Engine
, Matter.js повертає новий фізичний рушій і світ з типовою гравітацією — вектором , що вказує вниз. Ви можете змінити це дефолтне значення, звернувшись до властивості gravity
і змінивши її значення:
engine.gravity.x = 1;
engine.gravity.y = 0;
Зміна гравітації рушія у горизонтальному напрямку.
Звісно, гравітація не мусить бути фіксованою протягом усієї симуляції. Ви можете змінювати вектор гравітації під час роботи програми. Ви також можете зовсім вимкнути гравітацію, встановивши для її вектора значення .
Деструктуризація об’єкта
Деструктуризація об’єкта у JavaScript — це спосіб отримання властивостей об’єкта і присвоєння їх в окремі змінні. У випадку з Matter.js об’єкт Matter
містить властивість Engine
. Зазвичай посилання для цієї властивості можна встановити за допомогою запису let Engine = Matter.Engine
, але з деструктуруванням це можна зробити більш лаконічно:
const { Engine } = Matter;
Зачекайте. Чи ви помітили, що я використав тут const
? Я знаю, що сказав ще у Розділі 0, що для оголошення змінних у цій книзі використовуватиму лише let
. Однак робота із зовнішньою бібліотекою — це дійсно хороший момент, щоб використати const
. У JavaScript, const
використовується для оголошення змінних, значення яких більше не повинні перепризначатись після ініціалізації. У цьому випадку я хочу захистити себе від випадкової можливості перезаписати змінну Engine
пізніше у коді, що скоріш за все усе зламало б!
Тепер подивимось, як цей синтаксис деструктуризації справді виблискує, коли вам потрібно створити посилання для кількох властивостей одного об’єкта:
const { Engine, Vector } = Matter;
Використання деструктуризації об’єкта при створенні окремих посилань на Engine і Vector.
Це робить із змінних Engine
і Vector
посилання на Matter.Engine
та Matter.Vector
і все це в одному рядку. Я буду використовувати цей підхід і у подальших прикладах цього розділу.
Після ініціалізації світу в нього саме час щось додати — тіла!
Тіла
Тіло є основним елементом у світі Matter.js. Це еквівалент класу Vehicle
, розвинутому на базі класу Particle
, що виник на основі класу Mover
, який я побудував у попередніх розділа х — об’єкт, який рухається у просторі й відчуває силу. Тіло також може бути статичним (зафіксованим і нерухомим).
Тіла Matter.js створюються за допомогою фабричних методів класу Matter.Bodies
, який має різні методи для створення різних типів тіл. Фабричний метод — це функція, яка створює об’єкт. Хоча ви, ймовірно, більше знайомі з викликом конструктора для створення об’єкта, наприклад з new Particle()
, ви вже бачили фабричні методи раніше: createVector()
це фабричний метод для створення об’єкта p5.Vector
. Чи створюється об’єкт за допомогою конструктора, чи фабричного методу, залежить від стилю і вибору творця бібліотеки.
Усі фабричні методи для створення тіл можна знайти на сторінці документації Matter.Bodies
. Я розпочну з методу rectangle()
:
let box = Bodies.rectangle(x, y, w, h);
Створення тіла Matter.js прямокутної форми.
Яка вдача! Сигнатура методу rectangle()
точно така ж, як у функції rect()
із p5.js. Однак у цьому випадку метод не малює прямокутник, а створює об’єкт геометрії класу Body
. (Зауважте, що виклик Bodies.rectangle()
спрацює лише якщо ви попередньо встановили посилання змінної Bodies
на Matter.Bodies
.)
Тепер маємо створене тіло з позицією і розміром, а посилання на нього зберігається у змінній box
. Однак тіла мають і багато інших властивостей, які впливають на їхній рух. Наприклад, є щільність, яка зрештою визначає масу цього тіла. Тертя і реституція (віддача) впливають на взаємодію тіла при контакті з іншими тілами. Для більшості випадків достатньо значень за замовчуванням, але Matter.js дозволяє вам налаштувати ці властивості, передавши додатковий аргумент до фабричного методу у формі літерала об’єкта JavaScript, набору із пар ключ-значення, розділених комами й огорнутих фігурними дужками:
let options = {
friction: 0.5,
restitution: 0.8,
density: 0.002,
};
Налаштування властивостей для тіла у літералі об’єкта.
let box = Bodies.rectangle(x, y, w, h, options);
Кожен ключ у літералі об’єкта (наприклад, friction
) служить унікальним ідентифікатором, а його значення (0.5
) є даними, пов’язаними з цим ключем. Літерал об’єкта можна розглядати як простий словник або пошукову таблицю, що у цьому випадку зберігає бажані налаштування для нового тіла Matter.js. Зауважте, що хоча аргумент options
корисний для налаштування тіла, інші початкові значення, такі як лінійна або кутова швидкість, можна встановити за допомогою статичних методів класу Matter.Body
:
const v = Vector.create(2, 0);
Body.setVelocity(box, v);
Body.setAngularVelocity(box, 0.1);
Встановлення дов ільних значень для лінійної та кутової швидкостей.
Створення тіла і збереження його у змінній недостатньо. Будь-яке тіло має бути явно додано до світу, щоб його можна було симулювати за допомогою фізики. Фізичний світ — це об’єкт класу Composite
, який називається world
і зберігається всередині об’єкту engine
. Додати до цього світу тіло box
можна за допомогою статичного методу add()
:
Composite.add(engine.world, box);
Додавання об’єкту box до світу рушія.
Цей додатковий крок легко забути — це помилка, яку я і сам неодноразово робив. Якщо ви коли-небудь будете гадати, чому один із ваших об’єктів не з’являється або не рухається разом із фізикою світу, завжди перевіряйте, що ви дійсно додали його до світу!
Вправа 6.1
Знаючи те, що ви вже знаєте про Matter.js, заповніть пропуск у наведеному нижче коді, який демонструє, як створити кругле тіло:
let options = {
friction: 0.5,
restitution: 0.8,
};
let ball = Bodies.circle(x, y, radius, options);
Рендер
Щойно тіло додано у світ, Matter.js завжди знатиме про нього, перевірятиме його на зіткнення і відповідним чином оновлюватиме його положення згідно з будь-якими силами середовища. Він зробить усе це за вас, вам навіть пальцем не потрібно ворушити! Але як саме ви можете намалювати тіло?
У наступній секції я покажу вам, як дізнатися у Matter.js про положення різних тіл, щоб намалювати цей світ у межах програми p5.js. Ці знання дають змогу контролювати вигляд ваших анімацій. Це ваш зірковий час: ви можете бути дизайнером свого світу, використовуючи свої творчі здібності й навички p5.js для візуалізації тіл, одночасно ввічливо прохаючи Matter.js обчислити всю фізику у фоновому режимі.
Matter.js включає досить простий і зрозумілий клас Render
, неймовірно корисний для швидкого перегляду та налагодження спроєктованого світу. Для цілей відладки його можна налаштувати під свій смак, але стандартних параметрів цілком достатньо для швидкої перевірки вашого світу.
Першим кроком потрібно викликами Matter.Render.create()
(або Render.create()
при наявності змінної-псевдоніма). Цей метод очікує об’єкт із бажаними налаштуваннями для рендерера, який я назвав params
:
let canvas = createCanvas(640, 360);
Збереження у змінній посилання на полотно.
let params = {
canvas: canvas.elt,
engine: engine,
options: { width: width, height: height },
};
Налаштування параметрів для рендереру.
let render = Render.create(params);
Створення рендереру.
Зверніть увагу, що я зберігаю посилання на полотно p5.js у змінній canvas
. Це необхідно, оскільки рендереру потрібно сказати на якому саме полотні йому потрібно малювати. Matter.js не знає про p5.js, але йому потрібно передати відповідне нативне HTML5-полотно, яке зберігається у властивості elt
об’єкта полотна, створеного через p5.js. Рушій — це engine
, який я створив раніше. Розміри полотна за замовчуванням у Matter.js становлять 800 на 600 пікселів, тому, якщо я віддаю перевагу іншому розміру, мені потрібно налаштувати властивість options
з полями width
і height
.
Після створення об’єкта render
мені потрібно запустити його:
Render.run(render);
Запуск рендерера!
Є ще один важливий робочий момент: фізичним рушіям потрібно повідомляти коли робити крок у часі. Оскільки я використовую вбудований рендерер, я також можу використати вбудований бігунець, який запускатиме рушій із типовою частотою кадрів, що за замовчуванням складає 60 кадрів на секунду. Бігунець також можна налаштовувати, але ці деталі наразі не дуже важливі, оскільки мета полягає в тому, щоб перейти замість нього до використання циклу draw()
із p5.js (про це ще буде розказано далі).
Runner.run(engine);
Запуск рушія!
Ось весь код Matter.js разом із доданим об’єктом ground
— ще одним прямокутним тілом. Зверніть увагу на використання опції { isStatic: true }
для тіла ground
, що забезпечує його фіксоване положення. Я розповім більше про статичні тіла пізніше у частині про “Статичні тіла Matter.js”.
const { Engine, Bodies, Composite, Body, Vector, Render } = Matter;
Зверніть увагу на створення посилань на класи Matter.js, які необхідні для цієї програми.
function setup() {
let canvas = createCanvas(640, 360);
Збереження посилання на полотно.
let engine = Engine.create();
Створення фізичного рушія.
let render = Render.create({
canvas: canvas.elt, engine,
options: { width: width, height: height },
});
Render.run(render);
Створення рендерера і передача йому полотна p5.js.
let options = { friction: 0.01, restitution: 0.75 };
let box = Bodies.rectangle(100, 100, 50, 50, options);
Створення прямокутного тіла із власними значеннями тертя і віддачі.
Body.setVelocity(box, Vector.create(5, 0));
Body.setAngularVelocity(box, 0.1);
Встановлення початкової швидкості для тіла.
Composite.add(engine.world, box);
Додавання тіла до світу.
let ground = Bodies.rectangle(width / 2, height - 5,
width, 10, { isStatic: true });
Composite.add(engine.world, ground);
Створення статичного тіла грунту.
let runner = Matter.Runner.create();
Створення бігунця.
Matter.Runner.run(runner, engine);
Запуск рушія.
}
Тут немає функції draw()
, а всі змінні є локальними для setup()
. Фактично я не використовую тут жодних можливостей p5.js (окрім додавання полотна на сторінку). Але саме цим я і збираюся зайнятися далі!
Matter.js з p5.js
Matter.js зберігає список усіх тіл, які існують у світі й, як ви щойно бачили, може малювати та анімувати їх за допомогою об’єктів Render
і Runner
. (Цей список тіл до речі зберігається в engine.world.bodies
.) Однак зараз я хочу показати вам підхід зі збереження власного списку(ів) тіл Matter.js, щоб ви могли малювати їх за допомогою p5.js.
Так, цей підхід може додати надмірність і пожертвувати невеликою кількістю ефективності, але він з лишком компенсує це простотою використання і налаштування. За допомогою цієї методології ви зможете програмувати, як ви звикли в p5.js, відстежуючи потрібні тіла і малюючи їх відповідним чином. Розглянемо файлову структуру проєкту, показану на малюнку 6.3.
Структурно проєкт виглядає як ще одна програма p5.js. Є основний файл sketch.js, а також box.js. У такому додатковому файлі я зазвичай оголошую клас, потрібний для програми — у цьому випадку клас Box
, який описує прямокутне тіло:
class Box {
constructor(x, y) {
this.x = x;
this.y = y;
this.w = 16;
Тіло має координати положення (x, y) і ширину.
}
show() {
rectMode(CENTER);
fill(127);
stroke(0);
strokeWeight(2);
square(this.x, this.y, this.w);
Тіло малюється як квадрат за допомоги функції square().
}
}
Тепер я напишу вміст файлу sketch.js який при клацанні мишки створюватиме новий об’єкт класу Box
і зберігатиме всі об’єкти Box
у масиві. (Це такий самий підхід, який я використовував у прикладах системи частинок із Розділу 4.)
let boxes = [];
Масив для зберігання всіх об’єктів Box.
function setup() {
createCanvas(640, 360);
}
function draw() {
background(255);
if (mouseIsPressed) {
let box = new Box(mouseX, mouseY);
boxes.push(box);
}
При клацанні мишки додаватиметься новий об’єкт Box.
for (let box of boxes) {
box.show();
}
Відображення усіх об’єктів Box.
}
Зараз ця програма малює на екрані фіксовані квадрати. Ось моє завдання: як мені намалювати тіла, на які впливатиме фізика (розрахована за допомогою Matter.js), щойно вони з’являються на екрані, при цьому мінімально змінюючи код?
Для досягнення цієї мети мені потрібно виконати три кроки.
Крок 1: Додаємо Matter.js до програми p5.js
Наразі у програмі не має посилання на Matter.js. Це явно потрібно змінити. На щастя, ця частина не дуже складна: я вже продемонстрував усі елементи, необхідні для створення світу Matter.js. (І не забудьте переконатися, що бібліотека імпортована в index.html.)
Спершу мені потрібн о додати посилання на необхідні класи Matter.js і створити у функції setup()
об’єкт Engine
:
const { Engine, Bodies, Composite } = Matter;
Посилання для Engine, Bodies і Composite.
let engine;
Для рушія тепер використовується глобальна змінна!
function setup() {
engine = Engine.create();
Створення рушія.
}
Далі, у функції draw()
, мені потрібно викликати один важливий метод з Matter.js — Engine.update()
:
function draw() {
Engine.update(engine);
Оновлення рушія з плином часу!
}
Метод Engine.update()
рухає фізичний світ на один крок вперед у часі. Його виклик усередині функції draw()
забезпечує оновлення фізики на кожному кадрі анімації. Цей механізм замінює вбудований об’єкт Runner
з Matter.js, який я використовував у прикладі 6.1. Тепер роль бігунця виконує функція draw()
!
Внутрішньо, коли викликається Engine.update()
, Matter.js проходиться світом, переглядає всі його тіла і вирішує, що з ними робити. Звичайний виклик методу Engine.update()
рухає світ уперед зі стандартними параметрами. Але, як і у випадку з Render
, ці параметри можна налаштувати, як зазначено у документації Matter.js.
Крок 2: Пов’яжемо кожен об’єкт Box із тілом Matter.js
Я налаштував свій світ Matter.js і тепер мені потрібно зв’язати кожен об’єкт Box
у моїй програмі p5.js із тілом у цьому світі. Оригінальний клас Box
містить змінні для позиції та ширини. Тепер я хочу сказати: “Я відмовляюся від керування позицією цього об’єкта на користь Matter.js. Мені більше не потрібно відстежувати нічого, що пов’язано з положенням, швидкістю чи прискоренням. Натомість мені потрібно лише відстежувати існування тіла Matter.js і вірити, що фізичний рушій зробить решту”.
class Box {
constructor(x, y) {
this.w = 16;
this.body = Bodies.rectangle(x, y, this.w, this.w);
Замість звичайних значень змінних, збережемо посилання на тіло.
Composite.add(engine.world, this.body);
Не забуваємо додати тіло до світу!
}
Мені більше не потрібні змінні this.x
і this.y
для відстеження позиції. Конструктор Box
отримує початкові -координати, передає їх у Bodies.rectangle()
для створення нового тіла Matter.js, а потім забуває про них. Як ви побачите, тіло саме стежитиме за своїм положенням. Технічно тіло також може відстежувати свої розміри, але оскільки Matter.js зберігає їх у вигляд і списку вершин, зручніше зберігати ширину квадрата у змінній this.w
для використання її під час малювання тіла.
Крок 3: Малюємо тіло об’єкта
Майже готово. До того, як я додав у програму Matter.js, намалювати об’єкт Box
було легко. Позиція об’єкта зберігалася у змінних this.x
і this.y
:
show() {
rectMode(CENTER);
fill(127);
stroke(0);
strokeWeight(2);
square(this.x, this.y, this.w);
}
Малювання об’єкта за допомогою функції square().
Тепер, коли Matter.js керує положенням об’єкта, для малювання форми я вже не можу використовувати власні змінні x
і y
. Але не бійтеся! Об’єкт Box
має посиланн я на пов’язане з ним тіло Matter.js і це тіло знає свою позицію. Мені потрібно лише ввічливо запитати тіло: “Вибачте, де ви розташовані?”:
let position = this.body.position;
Але просто знати положення тіла недостатньо. Поточне тіло — це квадрат, тому мені також потрібно знати і його кут оберту:
let angle = this.body.angle;
Отримавши позицію і кут, я можу намалювати об’єкт за допомогою вбудованих функцій p5.js: translate()
, rotate()
і square()
:
show() {
let position = this.body.position;
let angle = this.body.angle;
Нам потрібні положення і кут тіла.
rectMode(CENTER);
fill(127);
stroke(0);
strokeWeight(2);
push();
translate(position.x, position.y);
rotate(angle);
Використовуємо положення і кут, щоб перемістити та повернути квадрат.
square(0, 0, this.w);
pop();
}
Важливо зауважити, що якщо ви видаляєте об’єкти типу Box
із масиву boxes
, наприклад, коли вони виходять за межі полотна або завершують своє існування, як це було продемонстровано у Розділі 4, вам також потрібно явно видалити тіло, пов’язане з цим об’єктом Box
зі світу Matter.js. Це можна зробити за допомогою методу removeBody()
класу Box
:
removeBody() {
Composite.remove(engine.world, this.body);
}
Ця функція видаляє тіло зі світу Matter.js.
У функції draw()
вам потім потрібно перебрати масив у зворотному порядку, так само як у прикладах із системою частинок, і викликати як removeBody()
так і splice()
, щоб видалити об’єкт зі світу Matter.js і з вашого масиву boxes
.
Вправа 6.2
Розпочніть з коду прикладу 6.2 і, використовуючи методику описану у цьому розділі, додайте код для реалізації фізики Matter.js. Видаляйте тіла, які виходять за межі полотна. Результат має бути подібним до зображеного прикладу. Проявіть творчість у малюванні тіл!
Статичні тіла Matter.js
У прикладі, який я щойно створив, об’єкти Box
з’являються у позиції курсора і падають вниз через вплив дефолтної гравітації. Що, якщо я хочу додати до світу нерухомі межі, які перекриватимуть шлях об’єктам Box
? Matter.js дозволяє це легко зробити за допомогою властивості isStatic
:
let options = { isStatic: true };
let boundary = Bodies.rectangle(x, y, w, h, options);
Створення фіксованого (статичного) тіла.
Я все ще створюю тіло за допомогою фабричного методу Bodies.rectangle()
, але додана властивість isStatic
гарантує, що тіло ніколи не буде рухатись. Я додам цей функціонал у програму на основі рішення вправи 6.2, створивши окремий клас Boundary
, який пов’язує прямокутник p5.js зі статичним тілом Matter.js. Для різноманіття я також рандомізую розміри кожної квадратної коробки. (Перегляньте онлайн-код для ознайомлення зі змінами у класі Box
.)
class Boundary {
constructor(x, y, w, h) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
Межа — це простий прямокутник з xy-координатами, шириною і висотою.
let options = { isStatic: true };
Фіксація тіла через властивість isStatic зі значенням true!
this.body = Bodies.rectangle(this.x, this.y, this.w, this.h, options);
Composite.add(engine.world, this.body);
}
show() {
rectMode(CENTER);
fill(127);
stroke(0);
strokeWeight(2);
rect(this.x, this.y, this.w, this.h);
Оскільки тіло ніколи не рухатиметься, метод show() може намалювати його стандартним способом, використовуючи оригінальні змінні без отримання їх у Matter.js.
}
}
Статичні тіла не включають властивостей матеріалу таких як restitution
або friction
. Переконайтеся, що ви встановили їх для динамічних тіл вашого світу.
Багатокутники та групи фігур
Тепер, коли я продемонстрував, наскільки легко з Matter.js використову вати примітивну форму, таку як прямокутник або круг, уявімо, що ви хочете використати цікавіше тіло, наприклад абстрактну фігуру як на малюнку 6.4.
Для створення таких складних форм можна використати дві стратегії. Загальний метод Bodies.polygon()
може створити будь-який правильний багатокутник (п’ятикутник, шестикутник тощо). Також є метод Bodies.trapezoid()
для створення трапеції — чотирикутника у якого одна пара сторін паралельна:
let hexagon = Bodies.polygon(x, y, 6, radius);
Гексагон (рівносторонній шестикутник).
let trapezoid = Bodies.trapezoid(x, y, width, height, slope);
Трапеція.
Більш універсальним методом є Bodies.fromVertices()
. Він будує фігуру з масиву векторів, розглядаючи їх як послідовність з’єднаних вершин. Я інкапсулюю цю логіку у класі CustomShape
.
class CustomShape {
constructor(x, y) {
let vertices = [];
vertices[0] = Vector.create(-10, -10);
vertices[1] = Vector.create(20, -15);
vertices[2] = Vector.create(15, 0);
vertices[3] = Vector.create(0, 10);
vertices[4] = Vector.create(-20, 15);
Масив із 5 векторів.
let options = { restitution: 1 };
this.body = Bodies.fromVertices(x, y, vertices, options);
Створення тіла за допомогою вершин фігури.
Body.setVelocity(this.body, Vector.create(random(-5, 5), 0));
Body.setAngularVelocity(this.body, 0.1);
Composite.add(engine.world, this.body);
}
Створюючи власний багатокутник у Matter.js, ви повинні пам’ятати про два важливих моменти. По-перше, вершини повинні бути вказані в годинниковому порядку. Наприклад, на малюнку 6.5 показано п’ять вершин, використаних для створення тіл у прикладі 6.4. Зверніть увагу, що у коді прикладу вони додані до масиву vertices
у годинниковому напрямку, починаючи з верхнього лівого кута.
По-друге, кожна форма повинна бути опуклою, а не увігнутою. Як показано на малюнку 6.6, увігнута форма має поверхню, що вигинається всередину, тоді як в опуклої це інакше. Кожен внутрішній кут в опуклій формі не перевищує 180 градусів. Насправді Matter.js може працювати з увігнутими формами, але вам потрібно скласти їх із кількох опуклих форм і скоро я покажу це на практиці.
Оскільки форма побудована з довільних вершин для малювання відповідного тіла ви зможете використовувати p5.js-функції beginShape()
, endShape()
і vertex()
. Для цілей малювання клас CustomShape
може містити масив для зберігання піксельних позицій вершин відносно до початкової точки . Однак краще дізнав атися позиції вершин через Matter.js. Таким чином не потрібно використовувати функції translate()
чи rotate()
, оскільки тіло Matter.js зберігає свої вершини як абсолютні позиції світу:
show() {
fill(127);
stroke(0);
strokeWeight(2);
beginShape();
Початок малювання фігури.
for (let v of this.body.vertices) {
vertex(v.x, v.y);
}
Перебір усіх вершини тіла.
endShape(CLOSE);
Завершення фігури із замиканням форми.
}
}
Тіло Matter.js зберігає масив позицій вершин у властивості vertices
. Зверніть увагу, що я можу використовувати цикл for...of
для проходження вершин між функціями beginShape()
і endShape()
.
Вправа 6.3
Використовуючи метод Bodies.fromVertices()
, створіть власну форму багатокутника (пам’ятайте, що він має бути опуклим). Деякі можливі варіанти показані нижче.
Власна форма, побудована з масиву вершин, дозволить вам зробити багато чого. Однак вимога щодо опуклої форми обмежує діапазон можливостей. Хороша новина в тому, що ви можете усунути це обмеження, створивши складене тіло, що збирається з кількох форм! Як щодо створення форми смачного льодяника з тонкого прямокутника і круга на його верхньому кінці?
Я почну зі створення двох окремих тіл: прямокутника і круга. Потім я можу об’єднати їх, помістивши у масив parts
і передавши його до методу Body.create()
:
let part1 = Bodies.rectangle(x, y, w, h);
let part2 = Bodies.circle(x, y, r);
Створення тіл.
let body = Body.create({ parts: [part1, part2] });
Об’єднання двох тіл у масиві.
Composite.add(engine.world, body);
Додавання складеного тіла до об’єкту світу.
Хоча це і створює складене тіло через поєднання двох форм, код не зовсім правильний. Якщо ви запустите його, то побачите, що обидві фігури відцентровані на одній і тій самій позиції , як показано на малюнку 6.7.
Натомість мені потрібно змістити центр кола по горизонталі відносно центру прямокутника, як на малюнку 6.8.
Для значення зсуву я використаю половину ширини прямокутника, щоб круг був вирівняний з краєм прямокутника:
let part1 = Bodies.rectangle(x, y, w, h);
let offset = w / 2;
let part2 = Bodies.circle(x + offset, y, r);
Додавання зміщення від x-позиції палички льодяника.
Оскільки тіло льодяника складається з двох частин, його малювання трохи складніше. Я можу використати кілька варіантів. Наприклад, я міг би використати масив вершин тіла vertices
і намалювати льодяник як індивідуальну форму, подібно до прикладу 6.4. (Кожне тіло зберігає масив вершин, навіть якщо воно не створювалося методом fromVertices()
. Але оскільки кожна частина льодяника є примітивною формою, я віддам перевагу окремому малюванню кожної частини через відносне переміщення їх у потрібне положення під потрібним кутом тіла.
show() {
let angle = this.body.angle;
Кут береться від складеного тіла.
let position1 = this.part1.position;
let position2 = this.part2.position;
Отримання позиції для кожної частини.
fill(200);
stroke(0);
push();
translate(position1.x, position1.y);
rotate(angle);
rectMode(CENTER);
rect(0, 0, this.w, this.h);
pop();
Переміщення і оберт прямокутника (перша частина тіла).
push();
translate(position2.x, position2.y);
rotate(angle);
circle(0, 0, this.r * 2);
pop();
Переміщення і оберт круга (друга частина тіла).
}
Перш ніж рухатися далі, я хочу наголосити про дещо: елементи які ви малюєте на полотні не набувають магічним чином потрібної візуальної взаємодії бажаної фізики лише завдяки акту створення тіл Matter.js. Приклади цього розділу працювали, оскільки я ретельно зіставляв відповідність між тим, як малюється кожна фігура p5.js із тим, як визначено геометрію кожного тіла Matter.js. Якщо ви випадково намалюєте фігуру інакше, то не отримаєте повідомлення про помилку — ні від p5.js, ні від Matter.js. Однак ваша програма виглядатиме дивно і фізика працюватиме неправильно, оскільки світ, який ви будете бачити не відповідатиме світу, яким розуміє його Matter.js.
Щоб проілюструвати це, дозвольте мені повернутися до прикладу 6.5. Льодяник — складне тіло, що складається з двох частин — прямокутника (this.part1
) і круга (this.part2
). Я малюю кожен льодяник, отримуючи позиції для двох частин окремо: this.part1.position
і this.part2.position
. Однак загальне складене тіло також має позицію — this.body.position
. Було б спокусливо використати його як положення для малювання прямокутника, а положення кола визначити вручну, використавши зміщення. Зрештою, саме так я уявляв складену форму спочатку (подивіться на малюнок 6.8):
show() {
let position = this.body.position;
Отримання положення сукупного тіла.
let angle = this.body.angle;
push();
translate(position.x, position.y);
rotate(angle);
rect(0, 0, this.w, this.h);
circle(0, this.h / 2, this.r * 2);
pop();
}
Малюнок 6.9 показує результат цієї зміни.
На перший погляд, ця нова версія може виглядати нормально, але якщо придивитися уважніше, то немає правильних зіткнень і фігури накладаються дивним чином. Це не тому, що фізика порушена, а тому, що я неналежним чином поєднав відповідність між p5.js і Matter.js. Виявляється, що загальне положення тіла — це не центр прямокутника, а скоріше “центр маси” між прямокутником і колом. Matter.js обчислює фізику і керує зіткненнями, як і раніше, але я малюю кожне тіло не там, де потрібно! (В онлайн-версії ви можете перемикати правильний і неправильний рендеринг, клацаючи мишкою.)
Вправа 6.4
Створіть власного маленького інопланетянина, використовуючи кілька фігур, прикріплених до одного тіла. Пам’ятайте, що ви не обмежені використанням лише базових функцій малювання фігур у p5.js, а можете використовувати зображення і кольори, малювати волосся за допомогою ліній тощо. Думайте про форми Matter.js як про кістяки для вашого оригінального фантастичного дизайну!
Обмежувачі Matter.js
Обмежувачі (обмеження) Matter.js — це механізм з’єднання одного тіла з іншим, що дозволяє моделювати коливальні маятники, пружні мости, м’які поверхні, об’єкти, які обертаються навколо вісі тощо. Існує три типи обмежувачів: обмежувач відстані й обмежувач обертання, які керуються класом Constraint
, та обмежувач мишки, керований класом MouseConstraint
.
Обмежувач відстані
Обмежувач відстані — це зв’язок фіксованої довжини між двома тілами, подібно до того, як сила пружини з’єднує дві фігури у Розділі 3. Обмеження прикріплюється до кожного тіла у певній якірній точці відносно центру тіла (див. малюнок 6.10). Залежно від жорсткості обмеження, “фіксована” довжина може змінюватися, подібно до того, як пружина може бути більш або менш жорсткою.
Додавання обмеження використовує подібний підхід, що і створення тіл, але тут необхідно вже мати готові тіла, які потрібно обмежити. Припустимо, що є два об’єкти Particle
, кожен з яких зберігає посилання на тіло Matter.js у властивості під назвою body
. Я назву їх particleA
і particleB
:
let particleA = new Particle();
let particleB = new Particle();
Я хочу створити обмеження між цими частинками. Для цього мені потрібно визначити ряд параметрів, які обумовлюють поведінку обмеження:
bodyA
: перше тіло, з яким з’єднується обмеження, формуючи один кінець обмеження.bodyB
: друге тіло, з яким з’єднується обмеження, утворюючи інший кінець.pointA
: позиція відносноbodyA
, де обмеження прикріплюється до першого тіла.pointB
: позиція відносноbodyB
, де обмеження прикріплюється до другого тіла.length
: цільова довжина обмеження або довжина у спокої. Під час симуляції обмеження намагатиметься підтримувати цю довжину.stiffness
: значення від 0 до 1, яке представляє жорсткість обмеження, де 1 означає повністю жорстке обмеження, а 0 — абсолютно м’яке.
Усі ці налаштування упаковуються у літерал об’єкта:
let options = {
bodyA: particleA.body,
bodyB: particleB.body,
pointA: Vector.create(0, 0),
pointB: Vector.create(0, 0),
length: 100,
stiffness: 0.5
};
Технічно обов’язковими параметрами є тільки bodyA
і bodyB
, два тіла, з’єднані обмеженням. Якщо ви не вкажете ніякі інші параметри, Matter.js вибере для них значення за замовчуванням. Наприклад, для кожної відносної точки кріплення (центр тіла) він встановить (0, 0)
, для length
встановить значення поточної відстані між тілами, а для stiffness
використає значення 0.7
. Ще два важливих параметри, які я не налаштував — це damping
і angularStiffness
. Опція damping
впливає на опір обмеження руху, де вищі значення призводять обмеження до швидшої втрати енергії. Опція angularStiffness
контролює жорсткість обмеження кутового руху, де вищі значення призводять до меншої кутової гнучкості між тілами.
Після налаштування параметрів можна створити саме обмеження. Як зазвичай, це передбачає створення нового скорочення де змінна Constraint
посилається на Matter.Constraint
:
let constraint = Constraint.create(options);
Composite.add(engine.world, constraint);
Не забуваємо додати обмеження до світу!
Я можу включити обмеження до класу для інкапсуляції та керування взаємозв’язком між кількома тілами. Ось приклад класу, який представляє маятник (подібний до прикладу 3.11 з Розділу 3).
class Pendulum {
constructor(x, y, len) {
this.r = 12;
this.len = len;
this.anchor = Bodies.circle(x, y, this.r, { isStatic: true });
this.bob = Bodies.circle(x + len, y, this.r, { restitution: 0.6 });
Створення двох тіл: одного для якоря (точки опори) та іншого для підвісу. Якір буде статичним.
let options = {
bodyA: this.anchor,
bodyB: this.bob,
length: this.len,
};
this.arm = Constraint.create(options);
Створення обмеження, що з’єднує якір і підвіс.
Composite.add(engine.world, this.anchor);
Composite.add(engine.world, this.bob);
Composite.add(engine.world, this.arm);
Додавання до світу обох тіл та обмеження.
}
show() {
fill(127);
stroke(0);
strokeWeight(2);
line(this.anchor.position.x, this.anchor.position.y, this.bob.position.x, this.bob.position.y);
Малювання лінії, що представляє плече маятника.
push();
translate(this.anchor.position.x, this.anchor.position.y);
rotate(this.anchor.angle);
circle(0, 0, this.r * 2);
line(0, 0, this.r, 0);
pop();
Малювання опорної точки (якоря).
push();
translate(this.bob.position.x, this.bob.position.y);
rotate(this.bob.angle);
circle(0, 0, this.r * 2);
line(0, 0, this.r, 0);
pop();
Малювання підвісу.
}
}
У прикладі 6.6 для параметру stiffness
використовується значення за замовчуванням 0.7
. Якщо ви спробуєте знизити значення, маятник буде схожим на м’яку пружину.
Вправа 6.5
Створіть симуляцію мосту, використовуючи обмеження відстані для з’єднання послідовності кругів (або прямокутників), як показано нижче. Для закріплення крайніх точок використовуйте властивість isStatic
. Поекспериментуйте з різними значеннями, щоб зробити міст більш або менш пружним. Зауважте, що з’єднання не мають фізичної геометрії тому, щоб у вашого моста не було отворів або щоб отвори були не дуже великими, налаштування відстані між вузлами є важливим моментом.
Обмежувач обертання
Інший поширений у фізичних рушіях вид зв’язку між тілами називається обертальним або шарнірним з’єднанням. Цей тип обмеження з’єднує два тіла у спільній опорній точці, яку також називають шарніром (див. малюнок 6.11). Хоча в Matter.js немає окремого обмежувача для обертання, ви можете створити його за допомогою звичайного Constraint
з довжиною рівною нулю. Таким чином тіла можуть обертатися навколо спільної опорної точки.
Першим кроком є створення з’єднаних тіл. Для першого прикладу я хочу створити обертовий прямокутник (схожий на пропелер або вітряк) у фіксованому положенні. Для цього випадку мені потрібне лише одне тіло, з’єднане з точкою. Це спрощує речі, оскільки мені не потрібно турбуватися про зіткнення між двома тілами, з’єднаними шарніром:
let body = Bodies.rectangle(x, y, w, h);
Composite.add(engine.world, body);
Створення тіла з наданими координатами, шириною і висотою.
Далі я можу створити потрібне обмеження. Властивість length
буде дорівнювати 0
, а для властивості stiffness
потрібно встановити значення 1
, інакше обмеження може бути недостатньо стабільним, щоб утримати тіло з’єднаним у точці прив’язки (якоря):
let options = {
bodyA: this.body,
pointB: { x: x, y: y },
length: 0,
stiffness: 1,
};
Обмеження з’єднує тіло з фіксованою xy-позицією, з довжиною рівною 0 і жорсткістю рівною 1.
let constraint = Matter.Constraint.create(options);
Composite.add(engine.world, constraint);
Створення обмеження та додавання його до світу.
Зібравши код разом, я напишу програму із класом Windmill
, який представляє тіло, що обертається. Програма також містить клас Particle
для скидування на вітряк частинок.
class Windmill {
constructor(x, y, w, h) {
this.w = w;
this.h = h;
this.body = Bodies.rectangle(x, y, w, h);
Composite.add(engine.world, this.body);
Тіло для обертання.
let options = {
bodyA: this.body,
pointB: { x: x, y: y },
length: 0,
stiffness: 1,
};
this.constraint = Constraint.create(options);
Composite.add(engine.world, this.constraint);
Налаштування обертового обмеження.
}
show() {
rectMode(CENTER);
fill(127);
stroke(0);
strokeWeight(2);
push();
translate(this.body.position.x, this.body.position.y);
push();
rotate(this.body.angle);
rect(0, 0, this.w, this.h);
pop();
line(0, 0, 0, height);
Малювання стійки вітряка (не відноситься до фізичного світу).
pop();
}
}
Зверніть увагу на лінію в цьому прикладі, яка представляє стійку вітряка. Вона не є частиною фізичного світу Matter.js і я ніде не створював для неї тіло. Це ілюструє важливий момент щодо роботи з фізичним рушієм разом із p5.js: ви можете додавати на полотно декоративні чи візуальні елементи, які не впливатимуть на фізичну симуляцію, якщо їх участь у цьому не потрібна.
Вправа 6.6
Створіть подобу автівки, яка має поворотні шарніри для своїх коліс. Враховуйте розмір і розташування коліс. Як зміна властивості stiffness
впливає на їх рух?
Обме жувач мишки
Перш ніж я представлю клас MouseConstraint
, подумайте про таке запитання: як встановити положення тіла Matter.js у позицію курсора миші? І навіщо вам для цього потрібен обмежувач? Зрештою, у вас є доступ і до позиції тіла і до положення курсора. Що поганого в тому, щоб призначити одне іншому?
body.position.x = mouseX;
body.position.y = mouseY;
Хоча цей код дійсно рухатиме тіло, він також призведе до неприємного результату із порушенням фізики. Уявіть, що ви побудували машину для телепортації, яка дозволяє вам миттєво переміщатися зі спальні на кухню (дуже зручно для нічного перекусу). Це досить легко уявити, але тепер спробуйте переписати закони руху Ньютона, щоб врахувати можливість телепортації. Тепер все не так просто, чи не так?
Matter.js має таку саму проблему. Якщо ви вручну призначаєте позицію тіла, це все одно, що сказати “телепортуйте це тіло” і Matter.js більше не знає, як у такому випадку обчислювати фізику належним чином. Однак Matter.js дозволяє вам прив’язати мотузок навколо талії й отримати помічника, який стоятиме на кухні та тягнутиме вас туди. Замініть свого помічника на курсор і ось вам обмежувач мишки.
Уявіть, що коли ви клацаєте мишкою по фігурі, вона прикріплюється до цього тіла за допомогою мотузка. Тепер ви можете рухати мишкою довкола і вона тягтиме за собою тіло, доки ви не відпустите мишку. Це працює подібно до шарнірного з’єднання, оскільки ви можете встановити довжину цього “мотузка” у 0, ефективно переміщуючи фігуру за допомогою мишки.
Однак перед тим, як зробити прив’язку до мишки, вам потрібно створити об’єкт Matter.js Mouse
, який слухатиме події мишки на полотні p5.js:
const { Mouse, MouseConstraint } = Matter;
Посилання на класи Mouse і MouseConstraint з Matter.js.
let canvas = createCanvas(640, 240);
Для прослуховування миші потрібне посилання на полотно p5.js.
let mouse = Mouse.create(canvas.elt);
Створення об’єкта Mouse, пов’язаного з нативним елементом canvas.
Далі використайте об’єкт mouse
для створення MouseConstraint
:
let mouseConstraint = MouseConstraint.create(engine, { mouse });
Composite.add(engine.world, mouseConstraint);
Це миттєво дозволить вам взаємодіяти з усіма тілами Matter.js за допомогою миші. Явно прикріплювати обмежувач до певного тіла тут не потрібно — будь-яке тіло, на яке ви клацнете, буде обмежено мишею автоматично.
Ви також можете налаштувати всі звичайні змінні обмеження, додавши властивість constraint
до параметрів, які передаються у метод MouseConstraint.create()
:
mouse = Mouse.create(canvas.elt);
let options = {
mouse,
constraint: { stiffness: 0.7 }
Налаштування обмеження з додатковою властивістю.
};
mouseConstraint = MouseConstraint.create(engine, options);
Composite.add(engine.world, mouseConstraint);
Ось приклад демонстрації MouseConstraint
з двома об’єктами Box
. Тут також налаштовні статичні тіла, що діють як стіни навколо периметру полотна.
У цьому прикладі ви побачите, що властивість stiffness
обмежувача встановлено на 0.7
, надаючи трохи еластичності уявному мотузку мишки. Інші властивості, такі як angularStiffness
і damping
також можуть впливати на взаємодію з мишею. Пограйте з цими значеннями й подивіться, що станеться?
Додаємо більше сил
У Розділі 2 я розповів, як побудувати середовище у якому діє кілька сил. Об’єкт може реагувати на гравітаційне тяжіння, вітер, опір повітря тощо. Очевидно, що в Matter.js також діють сили, коли прямокутники та круги обертаються та літають по екрану! Але наразі я продемонстрував, як контролювати лише одну глобальну силу — гравітацію:
let engine = Engine.create();
engine.gravity.x = 1;
engine.gravity.y = 0;
Налаштування сили тяжіння світу у горизонтальному напрямку.
Якщо я хочу використати будь-який підхід Розділу 2 із Matter.js, мені знадобиться лише надійний метод applyForce()
, який я написав як частину класу Mover
. Він приймав вектор, ділив його на масу і накопичував у прискоренні об’єкта. У Matter.js працює той самий підхід, тому мені більше не потрібно самостійно писати всі деталі! Я можу викликати його як статичний метод Body.applyForce()
. Ось як це виглядатиме у поточному класі Box
:
class Box {
applyForce(force) {
Body.applyForce(this.body, this.body.position, force);
Виклик методу applyForce() з класу Body.
}
}
Тут метод applyForce()
з класу Box
отримує вектор сили й просто передає його методу applyForce()
Matter.js для застосування його до відповідного тіла. Основна відмінність цього підходу в тому, що Matter.js має складніший механізм, ніж приклади з Розділу 2. У попередніх прикладах припускалося, що сила завжди застосовується у центрі рухомого об’єкта. Тут я вказав точне положення на тілі, де прикладається сила. У цьому випадку я просто застосував її до центру, як і раніше, вказавши положення тіла, але це можна було б налаштувати інакше. Наприклад, уявіть сценарій, де сила діє на край об’єкта, змушуючи його обертатися на полотні, подібно до перекочування кинутих гральних кубиків.
Як я можу ввести сили у програму, керовану за допомогою Matter.js? Скажімо, я хочу використати силу тяжіння. Пам’ятаєте код з прикладу 2.6 у класі Attractor
?
attract(mover) {
let force = p5.Vector.sub(this.position, mover.position);
let distance = force.mag();
distance = constrain(distance, 5, 25);
let strength = (G * this.mass * mover.mass) / (distance * distance);
force.setMag(strength);
return force;
}
Я можу переписати такий самий метод, використовуючи Matter.Vector
і включ ити його в новий клас Attractor
.
class Attractor {
constructor(x, y) {
this.radius = 32;
this.body = Bodies.circle(x, y, this.radius, { isStatic: true });
Composite.add(engine.world, this.body);
Атрактор — це статичне тіло Matter.js.
}
attract(mover) {
let force = Vector.sub(this.body.position, mover.body.position);
let distance = Vector.magnitude(force);
Метод attract() тепер використовує векторні методи Matter.js.
distance = constrain(distance, 5, 25);
let G = 0.02;
Використання невеликого значення для G утримує стабільність системи.
let strength = (G * mover.body.mass) / (distance * distance);
Оскільки атрактор статичне тіло, його маса тут буде проігнорована.
force = Vector.normalise(force);
force = Vector.mult(force, strength);
Інші векторні методи Matter.js.
return force;
}
}
Окрім написання власного методу attract()
для прикладу 6.9, є ще два ключові елементи, необхідні для програми, щоб поведінка була схожа на приклад з Розділу 2. По-перше, пам’ятайте, що Matter.js Engine
має дефолтну гравітацію, спрямовану вниз. Мені потрібно відключити її у функції setup()
за допомогою вектора (0, 0)
:
engine = Engine.create();
engine.gravity = Vector.create(0, 0);
Вимкнення гравітації за замовчуванням.
По-друге, тіла в Matter.js створюються з дефолтним опором повітря, який сповільнює їх рух. Мені також потрібно встановити це значення на 0
, щоб імітувати для тіла подобу перебування у космічному вакуумі:
class Mover {
constructor(x, y, radius) {
this.radius = radius;
let options = { frictionAir: 0 };
Вимкнення дефолтного опору повітря.
this.body = Bodies.circle(x, y, this.radius, options);
}
Це також хороша нагода повернутися до поняття маси. Хоча я маю доступ до властивості mass
тіла, пов’язаного з рухомим тілом у методі attract()
, я ніколи не встановлював її явно. У Matter.js маса тіла автоматично розраховується на основі його розміру (площі) і щільності. Отже, більші тіла матимуть більшу масу. Щоб збільшити масу відносно розміру, ви можете спробувати встановити властивість density
в об’єкті options
(за замовчуванням 0.001
). Для статичних тіл, таких як атрактор, маса вважається нескінченною. Так атрактор залишається заблокованим на місці, попри постійні зіткнення з ним інших рухомих об’єктів.
Вправа 6.7
Включіть Body.applyForce()
у новий метод spin()
класу Windmill
, щоб вітряк постійно виконував оберти.
Вправа 6.8
Переробіть будь-які приклади керувального руху з Розділу 5 із використанням Matter.js. Як виглядатиме флокі нг із зіткненнями?
Події зіткнення
Ця книга не називається Природа Matter.js, тому я не збираюся охоплювати всі можливості бібліотеки Matter.js. На цьому етапі я розглянув основи створення тіл і обмежень та показав деякі з можливостей бібліотеки. Сподіваюся, коли прийде час використовувати аспекти Matter.js, які я не розглядав тут, ваш процес навчання буде набагато менш болісним, завдяки отриманим тут навичкам. Однак перед наступними кроками є ще одна особливість бібліотеки, яку, на мою думку, варто розглянути — це події зіткнення.
Ось питання, яким ви, ймовірно, задавалися: “А якщо мені потрібно, щоб під час зіткнення двох тіл сталося щось додаткове? Не зрозумійте мене неправильно — я в захваті від того, що Matter.js справляється з усіма зіткненнями замість мене. Але якщо Matter.js піклується про зіткнення за мене, як я маю дізнатися, коли саме вони відбуваються?”
Ваша перша думка, щоб відповісти на це запитання, може бути такою: “Ну, я знаю всі тіла в системі, і я знаю де вони всі розташовані. Я можу просто почати порівнювати положення тіл і побачити, які з них перетинаються. Тоді для тіл, що будуть у стані зіткнення, я можу зробити щось додаткове”.
Гарна думка, але агов? Суть використання фізичного рушія, такого як Matter.js, полягає в тому, що він виконує всю цю роботу за вас. Якщо ви збираєтеся реалізовувати алгоритми обчислення геометрії для перевірки перетину, тоді по суті, ви робите свій власний Matter.js!
Звісно, бажання знати, коли тіла стикаються, є досить поширеною проблемою, тому Matter.js це врахував. Він може сповіщати вас про моменти зіткнення за допомогою слухача подій. Якщо в межах p5.js ви вже працювали із взаємодією з мишкою чи клавіатурою, тоді у вас уже є досвід роботи зі слухачами подій. Розгляньте наступний код:
function mousePressed() {
print("The mouse was pressed!");
}
Подія mousePressed, яку ви, напевно, вже писали багато разів раніше.
Глобальна функція mousePressed()
у p5.js виконується кожного разу, коли відбувається клацання мишкою. Це називається callback (колбек або зворотний виклик) — функція, яка викликається у відповідь на певну подію. Події зіткнення Matter.js працюють подібним чином, але на відміну від функції mousePressed()
, яку p5.js самостійно шукає, щоб виконати під час клацання, для події зіткнень Matter.js ви повинні явно створити та зареєструвати колбек функцію:
Matter.Events.on(engine, 'collisionStart', handleCollisions);
Цей код вказує, що функцію із назвою handleCollisions
слід виконувати щоразу, коли починається зіткнення між двома тілами. Matter.js також має події для 'collisionActive'
(виконується знову і знову під час дії зіткнення) та 'collisionEnd'
(виконується, коли два тіла припиняють зіткнення), але для базової демонстрації, розуміння, коли починається зіткнення, більш ніж достатньо.
Подібно до того, як функція mousePressed()
спрацьовує під час натискання мишки, функція handleCollisions()
(або будь-яка інша, яку ви оберете для функції зворотного виклику), спрацьо вує, коли дві фігури зіткнулися. Цю функцію можна записати наступним чином:
function handleCollisions(event) {
}
Зверніть увагу, що функція має параметр event
. Це об’єкт, який містить усі дані, пов’язані із зіткненням тіл (або кількома зіткненнями, якщо одночасно відбулося більше одного зіткнення). Matter.js автоматично створює цей об’єкт і передає його як параметр у колбек handleCollisions()
кожного разу, коли виникає колізія.
Скажімо, у мене є програма з об’єктами Particle
. Кожен містить посилання на тіло Matter.js і я хочу, щоб при зіткненні між собою частинки змінювали свій колір. Щоб це сталося, слід виконати наступний процес:
Крок 1. Подія, підкажи мені, які два тіла зіткнулися?
Що тут зіткнулося? Matter.js виявляє зіткнення між парою тіл. Будь-яка пара тіл, що зіткнулися, буде в масиві пі д назвою pairs
в об’єкті event
. Усередині функції handleCollisions()
я можу використовувати цикл for...of
, щоб перебрати ці пари:
for (let pair of event.pairs) {
}
Крок 2: Пара, підкажи мені, з яких двох тіл ти складаєшся?
Кожна пара в масиві pairs
є об’єктом із посиланнями на два тіла, що беруть участь у зіткненні: bodyA
і bodyB
. Я витягну ці тіла в окремі змінні:
for (let pair of event.pairs) {
let bodyA = pair.bodyA;
let bodyB = pair.bodyB;
}
Крок 3. Тіла, підкажіть, які частинки ви представляєте?
Знайти відповідні об’єкти Particle
, що пов’язані з тілами Matter.js трохи складніше. Зрештою, Matter.js нічого не знає про мій код. Звісно, він робить усілякі речі для відстеження зв’язків між тілами й обмеженнями Matter.js, але керування зв’язками між моїми власними об’єктами p5.js та елементами Matter.js вже залежать від мене. Втім, кожне тіло Matter.js створюється з порожнім об’єктом { }
, що має назву plugin
і готове для зберігання будь-яких додаткових користувацьких даних про це тіло. Я можу зв’язати тіло з власним об’єктом (у цьому випадку з Particle
), зберігаючи посилання на цей об’єкт у властивості plugin
.
Подивіться на оновлений конструктор у класі Particle
, де створюється тіло. Зверніть увагу, що функція створення тіла була розширена на один рядок коду, щоб додати властивість particle
усередину об’єкта plugin
. Важливо переконатися, що ви додаєте нову властивість до об’єкта plugin
, що вже існує (у цьому випадку, plugin.particle = this
), а не перезаписуєте сам об’єкт plugin
(наприклад, таким чином plugin = this
). Останнє може видалити з об’єкту plugin
інші додані функції або налаштування.
class Particle {
constructor(x, y, radius) {
this.radius = radius;
this.body = Bodies.circle(x, y, this.radius);
this.body.plugin.particle = this;
Ключове слово this вказує на поточний об’єкт Particle, говорячи тілу Matter.js зберегти посилання на цю частинку, щоб пізніше можна було отримати до неї доступ.
Composite.add(engine.world, this.body);
}
Пізніше, у функції зворотного виклику handleCollision()
, до цього об’єкта Particle
можна буде отримати доступ із самого тіла за допомогою об’єкта plugin
.
function handleCollisions(event) {
for (let pair of event.pairs) {
let bodyA = pair.bodyA;
let bodyB = pair.bodyB;
let particleA = bodyA.plugin.particle;
let particleB = bodyB.plugin.particle;
Отримання частинок за допомогою об’єкта plugin, які пов’язані з тілами, що зіткнулися.
if (particleA instanceof Particle && particleB instanceof Particle) {
particleA.change();
particleB.change();
}
Якщо обидва об’єкта є частинками, змінимо їх колір!
}
}
По замовчуванню ви не можете вважати, що об’єкти, які зіткнулися, будуть саме об’єктами Particle
. Зрештою, частинка могла зіткнутися з об’єктом Boundary
(або чимось іншим, залежно від того, що є у вашому світі). Ви можете перевірити тип об’єкта за допомогою JavaScript-оператора instanceof
, як я зробив у цьому прикладі.
Вправа 6.9
Створіть симуляцію в якій об’єкти Particle
зникатимуть при зіткненні один з одним. Де і як слід видаляти частинки? Чи зможете ви зробити так, щоб вони розламувалися на менші частинки?
Коротка інтерлюдія: Методи інтегрування
З вами колись таке траплялося? Ви на елегантній коктейльній вечірці, розповідаєте своїм друзям про неймовірні програми фізичних симуляцій. Раптом, як грім серед ясного неба, хтось запитує: “Чарівно! Але який метод інтегрування ви використовуєте?”
Що?! думаєте ви собі. Інтегрування?
Можливо, ви вже чули цей термін. Разом із диференціюванням, це одна з двох основних операцій числення.
Мені вдалося пройти більшу частину матеріалу в цій книзі, пов’язаного з моделюванням фізики, без необхідності занурюватися у числення. Однак, завершуючи першу половину цієї книги, варто приділити хвилину для розгляду числення, що лежить в основі того, що я продемонстрував, і як воно пов’язане з методологією певних фізичних бібліотек (наприклад, Box2D, Matter.js і Toxiclibs.js до якої ми підходимо). Таким чином, ви будете знати, що відповісти на наступній коктейльній вечірці, коли хтось запитає вас про інтегрування.
Я почну з питання: “Що спільного має інтегрування з положенням, швидкістю і прискоренням?” Щоб відповісти, я повинен спочатку визначити диференціювання — процес знаходження похідної. Похідна функції є мірою того, як функція змінюється з часом. Розглянемо положення та її похідну. Положення — це точка у просторі, тоді як швидкість — це зміна положення з часом. Тому швидкість можна описати як похідну положення. А що таке прискорення? Зміна швидкості з часом. Прискорення — це похідна швидкості.
Інтегрування — це процес знаходження інтеграла — операція, яка є оберненою до диференціювання. Наприклад, інтеграл швидкості об’єкта з плином часу повідомляє нам нове положення об’єкта після закінчення цього періоду часу. Положення є інтегралом швидкості, а швидкість є ін тегралом прискорення.
Оскільки фізичні симуляції в цій книзі ґрунтуються на ідеї обчислення прискорення на основі сил, інтегрування потрібне для визначення місця розташування об’єкта через певний період часу (наприклад, через один цикл роботи функції draw()
). Іншими словами, ви весь час вже займалися інтегруванням! Наступний код показує, як це виглядає:
velocity.add(acceleration);
position.add(velocity);
Ця методика відома як Інтегрування Ейлера (названа на честь математика Леонарда Ейлера), або метод Ейлера. По суті, це найпростіша форма інтегрування і її дуже легко реалізувати в коді — всього два рядки! Проте, хоча це просто у плані обчислення, це аж ніяк не найточніший або стабільний вибір для певних типів симуляцій.
Чому метод Ейлера неточний? Подумайте про це так: коли ви підстрибуєте на пого-палиці, чи залишається вона в одній позиції протягом 1 секунди, а потім зникає та раптово з’являється в новій позиції на другій секунді й потім те саме на 3 секунді, 4, 5...? Ні, звичайно, ні. Пого-палиця рухається у часі безперервно.
Але що відбувається у програмі p5.js? На першому кадрі об’єкт знаходиться в одному положенні, на наступному кадрі в іншому і так далі. Звісно, при 30-60 кадрах на секунду ви бачите ілюзію руху. Але нове положення обчислюється лише кожні -одиниць часу, тоді як реальний світ абсолютно безперервний. Це призводить до деяких неточностей, як показано на малюнку 6.12.
“Реальний світ” — це гладенька крива. Симуляція Ейлера — це послідовність прямих відрізків. Одним із варіантів покращення методу Ейлера є використання менших часових кроків — замість одного разу на кадр, ви можете перераховувати положення об’єкта 20 разів на кадр. Але це не практично, бо програма може почати працювати дуже повільно.
Я все ще вважаю, що метод Ейлера — це найкращий метод для вивчення основ і він також цілком підходить для більшості проєктів, які ви забажаєте зробити з p5.js. Усе, що втрачається в ефективності або неточності, компенсується простотою використання і зрозумілістю. Для покращення точності рушій Box2D, наприклад, використовує симплектичний або напів-експліцитний метод Ейлера, що є невеликою модифікацією метода Ейлера. Інші рушії використовують метод інтегрування, який називається методом Рунге-Кутти (названий на честь німецьких математиків Карла Рунге і Мартіна Кутти).
Іншим попу лярним методом інтегрування, який використовується у фізичних бібліотеках, включаючи Matter.js і Toxiclibs.js, є метод Верле. Простий спосіб описати метод Верле — це подумати про типовий алгоритм руху без явного зберігання швидкості. Зрештою, вам дійсно не потрібно зберігати швидкість оскільки, якщо ви завжди знаєте де об’єкт був у певний момент часу і де він знаходиться зараз, то зможете екстраполювати його швидкість. Метод Верле робить саме це, обчислюючи швидкість на льоту під час роботи програми, замість того, щоб зберігати окрему змінну для швидкості.
Метод Верле дуже добре підходить для систем із частинками, особливо з пружинними з’єднаннями між частинками. Фізичні бібліотеки приховують від вас деталі реалізації, тож вам не потрібно перейматися про те, як це все працює, але якщо ви зацікавлені у глибшому зануренні у фізику Верле, я пропоную прочитати основоположну статтю Томаса Якобсена, з якої походять майже всі симуляції Верле у комп’ютерній графіці: “Advanced Character Physics”.
Фізика Верле з використанням Toxiclibs.js
Приблизно у 2005 році Карстен Шмідт розпочав роботу над Toxiclibs, широкомасштабною та передовою бібліотекою з відкритим кодом для обчислювального проєктування, спеціально створеною для Java-версії програми Processing. Попри те, що бібліотека особливо не розвивалася понад 10 років, концепції й методи, які вона продемонструвала свого часу, сьогодні можна знайти у безлічі проєктів творчого програмування. Вебсайт бібліотеки описував її наступним чином:
Toxiclibs — це незалежна бібліотека з відкритим кодом, призначена для задач обчислювального проєктування на Java і Processing, розроблена Карстеном “toxi” Шмідтом. Класи бібліотеки цілеспрямовано зроблені досить загальними, щоб максимізувати можливість їх повторного використання у різних контекстах, включаючи генеративний дизайн, анімацію, інтерактивний дизайн, візуалізації даних для архітектурних та цифрових цілей, використання в якості навчального інструменту тощо.
Шмідт і сьогодні продовжує робити свій внесок у сферу креативного кодування завдяки своєму проєкту thi.ng/umbrella. Цю роботу можна вважати непрямим наступником Toxiclibs.js, але з набагато більшим обсягом і деталями. Якщо вам сподобалася ця книга, вам можливо особливо сподобається досліджувати thi.ng/vectors, який містить понад 800 функцій векторної алгебри, що використовують звичайні масиви JavaScript.
Хоча thi.ng/umbrella може бути більш сучасним і витонченим підходом, Toxiclibs.js залишається універсальним інструментом і я досі продовжую використовувати версію сумісну з останньою версією Processing (4.3 на момент написання цього тексту). Ми маємо подякувати нашим зіркам удачі за існування Toxiclibs.js, адаптації цієї бібліотеки на JavaScript, створеної Кайлом Філліпсом (відомий під нікнеймом hapticdata). Я покажу лише кілька прикладів, пов’язаних з фізикою Верле, але бібліотека Toxiclibs.js також містить низку інших пакетів із функціональністю, пов’язаною з кольорами, геометрією, математикою тощо.
Приклади, які я збираюся продемонструвати, також можна створити з використанням Matter.js, але я вирішив перейти до Toxiclibs.js з кількох причин. Ця бібліотека займає особливе місце в моєму серці як особистий фаворит і має важливе історичне значення. Також я вважаю, що розгляд більш ніж одної фізичної бібліотеки важливо для ширшого розуміння доступних інструментів і підходів.
Цей перехід від Matter.js до Toxiclibs.js порушує важливе питання: як вирішити, яку саме бібліотеку краще використовувати для проєкту? Matter.js, Toxiclibs.js чи щось інше? Якщо ви належите до однієї з наступних двох категорій, ваш вибір буде трохи легшим:
- Ваш проєкт передбачає зіткнення. Ви маєте круги, квадрати й інші об’єкти певних форм, які вдаряються і відскакують один від одного. У цьому випадку вам захочеться використовувати Matter.js, оскільки Toxiclibs.js не обробляє зіткнення твердих тіл.
- Ваш проєкт включає багато частинок, які рухаються по екрану. Іноді вони притягують одна одну. Іноді вони відштовхуються одна від одної. А іноді вони пов’язані пружинними з’єднаннями. У цьому випадку Toxiclibs.js, ймовірно, буде найкращим вибором. У деяких прикладах він простіший у використанні ніж Matter.js, і особливо добре підходить для з’єднаних систем частинок. Він також має високу продуктивність, оскільки ігнорує всю геометрію зіткнень.
Ось невелика порівняльна таблиця, яка охоплює деякі функції даних бібліотек:
Особливість | Matter.js | Toxiclibs.js |
---|---|---|
Зіткнення твердих тіл | Є | Нема |
3D фізика | Нема | Є |
Сили притягування і відштовхування частинок | Нема | Є |
Пружинні з’єднання (на основі сили) | Є | Є |
Обмеження (з’єднання загального призначення) | Є | Нема |
Усю документацію та файли бібліотеки можна знайти на вебсайті Toxiclibs.js. Для прикладів у цій книзі я працюватиму з версією, яка зберігається на CDN і зроблю відповідне посилання на неї у файлі index.html, так само як я робив це для Matter.js. Нижче приклад з підключенням бібліотеки через тег <script>
:
<script src="https://cdn.jsdelivr.net/gh/hapticdata/toxiclibsjs@0.3.2/build/toxiclibs.js"></script>
Мій огляд Matter.js зосереджувався на кількох ключових функціональностях цієї бібліотеки: світ, вектор, тіло, обмеження. Це також дасть вам початкове розуміння Toxiclibs.js, оскільки він має схожу структуру. У наступній таблиці показано відповідний функціонал зі сторони Toxiclibs.js:
Matter.js | Toxiclibs.js |
---|---|
|
|
|
|
|
|
|
|
Спершу я розгляну як деякі з цих функціональностей перекладаються у Toxiclibs.js, а потім почну поєднувати їх для створення цікавих прикладів.
Вектори
І ось ми знову на початку. Пам’ятаєте весь той час, витрачений на вивчення усіх деталей класу p5.Vector
? А пам’ятаєте, як потім вам довелося переглянути всі ці концепції з Matter.js і його класом Matter.Vector
? Що ж, настав час зробити це знову, оскільки Toxiclibs.js також містить власні векторні класи. Він має один клас для двох вимірів і один для трьох: Vec2D
та Vec3D
. Обидва містяться в пакеті toxi.geom
і можуть бути деструктуровані з нього як клас Vector
з Matter.js:
const { Vec2D, Vec3D } = toxi.geom;
Знову ж таки, концептуально вектори Toxiclibs.js такі ж, що і вектори p5.js, які ми знаємо й любимо, але вони мають власний стиль та синтаксис. Ось огляд того, як деякі основні операції векторної математики з p5.Vector
перекладаються на Vec2D
. (Я використовую 2D, щоб відповідати решті цієї книги, але закликаю вас так само досліджувати 3D-вектори):
p5.Vector | Vec2D |
---|---|
|
|
|
|
|
|
Зокрема зверніть увагу, що вектори Toxiclibs.js створюються шляхом виклику конструктора Vec2D
з ключовим словом new
, а не за допомогою фабричного методу, подібно до Matter.Vector()
або createVector()
.
Світ фізики
Класи для опису світу і його частинок й пружин у Toxiclibs.js містяться в toxi.physics2d.
Я також збираюся використовувати об’єкт Rect
для опису загальної межі прямокутника і GravityBehavior
для застосування до світу глобальної сили тяжіння. Разом із Vec2D
я створю для них наступні змінні-посилання:
const { Vec2D, Rect } = toxi.geom;
Необхідні класи геометрії для векторів і прямокутників.
const { VerletPhysics2D, VerletParticle2D, VerletSpring2D } = toxi.physics2d;
Змінні важливих класів з toxi.physics2d.
const { GravityBehavior } = toxi.physics2d.behaviors;
Клас для гравітації.
Перший крок — створення світу:
let physics;
function setup() {
physics = new VerletPhysics2D();
Створення світу Toxiclibs.
Маючи створений світ VerletPhysics
, я можу встановити глобальні властивості. Наприклад, якщо мені потрібні жорсткі кордони, за межі яких частинки не мають рухатися, я можу зробити це за допомогою прямокутника:
physics.setWorldBounds(new Rect(0, 0, width, height));
Крім того, я можу додати гравітацію за допомогою об’єкта GravityBehavior
. Для гравітації потрібно задати вектор, щоб вказати її силу та напрямок дії:
physics.addBehavior(new GravityBehavior(new Vec2D(0, 0.5)));
}
Нарешті, щоб обчислити фізику світу і переміщувати його об’єкти, потрібно викликати метод оновлення світу — update()
. Зазвичай це відбувається один раз на кадр у функції draw()
:
function draw() {
physics.update();
Це те ж саме, що і Engine.update() у Matter.js.
}
Тепер залишається лише наповнити світ.
Частинки
Еквівалент тіла Matter.js у Toxiclibs.js — річ, яка існує у світі й піддається фізиці — це частинка, представлена класом VerletParticle2D
. Однак, на відміну від тіл Matter.js, частинки Toxiclibs.js не зберігають геометрію — це просто точки в просторі.
Як мені інтегрувати частинки Toxiclibs.js у програму p5.js? Для прикладів Matter.js я створював власний клас (названий Particle
) і включав у ньому посилання на тіло Matter.js:
class Particle {
constructor(x, y, r) {
this.body = Bodies.circle(x, y, r);
}
}