Розділ 2. Сили

Не варто недооцінювати Силу.

— Дарт Вейдер

Сузір’я Олександра Колдера (фото Езри Столлера)
Сузір’я Олександра Колдера (фото Езри Столлера)

Олександр Колдер був американським митцем 20-го століття, відомим своїми кінетичними скульптурами, які врівноважують форму і рух. Його “сузір’я” були скульптурами, що складалися з взаємопов’язаних форм і дроту, які демонстрували напругу, рівновагу та постійне гравітаційне тяжіння.


У фінальному прикладі Розділу 1 я продемонстрував, як обчислити динамічне прискорення на основі вектора, що вказує від кульки на полотні до положення курсора. Отриманий рух нагадував магнітне притягання між фігурою і курсором, ніби якась сила тягнула кульку до курсора. У цьому розділі я детально розповім про концепцію сили та її зв’язок із прискоренням. Метою цього розділу буде створення простого фізичного рушія та пояснення того, як об’єкти рухаються на полотні, реагуючи на різні сили середовища.

Фізичний рушій — це комп’ютерна програма (або бібліотека коду), яка моделює поведінку об’єктів у фізичному середовищі. У програмі p5.js об’єкти — це 2D форми, а середовище — прямокутне полотно. Фізичні рушії можуть бути розроблені для високої точності (вимагають високопродуктивних обчислень) або реального часу (з використанням простих і швидких алгоритмів). У цьому розділі зосереджено увагу на створенні елементарного фізичного рушія з фокусом на його швидкість та простоту.

Сили й закони руху Ньютона

Почнімо з концептуального погляду на те, що означає сила в реальному світі. Як і слово вектор, термін сила може мати різноманітні значення. Він може вказувати на потужну фізичну інтенсивність, як у фразі “Вони штовхали валун із великою силою”, або на потужний вплив, як у вислові “Вони — сила, з якою необхідно рахуватися!” Визначення сили, яке мене цікавить для цього розділу, більш формальне і походить від трьох законів руху сера Ісаака Ньютона:

Сила — це вектор, який впливає на прискорення об’єкта з масою.

Сподіваюся, ви розпізнали першу частину визначення: сила — це вектор. Добре, що ви щойно пройшли цілий розділ, вивчаючи, що таке вектори і як з ними програмувати! Тепер я продовжу з пояснення, як три закони руху Ньютона пов’язані з тим, що ви вже знаєте про вектори, і по ходу розповіді побудую сцену для решти визначень сили.

Перший закон Ньютона

Перший закон Ньютона зазвичай формулюється так:

Об’єкт, що перебуває у спокої, залишається у стані спокою, а об’єкт, що рухається, залишається у русі.

Однак тут відсутній важливий елемент, пов’язаний із силами. Я можу розширити це визначення, заявивши:

Об’єкт у стані спокою залишається у спокої, а об’єкт у стані руху залишається у русі з постійною швидкістю і напрямком, якщо на нього не діє незбалансована сила.

До появи Ньютона, панівній теорії руху, сформульованій Арістотелем, було майже 2000 років. Вона стверджувала, що якщо об’єкт рухається, для його подальшого руху потрібна якась сила. Якщо цю рухому річ не підштовхувати або не тягнути, вона сповільниться або зупиниться. Ця теорія була підтверджена шляхом спостереження за світом. Наприклад, якщо ви кинете м’яч, він впаде на землю і зрештою зупиниться, оскільки сила поштовху більше не діятиме на нього.

Ця старіша теорія, звісно ж, не відповідає дійсності. Як встановив Ньютон, за відсутності будь-яких сил, щоб підтримувати рух тіла не потрібна сила. Коли об’єкт (наприклад, вищезгаданий м’яч) підкидається в земній атмосфері, його швидкість змінюється через невидимі сили, такі як опір повітря і гравітація. Швидкість об’єкта залишатиметься постійною лише за відсутності будь-яких сил або якщо сили, які діють на нього, компенсують одна одну, тобто сумарна сила дорівнюватиме нулю. Це часто називається рівновагою (див. малюнок 2.1). М’яч, що падає, досягне кінцевої швидкості (яка залишатиметься постійною), коли сила опору повітря дорівнюватиме силі тяжіння.

Малюнок 2.1: Іграшкова мишка не рухається, тому що всі сили компенсують одна одну (тобто сумарна сила дорівнює нулю)
Малюнок 2.1: Іграшкова мишка не рухається, тому що всі сили компенсують одна одну (тобто сумарна сила дорівнює нулю)

З урахуванням полотна p5.js я можу переформулювати перший закон Ньютона наступним чином:

Вектор швидкості об’єкта залишатиметься постійним, якщо він перебуває у стані рівноваги.

Іншими словами, у класі Mover функція update() не повинна застосовувати жодні математичні операції до вектора швидкості, якщо немає якоїсь сумарної сили, відмінної від нуля.

Третій закон Ньютона

Дозвольте мені на мить відкласти другий закон Ньютона (мабуть, найважливіший закон для цілей цієї книги) і перейти до його третього закону. Цей закон часто формулюється так:

На кожну дію є рівна протидія.

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

Скажімо, ви штовхаєте стіну. Стіна не вирішує активно відштовхувати вас назад, але все ж чинить опір з рівною силою у протилежному напрямку. У стіни немає власного “джерела” сили. Ваш тиск просто включає обидві сили, які називаються парою дії/протидії. Кращим способом формулювання третього закону Ньютона може бути наступне:

Сили завжди виникають парами. Обидві сили мають однакову міць, але протилежні напрямки.

Це все ще викликає плутанину, оскільки звучить так, ніби ці сили завжди нівелюють одна одну. Це не той випадок. Пам’ятайте, що сили діють на різні об’єкти. І просто тому, що обидві сили рівні, це не означає, що рухи об’єктів однакові (або що об’єкти перестануть рухатися).

Розглянемо штовхання нерухомої вантажівки. Попри те, що вантажівка набагато масивніша за вас, у нерухомому стані вона ніколи не здолає вас і не змусить відлетіти назад. Сила, з якою ваші руки діють на вантажівку, рівна і протилежна силі, з якою вантажівка діє на ваші руки. Результат залежить від низки інших факторів. Якщо вантажівка невелика і припаркована на зледенілій поверхні, ви, ймовірно, зможете змусити її рухатися. З іншого боку, якщо це дуже велика вантажівка на ґрунтовій дорозі, то штовхнувши її досить сильно (можливо, навіть з розбігу), можна ушкодити свою руку.

А що, якщо під час поштовху вантажівки ви будете на роликах, як показано на малюнку 2.2?

Малюнок 2.2: Демонстрація третього закону руху Ньютона шляхом штовхання важкої вантажівки на роликових ковзанах
Малюнок 2.2: Демонстрація третього закону руху Ньютона у вигляді штовхання важкої вантажівки на роликах

Ви рухатиметеся від вантажівки, ковзаючи по дорозі, поки вантажівка залишатиметься на місці. Чому ви ковзаєте, а не вантажівка? По-перше, вантажівка має набагато більшу масу (про це я розповім за допомогою другого закону Ньютона). Тут також діють й інші сили, зокрема тертя об дорогу шин вантажівки та ваших роликів.

Знову, враховуючи p5.js, я міг би переформулювати третій закон Ньютона наступним чином:

Якщо ви обчислюєте p5.Vector під назвою f, який представляє дію сили об’єкта AA на об’єкт BB, ви також повинні застосувати протилежну силу, з якою об’єкт BB діє на об’єкт AA. Ви можете обчислити цю іншу силу як p5.Vector.mult(f, -1).

Невдовзі ви побачите, що у світі програмування симуляцій часто не обов’язково дотримуватися третього закону Ньютона. Іноді, наприклад, у випадку гравітаційного притягання між тілами (див. приклад 2.8), я захочу моделювати рівні й протилежні сили. В інших випадках, наприклад, у сценарії де я скажу: “Гей, тут дмухає вітер”, я не збираюся моделювати зворотну силу, з якою тіло діятиме на повітря. Насправді я взагалі не буду моделювати повітря! Пам’ятайте, що приклади в цій книзі надихаються фізикою природного світу з метою творчості та інтерактивності. Вони не вимагають ідеальної точності.

Другий закон Ньютона

Настав час для найважливішого закону для кодерів на p5.js: другого закону Ньютона. Він формулюється наступним чином:

Сила дорівнює масі, помноженій на прискорення.

Або:

F=M×A\vec{F} = M \times \vec{A}

Чому це найважливіший закон для цієї книги? Що ж, запишемо це рівняння по-іншому:

A=F/M\vec{A} = \vec{F} / M

Прискорення прямо пропорційно силі й обернено пропорційно масі. Подумайте, що це означає, якщо вас штовхнути? Чим сильніше поштовх, тим швидше буде збільшення або зменшення вашої швидкості (прискорення). З іншого боку, чим ви більші, тим менш ефективна сила для вашого прискорення!

Вага проти маси

Масу не слід плутати з вагою. Маса є мірою кількості речовини в об’єкті (вимірюється в кілограмах). Об’єкт з масою 1-го кілограма на Землі матиме масу 1-го кілограма на Місяці.

Вага, яку часто плутають з масою, технічно є силою тяжіння, що діє на об’єкт. Згідно з другим законом Ньютона, ви можете обчислити вагу як масу, помножену на прискорення вільного падіння (w=m×gw = m \times g). Вага вимірюється в ньютонах, одиниці, яка вимірює величину сили тяжіння. Оскільки вага пов’язана з гравітацією, об’єкт на Місяці важить на одну шосту менше, ніж на Землі.

З масою пов’язане поняття густини, яке визначається як кількість маси на одиницю об’єму (наприклад, грам на кубічний сантиметр).

Що таке маса у світі p5.js? Хіба ми не маємо справу з пікселями? Почнімо з простого і скажемо, що в уявному піксельному світі всі об’єкти мають масу рівну 1. Будь-що, поділене на 1 дорівнює самому собі, отже, у цьому простому світі ми маємо наступне:

A=F\vec{A} = \vec{F}

Я фактично виключив масу з рівняння, зробивши прискорення об’єкта рівним силі. Це чудова новина. Зрештою, у Розділі 1 я описав прискорення як ключ до керування рухом об’єктів на полотні. Я сказав, що положення змінюється відповідно до швидкості, а швидкість — відповідно до прискорення. Здається, що прискорення — це те, з чого все починається. Тепер ви бачите, що насправді саме сила — це те, з чого все починається.

Візьмімо клас Mover, з позицією, швидкістю і прискоренням:

class Mover {

  constructor() {

    this.position = createVector();

    this.velocity = createVector();

    this.acceleration = createVector();

  }

}

Тепер потрібно отримати можливість додавати сили до цього об’єкта за допомогою подібного коду:

mover.applyForce(wind);

Або ось так:

mover.applyForce(gravity);

Тут wind і gravity — це об'єкти типу p5.Vector. Згідно з другим законом Ньютона, я міг би реалізувати цей метод applyForce() наступним чином:

applyForce(force) {

  this.acceleration = force;

Другий закон Ньютона у його найпростішому вигляді.

}

Виглядає досить добре. Зрештою, прискорення = сила — це буквальний переклад другого закону Ньютона (у світі без маси). Проте, цей код має досить велику проблему, з якою я швидко зіткнуся, коли повернусь до своєї початкової мети: створення об’єкта, який реагує на силу вітру і гравітації. Розгляньте цей код:

mover.applyForce(wind);

mover.applyForce(gravity);

mover.update();

Уявіть на мить, що ви комп’ютер. Спершу ви викликаєте метод applyForce() з параметром wind і тепер прискорення об'єкта Mover присвоює значення вектора wind. Потім ви викликаєте метод applyForce() з параметром gravity. Тепер прискорення об'єкта Mover встановлено вектором gravity. Нарешті, ви викликаєте метод update(). Що відбувається у update()? Прискорення додається до швидкості:

this.velocity.add(this.acceleration);

Якщо ви запустите цей код, то не побачите помилку в консолі, але бзиньк! Маємо серйозну проблему. Яке значення має прискорення, коли його додають до швидкості? Воно дорівнює вектору gravity, а значення wind було випущено! Кожного разу, коли викликається метод applyForce(), значення прискорення перезаписується. Як же мені впоратися з більш ніж однією силою?

Акумуляція сил

Відповідь полягає в тому, що сили повинні акумулюватися або додаватися разом. Це зазначено в повному визначенні другого закону Ньютона, яке мушу визнати, я спростив. Ось більш точне формулювання:

Сукупна сила дорівнює масі, помноженій на прискорення.

Іншими словами, прискорення дорівнює сумі всіх сил, поділеній на масу. У будь-який момент на об’єкт може діяти 1, 2, 6, 12 або 303 сили. Поки об’єкт знає, як їх додавати разом (акумулювати), не має значення, скільки саме цих сил. Загальна сума дасть вам прискорення об’єкта (знову ж таки, ігноруючи масу). Це цілком логічно. Зрештою, як ви бачили в першому законі Ньютона, якщо всі сили, що діють на об’єкт, у сумі дорівнюють нулю, об’єкт перебуває у стані рівноваги (тобто немає прискорення).

Тепер я можу переглянути метод applyForce(), щоб врахувати у ньому накопичення сил:

applyForce(force) {

  this.acceleration.add(force);

Другий закон Ньютона, але з акумуляцією сил через додавання усіх вхідних значень.

}

Але я ще не закінчив. Накопичення сил має ще одну частину. Оскільки я додаю всі сили разом у будь-який момент, я повинен переконатися, що очищаю прискорення (встановлюю його на 0) перед кожним викликом методу update(). Подумайте на мить про силу вітру. Іноді вітер дуже сильний, іноді слабкий, а іноді його зовсім немає. Наприклад, ви можете написати код, який створює порив вітру при утриманні кнопки:

if (mouseIsPressed) {

  let wind = createVector(0.5, 0);

  mover.applyForce(wind);

}

Коли кнопка відпускається, вітер має припинитися і, згідно з першим законом Ньютона, об'єкт має продовжити рухатися з постійною швидкістю. Однак, якщо я забуду скинути прискорення до 0, порив вітру все ще буде діяти. Навіть гірше, він додасться з попереднього кадру! Прискорення у фізичній симуляції на основі часу не має пам’яті, воно обчислюється на основі сил навколишнього середовища, присутніх у будь-який момент (кадр) часу. Це відрізняється від, скажімо, положення. Об'єкт повинен пам'ятати своє попереднє розташування, щоб правильно переміститися до наступного.

Один із способів очистити прискорення для кожного кадру — це помножити вектор прискорення на 0 у кінці функції update():

update() {

  this.velocity.add(this.acceleration);

  this.position.add(this.velocity);

  this.acceleration.mult(0);

Очищення прискорення після його застосування.

}

Можливість акумулювати та застосовувати сили наближає мене до робочого фізичного рушія, але тут я маю відзначити ще одну деталь, яку я замовчував, окрім маси. Це крок часу — частота оновлення симуляції. Розмір кроку часу впливає на точність і поведінку симуляції, тому багато фізичних рушіїв включають крок часу як змінну (часто позначається як dt, що означає delta time (дельта часу) або зміна у часі). Для спрощення я вирішив вважати, що кожен цикл функції draw() представляє один крок часу. Це припущення може бути не найточнішим, але воно дозволяє мені зосередитися на ключових принципах симуляції.

Я залишу це припущення до Розділу 6, коли я розгляну вплив різних кроків часу, описуючи сторонні фізичні бібліотеки. Однак зараз я можу і повинен взятися за величезного слона в кімнаті, якого досі ігнорував — масу:

Вправа 2.1

За допомогою сил, імітуйте повітряну кульку, наповнену гелієм, що пливе вгору і відскакує від поверхні полотна. Чи зможете ви додати силу вітру, яка змінюється з часом, можливо, залежну від шуму Перліна?

Фактор маси

Другий закон Ньютона це насправді F=M×A\vec{F} = M \times \vec{A}, а не F=A\vec{F} = \vec{A}. Як можна врахувати масу у симуляції? Для початку це так само просто, як додати властивість this.mass до екземпляра класу Mover, але мені треба приділити цьому питанню трохи більше часу через інше неминуче ускладнення.

Але спочатку я додам масу:

class Mover {

  constructor() {

    this.position = createVector();

    this.velocity = createVector();

    this.acceleration = createVector();

    this.mass = ????;

Додавання маси у вигляді числа.

  }

}

Одиниці вимірювання

Зараз, коли я вводжу масу, важливо коротко згадати одиниці вимірювання. У реальному світі речі вимірюються в конкретних одиницях: два об’єкти розташовані на відстані 3-х метрів один від одного, бейсбольний м’яч рухається зі швидкістю 90 миль на годину, або цей м’яч для боулінгу має масу 6 кілограмів. Іноді ви хочете взяти до уваги одиниці вимірювання реального світу. Однак у цьому розділі я буду дотримуватися піксельних одиниць вимірювання (“Ці два кола віддалені на відстані 100 пікселів”) і кадрів анімації, як кроків часу (“Це коло рухається зі швидкістю 2 пікселі на кадр”).

У випадку маси не існує відповідної цифрової одиниці вимірювання. Яку масу має будь-який піксель? Ви можете насолоджуватися вигадуванням власних одиниць маси для p5.js, таких як “10 пікселоїдів” або “10 юрклів”.

Для демонстрації я прив’яжу масу до пікселів (чим більший діаметр кола, тим більша маса). Це дозволить мені візуалізувати масу об’єкта, хоча й неточно. У реальному світі розмір не вказує на масу. Маленька металева кулька може мати набагато більшу масу, ніж велика повітряна куля, через свою більшу густину. Зауважу, що для круглих об’єктів маса повинна бути прив’язаною до формули площі круга: πr2\pi r^2. (Це буде розглянуто у вправі 2.11, а ще більше я розповім про π\pi та круги у Розділі 3.)

Маса є скалярною величиною, а не вектором, оскільки це лише одне число, яке описує кількість речовини в об’єкті. Я міг би обчислювати площу фігури як її масу, але простіше почати, сказавши: “Агов, маса цього об’єкта дорівнює… гм, ну я знаю… як щодо 10?”

constructor() {

  this.position = createVector(random(width), random(height));

  this.velocity = createVector(0, 0);

  this.acceleration = createVector(0, 0);

  this.mass = 10;

}

Це поки не настільки добре, оскільки все стає цікавим лише тоді, коли я маю об’єкти з різною масою, але цього достатньо, щоб почати. Де залучити масу? Мені потрібно поділити силу на масу, щоб застосувати до об’єкта другий закон Ньютона:

applyForce(force) {

  force.div(mass);
  this.acceleration.add(force);

Другий закон Ньютона (з акумуляцією сил та масою).

}

Знову ж таки, хоч код виглядає досить розумним, він має серйозну проблему. Розглянемо наступний сценарій із двома об’єктами типу Mover, що рухаються під дією сили вітру:

let moverA = new Mover();

let moverB = new Mover();


let wind = createVector(1, 0);


moverA.applyForce(wind);

moverB.applyForce(wind);

Знову уявіть, що ви комп’ютер. Об’єкт moverA отримує силу вітру (1,0)(1, 0) й ділить її на mass зі значенням 10 та додає до прискорення:

ДіяВекторні компоненти
moverA приймає силу вітру.(1,0)(1, 0)
moverA ділить силу вітру на масу 10.(0.1,0)(0.1, 0)

Тепер ви переходите до об’єкта moverB. Він також отримує силу вітру (1,0)(1, 0). Зачекайте, одну секунду. Яке значення сили вітру? Придивившись ближче, зараз воно дорівнює (0.1,0)(0.1, 0)! Пам’ятайте, що коли ви передаєте об’єкт (у цьому випадку p5.Vector) у функцію, ви передаєте посилання на цей об’єкт. Це не копія! Отже, якщо функція вносить зміни у цей об’єкт (що в цьому випадку вона робить шляхом ділення на масу), цей об’єкт змінюється назавжди. Але я не хочу, щоб moverB отримував силу, поділену на масу об’єкта moverA. Я хочу, щоб він отримав силу в її початковому стані — (1,0)(1, 0). Тому мені потрібно захистити оригінальний вектор і зробити його копію перед діленням на масу.

На щастя, клас p5.Vector має зручний метод для створення копії — copy(). Він повертає новий об’єкт p5.Vector з тими самими даними. Тож я можу переглянути метод applyForce() наступним чином:

applyForce(force) {

  let f = force.copy();

Створення копії вектора перед його використанням.

  f.div(this.mass);

Розділення на масу копії вектора.

  this.acceleration.add(f);

}

Давайте на мить підсумуємо те, що я вже розглянув. Я визначив, що таке сила (вектор), і показав, як застосовувати силу до об’єкта (поділити її на масу та додати до вектора прискорення об’єкта). Чого не вистачає? Що ж, мені ще належить зрозуміти, як розраховувати силу. Звідки беруться сили?

Вправа 2.2

Ви можете написати метод applyForce() іншим способом, використовуючи статичний метод div() замість copy(). Перепишіть applyForce() за допомогою статичного методу. Для допомоги у цій вправі, перегляньте “Статичні та нестатичні методи”.

applyForce(force) {

  let f = p5.Vector.div(force, this.mass);

  this.acceleration.add(f);

}

Створення сил

Ця частина розділу пропонує два способи для створення сил у світі p5.js:

  • Вигадайте силу! Зрештою, ви програміст, творець свого світу. Немає причин, чому ви не можете просто вигадати силу і застосувати її.
  • Змоделюйте силу! Сили існують у фізичному світі й підручники з фізики часто містять формули для цих сил. Ви можете взяти ці формули й перекласти їх у вихідний код для моделювання реальних сил у JavaScript.

Для початку я сфокусуюся на першому підході. Найпростіший спосіб створити силу — просто вибрати число або два числа. Почнемо з ідеї симуляції вітру. Як щодо сили вітру, спрямовану праворуч і досить слабку? З об’єктом mover, код буде наступним:

let wind = createVector(0.01, 0);

mover.applyForce(wind);

Результат не надто цікавий, але хороший для початку. Я створюю об’єкт p5.Vector, ініціалізую його та передаю в об’єкт Mover (який, своєю чергою, застосує його до власного прискорення). Щоб завершити цей приклад, я додам ще одну силу, гравітацію (спрямовану вниз), і включатиму силу вітру, лише при натиснутій кнопці мишки.

Приклад 2.1: Сили

Клацання кнопкою мишки застосовує силу вітру.
let gravity = createVector(0, 0.1);

mover.applyForce(gravity);


if (mouseIsPressed) {

  let wind = createVector(0.1, 0);

  mover.applyForce(wind);

}

Тепер я маю дві сили, спрямовані в різні боки й з різною магнітудою, обидві застосовані до об’єкта mover. Я починаю підходити до певних результатів. Я побудував світ, середовище з силами, які діють на предмети!

Подивимося, що станеться, коли я додам другий об’єкт з іншою масою. Щоб зробити це, вам, можливо, захочеться коротко пригадати ООП. Знову ж таки, я не розглядаю тут усі основи програмування (для цього ви можете переглянути будь-які вступні книги з p5.js або відеоуроки, перелічені у “вступі”). Однак, оскільки ідея створення світу наповненого об’єктами є фундаментальною для всіх прикладів цієї книги, варто приділити хвилинку, щоб здолати певні кроки від використання одного об’єкта до багатьох.

Ось як зараз виглядає клас Mover. Зверніть увагу, наскільки він ідентичний класу Mover, створеному в Розділі 1, з двома доповненнями: властивістю mass і новим методом applyForce():

class Mover {

  constructor() {

    this.mass = 1;

Наразі для простоти встановимо масу рівною 1.

    this.position = createVector(width / 2, 30);

    this.velocity = createVector(0, 0);

    this.acceleration = createVector(0, 0);

  }


  applyForce(force) {

Другий закон Ньютона.

    let f = p5.Vector.div(force, this.mass);
    this.acceleration.add(f);

Отримаємо силу, поділимо її на масу і додамо до прискорення.

  }


  update() {

    this.velocity.add(this.acceleration);
    this.position.add(this.velocity);

Базовий рух з Розділу 1.

    this.acceleration.mult(0);

Очищення прискорення після його застосування.

  }


  show() {

    stroke(0);

    fill(175);

    circle(this.position.x, this.position.y, this.mass * 16);

Масштабування розміру кругу відповідно до маси. Ми ще удосконалимо цей момент!

  }


  checkEdges() {
    if (this.position.x > width) {
      this.position.x = width;
      this.velocity.x *= -1;
    } else if (this.position.x < 0) {
      this.velocity.x *= -1;
      this.position.x = 0;
    }

Я також вирішив додати код, щоб об’єкт відскакував від країв полотна.


    if (this.position.y > height) {

      this.velocity.y *= -1;
      this.position.y = height;

Хоча я казав про бажання не змінювати напряму значення положення і швидкості, можливі винятки. Тут я роблю це як швидкий спосіб змінити напрямок руху об’єкта, коли він досягає краю.

    }

  }

}

Тепер, коли клас написаний, я можу створювати більше одного об’єкта типу Mover:

let moverA = new Mover();

let moverB = new Mover();

Але є проблема. Подивіться ще раз на конструктор об’єкта Mover:

constructor() {

  this.mass = 1;
  this.position = createVector(width / 2, 30);

Кожен об’єкт має масу 1 і стале положення (width / 2, 30).

  this.velocity = createVector(0, 0);

  this.acceleration = createVector(0, 0);

}

Наразі кожен об’єкт Mover створюється з однаковими даними. Я хочу мати можливість створювати об’єкти Mover зі змінними значеннями маси й положення. Хороший спосіб зробити це за допомогою аргументів конструктора:

constructor(x, y, mass) {

  this.mass = mass;
  this.position = createVector(x, y);

Тепер присвоїмо значення із аргументів.

  this.velocity = createVector(0, 0);

  this.acceleration = createVector(0, 0);

}

Зверніть увагу, що маса та положення більше не встановлюються жорстко закодованими числами, а ініціалізуються через аргументи x, y і mass, що передані конструктору. Це означає, що я можу створювати різні об’єкти типу Mover — великі, маленькі, ті, що починаються з лівого боку полотна, або такі, що починаються праворуч і будь-де між ними:

let moverA = new Mover(100, 30, 10);

Великий об’єкт на лівій стороні полотна.

let moverB = new Mover(400, 30, 2);

Менший об’єкт на правій стороні полотна.

Я міг вибрати ініціалізацію значень різними способами (випадковим чином, шумом Перліна, методом сітки тощо). Тут я просто вибрав кілька чисел для демонстраційних цілей. У цій книзі я ще покажу інші методи ініціалізації для симуляції.

Після того, як об’єкти оголошені й ініціалізовані, решта коду слідує як і раніше. Для кожного об’єкта передайте сили, щоб засовувати їх через applyForce() і насолоджуйтеся шоу!

Приклад 2.2: Сили, що діють на два об’єкти

Клацання кнопкою мишки застосовує силу вітру.
function draw() {

  background(255);


  let gravity = createVector(0, 0.1);
  moverA.applyForce(gravity);
  moverB.applyForce(gravity);

Вигадайте і створіть певну силу тяжіння та застосуйте її.

  if (mouseIsPressed) {
    let wind = createVector(0.1, 0);
    moverA.applyForce(wind);
    moverB.applyForce(wind);
  }

Вигадайте і створіть певну силу вітру та застосовуйте її при клацанні мишки.


  moverA.checkEdges();

  moverA.update();

  moverA.show();


  moverB.checkEdges();

  moverB.update();

  moverB.show();

}

Зверніть увагу, що кожна операція в коді записана двічі, один раз для moverA і один раз для moverB. На практиці, для управління кількома об’єктами, масив буде більш доречним вибором, ніж окремі змінні, особливо коли їх кількість зростає. Таким чином мені доведеться написати кожну операцію лише один раз і використати цикл, щоб застосувати її до кожного об’єкта Mover у масиві. Я продемонструю це пізніше, а масиви детальніше розгляну у Розділі 4.

Вправа 2.3

Замість того, щоб об’єкти відбивалися від краю полотна, створіть приклад, у якому невидима сила відштовхуватиме об’єкти назад, щоб утримати їх у межах вікна. Чи можете ви врахувати величину для сили в залежності від того, наскільки далеко об’єкт знаходиться від краю, так що чим ближче він до нього, тим більшою буде сила?

Вправа 2.4

Змініть відбивання кульки від стінок полотна так, щоб вона змінювала напрямок, коли торкається до межі своїм краєм, а не центром, як це відбувається зараз.

Вправа 2.5

Створіть мінливу силу вітру. Чи можете ви зробити її інтерактивною? Наприклад, подумайте про подобу вентилятора, розташованого у положенні курсора мишки й спрямованого у напрямку до кульок.

При запуску прикладу 2.2, зверніть увагу, що мале коло реагує на прикладену дію акумульованих сил відчутніше, ніж велике. Це пояснюється формулою: прискорення = сила, поділена на масу. Маса знаходиться у знаменнику, тому чим вона більше, тим менше прискорення. Це має сенс для сили вітру — чим масивніший об’єкт, тим вітру важче його штовхати, — але чи це підходить для симуляції гравітаційного тяжіння Землі?

Якби ви піднялися на вершину Пізанської вежі й впустили з неї дві кулі різної маси, яка з них першою вдарилася б об землю? Згідно з легендою, Галілей провів саме цей експеримент у 1589 році, виявивши, що вони падали з однаковим прискоренням, вдарившись об землю одночасно. Чому? Незабаром я розповім про це глибше, але швидка відповідь полягає в тому, що сила тяжіння обчислюється відносно маси об’єкта, тож чим більший об’єкт, тим дужча сила, але ця сила нівелюється, коли ділиться на масу, щоб визначити прискорення. Отже, прискорення сили тяжіння для різних об’єктів однакове.

Швидке виправлення програми — те, що наближає її на крок до більш реалістичного моделювання сили, а не простого її вигадування — полягає у помноженні сили тяжіння на масу.

Приклад 2.3: Гравітація, масштабована за масою

Клацання кнопкою мишки застосовує силу вітру.
let gravity = createVector(0, 0.1);

Створення сили тяжіння.

let gravityA = p5.Vector.mult(gravity, moverA.mass);
moverA.applyForce(gravityA);

Масштабування сили тяжіння на основі маси об’єкта moverA.

let gravityB = p5.Vector.mult(gravity, moverB.mass);
moverB.applyForce(gravityB);

Масштабування сили тяжіння на основі маси об’єкта moverB.

Тепер об’єкти падають з однаковою швидкістю. Я все ще обчислюю силу тяжіння, довільно встановивши для неї значення 0.1, але, масштабуючи силу відповідно до маси об’єкта, я обчислюю її у спосіб, який трохи точніше відповідає фактичній силі гравітаційного тяжіння Землі. Тим часом оскільки сила вітру не залежить від маси, коли кнопка мишки натиснута, менше коло все одно прискорюється праворуч швидше. (Онлайн-код для цього прикладу також містить рішення вправи 2.4 із доданою змінною radius у класі Mover.)

Моделювання сили

Вигадування сил може завести досить далеко і я щойно створив досить хороше наближення земної гравітації. Зрештою, світ p5.js — це оркестр пікселів, а ви — диригент, тому можете визначати як силу будь-що, що вважаєте за потрібне! Проте, може настати час, коли вам стане цікаво: “А як це все насправді працює?” Ось коли на сцену виходить моделювання сил, а не просто їх вигадування.

Розбір формул

За мить я збираюся написати формулу для тертя. Це вже не перший раз, коли ви бачите формулу в цій книзі і я щойно закінчив обговорення другого закону Ньютона: F=M×A\vec{F} = M \times \vec{A} (сила дорівнює масі, помноженій на прискорення). Сподіваюся, ви не довго хвилювались щодо цієї формули, оскільки це всього лише кілька символів і знаків. Проте, у цьому світі велика кількість термінів і символів. Просто подивіться на рівняння нормального розподілу, про який я говорив раніше, не розглядаючи саму його формулу, у “Нормальному розподілі випадкових чисел”:

1σ2πe(xμ)22σ2\frac{1}{\sigma\sqrt{2\pi}}e^{-\frac{(x-\mu)^2}{2\sigma^2}}

Формули часто записуються з багатьма символами, часто з літерами грецького алфавіту. Ось формула для тертя (позначена через f\vec{f}):

f=μNv^\vec{f} = -\mu N \hat{v}

Якщо вам давно не траплялись формули з підручників математики чи фізики, то перш ніж я продовжу, важливо розглянути три ключові моменти:

  • Обчисліть праву сторону та призначте результат лівій стороні. Це як у коді! У наведеному вище випадку ліва сторона представляє те, що я хочу обчислити — силу тертя, а права сторона пояснює, як це зробити.
  • Мова йде про вектор чи скаляр? Важливо розуміти, що в деяких випадках ви будете обчислювати вектор, а в інших — скаляр. Наприклад, у цьому випадку сила тертя є вектором. Він позначений стрілкою над символом ff. Він має магнітуду і напрямок. У правій частині рівняння також є вектор, позначений символом v^\hat{v}, який у цьому випадку означає одиничний вектор швидкості.
  • Коли символи розташовані поруч один з одним, це зазвичай означає їх перемноження. Права частина формули тертя містить чотири елементи: -, μμ, NN і v^\hat{v}. Їх слід перемножити, читаючи формулу як: f=1×μ×N×v^\vec{f} = -1 \times \mu \times N \times \hat{v}.

Відкрийте будь-який підручник з фізики середньої школи й ви знайдете діаграми та формули, які описують різні сили — гравітацію, електромагнетизм, тертя, натяг, пружність тощо. У решті цього розділу я збираюся розглянути три сили — тертя, опір і гравітаційне тяжіння — й покажу, як їх змоделювати за допомогою p5.js. Суть не в тому, що це фундаментальні сили, які завжди потрібні у ваших симуляціях. Скоріше я хочу продемонструвати ці сили як приклади для:

  1. Розуміння концепції сили
  2. Розбору формули сили на дві частини:
    1. Як обчислити напрямок сили?
    2. Як обчислити величину сили?
  3. Переведення цієї формули у код p5.js, який обчислює вектор для його передачі у метод applyForce() об’єкта Mover

Якщо, на наведених далі прикладах з силами, ви зможете опанувати ці кроки та зловите себе при гуглінні запитів о третій годині ранку типу “слабкої взаємодії атомних ядер” то, сподіваюсь, матимете достатні навички, щоб взяти знайдене та адаптувати це в межах p5.js.

Тертя

Почнемо з тертя і пройдемося вищезазначеними кроками. Щоразу, коли дві поверхні стикаються, між ними відбувається тертя. Тертя є дисипативною силою, тобто вона приводить до перетворення кінетичної енергії об’єкта в іншу форму, створюючи враження втрати або дисипації (розсіювання).

Припустимо, що ви керуєте автомобілем. Коли ви натискаєте ногою на гальмо автомобіля, його гальма використовують тертя об покришки, щоб сповільнити рух коліс. Кінетична енергія (рух) перетворюється на теплову енергію (тепло). Повна модель тертя включатиме окремі випадки статичного тертя (тертя спокою, тобто без руху) і кінетичного тертя (тертя ковзання, цебто з рухом), але для спрощення я розглядатиму тут лише кінетичний випадок. На малюнку 2.3 зображена формула для тертя.

Оскільки тертя є вектором, дозвольте мені розкласти цю формулу на дві складові, які визначають напрямок тертя, а також його магнітуду. Малюнок 2.3 показує, що тертя спрямоване в протилежному напрямку від швидкості. Фактично, це та частина формули, яка говорить: 1×v^-1 \times \hat{v}, або мінус один помножити на одиничний вектор швидкості. У p5.js це означало б взяти вектор об’єкта velocity і помножити його на -1:

let friction = this.velocity.copy();

friction.normalize();

friction.mult(-1);

Визначимо напрямок сили тертя (одиничний вектор у напрямку протилежному швидкості).

Зверніть увагу на два додаткові кроки. По-перше, важливо зробити копію вектора velocity, оскільки я не хочу випадково змінити напрямок об’єкта. По-друге, вектор нормалізується. Це тому, що величина тертя не пов’язана зі швидкістю об’єкта, і я хочу почати з вектора довжиною у 1, щоб його можна було легко масштабувати.

Малюнок 2.3: Тертя — це сила, яка спрямована в протилежний напрямок від швидкості саней під час їх ковзання по схилу пагорба
Малюнок 2.3: Тертя — це сила, яка спрямована в протилежний напрямок від швидкості саней під час їх ковзання по схилу пагорба

Згідно з формулою, магнітуда це μ×N\mu \times N. Грецька літера μ\mu (промовляється як мю), використовується тут для опису коефіцієнта тертя. Коефіцієнт тертя визначає величину сили тертя для певної поверхні. Чим вона вища, тим сильніше тертя, чим нижча — тим слабше тертя. Кубик льоду, наприклад, матиме набагато нижчий коефіцієнт тертя, ніж, скажімо, наждачний папір. Оскільки це умовний світ p5.js, я можу довільно встановити коефіцієнт для регулювання сили тертя:

let c = 0.01;

Тепер щодо другої частини. NN вказує на силу нормальної реакції — силу, перпендикулярну до руху об’єкта вздовж поверхні. Уявіть автомобіль, що їде дорогою. Автомобіль тисне на дорогу під дією сили тяжіння і третій закон Ньютона говорить нам, що дорога, своєю чергою, тисне назад і на авто. Це і є сила нормальної реакції (іноді кажуть нормальна реакція опори). Чим більша сила тяжіння, тим більша сила нормальної реакції.

Як ви побачите в наступному розділі, гравітаційне тяжіння пов’язане з масою, тому легке спортивне авто відчуватиме менше тертя, ніж масивна вантажівка з причепом. Однак на малюнку 2.3, де об’єкт рухається вздовж поверхні під кутом, обчислення магнітуди й напрямку сили нормальної реакції трохи складніше, оскільки вона не вказує в протилежному напрямку від сили тяжіння. Для цього вам потрібно знати дещо про кути й тригонометрію.

Усі ці особливості важливі. Однак, “досить хорошу” симуляцію можна досягнути й без них. Я можу, наприклад, налаштувати тертя з припущенням, що нормальна сила завжди матиме магнітуду 1. Коли я перейду до тригонометрії у наступному розділі, ви зможете повернутися до цього питання і зробити даний приклад тертя складнішим. Отож:

let normal = 1;

Тепер, коли я знаю магнітуду і напрямок тертя, я можу об’єднати все це у коді:

let c = 0.1;

let normal = 1;

let frictionMag = c * normal;

Обчислення величини тертя (насправді довільна константа).

let friction = mover.velocity.copy();

friction.mult(-1);

friction.normalize();

friction.mult(frictionMag);

Беремо одиничний вектор і множимо його на магнітуду. Отримуємо вектор сили!

Цей код обчислює силу тертя, але не відповідає на питання, коли її застосовувати. Відповіді на це запитання звісно немає, оскільки це все вигаданий світ, візуалізований на 2D полотні p5.js! Я зроблю довільне, але логічне рішення застосовувати тертя, коли кулька стикатиметься з низом полотна, що я зможу виявити, додавши до класу Mover метод під назвою contactEdge():

contactEdge() {

  return (this.position.y > height - this.radius - 1);

Об’єкт торкається краю, коли знаходиться на відстані до нього менше від 1 пікселя.

}

Це хороший момент для згадки, що тут фактичний відскок від краю імітує ідеалізоване пружне зіткнення, коли кінетична енергія не втрачається при зіткненні. Це рідко буває правдивим у реальному світі. Візьміть тенісний м’яч і киньте його на будь-яку поверхню. Висота, на яку він відскакуватиме, буде повільно зменшуватися, доки м’яч не зупиниться. Тут діє багато факторів (включаючи опір повітря, який я розгляну трохи далі), але швидкий спосіб симулювати непружне зіткнення — це зменшувати величину швидкості на певний відсоток із кожним відскоком:

bounceEdges() {

  let bounce = -0.9;

Нова змінна для симуляції непружного зіткнення з втратою 10% від швидкості для x або y компонентів.

  if (this.position.y > height - this.radius) {

    this.position.y = height - this.radius;

    this.velocity.y *= bounce;

  }

}

Нарешті, я можу додати всі ці фрагменти до коду з прикладу 2.3, і змоделювати вплив на кульку дії від трьох сил: вітру (коли натиснуто мишку), гравітації (завжди) і тепер тертя (під час контакту з нижньою частиною полотна).

Приклад 2.4: Із врахуванням тертя

Клацання кнопкою мишки застосовує силу вітру.
function draw() {

  background(255);


  let gravity = createVector(0, 1);

  mover.applyForce(gravity);

Для більшої точності потрібно враховувати масу, але в цьому прикладі лише одна кулька.

  if (mouseIsPressed) {

    let wind = createVector(0.5, 0);

    mover.applyForce(wind);

  }


  if (mover.contactEdge()) {

    let c = 0.1;

    let friction = mover.velocity.copy();

    friction.mult(-1);

    friction.setMag(c);

    mover.applyForce(friction);

Застосування до об’єкта вектора сили тертя.

  }


  mover.bounceEdges();

Виклик нового методу bounceEdges().

  mover.update();

  mover.show();


}

Запускаючи приклад, ви помітите, що кулька поступово сповільнюється до зупинки. Ви можете зробити цей ефект швидшим або повільнішим, змінюючи коефіцієнт тертя, а також відсоток втрати швидкості у методі bounceEdges().

Вправа 2.6

Додайте другий об’єкт до прикладу 2.4. Як ви обробите два об’єкти з різною масою? Що, якщо кожен об’єкт матиме свій коефіцієнт тертя відносно поверхні? Чи є сенс інкапсулювати обчислення сили тертя у методі класу Mover?

Вправа 2.7

Чи можете ви замість вітру додати таку функціональність до цього прикладу, яка дозволить вам підкидати кульку за допомогою мишки?

Опір повітря і води

Тертя також виникає при проходженні тіла через рідину або газ. Результівна сила має багато назв, які насправді означають те саме: сила в’язкості, сила опору, аеродинамічний опір або гідродинамічний опір (див. малюнок 2.4).

Ефект сили опору зрештою такий самий, як ефект у наших попередніх прикладах тертя: об’єкт сповільнюється. Однак точна поведінка та обчислення сили опору дещо відрізняються. Ось формула:

Fd=12ρv2ACdv^\vec{F_d} = - \frac{1}{2}\rho{v}^2 A C_d\hat{v}
Ма�люнок 2.4: Сила опору (опір повітря або рідини) пропорційна швидкості об’єкта та площі його поверхні, спрямованої в протилежному напрямку до швидкості об’єкта
Малюнок 2.4: Сила опору (опір повітря або рідини) пропорційна швидкості об’єкта та площі його поверхні, спрямованої в протилежному напрямку до швидкості об’єкта

Дозвольте мені розібрати цю формулу, щоб побачити, що насправді необхідно для ефективної симуляції у p5.js, зробивши в процесі спрощену формулу:

  • Fd\vec{F_d} — вказує на силу опору, вектор для обчислення і передачі у метод applyForce().
  • 1/2-1/2 — є константою. Хоча це важливий фактор для масштабування сили, тут він не дуже актуальний, оскільки я буду вигадувати значення для інших констант масштабування. Проте той факт, що константа від’ємна є важливим, оскільки це вказує на те, що сила спрямована в протилежному напрямку від швидкості (так само як у випадку із тертям).
  • ρ\rho — це грецька буква ро, інша константа, яка вказує на густину рідини. Наразі я вирішив проігнорувати її та вважати рівною 1.
  • vv — вказує на швидкість руху об’єкта. Гаразд, ви вже знаєте це! Швидкість об’єкта — це магнітуда вектора швидкості: velocity.mag(). А v2v^2 просто означає vv у квадраті, або v×vv \times v. (Зауважу, що це передбачає, що рідина або газ знаходиться на місці й не рухається. Якщо ви кинете об’єкт у потік річки, вам також потрібно врахувати відносну швидкість води.)
  • AA — вказує на площу фронтальної поверхні об’єкта, який проштовхується через рідину або газ. Уявіть плаский аркуш паперу, який падає у повітрі, і порівняйте його з гострим олівцем, спрямованим вертикально вниз. Олівець відчує менший опір, оскільки має меншу площу поверхні, спрямовану в напрямку його руху. Знову ж таки, це константа, і для простоти імплементації я вважатиму, що всі об’єкти мають сферичну форму та ігноруватиму цей показник.
  • CdC_d — це коефіцієнт опору, точно такий же, як і коефіцієнт тертя (μ). Ця константа визначатиме відносну силу опору.
  • v^\hat{v} — має виглядати знайомо. Це одиничний вектор швидкості, знайдений за допомогою velocity.normalize(). Так само як тертя, опір — це сила, яка спрямована в протилежний напрямок від швидкості.

Тепер, коли я проаналізував кожну із цих складових і визначив, що потрібно для моєї симуляції, я можу скоротити формулу, як показано на малюнку 2.5.

Малюнок 2.5: Моя спрощена формула для сили опору
Малюнок 2.5: Моя спрощена формула для сили опору

Хоча я написав спрощену формулу з CdC_d як єдиною константою, яка представляє коефіцієнт опору, я також можу думати про неї як про всі об’єднані константи разом (1/2-1/2, ρ\rho, AA). Складніша симуляція може розглядати ці константи окремо — ви можете спробувати врахувати їх окремою вправою.

Ось спрощена версія формули для опору:

let c = 0.1;

let speed = this.velocity.mag();

let dragMagnitude = c * speed * speed;

Перша частина формули (магнітуда).

let drag = this.velocity.copy();

drag.mult(-1);

Друга частина формули (напрямок).

drag.setMag(dragMagnitude);

Магнітуда і напрямок разом!

Реалізуємо цю силу на прикладі класу Mover. Але коли я повинен застосовувати її? Раніше я включав силу тертя, щоб уповільнити кульку, коли вона торкалася нижнього краю полотна. Тепер я введу новий елемент у середовище — об’єкт Liquid, який чинитиме опір, коли кулька проходитиме крізь нього. “Рідина” буде намальована у вигляді прямокутника з певним положенням, шириною і висотою та матиме коефіцієнт опору, який визначає, чи легко об’єктам рухатися крізь неї (як у повітрі), чи важко (як у патоці). Крім того, вона включатиме метод show(), щоб ми могли бачити її на полотні:

class Liquid {

  constructor(x, y, w, h, c) {

    this.x = x;

    this.y = y;

    this.w = w;

    this.h = h;

    this.c = c;

Об’єкт рідини містить змінну, що визначає її коефіцієнт опору.

  }


  show() {

    noStroke();

    fill(175);

    rect(this.x, this.y, this.w, this.h);

  }

}

Тепер у програмі потрібна змінна liquid, ініціалізована у функції setup(). Я розміщу рідину в нижній половині полотна:

let liquid;


function setup() {

  liquid = new Liquid(0, height / 2, width, height / 2, 0.1);

Ініціалізація об'єкта liquid. Я обираю низький коефіцієнт (0.1) для слабшого ефекту. Спробуйте більший!

}

Тепер виникає цікаве питання: як об’єкт Mover взаємодіє з об’єктом Liquid? Я хочу реалізувати наступну поведінку:

При проходженні кулькою рідини, ця кулька має відчувати силу опору.

Перекладемо це на об’єктно-орієнтовану мову:

if (liquid.contains(mover)) {
  let dragForce = liquid.calculateDrag(mover);
  mover.applyForce(dragForce);
} 

Якщо рідина містить об’єкт, застосуємо до нього силу її опору.

Цей код слугує інструкцією для того, що мені потрібно додати до класу Liquid: по-перше це метод contains(), який визначатиме, чи об’єкт Mover знаходиться всередині області об’єкта Liquid, і по-друге це метод drag(), який обчислюватиме та повертатиме відповідну силу опору, яка буде застосована до об’єкта Mover.

Перший метод простий: я можу використати логічний вираз, щоб визначити, чи вектор position розташований всередині прямокутника, визначеного рідиною:

contains(mover) {

  let pos = mover.position;

Збережемо положення в окремій змінній, щоб зробити код більш читабельним.

  return (pos.x > this.x && pos.x < this.x + this.w &&
          pos.y > this.y && pos.y < this.y + this.h);

Цей логічний вираз визначає, чи вектор положення розташований у прямокутнику, визначеному класом Liquid.

}

Метод calculateDrag() також досить простий: я практично вже написав для нього код, коли реалізував спрощену формулу опору! Сила опору дорівнює коефіцієнту опору, помноженому на квадрат швидкості об’єкта в протилежному напрямку від його швидкості:

calculateDrag(mover) {

  let speed = mover.velocity.mag();

  let dragMagnitude = this.c * speed * speed;

Обчислення величини магнітуди.

  let dragForce = mover.velocity.copy();
  dragForce.mult(-1);

Обчислення напрямку сили.

  dragForce.setMag(dragMagnitude);

Завершення визначення сили: об’єднання магнітуди і напрямку.

  return dragForce;

Повернення сили.

}

З цими двома методами, доданими до класу Liquid, я готовий зібрати все разом! У наступному прикладі я розширю код і використаю масив рівномірно розташованих об’єктів Mover, щоб продемонструвати, як сила опору поводиться з об’єктами різної маси. Це також ілюструє альтернативний спосіб ініціалізації симуляції, окрім випадкового. Знайдіть у коді ось цю частину: 40 + i * 70. Початкове значення для зміщення 40 надає невеликий відступ від краю полотна, а i * 70 використовує індекс об’єктів для їх рівномірного розташування. Відступ і множник довільні. Ви можете спробувати інші значення або розглянути інші способи обчислення відстані на основі розмірів полотна.

Приклад 2.5: Опір рідини

Клацання мишкою перезапустить програму.
let movers = [];

let liquid;


function setup() {

  createCanvas(640, 240);

  for (let i = 0; i < 9; i++) {

Цикл для ініціалізації масиву об’єктів типу Mover.

    let mass = random(0.1, 5);

Для кожного об’єкта готується випадкова маса.

    movers[i] = new Mover(40 + i * 70, 0, mass);

Рівномірне розподілення x-значень відповідно до індексу в масиві.

  }

  liquid = new Liquid(0, height / 2, width, height / 2, 0.1);

}


function draw() {

  background(255);


  liquid.show();

Малювання рідини.


  for (let i = 0; i < movers.length; i++) {

    if (liquid.contains(movers[i])) {

Чи об’єкт знаходиться у рідині?

      let dragForce = liquid.drag(movers[i]);

Обчислення сили опору.

      movers[i].applyForce(dragForce);

Застосування сили опору до об’єкту.

    }


    let gravity = createVector(0, 0.1 * movers[i].mass);

Тут гравітація масштабується відповідно масі!

    movers[i].applyForce(gravity);

Застосування гравітації.

    movers[i].update();
    movers[i].show();
    movers[i].checkEdges();

Оновлення і відображення об’єкта.

  }

}

Під час роботи прикладу, ви можете помітити, що він імітує падіння об’єктів у воду. Об’єкти сповільнюються під час проходження через сіру область у нижній частині вікна, яка являє собою рідину. Ви також помітите, що менші об’єкти сповільнюються значно сильніше, ніж більші. Пам'ятаєте другий закон Ньютона? Прискорення дорівнює силі поділеній на масу (A=F/M\vec{A} = \vec{F} / M), тому масивніший об’єкт прискорюватиметься менше, а менш масивний — більше. У цьому випадку прискорення є “уповільненням” через опір. Менші об'єкти сповільнюються з більшою швидкістю, ніж великі.

Вправа 2.8

Ви можете помітити, що якщо у прикладі 2.5 встановити занадто високий коефіцієнт опору, кульки можуть відскочити від рідини! Це пов’язано з неточністю великих кроків часу, про які я згадував раніше в цьому розділі. Сила опору призведе до зупинки об’єкта, але ніколи не змінить напрямок. Поміркуйте як ви можете використати векторний метод limit() для розв'язання цієї проблеми? Ви також можете спробувати скидати об’єкти з різної висоти. Як це впливає на опір при зіткненні з рідиною?

Вправа 2.9

Оригінальна формула для опору включала площу поверхні. Чи можете ви створити симуляцію падіння коробок у воду з таким опором, який залежатиме від довжини тої сторони, яка стикатиметься з водою?

Вправа 2.10

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

Гравітаційне тяжіння

Малюнок 2.6: Гравітаційна сила між двома тілами пропорційна масі цих тіл і обернено пропорційна квадрату відстані між ними
Малюнок 2.6: Гравітаційна сила між двома тілами пропорційна масі цих тіл і обернено пропорційна квадрату відстані між ними

Ймовірно, найвідомішою силою з усіх є гравітаційне тяжіння. Ми, люди на Землі, думаємо про гравітацію як про падіння речей вниз, як яблуко, що зривається з дерева і вдаряє по голові сера Ісаака Ньютона. Але це лише наш досвід сприйняття гравітації. Реальність набагато складніша.

Насправді так само як Земля притягує до себе яблуко завдяки гравітаційній силі, яблуко також притягує Землю (це третій закон Ньютона). Просто Земля настільки масивна, що переважає всі інші гравітаційні взаємодії. Фактично кожен об’єкт із масою впливає своєю силою тяжіння на будь-який інший об’єкт. Формула для обчислення величини цих сил зображена на малюнку 2.6.

Розглянемо цю формулу трохи детальніше:

  • Fg\vec{F_g} — позначає гравітаційну силу, вектор, який обчислюється і передається в метод applyForce().
  • GG — це універсальна гравітаційна константа, яка в нашому світі дорівнює 6.67428×10116.67428 \times 10^{-11} кубічних метрів на кілограм на секунду у квадраті. Це досить важливе число, якщо ви людина, але воно не так важливо, якщо ви якась фігура, що блукає по полотну p5.js. Знову ж таки, це константа, яку можна використовувати для масштабування сил у світі, роблячи їх сильнішими чи слабшими. Просто прирівняти її значення до 1 і ігнорувати буде не таким вже й жахливим вибором.
  • m1m_1 і m2m_2 є масами першого і другого об’єктів. Масу в цьому випадку теж можна було б проігнорувати, як я робив це на початку з другим законом Ньютона (F=M×A\vec{F} = M \times \vec{A}). Зрештою, фігури, намальовані на екрані, не мають фізичної маси. Однак, якщо ви враховуєте це значення, то можете створювати цікавіші симуляції, в яких “більші” об’єкти проявляють більший гравітаційний вплив, ніж “менші”.
  • r^\hat{r} — посилається на одиничний вектор, що вказує напрям від об’єкта 1 до об’єкта 2. Як ви незабаром побачите, цей вектор напрямку можна обчислити, віднімаючи положення одного об'єкта від іншого.
  • r2r^2 — це відстань між двома об’єктами піднесена у квадрат.

Подумайте трохи про цю формулу. Чим більше значення будь-якої складової у верхній частині формули — GG, m1m_1, m2m_2 — тим більшою буде сила. Велика маса — велика сила. Велике GG — велика сила. Однак для r2r^2 у нижній частині формули, ситуація протилежна: чим більше значення (чим далі знаходиться об’єкт), тим слабша сила. З математичної точки зору, сила гравітації обернено пропорційна квадрату відстані.

Настав час розібратися, як перекласти цю формулу в код на p5.js. Для цього я зроблю наступні припущення:

  • Є два об'єкти
  • Кожен об’єкт має позицію: position1 і position2
  • Кожен об’єкт має масу: mass1 і mass2
  • Змінна G представляє універсальну гравітаційну константу
Малюнок 2.7: Вектор прискорення, що вказує на положення курсора
Малюнок 2.7: Вектор прискорення, що вказує на положення курсора

Враховуючи ці припущення, я хочу обчислити силу тяжіння, а саме вектор. Я зроблю це у два кроки. Спочатку обчислю напрямок сили (r^\hat{r} у формулі). Потім розрахую величину сили, враховуючи маси об’єктів й відстань між ними.

Пам’ятаєте як у Розділі 1 я створив приклад об’єкта, що прискорюється до курсора (див. Малюнок 2.7)? Як я тоді показав, вектор можна розглядати як різницю між двома точками, тому щоб обчислити вектор, що направлений від кульки до курсора, я віднімав одну точку від іншої:

let direction = p5.Vector.sub(mouse, position);

Тепер я можу зробити те саме, щоб обчислити r^\hat{r} — напрямок сили притягання, яку об’єкт 1 чинить на об’єкт 2:

let direction = p5.Vector.sub(position1, position2);

direction.normalize();

Не забувайте, що оскільки мені потрібен одиничний вектор — вектор, який вказує лише напрямок — важливо нормалізувати вектор, отриманий після віднімання векторів положення об’єктів. (Пізніше я можу пропустити цей крок і використовувати замість нього метод setMag().)

Тепер, коли я знаю напрямок сили, мені потрібно обчислити її величину і відповідно масштабувати вектор:

let magnitude = (G * mass1 * mass2) / (distance * distance);

dir.mult(magnitude);

Малюнок 2.8: Вектор, який вказує з одного положення на інше, обчислюється як різниця між цими положеннями
Малюнок 2.8: Вектор, який вказує з одного положення на інше, обчислюється як різниця між цими положеннями

Єдина проблема в тому, що я не знаю відстані. Значення G, mass1 і mass2 вже відомі, але мені потрібно обчислити distance перш ніж запускати вищенаведений код. Але зачекайте, хіба я щосйно не зробив вектор, який вказує напрям від положення одного об’єкта до іншого? Довжина цього вектора має бути відстанню між двома об’єктами (див. малюнок 2.8).

Дійсно, якщо я додам ще один рядок коду і візьму магнітуду цього вектора до його нормалізації, то матиму відстань. І цього разу я пропущу крок з нормалізацією normalize() і використаю метод setMag():

let force = p5.Vector.sub(position2, position1);

Вектор, який вказує від одного об’єкта до іншого.

let distance = force.mag();

Довжина (магнітуда) цього вектора - це відстань між двома об'єктами.

let magnitude = (G * mass1 * mass2) / (distance * distance);

Формула гравітації для обчислення величини сили.

force.setMag(magnitude);

Нормалізація і масштабування вектора сили до відповідної величини.

Зауважте, що я також змінив назву вектора direction на force. Зрештою, коли обчислення закінчено, вектор, з якого я почав, виявляється фактичним вектором сили, який мені був потрібен.

Тепер, коли я опрацював математику і код для обчислення сили тяжіння (емуляції гравітаційного тяжіння), звернімо увагу на застосування цієї техніки в контексті програми p5.js. Я продовжу використовувати клас Mover як відправну точку — шаблон для створення об’єктів із векторами положення, швидкості й прискорення, а також методом applyForce(). Я візьму цей клас для нової програми де буде:

  • Один об'єкт типу Mover
  • Один об'єкт типу Attractor (новий клас, який матиме фіксоване положення)

Об’єкт Mover відчуватиме гравітаційне тяжіння до об’єкта Attractor, як показано на малюнку 2.9.

Малюнок 2.9: Одна кулька типу Mover і один атрактор. Кулька відчуває тяжіння до атрактора
Малюнок 2.9: Одна кулька типу Mover і один атрактор. Кулька відчуває тяжіння до атрактора

Я почну зі створення базового класу Attractor, надавши йому положення і масу, а також метод для малювання самого себе (де розмір залежатиме від маси):

class Attractor {

  constructor() {

    this.position = createVector(width / 2, height / 2);
    this.mass = 20;

Атрактор — це об'єкт, який у цьому випадку не рухається. Потрібна лише маса і позиція.

  }


  show() {

    stroke(0);

    fill(175, 200);

    circle(this.position.x, this.position.y, this.mass * 2);

  }

}

У коді я додам змінну для зберігання екземпляра об’єкта Attractor:

let mover;

let attractor;


function setup() {

  createCanvas(640, 360);

  mover = new Mover(300, 100, 5);

  attractor = new Attractor();

Ініціалізація об'єкта типу Attractor.

}


function draw() {

  background(255);

  attractor.show();

Малювання атрактора.

  mover.update();

  mover.show();

}

Це хороший початок: ескіз з об’єктами Mover і Attractor, створеними із класів які обробляють змінні та поведінку рухомих об’єктів і атракторів. Остання частина головоломки в тому, щоб один об’єкт притягував інший. Як ці два об’єкти можуть взаємодіяти? Це можна зробити кількома способами. Ось лише деякі з можливих варіантів:

ЗавданняФункція
  1. Глобальна функція, яка приймає і Attractor і Mover.
attraction(attractor, mover);
  1. Метод у класі Attractor, який приймає Mover.
attractor.attract(mover);
  1. Метод у класі Mover, який приймає Attractor.
mover.attractedTo(attractor);
  1. Метод у класі Attractor, який приймає Mover і повертає p5.Vector, який є силою притягання. Потім ця сила передається у метод applyForce() об'єкта Mover.
let force = attractor.attract(mover);
mover.applyForce(force);

Варто розглядати різні варіанти й ви, ймовірно, зможете навести аргументи на користь кожного з цих підходів. Я хотів би відкинути перший, оскільки віддаю перевагу об’єктно-орієнтованому підходу, а не довільній функції, яка не прив’язана до класу Mover або класу Attractor. Незалежно від вибору варіантів 2 чи 3, різниця буде у тому як про це говорити “Атрактор притягує рухоме тіло” чи “Рухоме тіло притягується до атрактора”. Насправді мій улюблейний 4-й варіант. Я витратив чимало часу на розробку методу applyForce() і гадаю, що приклади будуть більш зрозумілими, якщо продовжити підхід із його використанням для застосування сил.

Іншими словами, там де я колись писав таким чином:

let force = createVector(0, 0.1);
mover.applyForce(force);

Вигадана сила.

Тепер я матиму наступне:

let force = attractor.attract(mover);

Сила притягання між двома об'єктами.

mover.applyForce(force);

А значить функція draw() може бути написана так:

function draw() {

  background(255);


  let force = attractor.attract(mover);
  mover.applyForce(force);

Обчислення сили притягання і її застосування.

  mover.update();


  attractor.show();

  mover.show();

}

Я майже завершив. Оскільки я вирішив розмістити метод attract() всередині класу Attractor, мені його ще потрібно написати. Він повинен приймати об’єкт типу Mover і повертати p5.Vector:

  attract(m) {

    return ______________;

Необхідні математичні обчислення.

  }

Що має відбуватися у методі? Вся ця гарна математика для обчислення гравітаційного притягання!

  attract(mover) {

    let force = p5.Vector.sub(this.position, mover.position);

Який напрямок сили?

    let distance = force.mag();

    let strength = (this.mass * mover.mass) / (distance * distance);
    force.setMag(strength);

Обчислення величини сили притягання.


    return force;

Повернення сили, щоб її можна було застосувати там де вона потрібна!

  }

І ось я закінчив. Ну, майже. Мені потрібно виправити один маленький недолік. Подивіться ще раз на код методу attract(). Бачите похилу риску для ділення? Щоразу, коли вам трапляється така, ви повинні поставити собі таке запитання: “Що станеться, якщо відстань виявиться дуже, дуже малим числом чи (що ще гірше!) нулем?!” Ви не можете ділити число на нуль, а якщо ви поділите число на щось маленьке типу 0.0001, то це еквівалентно множенню цього числа на 10 000! Це може бути прийнятним результатом цієї формули для гравітаційного тяжіння в реальному світі, але p5.js — це не реальний світ. У світі p5.js рухомий об’єкт може виявитися дуже, дуже близьким до атрактора і результівна сила може бути настільки великою, що об’єкт відлетить далеко за межі полотна.

І навпаки, що, якби рухомий об’єкт був, скажімо, на відстані 500 пікселів від атрактора (що цілком реально у p5.js)? Ви підносите відстань у квадрат, тож це призведе до ділення сили на 250 000. Зрештою ця сила може виявитися настільки слабкою, що зовсім не матиме якогось впливу.

Щоб уникнути обох крайнощів, доцільно обмежити діапазон тих значень, які distance може приймати, перш ніж передавати її у формулу. Можливо, незалежно від того де фактично розташований Mover, для цілей обчислення сили гравітаційного притягання, вам ніколи не варто вважати, що він знаходиться на відстані менше ніж 5 пікселів або більше ніж 25 пікселів від атрактора:

  distance = constrain(distance, 5, 25);

Тут функція constrain() обмежує значення відстані між мінімумом (5) та максимумом (25).

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

Клас Mover взагалі не змінився, тому давайте просто подивимося на основний код і клас Attractor як ціле, додавши змінну G для універсальної гравітаційної константи. (На вебсайті книги ви побачите, що цей приклад також містить код, який дозволяє переміщувати об’єкт Attractor за допомогою мишки.)

let mover;
let attractor;

Рухома кулька і атрактор.

let G = 1.0;

Гравітаційна стала (для глобального масштабування).


function setup() {

  createCanvas(640, 240);

  mover = new Mover(300, 50, 2);

  attractor = new Attractor();

}


function draw() {

  background(255);

  let force = attractor.attract(mover);
  mover.applyForce(force);

Застосування сили тяжіння атрактора до кульки.

  mover.update();

  attractor.show();

  mover.show();

}


class Attractor {

  constructor() {

    this.position = createVector(width / 2, height / 2);

    this.mass = 20;

  }


  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;

  }


  show() {

    stroke(0);

    fill(175, 200);

    circle(this.position.x, this.position.y, this.mass * 2);

  }

}

У цьому коді діаметр кульки та атрактора масштабується відповідно до маси кожного об’єкта. Однак це не точно відображає співвідношення між масою та розміром у нашому фізичному світі. Площа круга обчислюється за формулою πr2\pi{r^2} де rr являє собою радіус (половину діаметра) кола. (Більше про π\pi буде у Розділі 3!) Таким чином, щоб точніше відобразити площу круга пропорційно наявній масі, мені потрібно взяти квадратний корінь від маси й подвоїти результат, щоб використати його як діаметр круга.

Вправа 2.11

Адаптуйте приклад 2.6, щоб зіставити масу об’єктів Attractor і Mover з площею відповідних їм кругів:

circle(this.position.x, this.position.y, sqrt(this.mass) * 2);

Звісно, ви можете розширити код, включивши один Attractor і масив багатьох об’єктів Mover, так само як я включив масив об’єктів Mover у прикладі 2.5.

let movers = [];
let attractor;

Тепер буде 10 об'єктів типу Mover!


function setup() {

  createCanvas(640, 360);

  for (let i = 0; i < 10; i++) {

    movers[i] = new Mover(random(width), random(height), random(0.5, 3));

Кожен об'єкт Mover ініціалізується випадковими значеннями.

  }

  attractor = new Attractor();

}


function draw() {

  background(255);

  attractor.show();

  for (let i = 0; i < movers.length; i++) {

    let force = attractor.attract(movers[i]);

Обчислення сили притягання для кожного об’єкта Mover.

    movers[i].applyForce(force);

    movers[i].update();

    movers[i].show();

  }

}

Це лише невелика порція можливостей з масивами об’єктів. У Розділі 4, який охоплює системи частинок, ви дізнаєтеся більше про додавання та видалення кількох об’єктів із полотна.

Вправа 2.12

У прикладі 2.7 є система (масив) об’єктів типу Mover і один об’єкт типу Attractor. Побудуйте приклад з системою і рухомих об’єктів і атракторів. Що, якщо зробити атрактори невидимими? Чи можете ви створити візерунок, чи дизайн на основі слідів від об’єктів, що рухаються навколо атракторів?

Вправа 2.13

Цей розділ не вказує, що кожна хороша симуляція p5.js повинна включати гравітаційне тяжіння. Напроти, вам слід творчо міркувати про те, як розробити власні правила для керування поведінкою об’єктів, використовуючи мій підхід до симуляції гравітаційного тяжіння як модель. Наприклад, що якщо спроєктувати силу тяжіння, яка слабшає, коли об’єкти наближаються, і посилюється, коли вони віддаляються? Або що, якщо ви розробите свій атрактор для притягування далеких об’єктів, але відштовхування близьких?

Гравітаційна задача N тіл

Я почав досліджувати гравітаційне тяжіння з простого сценарію, де один об’єкт притягує інший об’єкт, а потім перейшов до трохи складнішого, коли один об’єкт притягує багато об’єктів. Наступним логічним кроком є дослідження кейса, коли багато об’єктів притягують багато об’єктів!

Хоча досі й було корисно мати окремі класи Mover та Attractor, це розрізнення насправді дещо оманливе. Все-таки, згідно з третім законом Ньютона, всі сили діють попарно: якщо атрактор притягує рухомий об’єкт, то цей об’єкт також повинен притягувати й атрактор. Замість двох різних класів тут насправді потрібен один тип об’єкта під назвою, наприклад, Body. Кожне із таких тіл притягуватиме будь-яке інше.

Описаний тут сценарій зазвичай називають гравітаційною задачею N тіл. Вона передбачає розв’язання руху групи об’єктів, які взаємодіють через гравітаційні сили. Задача двох тіл є відомою вирішеною проблемою, де за участі лише двох тіл їх рухи можна точно обчислити за допомогою математичних рівнянь. Однак додавання ще одного тіла перетворює задачу двох тіл на задачу трьох тіл, і тут вже немає формального розв’язку (див. малюнок 2.10).

Малюнок 2.10: Приклад передбачуваних траєкторій для задачі двох тіл проти складних траєкторій для задачі трьох тіл
Малюнок 2.10: Приклад передбачуваних траєкторій для задачі двох тіл проти складних траєкторій для задачі трьох тіл

Хоча приклади, побудовані в цьому розділі, менш акуратні, ніж використання точних рівнянь руху, вони можуть моделювати проблеми як двох, так і трьох тіл. Для початку я переміщу метод attract() з класу Attractor у клас Mover (який я тепер називатиму Body):

class Body {

Клас Mover тепер називається Body.


  /* Весь інший код такий самий як раніше */


  attract(body) {
    let force = p5.Vector.sub(this.position, body.position);
    let d = constrain(force.mag(), 5, 25);
    let strength = (G * (this.mass * body.mass)) / (d * d);
    force.setMag(strength);
    body.applyForce(force);
  }

Метод attract() тепер є частиною класу Body.

}

Тепер залишається лише створити два об’єкти типу Body (назвемо їх bodyA і bodyB) та забезпечити їх взаємне притягання один до одного:

let bodyA;

let bodyB;


function setup() {

  createCanvas(640, 240);

  bodyA = new Body(320, 40);
  bodyB = new Body(320, 200);

Створення двох об’єктів Body: A і B.

}


function draw() {

  background(255);

  bodyA.attract(bodyB);
  bodyB.attract(bodyA);

A притягує B, а B притягує A.

  bodyA.update();

  bodyA.show();

  bodyB.update();

  bodyB.show();

}

Для будь-якої задачі N тіл, отриманий рух і його патерни повністю залежать від початкових умов. Наприклад, якщо я застосую конкретні значення для векторів прискорення кожного тіла, де один вказуватиме праворуч, а інший — ліворуч, то в результаті отримаю кругову орбіту.

function setup() {

  createCanvas(640, 240);

  bodyA = new Body(320, 40);

  bodyB = new Body(320, 200);

  bodyA.velocity = createVector(1, 0);
  bodyB.velocity = createVector(-1, 0);

Призначення кожному тілу горизонтальних швидкостей (у протилежних напрямках).

}

Код прикладу 2.8 можна поліпшити, додавши у конструктор класу Body параметри для швидкості. Однак, поки що цей спрощений підхід слугує швидким способом для експериментів з варіаціями на основі різних початкових положень і швидкостей.

Вправа 2.14

У статті “Classification of Symmetry Groups for Planar n-Body Choreographies” Джеймса Монтальді та Катріни Стеклз, досліджується концепція хореографічних рішень для задачі N тіл (визначається періодичними рухами, коли тіла слідують одне за одним через регулярні інтервали). Викладач і митець Ден Ґріс створив інтерактивну демонстрацію цих хореографій. Спробуйте додати третє або більше тіл до прикладу 2.8 і поекспериментуйте з налаштуванням початкових положень та швидкостей. Які хореографії зможете побудувати ви?

Тепер я готовий перейти до прикладу з N тілами й використання масиву:

let bodies = [];

Починаємо з порожнього масиву.


function setup() {

  createCanvas(640, 240);

  for (let i = 0; i < 10; i++) {
    bodies[i] = new Body(random(width), random(height));
  }

Заповнюємо масив об’єктами Body.

}


function draw() {

  background(255);

  for (let i = 0; i < bodies.length; i++) {
    bodies[i].update();
    bodies[i].show();
  }

Перебираємо масив для оновлення та відображення всіх тіл.

}

У функцію draw() потрібно додати трохи магії, щоб кожне тіло впливало своєю гравітаційною силою на усі інші тіла. Наразі код читається так: “Для кожного тіла i застосуй оновлення та намалюй його”. Щоб притягнути кожне інше тіло j до кожного тіла i, потрібно додати вкладений цикл і відредагувати код, щоб він казав: “Для кожного тіла i притягни кожне інше тіло j, онови та намалюй його”:

  for (let i = 0; i < bodies.length; i++) {
    for (let j = 0; j < bodies.length; j++) {

Для кожного тіла оновіть кожне тіло з масиву!

      let force = bodies[j].attract(bodies[i]);

      movers[i].applyForce(force);

    }

    movers[i].update();

    movers[i].show();

  }

Однак код має одну маленьку проблему. Коли кожне тіло i притягує кожне тіло j, що відбувається, коли i дорівнює j? Чи повинно тіло під індексом 3 притягувати тіло під індексом 3? Відповідь, звісно, ні. Якщо є п’ять тіл, то потрібно, щоб тіло 3 притягувало лише тіла 0, 1, 2 і 4, пропускаючи себе. Я врахую це, додавши умовний оператор, щоб пропустити застосування сили, коли i дорівнює j.

let bodies = [];


function setup() {

  createCanvas(640, 240);

  for (let i = 0; i < 10; i++) {

    bodies[i] = new Body(random(width), random(height), random(0.1, 2));

  }

}


function draw() {

  background(255);


  for (let i = 0; i < bodies.length; i++) {

    for (let j = 0; j < bodies.length; j++) {

      if (i !== j) {

Не притягувати тіло само до себе!

        let force = bodies[j].attract(bodies[i]);

        bodies[i].applyForce(force);

      }

    }

    bodies[i].update();

    bodies[i].show();

  }

}

Рішення з вкладеним циклом у прикладі 2.9 призводить до так званої “квадратичної складності алгоритму” — O(n2)O(n^2) — кількість обчислень дорівнює кількості тіл у квадраті. Якщо збільшити кількість тіл, симуляція почне значно сповільнюватися через кількість необхідних обчислень.

У Розділі 5 я розгляну стратегії оптимізації програм, подібних до цієї, з особливим акцентом на алгоритмах просторового розподілу. Просторовий розподіл, у поєднанні з концепцією дерева квадрантів (квадродерев) і алгоритмом Барнса-Хата, особливо ефективний для підвищення ефективності у симуляціях, подібних до N тіл, які тут обговорювалися.

Вправа 2.15

Змініть силу притягання в прикладі 2.9 на силу відштовхування. Чи можете ви створити приклад, у якому всі об’єкти Body притягуються до курсора, але відштовхують одне одного? Подумайте про те, як вам потрібно збалансувати відносну величину сил і як найбільш ефективно використовувати відстань у ваших обчисленнях сил.

Вправа 2.16

Чи можете ви розташувати тіла у симуляції N тіл так, щоб вони обертались навколо центру полотна, що нагадуватиме спіральну галактику? Можливо, вам знадобиться включити додаткове велике тіло у центрі, щоб утримувати все разом. Одне з рішень пропонується у моєму відео “Mutual Attraction” із серії Nature of Code на сайті Coding Train.

Проєкт “Екосистема”

Впровадьте концепцію сил у свою екосистему. Як інші фактори середовища (наприклад, вода з ділянками мулу або течія річки) можуть впливати на рух персонажа в екосистемі?

Спробуйте ввести в оточення інші елементи (їжу, хижака) з якими створіння може взаємодіяти. Чи створіння відчуває тяжіння, чи відштовхування до певних речей у своєму світі? Чи можете ви мислити абстрактніше і створювати сили на основі бажань чи цілей створіння?