В рубриці DOU Labsми запрошуємо IT-компанії ділитись досвідом власних цікавих розробок та внутрішніх технологічних ініціатив. Питання і заявки на участь надсилайте на valentina@dou.ua.
Коли від Різдва вже півроку прожито, як співалося в одній пісні, ми нарешті готові поділитися з вами рецептом створення нашої різдвяної гри Back2Pack. Кожного року, напередодні різдвяних свят, ми активно шукаємо нові, незаїжджені ідеї, і цього року ми спробували відійти від звичних шаблонів та створили нову, неочікувану концепцію різдвяного привітання.
Отож, наша R&D-команда розробила тематичну різдвяну мобільну 3D-веб-гру з використанням віртуальної реальності для Google Cardboard. Замість традиційної різдвяної листівки, ми надіслали нашим партнерам та замовникам лінк на сайт b2p.eleks.com, пропонуючи їм вирушити у зимову подорож із Сантою та допомогти йому зі всіма передсвятковими клопотами.
Створення інтерактивного ігрового досвіду не було нашою єдиною метою — нам також не терпілося випробувати інтеракцію з віртуальною реальністю на реальному проекті.
До нашої команди входило четверо девелоперів, які працювали над впровадженням у гру технології розпізнавання рухів, WebGL, 3D та анімації, тож ця стаття поділена на чотири частини, у якій кожен з розробників ділиться своїм досвідом.
Розпізнавання рухів
Дизайн нашої гри передбачав використання одразу двох телефонів: першого — у ролі ігрового контролера, другого — як екрану VR для Google Сardboard. Їх ми вирішили з’єднати за допомогою веб-сокетів. Спочатку ми хотіли використати телефон в якості контролера меча. Гравець повинен був їхати на оленях через ліс та розмахувати мечем, намагаючись знищити тролів та врятувати Різдво:
На жаль, ми одразу зіткнулися з деякими проблемами, пов’язаних з відстежуванням жестів контролером. Річ у тім, що якість акселерометрів в телефонах зазвичай залишає бажати кращого. Дані, які ми отримували з них, відрізнялися в залежності від пристрою та мали багато шумів. Відповідно, ми повинні були зупинитися на якомусь одному жесті, який би було легко відстежувати — і ми обрали помах рукою.
Такий хід подій змусив нас змінити концепцію гри. У новій версії гравець повинен кидати ельфів у іграшки, які тікають, і швидко упаковувати їх, щоби потім відправити дітям як різдвяні подарунки:
Однак результат, який ми отримали, був ще далеким до бажаного. Між рухом контролера і киданням ельфа в грі була деяка затримка в часі, оскільки ми трекали увесь жест — від початку до кінця, а тоді відправляли повідомлення про івент через веб-сокети.
Часто прості рішення і є найкращими. Таким чином, замість того, щоб відстежувати повний жест, ми просто відстежували значення прискорення руху телефону. Коли прискорення набувало певного значення (яке ми обрали в ході експериментів із різними телефонами) на одній із трьох осей, система розуміла, що користувач замахнувся рукою, щоб кинути ельфа, і надсилала повідомлення про цю подію завчасно. Ось як ми досягнули якісного геймплею і позбулися зависання між помахом руки користувача і киданням ельфа у грі.
WebGL
При розробці на WebGL усі основні проблеми пов’язані зі швидкодією та пам‘яттю. Ти збираєшся зробити багато крутих штук, але дуже швидко усі твої плани розбиваються об ліміт GPU. Відповідно, ти повинен щохвилини бути готовим щось оптимізовувати, незважаючи на свої очікування щодо того, наскільки «важким» чи «легким» має бути для процесора якийсь із елементів — тому що (несподівано) вирішальним виявляється не розмір фігури, а кількість її вершин і розміри текстури. Окрім цього, ти постійно повинен усе вдосконалювати: що менше в кожному кадрі проводитиметься обчислень і звернень до графічного процесора, тим вищим буде FPS.
Ми планували, що WebGL-частина проекту буде простою, як два плюс два: нам потрібно було всього лише створити захопливу гру з ефектом віртуальної реальності і зробити так, щоби вона працювала зі швидкістю 60 FPS у мобільному браузері на пристроях, що підтримують WebGL (принаймні на менш-більш сучасних). Погодьтеся — не так вже й складно ;)
Спершу ми повинні були створити ігрове середовище з зимовим пейзажем, і, щоби зробити його реалістичним, ми вирішили використати ефект паралаксу. Але як зробити його деталізованим і зберегти при цьому кількість вершин і полігонів незначною? Саме так — махлюючи. Ми зробили два циліндри різної ширини і згенерували для них текстури: із зображенням низьких гір на прозорому фоні для вужчого і з зображенням розмитих гір на фоні неба — для ширшого. Тоді ми додали ефект паралаксу, злегка обертаючи один циліндр при повороті пристрою. Вид зверху був все ще не надто захопливим:
Проте для гравця він відкривався зі значно привабливішого боку:
Також нам потрібно було створити рельєф. У нас було обмаль часу на те, щоби змоделювати рельєф у 3Ds MAX або аналогічній програмі, до того ж він би виглядав завжди однаково при старті гри, а це, погодьтеся, дуже нудно. Тому ми вирішили генерувати його динамічно.
Перш за все, перепади висот пейзажу повинні були бути плавними, як крива математичної функції. Ми використали клас CatmullRomCurve3 бібліотеки three.js, щоби створити доріжку — лінію, по якій повинен рухатися гравець, і розмістили усі об’єкти, відповідно до точок цієї доріжки. Рельєф ми підлаштовували відповідно до цієї лінії. Так ми створили велику площину, обрахували позиції вершин її трикутників відповідно до координат точок кривої і «розфарбували» їх випадковими кольорами певного діапазону. Ось як ми отримали рельєф з пологими вершинами:
І... правду кажучи, це виглядало не надто привабливо. Такий собі «паркетний» пейзаж, але аж ніяк не засніжена панорама, яку ми хотіли бачити. Окрім цього — кольори були неправдивими... Для того, щоби створити більш реалістичний пейзаж, нам потрібно було використати джерело світла, щоб засобами бібліотеки three.js обчислити тіні. Однак таке обчислення для великої кількості трикутників — це дуже обтяжливо для процесора. Отже... Так, ми знову змахлювали. Ми обчислили нормалі всіх трикутників і використали ці дані, щоб колір трикутника залежав від кута падіння на нього світла. Враховуючи, що в одному поперечному ряді всі трикутники мали однаковий кут нахилу, то кількість обчислень зменшилася в рази. Для того, щоб колір сусідніх трикутників в ряді не був повністю ідентичним, до нього додавалося випадкове значення з вузького діапазону. Так нам вдалося створити більш природній пейзаж і зекономити ресурси на обчисленні освітлення:
Після цього нам потрібно було ще додати ландшафтні об’єкти — дерева, каміння, гілки, пеньки і таке інше. Ми використали деякі 3D-моделі, розташувавши їх відповідно до координат доріжки. Ще одна підказка щодо оптимізації: нам не потрібно заповнювати об’єктами усю площину, а лише територію навколо доріжки — решта площини і так залишається невидимою для користувача.
Щоби зменшити час завантаження, ми вирішили використати лише одну модель дерева, яку згодом скопіювали потрібну нам кількість разів, а кожну з цих копій довільно обернули навколо осі. Щоби дерева трохи відрізнялися між собою і не зливалися, ми зафарбували їх різними відтінками зеленого кольору — так ми змогли досягнути того, щоб ліс виглядав менш одноманітним і більш реалістичним.
Аби уникнути обрахування світла і тіней на деревах, ми «запекли» тіні в текстуру засобами 3ds Max. Ми також повинні були заповнити простір біля горизонту, оскільки там були деякі прогалини. Щоби зробити це, ми створили фальшивий горизонт — звичайну білу площину.
В результаті ми отримали зимовий лісовий пейзаж з «просікою», через яку буде рухатися гравець:
Наступним кроком було додати рух користувача. Були дві опції: переміщати камеру через простір, або навпаки — рухати всіма об’єктами, залишаючи камеру на її початковій позиції. Ми вирішили обрати щось середнє між цими двома варіантами: площина рухається у напрямку до користувача, симулюючи його біг, в той час як камера обертається і переміщується, відповідно до координат і кута доріжки.
Щоби гра могла тривати нескінченно довго, ми створили таку ж площину, теж згенерували для неї доріжку і довільно розмістили на ній об’єкти та аналогічним чином скорегували рельєф. Розмір кожної площини дорівнював діаметру циліндра (для користувача це виглядало так, що площина тягнеться від одного горизонту до іншого).
Щоби приховати стики між двома площинами, ми задавали однакову висоту рельєфу для початку і кінця площини. У кожному кадрі ми перевіряли розташування площин у просторі. Якщо остання опинялася за горизонтом, який був позаду гравця, ми просто переміщали її за горизонт, який був попереду, і генерували нову доріжку та адаптатовували рельєф і розташування об’єктів на ній, щоби гравцю здавалося, ніби перед ним — новий рельєф. Таким чином ми економили значну кількість ресурсів, не створюючи нові об’єкти, а змінюючи вже існуючі.
Опісля нам потрібно було додати анімацію і рухи іграшок. Спершу ми вирішили завантажити анімовані файли у форматі DAE, тому що вони мали невеликий розмір і, відповідно, швидко завантажувалися. Але було одне але. Відповідно до концепції нашої гри, ми повинні були створювати копії іграшок, однак DAE анімації не тиражувалися коректно, тому що у бібліотеці three.js DAE-анімації де-факто є групою об’єктів. Після тиражування моделі, її анімації не програвалися коректно, а також були деякі проблеми з динамічною зміною текстури. Ми так і не знайшли рішення цієї проблеми, тому вирішили змінити формат анімації на JSON. Єдиною анімацією, яку ми залишили у форматі DAE, була анімація оленя, на якому їде гравець, тому що її не потрібно було копіювати.
Отже, у нас були готові всі потрібні для гри об’єкти, іграшки та увесь візуал:
Тепер нам треба було розробити алгоритм попадання в іграшку. Відповідно до нашого сценарію, гравець повинен був кидати ельфа в бік іграшки, а ельф — ловити іграшку і упаковувати її в подарункову коробку. Найпоширенішим методом перевірити зіткнення об’єктів в бібліотеці three.js є використання класу Raycastеr. Але цей метод потребує великих затрат з боку процесора при перевірці такої великої кількості об’єктів. Тому в кожному кадрі, поки летів ельф, ми обраховували відстань від ельфа до активних іграшок, і якщо ця відстань була меншою за обране нами значення, ми знали, що ельф є достатньо близько до іграшки, щоби піймати її. Тоді ми програвали гарну анімацію з хмарою куряви, а потім анімацію упакування іграшки в коробку, що додало нашій грі драйву.
І ось нарешті, пані та панове, наша гра готова! Ми от-от скажемо: стережіться, «Angry birds», ми йдемо! Але чекайте... як це — «15 FPS на iPhone 6»?
Після довгого стукання головою об стіну і читання повних мудрості постів і статей, ми врешті знайшли причину. Вона крилась в наших деревах та інших об’єктах. У нас було їх понад 400, і, як ви пам’ятаєте, ми намагалися зменшити використання пам’яті і час завантаження, і тому завантажували лише одну модель дерева та тиражували її. Таким чином ми створили понад 400 об’єктів і додали їх на сцену. А кожен об’єкт — це додаткове окреме звертання до графічного процесора. Таким чином в кожному кадрі ми мали понад 400 надлишкових звернень до графічного процесора, в той час як ці об’єкти зовсім не змінювалися. Тому нам треба було об’єднати ці об’єкти в один, щоби зменшити кількість звернень. Просте обгортання їх в об’єкт-контейнер не допомогло. Потрібно було об’єднати геометрії. Маючи геометрію одного дерева, нам треба було створити геометрію цілого лісу. Коли ми вирішили цю проблему, FPS нашої гри стрімко зріс.
І остання нотатка для розробників. На старті гри гравець повинен дивитися крізь роги оленя. Але це по-різному виглядає на пристроях з Android та iOS. У випадку iOS, кожного разу, коли ти починаєш гру та ініціалізуєш компоненту акселерометра, гравець дивиться прямо — саме так, як нам потрібно. Проте на Android-пристрої початковий кут повороту залежить від фізичного кута повороту телефону. Отже, тут ми мусимо зберігати у змінну початкове значення кута повороту, отримане в першому кадрі гри, і додавати його у всіх наступних.
3D
Усі моделі героїв у грі були створені в Maya за допомогою простих інструментів полігонального моделювання. Однак через технічні обмеження нашого ігрового движка і таргетованих пристроїв, моделі були дуже низькополігональні — менш ніж 1, 000 трикутників, і мали дуже низьку роздільну здатність текстури — 128×128 пікселів. Найбільш деталізовану текстуру мали обличчя героїв.
Щоби спростити процеси ріґінгу та скінінгу, ми змоделювали більшість персонажів у Т-позі:
Процес анімації:
З технічних причин всередині ігрового движка для анімації ми використовували морфінг. Але перед запіканням морф-об’єктів, ми створили скелет і контролки в Maya для всіх персонажів.
Опісля ми створили анімаційні цикли для персонажів і перетворили анімацію в такий формат, щоби він підходив для рушія гри.
Експерименти з анімацією
Хоча WebGL-середовище відігравало провідну роль у розробці гри, усе почалося зі створення розмітки. Додати HTML та CSS не було великою проблемою навіть попри те, що ми мали багато елементів складної форми. Однак, найважчим завданням було впоратися з великою кількістю анімації, тому ми повинні були думати про неї з самого початку.
Щоби повністю контролювати процес послідовності змін анімованих елементів, ми вирішили відмовитися від нативної CSS-анімації. Найбільша проблема, яка виникає з нею полягає в тому, що послідовність таких анімацій доволі важко зберегти. Щоб уникнути мороки з цим, ми вирішили працювати з JavaScript. Таким чином, в процесі розробки це дозволило швидше будувати сюжет анімації та при потребі легко вносити зміни.
Північний Полюс без снігу — не Північний Полюс, тому ми вирішили додати в нашу гру трохи заметілі. На жаль, спочатку сніг не падав плавно і природньо. Тонни HTML-сніжинок, що налітають з гори екрану, дуже негативно впливають на швидкодію інтерфейсу, тому ми віддали перевагу canvas-снігу. В нашому випадку це було чудовим рішенням для того, щоби працювати з багатьма маленькими елементами.
Результат: In web VR we trust
Що ми можемо сказати? Взаємодія з VR може стати для вас неймовірним досвідом. Особливо, коли вам не доведеться завантажувати жодних додаткових аплікацій, а лише взяти свій смартфон і одразу ж насолоджуватися дійством.
Для нас розробка гри з віртуальною реальністю була суцільним задоволенням, незважаючи навіть на те, що нам доводилося працювати швидко і наполегливо, аби встигнути до дедлайну — а саме Різдва, яке ми не могли пересунути в часі. У ході тестування нашої гри ми навіть розбили один телефон. На щастя, це був старий iPhone 4. Але результат однозначно був вартий цього.
11 761 користувачів спробували нашу гру впродовж святкового періоду, і надіслали нам безліч позитивних відгуків. Ми також пишаємося нагородами, які здобула наша гра — Mobile Site of the Day (MOTD) від Favourite Website Awards (FWA) та People’s Champ у категорії Experimental від Pixel Awards.
Учасники команди:
Назар Дольний, Front-end Developer
Олег Гасьошин, Art Director
Ярослав Карабінович, Front-end Developer
Андрій Луцюк, Localization ART Engineer
Арсен Збідняков, Front-end Developer
Олег Кравець, QA Engineer