Створюємо реалістичний ландшафт за 130 рядків коду на JavaScript [*]


Програмісти – ледачі істоти з тонкою душевною організацією, що допомагає нам знаходити прості і красиві рішення завдань з мінімальними витратами. У цій статті ми створимо реалістичний ландшафт за допомогою алгоритму “diamond-square”. Ми довго не промальовуємо вручну кам’янистий рельєф, який у результаті, швидше за все, виявиться дуже убогим. Замість цього, завдяки генерації фракталів, ми навчимо комп’ютер, що означає бути каменем.

Карта висот

Зберігатимемо ландшафт у вигляді карти висот – двовимірного масиву, в якому міститься інформація про висоту кожної точки місцевості по координатах x і y. За допомогою цієї простої структури даних можна візуалізувати висоту як завгодно – з Canvas, WebGL і так далі. Основне обмеження полягає в тому, що ми не можемо відображати вертикальні отвори ландшафту, такі як печери або тунелі.

function Terrain (detail) { this.size = Math.pow (2, detail)  + 1; this.max = this.size - 1; this.map = new Float32Array (this.size * this.size);}

Цей алгоритм можна застосовувати до сітки будь-якого розміру, але найзручніше використати квадрат розміру міри двійки + 1. Ми використовуватимемо одно і те ж значення size для осей x, y і z, оформляючи наш ландшафт в куб. Конвертуємо detail у міру двійки + 1, щоб при детальнішій деталізації генерувалися куби більшого розміру.

Алгоритм

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

Це алгоритм “diamond-square”. У нашому випадку він трохи вдосконалений для отримання реалістичнішого результату: простір поперемінно ділиться на квадрати (squares) і ромби (diamonds).

Встановлюємо кути

Спочатку треба встановити кутам початкове значення seed, яке вплине на іншу візуалізацію. Код нижче підніме усі кути на половину висоти куба :

this.set (0, 0, self.max / 2);this.set (this.max, 0, self.max / 2);this.set (this.max, this.max, self.max / 2);this.set (0, this.max, self.max / 2);

Ділимо карту

Тепер рекурсивно спостерігатимемо за все меншими діленнями карти висот. При кожному діленні ми розбиватимемо карту на квадрати і оновлюватимемо положення центральної точки кожного з них під час фази ” square”. Потім ми розділимо карту на ромби і оновимо їх центральні точки на етапі ” diamond”.

divide (this.max);function divide (size) { var x, y, half = size / 2; var scale = roughness * size; if (half < 1) return; for (y = half; y < self.max; y += size) { for (x = half; x < self.max; x += size) { square (x, y, half, Math.random () * scale * 2 - scale); } } for (y = 0; y <= self.max; y += half) { for (x =  (y + half)  % size; x <= self.max; x += size) { diamond (x, y, half, Math.random () * scale * 2 - scale); } } divide (size / 2);}

Використання змінної scale гарантує, що величина зрушень зменшується разом з величиною ділень. Для кожного ділення ми множимо поточний розмір на коефіцієнт нерівності roughness, який визначає, буде ландшафт гладким (значення близько 0) або гористим (значення близько 1).

Форми

Обидві форми (square і diamond) працюють за одним принципом, але отримують дані з різних точок. На фазі square перед випадковим зрушенням ми знаходимо середнє від чотирьох кутових точок, а на фазі diamond – від чотирьох точок на ребрах.

function diamond (x, y, size, offset) { var ave = average ([ self.get (x, y - size), // top self.get (x + size, y), // right self.get (x, y + size), // bottom self.get (x - size, y)  // left ]); self.set (x, y, ave + offset);}

Візуалізація

Цей алгоритм лише дає нам дані, які ми вже можемо візуалізувати різними способами. Тут ми поєднаємо декілька технік для створення растрової ізометричної 3d-проекции ландшафтної карти на сітці.

Задом наперед

Спочатку ми створюємо вкладені цикли, які витягують прямокутники з “задньої частини” нашої карти (y = 0) “вперед” (y = this.size). Такий же цикл ми б використали для візуалізації простого плоского квадрата.

for (var y = 0; y < this.size; y++) { for (var x = 0; x < this.size; x++) { var val = this.get (x, y); var top = project (x, y, val); var bottom = project (x + 1, y, 0); var water = project (x, y, waterVal); var style = brightness (x, y, this.get (x + 1, y) - val); rect (top, bottom, style); rect (water, bottom, 'rgba (50, 150, 200, 0.15)'); }}

Світлотіні

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

var b = ~~ (slope * 50)  + 128;return ['rgba (', b, ',', b, ',', b, ',1) '].join ('');

Ізометрична проекція

Візуально цікавіше перевести наш ландшафт з фази “square” у фазу “diamond” перш ніж робити його 3d-проекцию. Ізометрична проекція зводить верхній лівий і нижній правий кути в центр зображення.

function iso (x, y) { return { x: 0.5 *  (self.size + x - y), y: 0.5 *  (x + y) };}

Центральна (перспективна) проекція

Ми використовуватимемо таку ж просту 3d-проекцию для конвертації значень x, y і z у плоску картинку з перспективою на 2d-екране.

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

 function project (flatX, flatY, flatZ) { var point = iso (flatX, flatY); var x0 = width * 0.5; var y0 = height * 0.2; var z = self.size * 0.5 - flatZ + point.y * 0.75; var x =  (point.x - self.size * 0.5)  * 6; var y =  (self.size - point.y)  * 0.005 + 1; return { x: x0 + x / y, y: y0 + z / y }; }};

Збираємо воєдино

Створюємо новий екземпляр Terrain з необхідним рівнем деталізації. Потім генеруємо його карту висот зі значенням нерівності (roughness) між 0 і 1. Нарешті, переносимо ландшафт на сітку.

var terrain = new Terrain (9);terrain.generate (0.7);terrain.draw (canvasContext, width, height);


Можна подивитися результат на сайті автора, а також вивчити код на GitHub.


Якщо вам цікава розробка ландшафтів, радимо шанувати наш переклад керівництва “Створення ландшафту на Unity за 24 години”.

Переклад статті “Realistic terrain in 130 lines”

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *