Об'ємні планети в 2D через шейдер

А пам'ятаєте, як ви просили мене про шейдери написати? Пам'ятаєте? Ні? А ось я пам'ятаю і навіть написав. Ласкаво просимо, поговоримо про прекрасне.

Сьогодні я розкажу про те, як я робив об'ємні обертаються планети для нашої гри blast-off. Тобто вони, звичайно, зовсім плоскі, всього пара трикутників, але виглядають як об'ємні.



Зацікавило? Прошу під кат. Картинок пристойно.


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

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

Взагалі багато хто задається питанням — «а чого б вам не найняти художника?» або «а чому не 3Д?». Ну серйозно, ну навіщо? Ми з вами дорослі люди і знаємо, що інді розробники роблять ігри для задоволення і роблять їх ретельно. Пестять і лилеят. І звичайно ж, дуже люблять велосипеди. Я не виняток, я їх теж люблю і періодично роблю. Тут повинна була б бути тирада про «псевдо-інді» розробників, що маскуються під незалежних і намагаються зрубати куш, але її я опущу. Отже, ти створюєш світ, ти робиш те, що хочеш і як хочеш. Ти отвественнен за кожен піксель на фінальній картинці і тільки ти в силах зробити його таким, яким він має бути!

2D в повний зріст
Сьогодні я розкажу про те, як я робив об'ємні обертаються планети для нашої гри blast-off. Тобто вони, звичайно, зовсім плоскі, всього пара трикутників, але виглядають як об'ємні. Гра у нас повністю двовимірна, так що варіантів реалізації такого ефекту не сказати щоб багато. Варіант пререндерить багато кадрів для кожної планети, а потім міняти кадр за кадром, як це робили в старих іграх, можна відсікти. Він громіздкий і не дозволяє зробити, наприклад, динамічні хмари, грози на поверхні планети або змінювати напрямок освітлення. Тобто, звичайно, дає, але для цього потрібно для кожного виду планети робити просто купу спрайтів. Зрозуміло, це не прийнятно.

Рішення було очевидно. Потрібно зробити все на льоту, шейдером. У багатьох тут же виникне думка «ну ніфіга собі очевидно! Очевидно — це робити 3D, 3D планети, хіба ні?». Немає. Тому що гра у нас виключно двовимірна. Залишається тільки питання як. Змінювати геометрію текстури достовірно — досить громіздкий код, тільки якщо не використовувати карту зміщень. З картою зміщень код стає простіше і, як результат, набагато швидше.

До речі про карті зсувів, так як вона містить у собі вектора, а кожна складова кольору всього лише 8 біт, то нескладно порахувати, що ми можемо закодувати зміщення не більше ніж на 128 пікселів. Звичайно, ми можемо взяти за розрахунок, що кожна градація це 2 пікселя, і тоді ми зможемо кодувати зміщення до 256 пікселів в кожну сторону. Проте в нашому випадку нам вистачало розміру звичайної карти зміщень. Збільшення розміру я компенсував лінійної фільтрацією в шейдере.
Тут треба обмовитися, що на поточний момент мій движок quad-engine підтримує тільки текстури формату A8R8G8B8.

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

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

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

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


Інструментарій
  • Delphi XE5 Starter — для написання коду
  • Quad-engine — для виводу графіки
  • fxc.exe — для компіляції шейдерів в бінарний код
  • QuadShade — для редагування коду шейдерів
  • FilterForge — для процедурної генерації текстур


Шейдер
float2 Velocity : register(c0);
sampler2D DiffuseMap : register(s0); 
sampler2D DuDvMap : register(s1);

float4 std_PS(vertexOutput Input) : COLOR { 

float2 source = Input.TexCoord; 

source.y -= 2.0 / 512.0;
source.y = max(source.y, 0.0);

source.x += 2.0 / 512.0;
float4 right = (tex2D(DuDvMap, source) * 2.0) - 1.0;

source.x -= 4.0 / 512.0; 
source.y = max(source.x, 0.0);
float4 left = (tex2D(DuDvMap, source) * 2.0) - 1.0;


source.y += 4.0 / 512.0;

float4 left2 = (tex2D(DuDvMap, source) * 2.0) - 1.0;

source.x += 4.0 / 512.0;
float4 right2 = (tex2D(DuDvMap, source) * 2.0) - 1.0;


float4 middle = right / 4.0 + left / 4.0 + right2 / 4.0 + left2 / 4.0;

float2 coord = middle.rg;
coord.x = coord.x / 4.0;
coord.y = coord.y / 2.0;

coord += Velocity;

float4 result = tex2D(DiffuseMap, coord);
return float4(result.rgb, middle.a * result.a) * Input.Color;
}



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



Другий потрібної текстурою стане та сама dUdV карта з закодованими векторами. Вона у нас дорівнює 512х512 пікселів. Тут розміри менше. Чому? Тому що бачимо ми лише квадратний шматочок сітки, спотворений шейдером. А значить нам потрібно сказати шейдеру який саме квадратний шматок сітки ми віддаємо. Поступово змінюючи квадрат зі зсувом, ми отримаємо ефект обертання.



Накладаємо шейдер і перекручуємо вихідну структуру сітки з допомогою другої текстури.



На скріншоті досить чітко видно зашумленість чорних ліній. Вони ніби вкрилися піксельними хвилями. Зір вас не обманює і проблема в обмеженнях dUdV текстури. Карта зміщень має всього 256 градацій (як я писав вище), тобто по 128 в плюс і мінус. Це обмеження текстур у форматі A8R8G8B8. А розмір текстури 512 пікселів. Із за цього градієнт виходить ступінчастий, місцями з повторюваними пікселями. Як підсумок — картинка спотворюється в цілому вірно, але на піксельному рівні містить артефакти. Звичайно, розмір планети і її текстура дозволяють знехтувати цим візуальним артефактом, але прикро.

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

У dUdV карті у нас не 2, а 3 каналу. RGB. Альфа гововоріт шейдеру про форму планети, згладжуванні по краях і тому не розглядається. R і G використовуються для кодування зміщення. Залишається ще 1 байт порожній. Його можна було б використовувати, щоб закодувати за додаткових 4 біта на кожен колір (8+4). Це дозволило б дуже істотно збільшити точність і уникнути будь-яких проблем з мерехтінням і спотвореннями. Очевидно, це наступний крок на шляху розвитку ефекту.

Отже, йдемо далі! Додаємо затінення. Це теж можна робити шейдером, але на даному етапі це абсолютно не принципово, тому я просто поставлю напівпрозору структуру в режимі віднімання:


Міняємо текстурку на поверхню планетны і починаємо гратися вже з нею:



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


Комбінуємо хмари з уже наявною планетою. Зробимо хмари на 1.5% ширше, ніж планету, вони ж не дуже низькі. І в підсумку отримаємо ось таке ось розпуста:


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



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



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


Ось. Майже красиво, але є одне АЛЕ — на темній стороні планети нічого не світиться. Беремо в руки карту світіння (також згенеровану разом з текстурою поверхні) і додаємо ще й її. Логічно буде додати світіння вже поверх всього, щоб ніякі тіні не впливали на світло, вірно?


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

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

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



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


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

Diablo3:


Наша реалізація:


Висновки
Та чого вже тут. Шейдери це круто. Ось і всі висновки! Серйозно, без шейдерів зробити дуже красиву і динамічну гру буде подібно каторзі. Майже вся графіка (крім поверхні Землі) генерована і втручання художника не вимагає. Це дозволяє робити гру з мінімальним залученням людей ззовні.

Наостанок даю відео:


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.