Создание Glitch Effect«а» Джонни Сильверхенда на Unity
Доброго времени суток всем Unity нетранерам, а также тем, кто случайно наткнулся на эту статью!
Как бы многие на этом ресурсе не критиковали киберпанк, одно у него точно не отнять — это восхитительную работу всех специалистов, занимавшихся графической составляющей проекта. Лично я был в восторге от освещения в данной игре (даже с выключенным RTX"ом"), а эмоции от графических гличей, кои появляются в моменты сбоя чипа или возникновения голлограммы Джонни Сильверхенда никак кроме восторженными словами у меня описать не получится.
По этой причине, после прохождения игры у меня возникло непреодолимое желание разобрать один из эффектов появления Киану Ривза. Надо признать, что его голлаграмма это комплексный механизм, состоящий из десятков спецэффектов и намерянных гличей. Мы же сегодня постараемся реплицировать лишь один из них — а именно эффект появление голлограммы из вертикальных "линий", идущих в разнобой, но по итогу совмещаемых в одно единое целое.
Небольшой Viewer discretion перед тем, как я начну разбирать механизм. Данный прототип будет работать немного иначе, чем в оригинальной игре. Там, судя по моему наблюдению, для эффектов Джонни использовался BillBoard с грабпасом. Достаточно элегантное и экономичное решение в плане затраты ресурсов, но честно говоря моих текущих навыков пока не хватает чтобы реализовать это. Однако, если вдруг появятся знающие люди в комментариях, и у них будет желание пообщаться со мной по этому поводу — буду искринне рад.
Также данный алгоритм никак не претендует на вообще что либо, в нём скорее всего будет совершена куча ошибок, поэтому если появятся предложения по его улучшению — я только за. Вообще будет супер, если после этой статью я научусь чему — нибудь.
Ну и последнее — этот механизм будет сделан с помощью Шейдер Графа. По тому что во — первых sg более нагляден для представления, во — вторых менее сложен чем язык шейдеров, и в третьих — у такого шейдера на hsls всего пару строк кода, а из такого материала не удастся вытянуть целую статью!
Ну что — ж, поехали!
Глава 1: Первые шаги
Опустим шаги с импортом шейдер графа, установкой Render Pipelin«a» и разбрасыванием геймобджектов на сцене. В конечном итоге создаём простенькую композицию, на которой и будем тестировать наш эффект.
Первая итерация нашего механизма будет работать следующим образом: Рендерим только объекты с "гличами" в одну рендер текстуру, а все оставшиеся — в другую. В первой текстуре рисуем эффекты, а потом просто смешиваем 2 этих изображения и выводим его на экран. А теперь более конкретно:
Создаём отдельный слой, называемый Glitch.
Помечаем им все наши объекты, которые будут «корёжиться». У нашей главной камеры в свойстве «Culling Mask» убираем слой «Glitch». Также создаём в проекте Render Textur'у', и линкуем её в свойство Target Texture у камеры.
Дублируем нашу камеру, ставим в Culling Mask уже только слой "Glitch", а "Clear Flags" устанавливаем в Solid Color и сэтаем цвет бэкраунда в чОрны, а также линкуем другую рендер текстуру в поле Target Texture. В конечном итоге у вас должно получиться что — то нападобие этого:
Наконец — то настало время заняться шейдерами! Так как наш шейдер будет проецировать уже отрендеренные изображения, никакая работа со светом, нормалями и прочим ему не нужна, поэтому создаём Unlit Graph.
В качестве первого шага нам нужно просто объединить эти две текстуры в одну. Для этого создаём 2 ноды Sample Texture, и объединяем их в ноде Lerp. Ну а результат пихаем в Color ноды Unlit Master. И получаем:
А теперь немного теории, максимально упрощённой для понимания. Функция Lerp (линейная интерполяция) — крайне удобная штука в геймдеве, которая "преобразует" A в B с силой T, где T принимает значения от 0 до 1.
Как я показал на изображении, Альфа канал Glitch текстуры как раз отлично подходит под определение параметра T. А получился у нас такой красивый канал потому что у камеры мы поменяли значение "Clear Flags" с Background на Solid Color.
Осталось как то теперь это вывести на экран. Ну, вы можете воспользоваться третьей камерой, а можете схитрить как я — создать канвас, растянуть на нём Raw Image и установить туда материал созданный из нашиего шейдера. В конечном итоге мы получаем такое изображение:
Как вы можете заметить, пропали тени... Ну, ввиду того что мы делаем голлограму, а они, как известно, не отбрасывают тень, предлагаю забить на это дело. Хотя конечно такой результат выглядит неорганично для нашего восприятия (я думаю именно по этой причине Джонни в киберпанке отбрасывает тень). Если хотите получить её — могу предложить вам сдублировать объекты гличей, устанавить им default layer и в настройках рендера установить им Cast Shadows как Shadows Only.
Глава 2: Эффект Глича
Опять немного теории — расположение каждого пикселя на нашей текстуре определяется его UV координатой. Значения UV является абсолютной величиной, а значит у самого правого пикселя по оси Х будет значение 1, а левого — 0. Аналогично и на оси ординат. Вот вам наглядный пример цветовой палитры UV по OY:
Двигая координаты UV мы можем смещать наше изображение. Как уже нетрудно догадаться, нам нужно просто добавить в Y какое — нибудь значение, не трогая Х. В нашем случае мы двигаем координаты целыми полосками, поэтому я заранее заготовил текстуру шума. Хотя конечно для рандомности её можно было бы и сгенерировать...
Для создания такого эффекта в Shader Graph«е» создадим ноду «UV», разделим функцией Split её на каналы, прибавим к Y каналу значение нашего шума (R канала шума будет вполне достаточно) и снова объединим значения в вектор UV, а затем укажем нашей текстуре полученный UV. Выглядеть это будет так:
Ну и результат:
Честно говоря выглядит не очень впечатляюще. Хотелось бы больше контроля. Для этого перед тем как добавлять к Y наш шум умножим его значение на какое — нибудь число, а сам наш шум затайлим по горизонтали. Оба этих значения вынесем в Property, и вот теперь мы можем дёргая эти значения получать ожидаемый результат:
Кстати, та часть графа теперь стала вот такой:
Глава 3: Баг глубины
Знаете, я ведь не просто так ничего не ставил впереди наших объектов с гличом. Давайте попробуем поставить что — нибудь перед ботом:
Очевидно это возникает потому, что мы бездумно накладываем одно изображение на второе. Мы совсем забыли о таком понятии, как глубина изображения. На юнити из себя она представляет красный канал, в котором дальность пикселя определяется его яркостью.
А вот теперь я совершу каминг аут — я понятия не имею как одной камерой рендерить отдельно цвет и глубину в разные рендер текстуры. Вроде у камеры есть метод camera.setTargetBuffers, в котором по идее можно определить буферы для вывода в разные рендертекстуры, но что — то он у меня не работает.
Поэтому сегодня, и только сегодня мы добавим на сцену ещё 2 камеры — одну для глубины мира, вторую — гличей. На этот раз обойдёмся без примеров, просто не забудьте у новых рендер текстур указать Color Format как Depth.
В нашем графе добавляем соответствующие Sample Textur«ы», и в текстуру глубины глича передаём ту же UV, что и у текстуры глича.
Дальше нам поможет волшебная функция Step — она принимает у себя 2 параметра — A и B, и возвращает 0 Если А > B, и 1 — если меньше. Кароче, я всегда в ней путаюсь. А стоит просто запомнить, что:
Но так как результат у нас получается немного вверх тормашками, просто проносим наш его через ноду One Minus, которая под капотом отнимает от единицы входящий параметр, и меняем альфу из ноды Lerp на наш новый результат. Выглядеть в Shader Graphe это будет следующим образом:
Ну, а в самом проекте вот так:
Глава 4: Баг повторения текстуры (clamping’a')
Я называю эту проблему багом длинношеев, в честь прикола из 3ей батлухи. Для его воспроизведения необходимо расположить камеру таким образом, чтобы объект с гличом выходил за пределы камеры, а потом сдвинуть объект эффектом в противоположную сторону. Полученный результат будет выглядеть вот так:
Почему так происходит? Ну, конкретно в нашем случае мы сдвигаем все пиксели по оси Y на определённое расстояние, но когда шейдёр заходит за границы текстуры, вместо того чтобы передать пустоту, он тянет самые крайние пиксели. Слава богу мы сами знаем на сколько мы смещаем UV по Y (это значение которое мы помещаем в вектор UV.Y на втором шаге), поэтому вооружившись этим знанием, а также «степами» для верхней и нижней границ, мы узнаем какие координаты, которые необходимо удалить из текущих текстур глича и глубины глича. Вот что в итоге получится:
Заключение
В заключении я создал анимацию, которая с помощью нехитрого скрипта устанавливает значения внутрь материала, и таким образом лишил себя необходимости каждый раз руками устанавливать это значение. Результат получился такой:
Ну, если бы я преследовал своей целью набрать плюсов, я бы оставил здесь фотографию с милым рождественским котиком. Но вместо этого я оставлю ссылку на репозиторий, если вдруг кому — то захочется посмотреть как это выглядит вживую, не продираясь через дебри копирования нод и читания моей писанины.
Повторюсь, если в комментариях будут знающие люди, которые знают ответ на:
1) Как достать из одной камеры глубину и цвет изображения.
2) Как реализовать в юнити билборд, который рендерит всё, что за ним находится и может менять эти пиксели на ходу.
3) Заменить Step и OneMinus на одну ноду.
Да и просто обсудить настроение и планы на новый год — Буду счастлив пообщаться в комментариях :)
Ну а так — удачи всем в новом году! Оттянитесь сегодня на полную! :)