Urho3D: Постэффекты

Продовжуємо розбиратися в графічній підсистемі Urho3D. На цей раз поговоримо про ефекти постобробки. У комплект гри входить безліч вже готових ефектів, і один з них (Bloom) ми навіть використовували минулій статті. Але ні один движок не здатний задовольнити потреби будь-якого розробника, тому буде корисно навчитися створювати свої власні ефекти. В якості прикладу я вирішив вибрати ефект просвічування персонажа через стіни, яка нерідко використовується в стратегії і РПГ.

image

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

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

Отже, нам потрібно:

1) Отрендерить сцену.
2) Отримати маску невидимої частини персонажа.
3) Пофарбувати маску в який-небудь колір і накласти її на рендер сцени.

Щоб отримати маску невидимої частини можна:

1) Повторно отрендерить персонажа з включеним тестом глибини, використовуючи простий шейдер, виводить білий піксель (отримуємо таким чином чорно-білу текстуру видимої частини персонажа).
2) Повторно отрендерить персонажа, використовуючи той же шейдер, але вже ігноруючи буфер глибини (отримуємо таким чином чорно-білу маску всього персонажа).
3) Якщо в якомусь місці повна маска біла, а маска видимої частини чорна, то це і є шукана невидима частина персонажа.

Разом нам потрібно отримати і скомбінувати три текстури:



Реалізація
Готова демка тут (OpenGL). Для запуску використовуйте START_DEMO.bat. В папці Data і CoreData знаходяться стандартні ресурси движка, а всі нові / змінені файли поміщені в MyData. Ну і за традицією використовувана версія движка. А тепер більш докладно :)

Завантаження рендерпасов
Раніше ми розглядали рендерпасы як спосіб завдання черговості проходів в матеріалах (розділ «Процес візуалізації»). Але рендерпасы виконують також і інші функції.

Стандартні рендерпасы знаходяться в папці CoreData/RenderPaths. За замовчуванням використовується Forward.xml. Змінити рендерпас можна різними способами:

  • Викликати функцію Renderer::SetDefaultRenderPath() перед створенням вьюпорта. При цьому наступні створювані вьюпорты будуть використовувати зазначений рендерпас. Даний метод і використовується в демці.
  • Вказати рендерпас в параметрах движка (за допомогою параметрів командного рядка при запуску додатка або через engineParameters_ в тексті програми). При цьому викликається все та ж функція Renderer::SetDefaultRenderPath().
  • Використовувати функцію Viewport::SetRenderPath() після створення вьюпорта.
  • У редакторі рендерпас можна вказати у вікні View > Editor Settings.
Рендерпасы можна не тільки завантажувати з файлів, але і динамічно змінювати у процесі роботи програми. Наприклад, коли ви використовуєте який-небудь постэффект з папки Data/PostProcess, відбувається не що інше, як додати команд в поточний рендерпас. Іншими словами ви можете просто скопіювати вміст якогось файлу (або файлів) з Data/PostProcess в кінець якогось файлу CoreData/RenderPaths.

https://github.com/1vanK/Urho3DHabrahabr06/blob/master/MyData/Scripts/Main.as
void Start()
{
...
renderer.SetDefaultRenderPath(cache.GetResource("XMLFile", "RenderPaths/MyForward.xml"));
Viewport@ viewport = Viewport(scene_, cameraNode.GetComponent("Camera"));
viewport.renderPath.Append(cache.GetResource("XMLFile", "PostProcess/FXAA3.xml"));
renderer.viewports[0] = viewport;
}

Тут відбувається завантаження рендерпаса MyForward.xml (який заснований на Forward.xml)а потім до нього додається ефект повноекранного згладжування FXAA3.xml).

Рендератергеты
Рендерпасы складаються з рендертаргетов (rendertarget) і команд (command).

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

https://github.com/1vanK/Urho3DHabrahabr06/blob/master/MyData/RenderPaths/MyForward.xml
<renderpath>
<rendertarget name="visiblemask" tag="WallHack" sizedivisor="1 1" format="a" />
<rendertarget name="fullmask" tag="WallHack" sizedivisor="1 1" format="a" />
...
</renderpath>

Тут оголошується два рендертаргета для масок (visiblemask — макска видимої частини персонажа і fullmask — маска всього персонажа).

Параметр name визначає ім'я рендертаргета, по якому до нього можна звертатися.

Параметр tag дозволяє визначити рендертаргеты і команди в якусь групу, яку можна буде динамічно включати і відключати в грі за допомогою функцій RenderPath::SetEnabled() і RenderPath::ToggleEnabled(). Зверніть увагу, що всі стандартні постэффекты мають власний тег. Таким чином можна, наприклад, включати розмиття екрану тільки при відкритті меню. Ну а в нашій демці після натискання пробілу проводиться перемикання вибиття.

https://github.com/1vanK/Urho3DHabrahabr06/blob/master/MyData/Scripts/Main.as
void HandleUpdate(StringHash eventType, VariantMap& eventData)
{
...
if (input.keyPress[KEY_SPACE])
renderer.viewports[0].renderPath.ToggleEnabled("WallHack");
...
}

Параметр sizedivisor дозволяє створити рендертаргет з розміром, відмінним від розміру вьюпорта. У нашій демці розмір масок ідентичний розміру вікна (так як вьюпорт займає все вікно гри). Але ось, наприклад, в постэффекте Bloom.xml розмір рендертаргета в 4 рази менше розміру вьюпорта в цілях продуктивності (для накладеного світіння високий дозвіл не вимагається).

Параметр format визначає, власне, формат рендертаргета. Найбільш часто використовуються формати «rgb» і «rgba\», але в нашому випадку для зберігання масок достатньо одного каналу, тому використовується одноканальний формат «a». Тут є нюанс. В OpenGL 2 формату «a» відповідає GL_ALPHA (а значить потрібно працювати з каналом alpha), а в OpenGL 3 — GL_R8 (потрібно працювати з каналом red). Ми до цього ще повернемося при розгляді шейдерів.

Зверніть увагу, що рендертаргеты не обов'язково повинні оголошуватися на початку файлу. Вони можуть перебувати в будь-якому місці, навіть в кінці файлу. Гарантується, що всі рендертаргеты будуть створені перед виконанням команд.

І відразу ж про першій команді, яка нам знадобиться — clear.

https://github.com/1vanK/Urho3DHabrahabr06/blob/master/MyData/RenderPaths/MyForward.xml
<renderpath>
...
<command type="clear" tag="WallHack" color="0 0 0 0" output="visiblemask" />
<command type="clear" tag="WallHack" color="0 0 0 0" output="fullmask" />
...
</renderpath>

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

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

Команда scenepass
Це ті самі проходи рендера, які були згадані в минулій статті.

https://github.com/1vanK/Urho3DHabrahabr06/blob/master/MyData/RenderPaths/MyForward.xml
<renderpath>
...
<command type="scenepass" tag="WallHack" pass="visiblemask" output="visiblemask" />
<command type="scenepass" tag="WallHack" pass="fullmask" output="fullmask" />
...
</renderpath>

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

https://github.com/1vanK/Urho3DHabrahabr06/blob/master/MyData/Techniques/DiffNormalWallHack.xml
<technique ...>
...
<pass name="visiblemask" vs="Mask" ps="Mask" depthwrite="false" depthtest="equal" psexcludes="PACKEDNORMAL" />
<pass name="fullmask" vs="Mask" ps="Mask" depthwrite="false" depthtest="always" psexcludes="PACKEDNORMAL" />
</technique

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

До моменту проходів visiblemask і fullmask буфер глибини вже заповнений. Нам не потрібно туди нічого писати, а тільки використовувати його. Тому в обох проходів параметр depthwrite виставлений false. Однак параметр depthtest різний. При значенні depthtest=«always» буфер глибини ігнорується, і малюється повна маска персонажа. При значенні depthtest=«equal» тест глибини буде пройдено, коли значення в Z-буфері збігається з глибиною виведеної геометрії, тобто коли та ж сама геометрія рендерится повторно.

Команда quad
Саме ця команда і виводить закриває екран прямокутний полігон, призначений для реалізації постэффектов (так званий screen quad).

https://github.com/1vanK/Urho3DHabrahabr06/blob/master/MyData/RenderPaths/MyForward.xml
<renderpath>
...
<command type="quad" tag="WallHack" vs="WallHack" ps="WallHack" output="viewport">
<texture unit="diffuse" name="viewport" />
<texture unit="normal" name="fullmask" />
<texture unit="specular" name="visiblemask" />
</command>
</renderpath>

Тут для відтворення квад використовується шейдер WallHack, і на вхід цього шейдера передаються 3 текстури (viewport — отрендеренная сцена, fullmask — повна маска персонажа і visiblemask — маска видимої частини персонажа). Нехай вас не плутає назви текстурних юнітів (diffuse, normal і specular). Ви можете передавати через них що завгодно, і використовувати в своїх шейдери як завгодно.

Шейдер WallHack дуже простий.

1) Отримуємо колір текселя отрендеренной сцени (нагадаю, що ми в рендерпасе передали рендер сцени через текстурний юніт diffuse):

https://github.com/1vanK/Urho3DHabrahabr06/blob/master/MyData/Shaders/GLSL/WallHack.glsl
void PS()
{
vec3 viewport = texture2D(sDiffMap, vTexCoord).rgb;
...
}

2) Отримуємо обидві маски:

https://github.com/1vanK/Urho3DHabrahabr06/blob/master/MyData/Shaders/GLSL/WallHack.glsl
void PS()
{
...
#ifdef GL3
float fullmask = texture2D(sNormalMap, vTexCoord).r;
float visiblemask = texture2D(sSpecMap, vTexCoord).r;
#else
float fullmask = texture2D(sNormalMap, vTexCoord).a;
float visiblemask = texture2D(sSpecMap, vTexCoord).a;
#endif
...
}

Повертаючись до згаданого вище нюансу, ми бачимо, що в залежності від версії OpenGL, при роботі з одноканальної текстурою ми використовуємо різні канали.

Source/Urho3D/Graphics/OpenGL/OGLGraphics.cpp
unsigned Graphics::GetAlphaFormat()
{
#ifndef GL_ES_VERSION_2_0
// Alpha format is deprecated on OpenGL 3+
if (gl3Support)
return GL_R8;
#endif
return GL_ALPHA;
}

Якщо ваше обладнання підтримує OpenGL 3, то за замовчуванням використовується ця версія. В цілях тестування ви можете змусити движок використовувати OpenGL 2 за допомогою параметра командного рядка "-gl2".

3) І нарешті всі три текстури комбінуються:

https://github.com/1vanK/Urho3DHabrahabr06/blob/master/MyData/Shaders/GLSL/WallHack.glsl
void PS()
{
...
if (fullmask == 0.0 || visiblemask > 0.0)
gl_FragColor = vec4(viewport, 1.0);
else 
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

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

Висновок проміжних текстур
Іноді в цілях налагодження ви можете захотіти вивести проміжні рендертаргеты на екран. Для цього зручно використовувати стандартний шейдер CopyFramebuffer. Потрібно просто додати в кінець рендерпаса:

<renderpath>
...
<command type="quad" vs="CopyFramebuffer" ps="CopyFramebuffer" output="viewport">
<texture unit="diffuse" name="ІМ'Я ПОТРІБНОЇ ТЕКСТУРИ" />
</command>
</renderpath>

Цей шейдер очікує, що ви передасте йому потрібну текстуру у форматі rgb(a)» через текстурний юніт diffuse. Для виведення нашої одноканальної маски він не підходить. Тому я трохи модифікував цей шейдер, щоб він очікував на вході текстуру у форматі «a» (дивіться шейдер ShowATexture.glsl). Саме з його допомогою зроблені скріншоти для статті.

<renderpath>
...
<command type="quad" vs="ShowATexture" ps="ShowATexture" output="viewport">
<texture unit="diffuse" name="fullmask" />
</command>
</renderpath>

Більше прикладів
Розмиття при повороті камери (Motion Blur):

image

Обведення як в Left 4 Dead та Dota 2:

image

Розчинення об'єктів як в Doom 3:


Soft Particles, покращений шейдер води, SSAO, а також багато іншого дивіться на офіційному форумі.
Джерело: Хабрахабр

0 коментарів

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