«Живий» прогноз погоди, або Як використати генеративне мистецтво у вебі

У рубриці DOU Проектор спеціалісти презентують свій продукт (як стартап, так і ламповий pet-проект).

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

Що таке генеративне мистецтво

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

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

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

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

Дерево

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

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

Крона дерева неоднорідна, як куля, а складається з груп, наче кілька кульок. Це впливає на колір листочків, де одні більш затінені, а другі світліші. Для визначення цього кольору крона дерева ділиться на групи, де кожна група замальовується зліва направо в палітрі від найсвітліших до найтемніших кольорів і назад. Функція divide(start, finish, intervalsAmount, n) розділяє числовий проміжок між start та finish на задану кількість інтервалів intervalsAmount і визначає, в якому проміжку перебуває n.

const branchGroupDepth = 10;
const leavesGroupSize = 2 ** (branchGroupDepth-1);
let groupCounter = 0;

function generate(angle, depth, arr) {
 let leafColor = colors[divide(0, leavesGroupSize, colors.length, groupCounter)];
  arr.push({
   angle,
   branchArmLength: random(minBranchLenght, maxBranchLenght),
   color: leafColor
 });
 if (depth === branchGroupDepth) { groupCounter = 0; }
 if (depth === 0) { groupCounter++; }
 if (depth != 0) {
   if (depth > 1) {
     generate(angle - random(minAngle, maxAngle), depth - 1, arr);
     generate(angle + random(minAngle, maxAngle), depth - 1, arr);
   } else {
     generate(angle, depth - 1, arr);
   }
 }
}

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

windSideWayForce — прорахування напрямку гілки залежно від напрямку вітру.

bendabiityOfCurrentBranch — прорахування коефіцієнта сили нахилу гілки від вітру залежно від товщини гілки.

calcX(angle, r)/calcY(angle, r) — функції, що виконують r * cos(angle)/r * sin(angle) для того, щоб знайти координати точки закінчення гілки.

let branchCounter = 0;
const bendability = 2;
const leafBendability = 17;

function branch(x1, y1, arr, depth, windConfig) {
 if (depth != 0) {
   const xx = calcX(dir, depth * branchArmLength);
   const yy = calcY(dir, depth * branchArmLength);
   const windSideWayForce = windX * yy - windY * xx;
  const bendabiityOfCurrentBranch = (1 - (depth * 0.7) / (maxDepth * 0.7)) **        bendability;
   dir = angle + wind * bendabiityOfCurrentBranch * windSideWayForce;
   let x2 = x1 + calcX(dir, depth * branchArmLength);
   let y2 = y1 + calcY(dir, depth * branchArmLength);
   lines[depth].push([x1, y1, x2, y2]);
  
   if (depth > 1) {
     branch(x2, y2, arr, depth - 1, windConfig);
     branch(x2, y2, arr, depth - 1, windConfig);
   } else {
     branch(x2, y2, arr, depth - 1, windConfig);
   }
 } else {
   const leafAngle = angle + wind * windSideWayForce * leafBendability;
   leaves[color].push([x1, y1, leafAngle]);
 }
}

Малювання гілок відбувається простою lineTo() функцією в канвасі. Кожна гілочка — це проста лінія.

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

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

Земля

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

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

function generate(number) {
 for (var i = 0; i < number; i++) {
   var y = random(fieldTopStart, h + fieldBottomDeviation);
   var x = random(0, w);
  var colorGroup = divide(fieldTopStart, h + fieldBottomDeviation  + 1,  fieldAreas, y);                                             
   var color = colors[colorGroup][random(0, colors[colorGroup].length)];
   var angle = random(-maxAngleDeviation, maxAngleDeviation);
   var speed = random(minSpeed, maxSpeed);
   dots.push([x, y, color, angle, speed]);
 }
 dots.sort();
}

Кожна травинка малюється за допомогою двох кривих Безьє, як це було у випадку з листочками на дереві. Щоб змусити травинку реагувати на вітер, координати контрольних (жовтих) точок і координата кінчика травинки залежать від значення вітру.

Небо й опади

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

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

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

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

Результат

Для того, щоб оживити застосунок, у ньому використовуються справжні дані про погоду у Львові на цю мить з ресурсу OpenWeather. Дані опрацьовуються, а саме переводяться в зрозумілу для системи шкалу.

Трішки гри з кольорами для різних пір року, застосування реальної погоди — і рік крізь це «живе вікно» на відео матиме такий вигляд:

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

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

Дякую за увагу!

Все про українське ІТ в телеграмі — підписуйтеся на канал DOU

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному3
LinkedIn



17 коментарів

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

Можливо, я щось неправильно зрозумів, але який сенс? Аби збережений рендер можливо було використати повторно з оперативки, ця сторінка має бути відкрита в браузері постійно. А наразі це й технічно неможливо: в мене за хвилину воно зжерло 14Гб оперативки й продовжувало її жерти до закриття сторінки. Тобто сторінка дуже швидко просто призведе до фрізу системи.

Прикольно!
Вопрос — а не отжирает ли этот хром-процесс 100% CPU юзера, который туда зайдет?
Как когда-то пресловутые снежинки на сайтах )

думаю що рендер варто перенести на canvas, three.js в помощь
а так ідея хороша

Дякую за поради, Юра!
Власне, рендер і так на canvas :)
Чи я Вас неправильно зрозуміла?

Ймовірно, йдеться про WebGL, аби воно рендерилося відеокартою замість процесора.

Теж вважаю що йдеться про WebGL, адже бібліотека three.js працює з ним. Але вона працює вже з тривимірними зображеннями, а тут двовимірне. Але сама думка про перенос рендерінгу на відеокарту слушна.

Круто, круто! На эту тему когда-то была очень прикольная демонстрационная программа Sonnet:
sonnet — threestate | 64k

Это, кстати, хорошая шутка над автором статьи, потому что эта программа занимает 64к памяти )

Наверняка в ужатом UPX или чем-то подобным виде... но дело даже не в этом.

Подобные демонстрашки очень далеки от коммерческого программирования. Делаются они, не побоюсь этого слова, упоротыми энтузиастами, готовыми тратить кучу личного времени на оптимизации и «вылизывание».

А в реальных бизнес-задачах за такое редко кто готов платить... разве что, в embedded, где программная оптимизация может «отбиться» на масштабах производства за счёт более дешёвой SoC.

UPX — это ленивая оптимизация. В таких демках жесткий ассемблер и математика. Можете поискать еще .kkrieger — там 96к и очень нехилая графика для такого размера. Просто тратить сотни мегабайт на что? Чтобы у дерева шевелились листики, а тучка капала дождиком?

UPX — это ленивая оптимизация. В таких демках жесткий ассемблер и математика.

Разумеется, одно не исключает другого. Я сам в конце 90х — начале 2000х немного упарывался по этой теме, так что в курсе :)

Единственное — позволю себе усомниться про «жёсткий ассемблер». К началу нулевых толковые компиляторы научились оптимизировать правильно написанный на C/C++ код в подавляющем большинстве случаев не хуже человека.

Собственно, вот исходники движка kkrieger — как видим, C++ во все поля, кроме шейдеров — вот они-таки написаны на некоем подобии ассемблера.

Вот, разве что здесь, «жесть как она есть»: github.com/...​/materials/material11.vsh

поискать еще .kkrieger — там 96к и очень нехилая графика для такого размера.

Дык... это же farbraush, они вообще гениальные вещи для своего времени творили в плане графики. Впрочем, в плане оптимизации размера, как раз, там всё просто как двери — использовалась процедурная генерация текстур и, по-моему, музыкальных сэмплов тоже.

научились оптимизировать правильно написанный на C/C++ код в подавляющем большинстве случаев не хуже человека

Они не способны оптимально использовать инструкции векторизации (типа AVX), они не способны учитывать кэш, они не способны распараллеливать вычисления, заменять брэнчинг на спекулятивные вычисления и использовать оптимальный аллокатор объектов.

разве что, в embedded, где программная оптимизация может «отбиться» на масштабах производства за счёт более дешёвой SoC.

Там тоже нюанс — разработка и производство железа занимают овердофига времени и средств. Поэтому обычно размер железки берут с запасом, и параллельно с железом делают софт на демоборде или образцах.

Цікава ідея. Думаю, має буть якесь супер використання, не для прогноза погоди.

Підписатись на коментарі