Благодаря физически точному рендерингу в Unreal Engine 4 удобно разрабатывать реалистичные игры. Модель рендеринга имитирует взаимодействие света с материалами, что приводит к созданию реалистичной картинки. Однако если вы хотите разработать игру со стилизованным внешним видом, то вам придётся исследовать другие техники.
Один из способов создания стилизации — использование cel shading (также известного как toon-шейдинг). Эта техника подражает затенению, обычно используемому в мультфильмах и аниме. Примеры её использования можно увидеть в таких играх, как Jet Set Radio, The Legend of Zelda: The Wind Waker и Gravity Rush.
В этом туториале вы научитесь следующему:
- Создавать и использовать материал постобработки
- Создавать cel-шейдер
- Изолировать cel-шейдер для отдельных мешей
- Управлять цветовыми полосами с помощью таблиц поиска
Примечание: В этом туториале подразумевается, что вы уже знакомы с основами Unreal Engine. Если вы новичок в Unreal Engine, то вам стоит изучить сначала туториал из десяти частей Unreal Engine для начинающих.
Приступаем к работе
Скачайте заготовку проекта и распакуйте её. Перейдите в папку проекта и откройте CelShader.uproject. Вы увидите следующую сцену:
Это персонаж, к которому мы будем применять cel shading. Прежде чем приступать, нужно разобраться, что же такое cel shading.
Что такое Cel Shading?
Cel shading — это процесс рендеринга с помощью нескольких полос цвета, а не непрерывного градиента.
Ниже представлен пример использования cel shading в The Legend of Zelda: Breath of the Wild. Заметьте, что cel shading реализован только для персонажа, а фон остался обычным.
На этом изображении есть три полосы: одна для теней, одна для средних тонов, и ещё одна для светлых участков.
Часто ошибочно считается, что cel shading применён, если у объектов есть контуры. Примером этого может служить Borderlands. Хотя эта игра имеет стилизованный внешний вид, на самом деле в ней нет cel shading. Это можно увидеть на изображении ниже. Заметьте, что в расцветке персонажа не используются полосы цвета.
Контуры не являются cel shading, но их часто используют совместно. Благодаря этому картинка становится похожей на нарисованную краской или тушью. Такой приём часто используется в играх с аниме-стилистикой, таких как Guilty Gear Xrd и Dragon Ball FighterZ.
В следующем разделе мы узнаем, как реализовать cel shading.
Способ реализации Cel Shading
Самый распространённый способ реализации — сравнение направления поверхности (известного как «нормаль») и направления света. Вычислив скалярное произведение между нормалью и направлением света, мы получим значение от -1 до 1.
Значение -1 означает, что поверхность и свет имеют противоположные направления. 0 означает, что они перпендикулярны. 1 означает, что они направлены одинаково.
Задав пороговые значения для скалярных произведений, можно создать несколько полос. Например, если скалярное произведение больше -0,8, то поверхности можно присвоить тёмный цвет, а если скалярное произведение меньше -0,8, то светлый цвет. Так мы создадим двухполосный cel-шейдер.
Ограничение этого способа заключается в том, что на объекты с cel-шейдингом не могут влиять другие источники освещения. Кроме того, объекты не могут отбрасывать тени на объекты с cel-шейдингом.
Чтобы решить эту проблему, мы должны использовать другой способ. Вместо вычисления скалярного произведения мы будем вычислять освещённость поверхности. Затем при задании пороговых значений можно будет использовать это значение вместо скалярного произведения.
Теперь, когда вы знаете, что такое cel-шейдер и как он работает, настало время его создать.
Создание Cel-шейдера
В этом туториале cel shading будет эффектом постобработки. Постобработка (Post processing) позволяет изменять изображение после того, как движок закончит его рендеринг. Постобработку обычно используют для таких эффектов, как глубина резкости, motion blur и bloom.
Для создания собственного эффекта постобработки нам нужно воспользоваться материалом постобработки (post process material). Перейдите в папку Materials и создайте новый Material. Переименуйте его в PP_CelShader и откройте.
Чтобы преобразовать материал в материал постобработки, нужно изменить его домен (domain). Перейдите в панель Details и измените Material Domain на Post Process.
Первый шаг в создании cel-шейдера — вычисление освещённости каждого пикселя. Мы назовём это буфером освещения (lighting buffer).
Вычисление буфера освещения
Когда Unreal рендерит изображение на экран, то сохраняет проходы в буферы. Для вычисления буфера освещения нам нужно будет получить доступ к двум таким буферам:
- Post Process Input: после того, как Unreal выполнил освещение и постобработку, он сохраняет изображение в этот буфер. Именно его Unreal будет отображать игроку, если вы не выполняете дальнейшую постобработку.
- Diffuse Color: это сцена без всякого освещения и постобработки. Она будет содержать диффузный цвет всего, что есть на экране.
Для доступа к этим буферам нужно использовать нод SceneTexture. Создайте его, выберите и перейдите в панель Details. Чтобы получить доступ к буферу Post Process Input, измените Scene Texture Id на PostProcessInput0.
Для доступа к Diffuse Color создайте ещё один нод SceneTexture. Измените его Scene Texture Id на DiffuseColor.
Буфер освещения должен содержать только значения в градациях серого (описывающие степени освещённости пикселей). Это значит, что нам не потребуется информация о цвете из обоих буферов. Чтобы отбросить цвета, соедините выход Color обоих нодов SceneTexture с Desaturation. Это полностью обесцветит оба буфера.
Для вычисления буфера освещения просто разделим SceneTexture:PostProcessInput0 на SceneTexture:DiffuseColor. Порядок здесь важен!
Затем используем Clamp, чтобы значения оставались в интервале от 0 до 1. Это упростит создание порогов, потому что мы будем знать возможные значения.
Вот визуализация буфера освещения:
Как вы видите, освещённые области ближе к белому, а неосвещённые — к чёрному.
Затем мы используем буфер освещения для создания порога.
Создание порога
В нашем cel-шейдере любой пиксель со значением больше 0,5 будет использовать обычный диффузный цвет. Пиксели со значениями меньше 0,5 будут использовать диффузный цвет половинной яркости.
Для начала создадим нод If. Он позволит нам сравнивать два значения. В зависимости от результатов сравнения мы сможем указывать разные выходы.
Далее соединим Clamp со входом A. Затем создадим Constant со значением 0.5 и соединим её со входом B.
Примечание: для изменения порога можно менять значение входа B.
Чтобы получить цвета, создадим SceneTexture и зададим для его Scene Texture Id значение Diffuse Color. Затем умножим Color на 0.5, чтобы получить диффузный цвет половинной яркости.
И наконец, соединим всё следующим образом:
Подведём итог:
- Desaturation преобразует Post Process Input и Diffuse Color в изображения в градациях серого
- Divide делит Post Process Input на Diffuse Color. Так мы создаём буфер освещения.
- Clamp ограничивает значения интервалом от 0 до 1
- If выводит обычный диффузный цвет, если значение освещения больше 0.5. Если оно меньше 0.5, то он выводит диффузный цвет половинной яркости.
Теперь, когда у нас есть cel-шейдер, нам нужно применить его к сцене.
Использование материалов постобработки
Чтобы использовать материалы постобработки, нам нужно создать Post Process Volume. Он обычно используется для управления такими эффектами постобработки, как баланс белого, насыщенность и контрастность.
Нажмите на Apply и вернитесь в основной редактор. Для создания Post Process Volume перейдите в панель Modes и выберите категорию Volumes. Затем перетащите Post Process Volume во Viewport, чтобы создать его.
Теперь нам нужно сказать Post Process Volume, чтобы он использовал cel-шейдер. Выбрав Post Process Volume, перейдите в панель Details. Затем найдите Rendering Features\Post Process Materials и нажмите на значок +. Так вы добавите в массив новый элемент.
Затем нажмите на раскрывающийся список Choose и выберите Asset Reference.
Это позволит выбрать материал. Нажмите на раскрывающийся список None и выберите PP_CelShader.
По умолчанию Post Process Volume воздействует только тогда, когда мы находимся внутри. Однако в нашем случае нужно, чтобы он влиял на весь мир. Для этого прокрутите до Post Process Volume Settings и включите Infinite Extent (Unbound).
Теперь, когда cel-шейдер применён ко всей игре, мы увидим следующее:
«Постойте-ка, это не похоже на тот cel-шейдер, который вы показывали раньше!»
Главная причина такого отличия в том, что движок применяет cel-шейдер после тональной компрессии. Чтобы исправить это, нам нужно попросить движок использовать cel-шейдер перед тональной компрессией.
Применение Cel Shading перед тональной компрессией
Прежде чем показать изображение игроку, Unreal выполняет процесс, известный как «тональная компрессия» (tonemapping). Одна из целей тональной компрессии — сделать изображение более естественным. Она берёт входной цвет, а затем использует кривую, чтобы сдвинуть его в новое значение.
Вот два изображения, до и после тональной компрессии:
Как вы видите, светлые участки до тональной компрессии слишком яркие. Однако после тональной компрессии яркие области становятся более мягкими.
Хотя тональная компрессия полезна для изображений, которые нужно отображать, мы не должны выполнять тональную компрессию для изображений, которые хотим использовать в вычислениях. Из-за смещения значений мы будем использовать не те значения, которые ожидаем.
Откройте PP_CelShader и убедитесь, что ничего не выбрано. Затем зайдите в панель и найдите раздел Post Process Material. Задайте Blendable Location значение Before Tonemapping.
Нажмите на Apply, а затем вернитесь в основной редактор. Цвета теперь выглядят гораздо лучше!
В следующем разделе мы научимся тому, как применять cel shading только к отдельным объектам.
Изолирование Cel Shader
Чтобы изолировать эффекты постобработки, нам нужно использовать функцию под названием Custom Depth. Как и Post Process Input с Diffuse Color, это тоже буфер, который можно использовать в материалах постобработки.
Прежде чем понять, что такое Custom Depth, вам нужно разобраться с буфером Scene Depth. Scene Depth хранит отдалённость каждого пикселя от камеры. Вот как выглядит визуализация Scene Depth:
Custom Depth хранит ту же информацию, но только для выбранных вами мешей. Вот его визуализация с викингом, отрендеренным в Custom Depth:
Сравнивая Scene Depth с Custom Depth, мы можем изолировать объекты. Если Scene Depth меньше, чем Custom Depth, то мы используем обычное изображение. Если Scene Depth больше Custom Depth, то используется изображение с cel-шейдингом.
Первый шаг — рендеринг викинга в Custom Depth.
Использование Custom Depth
Перейдите в World Outliner и выберите SK_Viking. Затем перейдите в панель Details и найдите раздел Rendering. Затем включите Render CustomDepth Pass.
Далее нам нужно выполнить сравнение глубин. Откройте PP_CelShader и создайте следующую схему:
Примечание: ноды Mask ( R ) являются масками компонентов (Component Mask). Они позволяют преобразовывать многоканальные данные в скалярные значения. Наложить маску на Scene Depth и Custom Depth нам нужно потому, что нод If для входов A и B принимает только скалярные значения.
Затем соедините выход сети cel shading с A > B. Наконец, соедините выход только что созданного If с Emissive Color.
Теперь cel shading будет применяться только к мешам, отрендеренным в Custom Depth.
Нажмите на Apply, а затем вернитесь в основной редактор. Вы увидите, что cel shading теперь выполняется только для викинга.
Сel-шейдер работает замечательно, но он довольно прост. Что, если нам понадобится большее количество полос? Что, если мы захотим создать более плавные переходы между полосами? Всё это можно реализовать с помощью таблиц поиска (lookup tables) (LUT).
Что такое «таблица поиска»?
В детстве мы учились тому, что такое умножение. Однако юный мозг не всегда мог выполнять такие вычисления. Поэтому вместо расчётов вы могли пользоваться таблицей умножения для «поиска» ответов.
По сути, это и есть LUT. Это массив значений (обычно предварительно вычисленных), к которым можно получить доступ с помощью входных данных. В случае таблицы умножения входными данными являлись множитель и множимое.
В контексте нашего cel-шейдера LUT — это текстура с неким градиентом. Вот четыре примера того, как может выглядеть LUT:
Пока мы вычисляем цвет тени, умножая диффузный цвет на 0,5. Вместо умножения на постоянную 0,5 мы будем использовать значение из LUT. Благодаря этому мы можем управлять количеством полос и их переходами. Понять, как будет выглядеть затенение, можно по внешнему виду LUT.
Прежде чем использовать LUT, нужно изменить некоторые из её параметров текстуры.
Изменение параметров LUT
Перейдите в папку Textures и откройте T_Lut_01. Вот, как выглядит LUT:
Первый параметр, который нам нужно изменить — это sRGB. При рендеринге Unreal преобразует все текстуры со включенным sRGB в линейный цвет. Это упрощает движку выполнение вычислений рендеринга.
Параметр sRGB полезен для текстур, описывающих внешний вид. Однако текстуры наподобие карт нормалей и LUT содержат данные, предназначенные для математических вычислений. Поэтому Unreal должен считать их значения уже верными. Если отключить sRGB, то Unreal не будет выполнять преобразование в линейный цвет.
Для этого снимите флажок sRGB. Этот параметр находится в разделе Texture.
Следующий параметр, который нам нужно изменить — способ тайлинга текстуры. Поскольку мы не будем отображать эту текстуру, то тайлинг ей не нужен. Более того, если оставить тайлинг включённым, то это добавит проблем при сэмплировании на краях текстуры. Например, если мы будем сэмплировать пиксель с левого края, то из-за тайлинга он попытается смешаться с правым краем.
Для отключения тайлинга измените значение X-axis Tiling Method на Clamp. Тоже самое сделайте для Y-axis Tiling Method.
И на этом мы закончили с параметрами. Теперь нам нужно использовать LUT в материале постобработки.
Использование LUT
Закройте T_Lut_01 и откройте PP_CelShader. Сначала удалите выделенные ноды:
Затем создайте Texture Sample и измените его Texture на T_Lut_01. Эта таблица LUT будет создавать три полосы с плавным переходом.
Как мы помним, для определения значений, которые нужно выводить, LUT используют входные данные. В нашем случае в качестве входных данных будет использоваться буфер освещения.
Чтобы реализовать это, соедините Clamp с UVs в Texture Sample.
Это работает, потому что значения буфера освещения и координаты текстуры находятся в интервале от 0 и 1. Например, если пиксель из буфера освещения равен 0,5, то LUT подаст на выход значение пикселя из середины текстуры.
Далее нам нужно умножить диффузный цвет на LUT. Для этого воссоздайте следующую схему:
Мы используем Append для преобразования вывода Texture Sample’s в четырёхканальный вектор. Это нужно нам, потому что мы не можем умножать трёхканальный вектор на четырёхканальный (SceneTexture).
Наконец, соединим всё следующим образом:
Теперь вместо умножения диффузного цвета на константу мы умножаем его на значение из LUT. Так мы контролируем количество полос цвета и их переходы (зависящие от LUT). Выводимое значение LUT определяется буфером освещения.
Нажмите на Apply, а затем закройте PP_CelShader. Теперь затенение будет иметь три полосы с более плавными переходами между полосами.
Ниже представлено сравнение того, как могут выглядеть различные LUT. Эти LUT тоже добавлены в проект.
Куда двигаться дальше?
Готовый проект можно скачать отсюда.
Как вы видите, материалы постобработки — очень мощный инструмент. Они позволяют создавать множество реалистичных и стилизованных эффектов. Если вы хотите больше узнать о постобработке, то изучите документацию по постобработке UE.
Комментарии (5)
Flakky
28.02.2018 23:03Когда увидел статью, ожидал увидеть там реализацию написания своего шейдера через плагин. Просто через постобработку делать целл-шейдинг на конкретных объектах нецелесообразно. Но все равно новичкам будет полезно :)
Кстати говоря, ограничение первого способа — отсутствие влияния теней, можно решить, отрендерив вручную каскады, вместо самого источника света. Ну и наложив в шейдере) Но это гемор, согласен)
И ещё… Я бы избегал использование if'ов. Есть риск нарваться на алиасинг.
В остальном спасибо за перевод!
asaq
01.03.2018 10:22Надо заменить Clamp(0,1) на Saturate.
Как узнать производит ли UE самостоятельно такую оптимизацию при компиляции шейдера?Flakky
01.03.2018 12:01Посмотреть скомпилированный HLSL (Windows > HLSL). Но он не делает такой оптимизации, насколько я знаю. Saturate идет как отдельная нода.
Flakky
01.03.2018 12:19yadi.sk/i/aBX9f9I93Stm6d Вот, кстати, и то, как он разворачивает Clamp.
При Saturate он ставит сам saturate() как и положено.
keydon2
Круто! Пишите еще.