Розділ 0. Випадковість

Генерація випадкових чисел надто важлива,

щоб залишати її на волю випадку.

— Роберт Кав’ю

Таблиці випадкових чисел із книги A Million Random Digits with 100,000 Normal Deviates, корпорація RAND
Таблиці випадкових чисел

У 1947 році корпорація RAND випустила особливу книгу під назвою “A Million Random Digits with 100,000 Normal Deviates”. Книга не була літературним твором чи філософським трактатом про випадковість. Радше, це була таблиця випадкових чисел, згенерованих за допомогою електронної симуляції колеса рулетки. Ця книга була однією з останніх у серії таблиць випадкових чисел, створених із середини 1920-х до 1950-х років. З розвитком високошвидкісних комп’ютерів генерувати псевдовипадкові числа стало швидше, ніж зчитувати їх із таблиць, тому ця ера друкованих таблиць з випадковими числами остаточно завершилася.


Ось ми на самому початку. Якщо ви давно не програмували на JavaScript, або не займалися математикою, цей розділ допоможе вам повернутися до обчислювального мислення. Щоб почати свою подорож із кодування природи, я познайомлю вас із деякими базовими інструментами для програмування симуляцій: випадковими числами, випадковими розподілами та шумом. Думайте про цей розділ як про перший (нульовий!) елемент масиву, який складає цю книгу, — нагадування основ та ворота до можливостей, які чекають попереду.

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

Випадкове блукання

Уявіть собі, що ви стоїте на середині гімнастичної колоди чи рейки. Кожні 10 секунд ви підкидаєте монету. Орел — крок вперед. Решка — крок назад. Це — випадкове блукання — шлях, визначений послідовністю випадкових кроків. Обережно зійшовши на підлогу, ви можете розширити своє випадкове блукання з одного виміру (переміщення лише вперед і назад) до двох вимірів (переміщення вперед, назад, ліворуч та праворуч). Тепер, маючи чотири можливі напрямки, вам для визначення наступного кроку потрібно підкидати монету двічі:

Підкидання 1Підкидання 2Результат
ОрелОрелКрок вперед
ОрелРешкаКрок вправо
РешкаОрелКрок вліво
РешкаРешкаКрок назад

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

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

Спершу я трохи розгляну ООП, закодувавши клас Walker для створення об’єктів типу Walker, які зможуть робити випадкове блукання. Тут буде лише поверхневий огляд. Якщо ви раніше ніколи не працювали з ООП, вам може знадобитися більш комплексний матеріал для ознайомлення. У такому разі я пропоную зупинитися тут і переглянути розділ “Objects” мого відеокурсу “Code! Programming with p5.js” на сайті Coding Train.

Клас випадкового блукача

Об’єкт у JavaScript — це сутність, що має як дані, так і певну функціональність. В нашому випадку, об’єкт типу Walker повинен мати дані про своє положення на полотні й функціональні можливості, такі як здатність намалювати себе чи зробити крок.

Клас є шаблоном для створення конкретних екземплярів об’єктів. Уявіть клас, як форму для печива, а об’єкти як саме печиво. Щоб створити об’єкт Walker, я почну з визначення класу Walker, що описуватиме блукача.

Блукачу потрібні всього лише дві частини даних: одне число для його xx-положення і друге число для його yy-положення. Щоб встановити початкове положення об’єкта, я ініціалізую ці координати значеннями, що відповідатимуть центру полотна. Я можу зробити це у функції-конструкторі класу, яка має відповідну назву constructor(). Ви можете розглядати конструктор як функцію setup() у програмі p5.js. Він відповідає за визначення початкових властивостей об’єкта, подібно до того, як setup() робить це для всієї програми p5.js:

class Walker {

  constructor() {

Об’єкти мають конструктори де відбувається їх ініціалізація.

    this.x = width / 2;
    this.y = height / 2;

Об’єкти мають дані.

  }

Зверніть увагу на використання ключового слова this для додавання властивостей об’єкту всередині самого об’єкта: this.x і this.y.

Крім даних, класи можуть містити функціональність. У цьому прикладі об’єкт Walker має дві функції, які в ООП контексті називаються методами. Хоча методи фактично є функціями, відмінність полягає в тому, що методи визначаються всередині класу й пов’язані з об’єктом або класом, тоді як функції ні. Ключове слово function є хорошою підказкою: ви побачите його, при визначенні незалежних функцій, але воно не з’явиться всередині класу. Я намагатимусь використовувати ці терміни відповідним чином, але зазвичай програмісти використовують терміни функція та метод рівнозначно.

Перший метод show() включає код для малювання об’єкта у вигляді чорної точки. При посиланні на внутрішні властивості (змінні) об’єкта завжди використовуйте синтаксис this.:

  show() {
    stroke(0);
    point(this.x, this.y);
  }

Об’єкти можуть мати методи.

Наступний метод step(), направлятиме кроки об’єкта Walker. Це той момент, коли речі стають трохи цікавішими. Пам’ятаєте, як ви робили кроки у випадкових напрямках на підлозі? Тепер, для зображення цієї підлоги, я використовуватиму полотно p5.js. Існують чотири можливих кроки. Крок вправо можна симулювати, збільшивши координату x за рахунок операції інкременту x++; крок вліво — зменшивши x за допомогою декременту x--; вперед — піднявшись на піксель вгору (y--); і назад — спустившись на один піксель вниз (y++). Але як код може вибрати один із цих чотирьох варіантів?

Раніше я казав, що можна підкидати дві монети. Проте у p5.js, коли вам потрібно зробити випадковий вибір зі списку варіантів, ви можете просто згенерувати випадкове число за допомогою функції random(). Вона повертає випадкове число з рухомою крапкою в межах потрібного вам діапазону. Тут я використовую число 4, щоб позначити діапазон значень від 0 до 4:

let choice = floor(random(4));

Я оголосив змінну choice і присвоїв їй випадкове число, округливши його до цілочисельного значення за допомогою функції floor(). Технічно кажучи, число згенероване після виклику random(4) знаходиться у діапазоні від 0 (включно) до 4 (не включно), тобто воно ніколи не може бути 4.0. Найбільше можливе число цього діапазону буде трохи нижче від 4 — 3.999999999 (з такою кількістю дев’яток після крапки, скільки дозволяє JavaScript), яке потім округляється за допомогою функції floor() до 3, видаляючи десяткову частину. Тож я фактично присвоюю змінній choice значення 0, 1, 2 або 3.

Домовленості про стиль коду

У JavaScript змінні можна оголошувати за допомогою ключових слів let або const. Зазвичай рекомендується оголошувати всі змінні за допомогою const і змінювати їх на let, коли це потрібно. У цьому першому прикладі const буде більш відповідним для оголошення змінної choice, оскільки вона ніколи не отримує нового значення протягом свого використання всередині функції step(). Попри важливість цієї відмінності, для прикладів p5.js я обрав домовленість, що оголошуватиму всі змінні за допомогою let.

Я розумію, що існують вагомі причини для використання і const і let. Однак ця відмінність може відвертати увагу й заплутувати початківців. Я закликаю вас, читачів, дослідити цю тему глибше й самостійно вирішити, як найкраще оголошувати змінні у власних скриптах. Для докладнішої інформації ви можете прочитати обговорення, пов'язане з питанням #3877 у репозиторії p5.js на GitHub.

Я також зазначу, що вирішив використовувати ”суворий” оператор порівняння === (і його відповідник для визначення нерівності !==). Цей логічний оператор перевіряє рівність як значення, так і типу. Наприклад, вираз 3 === '3' буде оцінено як false через різність типів (число проти рядка), навіть якщо вони виглядають схожими. З іншого боку, використання простого оператора порівняння == для виразу 3 == '3' призведе до результату true через приведення різних типів до порівняльного стану. Хоча просте порівняння часто працює нормально, іноді воно може призвести до неочікуваних результатів, тому === безпечніший вибір.

Далі, блукач виконує відповідний крок (вліво, вправо, вгору або вниз), в залежності від того, яке випадкове число було вибрано. Ось повний код методу step(), що завершує клас Walker:

  step() {

    let choice = floor(random(4));

0, 1, 2 або 3. Випадкове значення визначає напрям кроку.

    if (choice === 0) {

      this.x++;

    } else if (choice === 1) {

      this.x--;

    } else if (choice === 2) {

      this.y++;

    } else {

      this.y--;

    }

  }

}

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

let walker;

Змінна для зберігання екземпляру класу Walker.

Потім, у функції setup(), створіть екземпляр об’єкта посилаючись на ім'я класу з оператором new:

function setup() {

Пам’ятаєте, як працює p5.js? Функція setup() виконується лише один раз при запуску програми.

  createCanvas(640, 240);

  walker = new Walker();
  background(255);

Створення об'єкта Walker.

}

Нарешті, під час кожного циклу функції draw(), блукач робить крок і малює точку:

Приклад 0.1: Класичне випадкове блукання

Кожного разу, коли ви бачите один з таких блоків із прикладом, це означає, що його відповідний код доступний у вебредакторі p5.js і на вебсайті книги. Якщо ви читаєте цю книгу офлайн, то побачите лише знімок екрана з результатом програми.
function draw() {

Функція draw() виконується нескінченну кількість разів (поки ви її не зупините чи не вийдете з програми).

  walker.step();
  walker.show();

Виклик методів об’єкту Walker.

}

Оскільки фон малюється лише один раз у функції setup(), а не очищається щоразу під час постійного виконання функції draw(), сліди випадкового блукання залишаються видимими на полотні.

Я міг би внести до випадкового блукача кілька коригувань. По-перше, кроки цього Walker-об'єкта обмежені чотирма варіантами: вгору, вниз, ліворуч і праворуч. Але будь-який піксель на полотні може мати вісім можливих сусідів, включаючи діагональних (див. малюнок 0.1). Можливість залишитися на місці може бути дев'ятим варіантом.

Малюнок 0.1: Кроки випадкового блукача, з діагоналями й без них
Малюнок 0.1: Кроки випадкового блукача, з діагоналями й без них

Для імплементації об'єкта Walker, який може зробити крок на будь-який сусідній піксель або залишитися на місці, я міг би вибрати число від 0 до 8 (дев'ять можливих варіантів). Однак іншим способом реалізації нашого наміру буде вибір з трьох можливих кроків вздовж вісі xx (-1, 0 або 1) і трьох можливих кроків вздовж вісі yy:

  step() {

    let xstep = floor(random(3)) - 1;
    let ystep = floor(random(3)) - 1;

Отримання значення -1, 0 або 1.

    this.x += xstep;

    this.y += ystep;

  }

Я також можу позбутися функції floor() і напряму використовувати вихідні числа з рухомою крапкою від функції random(), щоб створити безперервний діапазон можливих довжин кроків від -1 до 1, як показано далі:

  step() {

    let xstep = random(–1, 1);
    let ystep = random(–1, 1);

Будь-яке число з рухомою крапкою від -1 до 1.

    this.x += xstep;

    this.y += ystep;

  }

Всі ці варіації традиційного випадкового блукання мають спільну рису: в будь-який момент часу ймовірність того, що блукач зробить крок у певному напрямку, дорівнює ймовірності, що він зробить цей крок й у будь-якому іншому напрямку. Іншими словами, якщо є чотири можливих кроки, то шанс того, що блукач зробить будь-який з них дорівнює 1 з 4 (або 25%). З дев'ятьма можливими кроками цей шанс 1 з 9 (близько 11.1%).

Дуже зручно, що саме так і працює функція random(). Генератор випадкових чисел у p5.js створює рівномірний розподіл чисел. Ви можете перевірити цей розподіл за допомогою прикладу, який підраховує кількість разів, коли вибирається певне випадкове число, і зображує цю кількість через висоту прямокутника.

let randomCounts = [];

Масив для відстеження частоти вибору випадкових чисел.

let total = 20;

Загальна кількість чисел.


function setup() {

  createCanvas(640, 240);

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

    randomCounts[i] = 0;

  }

}


function draw() {

  background(255);

  let index = floor(random(randomCounts.length));
  randomCounts[index]++;

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

  stroke(0);

  fill(127);

  let w = width / randomCounts.length;

  for (let x = 0; x < randomCounts.length; x++) {
    rect(x * w, height - randomCounts[x], w - 1, randomCounts[x]);
  }

Побудова результатів на графіку.

}

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

Псевдовипадкові числа

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

Вправа 0.1

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

Імовірність і нерівномірний розподіл

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

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

Створення нерівномірного розподілу випадкових чисел буде корисним впродовж усієї книги. Наприклад, у Розділі 9 про генетичні алгоритми мені знадобиться методологія відбору — які представники популяції повинні бути вибрані, щоб передати свою ДНК наступному поколінню? Це схоже на дарвінівську концепцію виживання найпристосованіших. Скажімо, у вас є популяція мавп, що розвивається. Не кожна мавпа має рівні шанси на розмноження. Щоб симулювати дарвінівський природний відбір, ви не можете просто вибрати двох випадкових мавп для батьківства. Більш “пристосовані” повинні мати більше шансів для відбору. Це можна вважати ймовірністю найпристосованіших.

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

Якщо у вас є сценарій, де кожен результат так само вірогідний, як і інші, ймовірність виникнення даної події дорівнює кількості результатів, які відповідають цій події, поділеній на загальну кількість всіх можливих результатів. Простим прикладом є підкидання монети — воно має лише два можливі результати: орел або решка. Є лише один спосіб отримати орла, тому ймовірність того, що монета випаде орлом дорівнює одиниці поділеній на два: 1/2 або 50 відсотків.

Візьмемо колоду з 52 карт. Порахуємо ймовірність витягнути туза з цієї колоди:

кількість тузів / кількість карт=4/52=0.0778%\textrm{кількість тузів } / \textrm{ кількість карт} = 4 / 52 = 0.077 \approx 8\%

Імовірність витягнути бубнову масть вираховується наступним чином:

кількість бубнових / кількість карт=13/52=0.25=25%\textrm{кількість бубнових }/ \textrm{ кількість карт} = 13 / 52 = 0.25 = 25\%

Ви також можете обчислити ймовірність кількох послідовних подій, перемноживши окремі ймовірності кожної події. Наприклад, ймовірність того, що монета тричі поспіль випаде орлом, становить:

(1/2)×(1/2)×(1/2)=1/8=0.125=12.5%(1/2) \times (1/2) \times (1/2) = 1/8 = 0.125 = 12.5\%

Це означає, що монета випадатиме орлом догори тричі поспіль в середньому один раз із восьми. Якби ви підкидували монету тричі поспіль 500 разів, то могли очікувати побачити результат із трьох послідовних орлів у середньому в одному із восьми випадків, або приблизно 63 рази.

Вправа 0.2

Яка ймовірність витягнути поспіль два тузи із перетасованої колоди з 52 карт, якщо після першого вибору карти ви повернете її у колоду і перед другим вибором знову перетасуєте колоду? Якою була б ця ймовірність, якби ви не перетасували колоду після першого вибору?

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

let stuff = [1, 1, 2, 3, 3];

Числа 1 і 3 розташовані у масиві двічі, що підвищує ймовірність їх вибору по відношенню до 2-ки.

let value = random(stuff);

Вибір випадкового елемента із масиву.

print(value);

П’ятиелементний масив має дві одиниці, тому для вибору одиниці цей код дає ймовірність 2 із 5 (2/5 або 40%). Подібним чином маємо ймовірність у 20 відсотків для виведення двійки й 40 відсотків для трійки.

Ви також можете згенерувати випадкове число й дозволити якійсь події відбутися лише якщо це число знаходиться у певному діапазоні. Для спрощення розглянемо лише випадкові значення від 0 до 1, наприклад:

let probability = 0.1;

Імовірність у 10%.

let r = random(1);

Випадкове число від 0 до 1.

if (r < probability) {
  print("Співай!");
}

Якщо випадкове число менше ніж 0.1, тоді співаємо!

Одна десята усіх значень від 0 до 1 буде меншою за 0.1, тому цей код призведе до співу лише у 10 відсотках випадків.

Ви можете використовувати той самий підхід, щоб застосувати нерівні ваги до кількох результатів. Скажімо, ви хочете, щоб спів відбувався з ймовірністю у 60 відсотків, танці — з ймовірністю у 10 відсотків, а сон — з ймовірністю у 30 відсотків. Знову ж таки, ви можете вибрати випадкове число від 0 до 1 і подивитися в який діапазон воно потрапляє:

  • Від 0.0 до 0.6 (60 відсотків) → Спів
  • Від 0.6 до 0.7 (10 відсотків) → Танці
  • Від 0.7 до 1.0 (30 відсотків) → Сон
let num = random(1);

if (num < 0.6) {
  print("Співай!");

Якщо випадкове число менше від 0.6.

} else if (num < 0.7) {
  print("Танцюй!");

Від 0.6 до 0.7.

} else {
  print("Спи!");
}

Усі інші випадки (значення більше за 0.7 або рівне йому).

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

  • Імовірність руху вгору: 20 відсотків
  • Імовірність руху вниз: 20 відсотків
  • Імовірність руху вліво: 20 відсотків
  • Імовірність руху вправо: 40 відсотків
  step() {

    let r = random(1);

    if (r < 0.4) {
      this.x++;

Вірогідність у 40% до руху вправо

    } else if (r < 0.6) {

      this.x--;

    } else if (r < 0.8) {

      this.y++;

    } else {

      this.y--;

    }

  }

Іншим поширеним використанням цієї техніки є контроль ймовірності події, яку ви хочете у своєму коді виконувати спорадично. Наприклад, ви створюєте програму, яка запускає нового випадкового блукача через регулярні проміжки часу, скажімо, кожні 100 кадрів. Замість цього, за допомогою функції random(), ви можете надати 1-відсоткову ймовірність появи нового блукача. Кінцевий результат майже такий самий (новий блукач у середньому на кожні 100 кадрів), але останній варіант містить в собі елемент випадковості й виглядає більш динамічним та непередбачуваним.

Вправа 0.3

Створіть випадкового блукача з динамічними ймовірностями. Наприклад, чи зможете ви надати йому 50-відсотковий шанс до руху в напрямку курсора? Пам'ятайте, що для отримання поточної позиції курсора у p5.js, ви можете використовувати змінні mouseX і mouseY!

Нормальний розподіл випадкових чисел

Ще одним способом створення нерівномірного розподілу випадкових чисел є використання нормального розподілу, де більшість чисел групується навколо середнього значення. Щоб зрозуміти, чому це корисно, повернімось до імітації популяції мавп і припустімо, що ваша програма генерує тисячу об’єктів Monkey, кожен із випадковим значенням висоти від 200 до 300 (оскільки це світ мавп зі зростом від 200 до 300 пікселів):

let h = random(200, 300);

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

Саме так працює нормальний розподіл. Іноді його називають розподілом Гаусса на честь математика Карла Фрідріха Гаусса. Графік цього розподілу, неофіційно відомий як дзвоноподібна крива. Ця крива генерується математичною функцією, яка визначає ймовірність появи будь-якого даного значення як функцію середнього (часто записується грецькою літерою мю — μμ) і стандартного відхилення (позначається грецькою літерою сигма — σσ).

У випадку значень для зросту між 200 і 300 ви, ймовірно, маєте інтуїтивне відчуття, що середнє значення становить 250. Однак що, якщо я скажу, що стандартне відхилення дорівнює 3? Або 15? Що це означає для чисел? Графік, зображений на малюнку 0.2, повинен дати вам підказку.

Малюнок 0.2: Два приклади дзвоноподібних кривих: нормального розподілу з низьким (ліворуч) і високим (праворуч) стандартним відхиленням

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

Результати обчислюються наступним чином: у певній популяції 68 відсотків її членів матимуть значення в діапазоні одного стандартного відхилення від середнього, 95 відсотків — у межах двох стандартних відхилень, і 99.7 відсотків — у межах трьох стандартних відхилень. При стандартному відхиленні у 5 пікселів, лише 0.3 відсотка мавп будуть нижче 235 пікселів (три стандартних відхилення нижче середнього 250) або вище за 265 пікселів (три стандартних відхилення вище від середнього 250). Тим часом зріст 68 відсотків мавп буде між 245 і 255 пікселями.

На щастя, для використання нормального розподілу випадкових чисел у програмах p5.js, вам не потрібно робити жодні з цих обчислень вручну. Функція randomGaussian() виконує всю необхідну математику і повертає випадкові числа з нормальним розподілом:

function draw() {

  let num = randomGaussian();

Генерація випадкового числа з нормальним розподілом.

}

Обчислення середнього значення та стандартного відхилення

Розглянемо клас з десяти студентів, які отримали за тест наступні бали зі 100 можливих: 85, 82, 88, 86, 85, 93, 98, 40, 73 і 83.

Середнє значення становить: 81.3.

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

БалиВідхилення від середньогоДисперсія
85858581.3=3.785 - 81.3 = 3.7(3.7)2=13.69(3.7)^2 = 13.69
40404081.3=41.340 - 81.3 = -41.3(41.3)2=1705.69(-41.3)^2 = 1705.69
. . .
Середня дисперсія:228.21228.21

Стандартне відхилення — це квадратний корінь від середньої дисперсії. Тут це 15.13.

Що далі? Що якщо, наприклад, нашою метою є призначення xx-позиції для малювання фігури?

За замовчуванням функція randomGaussian() повертає нормальний розподіл випадкових додатних і від'ємних чисел із середнім значенням 0 і стандартним відхиленням 1. Що відомо як стандартний нормальний розподіл. Однак часто ці параметри за замовчуванням не підходять. Наприклад, ви хочете призначити випадковим чином xx-позицію для фігури за допомогою нормального розподілу із середнім значенням 320 (центральний горизонтальний піксель у вікні шириною 640 пікселів) і стандартним відхиленням у 60 пікселів. У цьому випадку ви можете налаштувати параметри, передавши функції randomGaussian() два аргументи: середнє значення та стандартне відхилення.

function draw() {

  let x = randomGaussian(320, 60);

Нормальний розподіл із середнім значенням 320 і стандартним відхиленням 60

  noStroke();

  fill(0, 10);

  circle(x, 120, 16);

}

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

let x = 60 * randomGaussian() + 320;

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

Вправа 0.4

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

Вправа 0.5

Гауссове випадкове блукання визначається як таке, у якому розмір кроку (те наскільки далеко об'єкт рухається у заданому напрямку) генерується нормальним розподілом. Імплементуйте подібний варіант класу Walker.

Власний розподіл випадкових чисел

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

Однією зі стратегій, для уникнення такої проблеми, є здійснення час від часу доволі великих кроків. Це дозволяє блукачу рухатися випадковим чином навколо певної позиції, періодично стрибаючи достатньо далеко, щоб зменшити кількість перевибірки. Цей варіант випадкового блукання, відомий як політ Ле́ві, вимагає власного набору ймовірностей. Хоча це не буде точною реалізацією польоту Ле́ві, ви можете сформувати розподіл ймовірностей наступним чином: чим довший крок, тим менша ймовірність для його вибору; чим коротший крок, тим імовірніший його вибір.

Раніше я писав, що ви можете генерувати власні кастомізовані розподіли ймовірностей, заповнюючи масив певними значеннями (деякі з них можуть дублюватися для їх частішого вибору) або орієнтуючись на певні результати функції random(). Одним зі способів реалізації польоту Ле́ві може бути визначення 1-відсоткової ймовірності для того, щоб блукач робив великий крок:

let r = random(1);

if (r < 0.01) {
  xstep = random(-100, 100);
  ystep = random(-100, 100);

1%-а вірогідність зробити великий крок.

} else {

  xstep = random(-1, 1);

  ystep = random(-1, 1);

}

Однак це зводить ймовірність до фіксованої кількості варіантів: 99 відсотків часу — невеликі кроки, 1 відсоток часу — великі кроки. Що, якби ви натомість хотіли створити більш загальне правило: чим вище число, тим більша ймовірність його вибору. Наприклад, 0.8791 матиме більше шансів на вибір, ніж 0.8532, навіть якщо ця ймовірність лише трошки вища. Іншими словами, якщо xx є випадковим числом, то ймовірність його вибору можна зобразити на осі yy за допомогою функції y=xy = x (малюнок 0.3).

Малюнок 0.3: Графік функції y = x де y — ймовірність вибору значення x
Малюнок 0.3: Графік функції y=xy = x де yy — ймовірність вибору значення xx

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

Одним із рішень для власного розподілу є вибір двох випадкових чисел замість одного. Перше випадкове число — це саме випадкове число. Однак друге я називатиму кваліфікаційним випадковим значенням. Воно вирішить чи використовувати це перше число, чи відкинути його та вибрати інше. Числа, які проходять легший відбір, вибиратимуться частіше, а числа з рідшою кваліфікацією — нечасто. Ось відповідні кроки де наразі я розглядаю лише випадкові значення від 0 до 1:

  1. Виберіть випадкове число: r1.
  2. Обчисліть ймовірність p, що визначатиме кваліфікацію для r1. Спробуємо p = r1.
  3. Виберіть інше випадкове число: r2.
  4. Якщо r2 менше за p, тоді ви знайшли своє число для використання і це r1!
  5. Якщо r2 не менше за p, поверніться до кроку 1 і почніть спочатку.

Тут ймовірність того, що випадкове значення буде кваліфіковано, дорівнює самому випадковому числу, як зображено на малюнку 0.3. Наприклад, якщо r1 дорівнює 0.1, тоді r1 матиме 10-відсотковий шанс пройти кваліфікацію. Якщо r1 дорівнює 0.83, його ймовірність на кваліфікацію становитиме 83 відсотки. У цьому прикладі, чим вище число, тим більша ймовірність, що воно буде використано.

Цей процес називається алгоритмом прийняття-відхилення і є різновидом методу Монте-Карло (названого на честь казино Монте-Карло). У наступному прикладі представлено функцію, яка реалізує алгоритм прийняття-відхилення, повертаючи випадкове значення від 0 до 1.

function acceptreject() {

  while (true) {

Повторюйте цей процес доки не знайдете випадкове значення, яке пройде кваліфікацію.

    let r1 = random(1);

Виберіть випадкове значення.

    let probability = r1;

Визначте ймовірність.

    let r2 = random(1);

Виберіть друге випадкове значення.

    if (r2 < probability) {
      return r1;
    }

Якщо друге значення задовільняє вимогам, тоді ми закінчили!

  }

}

Хоча алгоритм прийняття-відхилення працює для генерування власних розподілів випадкових чисел, це не дуже ефективний спосіб. Він може призвести до значної кількості марних обчислень, коли велика кількість випадкових значень відхиляється, особливо коли ймовірність кваліфікації дуже низька. Коли ми дійдемо до генетичних алгоритмів у Розділі 9, я розгляну інший, більш оптимальний підхід.

Вправа 0.6

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

let step = 10;
let stepx = random(-step, step);
let stepy = random(-step, step);
this.x += stepx;
this.y += stepy;

Рівномірний розподіл випадкових величин кроку. Змініть його!

(У Розділі 1 я покажу, як змінювати розміри кроків більш ефективно за допомогою векторів.)

Більш плавний підхід з шумом Перліна

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

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

Малюнок 0.4: Графіки значень шуму у часі. Зліва шум Перліна, а справа шум випадкових значень

Кен Перлін розробив оригінальний алгоритм шуму Перліна під час роботи над фільмом “Трон” на початку 1980-х років. Пізніше за цю роботу він отримав Оскар за досягнення у технічній сфері. Алгоритм був розроблений для створення процедурних текстур для комп’ютерних ефектів. Процедурний означає генерування візуальних елементів алгоритмічно, на відміну від художника, що розробляє їх вручну. Протягом багатьох років різні автори розробили низку інших шумів на різні смаки. Серед відомих: Value noise (шум Значення), Worley noise (шум Ворлі) та Simplex noise (Симплекс-шум, розроблений самим Перліном у 2001). Ви можете дізнатися більше про історію шуму на вебсайті Кена Перліна, а також про його варіації впродовж років у моєму відео “Що таке OpenSimplex шум?” на вебсайті Coding Train.

Бібліотека p5.js містить реалізацію класичного алгоритму шуму Перліна з 1983 року у функції під назвою noise(). Вона може приймати один, два або три аргументи, оскільки шум обчислюється в одному, двох або трьох вимірах. Я почну з демонстрації одновимірного (1D) шуму.

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

let x = random(0, width);

Випадкова x-позиція.

circle(x, 180, 16);

Тепер, замість випадкової xx-позиції, вам потрібна плавніша xx-позиція шуму Перліна. Ви можете подумати, що вам потрібно лише замінити функцію random() аналогічним викликом функції noise(), наприклад так:

let x = random(0, width);

Замінити random() на noise()?

let x = noise(0, width);
circle(x, 180, 16);

Заманливо, але це не правильно!

Концептуально це саме те, що ви хочете зробити — вираховувати значення xx, яке знаходиться у межах від 0 до ширини полотна за допомогою функції шуму Перліна, але це не правильна реалізація. Аргументи функції random() вказують на діапазон можливих значень між мінімумом і максимумом, проте функція noise() працює іншим чином. Натомість вона має фіксований вихідний діапазон і завжди повертає значення від 0 до 1. За мить ви побачите, що можете легко обійти це обмеження за допомогою функції map(), але спочатку розглянемо, які саме аргументи очікує від вас функція noise().

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

ЧасЗначення шуму
00.365
10.363
20.363
30.364
40.366

Щоб отримати доступ до певного значення шуму, вам потрібно обрати “момент часу” і передати його функції noise(). Наприклад:

let n = noise(3);

Згідно з наведеною вище таблицею, noise(3) повертає 0.364. Наступним кроком є використання змінної для часу і постійне отримання значення шуму у функції draw():

let t = 3;


function draw() {

  let n = noise(t);

Вам потрібне значення шуму для конкретного моменту часу.

  print(n);

}

Близько, але не зовсім. Цей код просто друкуватиме одне й те саме значення знову і знову, тому що він продовжує запитувати результат функції noise() для того самого незмінного часу зі значенням 3. Однак, якщо збільшувати змінну часу t, то під час виклику функції ви отримаєте різні значення шуму для кожного нового значення часу:

let t = 0;

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


function draw() {

  let n = noise(t);

  print(n);

  t += 0.01;

Тепер ви рухаєтеся вперед у часі!

}

Я вирішив збільшувати змінну t на 0.01, але використання інших значень вплине на плавність шуму. Більші стрибки в часі, які перескакують вперед через простір шуму і повертають значення, виглядають менш плавно та більш випадково (малюнок 0.5).

Малюнок 0.5: Демонстрація результатів шуму Перліна для коротких і довгих стрибків у часі
Малюнок 0.5: Демонстрація результатів шуму Перліна для коротких і довгих стрибків у часі

У наступних прикладах коду, які використовують шум Перліна, зверніть увагу на те, як змінюється анімація при різних значеннях змінної t.

Діапазони шуму

Коли у вас є значення шуму в діапазоні від 0 до 1, то ви можете зробити мапінг (зіставлення, масштабування, проєкціювання) цього діапазону на будь-який інший, який відповідає вашим цілям (малюнок 0.6). Зробити це найпростіше з функцією p5.js під назвою map(). Вона приймає п’ять аргументів. Перше — це значення, яке потрібно зіставити, у цьому випадку n. Далі вказується поточний діапазон значення (мінімум і максимум), а потім бажаний діапазон.

Малюнок 0.6: Відображення значення з одного діапазону в інший
Малюнок 0.6: Перетворення значення з одного діапазону в інший

У цьому випадку, хоча шум має діапазон від 0 до 1, я хотів би намалювати круг з xx-позицією в діапазоні від 0 до ширини полотна:

let t = 0;


function draw() {

  let n = noise(t);

  let x = map(n, 0, 1, 0, width);

Використання функції map() для налаштування діапазону шуму Перліна.

  ellipse(x, 180, 16, 16);

  t += 0.01;

Рух вперед у часі.

}

Таку ж логіку можна застосувати до випадкового блукання, використовуючи шум Перліна та відповідний мапінг для його xx- і yy-властивостей. Це створює плавніше, більш органічне випадкове блукання.

Приклад 0.6: Випадковий блукач з використанням шуму Перліна

Як згадувалося у вступі, сліди на цьому й інших знімках екрана призначені для вираження відчуття руху.
class Walker {

  constructor() {

    this.tx = 0;

    this.ty = 10000;

  }


  step() {

    this.x = map(noise(this.tx), 0, 1, 0, width);
    this.y = map(noise(this.ty), 0, 1, 0, height);

Масштабування x- та y-позицій на основі шуму.

    this.tx += 0.01;
    this.ty += 0.01;

Зміни у часі.

  }

}

Зверніть увагу, що для цього прикладу потрібна нова пара змінних: tx і ty. Це тому, що мені потрібно відстежувати дві змінні часу: одну для xx-положення об’єкта Walker, а іншу для yy-положення. Але в цих змінних є щось трохи дивне. Чому tx починається з 0, а ty з 10 000?

Хоча ці числа є довільними, я навмисно ініціалізував дві часові змінні різними значеннями, оскільки функція шуму є детермінованою: для певного часу t вона кожного разу повертає той самий результат. Якби я запитав значення шуму t одночасно для x і y, тоді x та y завжди були б однаковими, тобто об’єкт Walker рухався б лише по діагоналі. Замість цього я використовую дві різні частини простору шуму, починаючи з 0 для x і 10 000 для y, так що поведінка x та y положень виглядають незалежними одна від одної (малюнок 0.7).

Малюнок 0.7: Використання різних зміщень уздовж осі x для варіювання значень шуму Перліна
Малюнок 0.7: Використання різних зміщень уздовж осі xx для варіювання значень шуму Перліна

По правді кажучи, тут немає фактичної концепції часу. Це корисна метафора, яка допомагає описати, як працює функція шуму, але насправді ви маєте справу з простором, а не часом. Графік на малюнку 0.7 зображує лінійну послідовність значень шуму в 1D просторі, розташованих вздовж лінії. Значення беруться у певній xx-позиції, тому в прикладах ви часто побачите змінну з назвою xoff, яка вказує на зміщення по осі xx уздовж графіка шуму, а не змінну t для часу.

Вправа 0.7

У випадковому блукачі на основі шуму Перліна результат функції noise() масштабується напряму у xyxy-властивості об’єкта. Створіть випадкового блукача, але натомість зіставте результат функції noise() з розміром кроку блукача.

Двовимірний шум

Дослідивши концепцію значень шуму в одновимірному просторі, розгляньмо, як вони можуть існувати також і у двовимірному просторі (2D). У 1D шумі існує послідовність значень в якій будь-яке дане значення подібне до свого сусіда. Уявіть аркуш міліметрового паперу або папір в клітинку, де значення одновимірного шуму записані в одному рядку, одне значення на комірку. Оскільки ці значення знаходяться в одному вимірі, кожне має лише двох сусідів: значення, яке йде перед ним (ліворуч), і значення, яке йде після нього (праворуч), як показано зліва на малюнку 0.8.

Малюнок 0.8: Порівняння сусідніх значень шуму Перліна в одному (ліворуч) і двох (праворуч) вимірах. Комірки затінені відповідно до їх значення шуму Перліна.
Малюнок 0.8: Порівняння сусідніх значень шуму Перліна в одному (ліворуч) і двох (праворуч) вимірах. Комірки затінені відповідно до їх значення шуму Перліна

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

Якщо ви візуалізуєте цю сітку із кожним значенням, зіставленим із яскравістю кольору, то отримаєте щось схоже на хмари. Білий колір сидить поруч зі світло-сірим, який сидить поруч із сірим, який сидить поруч із темно-сірим, який сидить поруч із чорним, який сидить поруч із темно-сірим і так далі (малюнок 0.9).

Малюнок 0.9: У цьому результаті програми p5.js, що візуалізує 2D-шум, кожен піксель представляє значення шуму, який зображено кольором у градаціях сірого

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

Деталізація шуму

У довідці про шум пояснюється, що він обчислюється за кількома октавами. Виклик функції noiseDetail() змінює як кількість октав, так і їх вплив по відношенню одна до одної. Це, своєю чергою, змінює якість отриманих значень шуму.

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

loadPixels();

for (let x = 0; x < width; x++) {

  for (let y = 0; y < height; y++) {

    let index = (x + y * width) * 4;

    let bright = random(255);

Випадкове значення для яскравості!

    pixels[index] = bright;
    pixels[index + 1] = bright;
    pixels[index + 2] = bright;

Встановлення значень червоного, зеленого та синього кольорів.

    pixels[index + 3] = 255;

Встановлення для альфа-каналу значення 255 (без прозорості).

  }

}

updatePixels();

Щоб забарвити кожен піксель з більш плавним переходом, ви можете зробити те саме, тільки замість виклику функції random() викликати функцію noise():

let bright = map(noise(x, y), 0, 1, 0, 255);

Яскравість з шумом Перліна!

Концептуально це хороший початок — код обчислює значення шуму для кожної позиції (x,y)(x, y) у 2D просторі. Проблема в тому, що результат не матиме плавної хмарності, яку ви хочете. Одиниця буде завеликим стрибком для переходу через простір шуму від одного пікселя до наступного. Пам’ятайте, що з 1D шумом на кожний кадр анімації я збільшував змінну часу на 0.01, а не на 1!

Досить хорошим розв’язання цієї проблеми є використання інших змінних для аргументів шуму, ніж ті, які ви використовуєте для доступу до пікселів на полотні. Наприклад, у вкладених циклах ви можете збільшувати змінну під назвою xoff на 0.01 кожного разу, коли x збільшується на 1 для горизонтального напрямку, і змінну yoff на 0.01 кожного разу, коли y збільшується на 1 для вертикального напрямку, як показано далі:

let xoff = 0.0;

Для xoff почнемо зі значення 0.


for (let x = 0; x < width; x++) {

  let yoff = 0.0;

Для кожного значення xoff почнемо значення yoff з 0.


  for (let y = 0; y < height; y++) {

    let bright = map(noise(xoff, yoff), 0, 1, 0, 255);

Використаємо xoff і yoff для noise().

    let index = (x + y * width) * 4;

Використаємо x і y для позиції пікселя.

    pixels[index] = bright;
    pixels[index + 1] = bright;
    pixels[index + 2] = bright;
    pixels[index + 3] = 255;

Встановимо значення червоного, зеленого, синього та альфа-каналу.

    yoff += 0.01;

Збільшимо yoff.

  }

  xoff += 0.01;

Збільшимо xoff.

}

Мушу зізнатися, що зробив щось досить заплутане. Я використовував 1D шум, щоб встановити значення двом змінним (this.x і this.y), що контролюють 2D рух блукача. Потім я швидко перейшов до використання 2D шуму, щоб встановити значення одній змінній (bright) для контролю яскравості кожного пікселя на полотні.

Ключова відмінність тут полягає в тому, що моя мета для блукача полягала в тому, щоб він мав два незалежних 1D значення шуму. Це просто збіг, що я використовую їх для переміщення об’єкта у 2D просторі. Спосіб досягнення цього полягає у використанні двох різних зміщень (this.tx і this.ty), щоб отримувати значення з різних частин того самого 1D простору шуму. Тим часом у прикладі з 2D-шумом обидві змінні xoff і yoff починаються з 0, оскільки я шукаю лише одне значення (яскравість пікселя) для певної точки у 2D просторі шуму. Блукач фактично рухається двома окремими 1D шляхами шуму, тоді як пікселі є окремими значеннями у 2D просторі.

Вправа 0.8

Пограйте з кольорами, функцією noiseDetail() і швидкістю інкрементації xoff та yoff для досягнення різних візуальних ефектів.

Вправа 0.9

Додайте до шуму третій аргумент, який збільшуватиметься один раз під час кожного виклику функції draw(), щоб анімувати 2D шум.

Вправа 0.10

Використовуйте значення шуму як висоту для побудови ландшафту.

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

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

Малюнок 0.10: Дерево з шумом Перліна ліворуч і поле потоків з шумом Перліна праворуч

Так само як ви можете зловживати випадковістю, легко потрапити й у пастку надмірного використання шуму Перліна. Як об’єкт повинен рухатися? Шум Перліна! Як його розфарбувати? Шум Перліна! Як швидко він повинен зростати? Шум Перліна! Якщо це стає вашою відповіддю на кожне запитання, тоді продовжуйте читати! Моя мета — познайомити вас із всесвітом нових можливостей для визначення правил ваших систем. Зрештою, ви самі визначаєте ці правила, і що більше можливостей у вашому розпорядженні, то більш обґрунтований та усвідомлений вибір ви зможете зробити. Випадковість і шум Перліна — лише перші зірки у величезному космосі творчості, який ми досліджуватимемо у цій книзі.

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

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

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

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

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