Создание Glitch Effect«а» Джонни Сильверхенда на Unity

Доброго времени суток всем Unity нетранерам, а также тем, кто случайно наткнулся на эту статью!

Как бы многие на этом ресурсе не критиковали киберпанк, одно у него точно не отнять — это восхитительную работу всех специалистов, занимавшихся графической составляющей проекта. Лично я был в восторге от освещения в данной игре (даже с выключенным RTX"ом"), а эмоции от графических гличей, кои появляются в моменты сбоя чипа или возникновения голлограммы Джонни Сильверхенда никак кроме восторженными словами у меня описать не получится.

По этой причине, после прохождения игры у меня возникло непреодолимое желание разобрать один из эффектов появления Киану Ривза. Надо признать, что его голлаграмма это комплексный механизм, состоящий из десятков спецэффектов и намерянных гличей. Мы же сегодня постараемся реплицировать лишь один из них — а именно эффект появление голлограммы из вертикальных "линий", идущих в разнобой, но по итогу совмещаемых в одно единое целое.

Именно этот эффект и побудил меня на создание данного туториала.

Небольшой Viewer discretion перед тем, как я начну разбирать механизм. Данный прототип будет работать немного иначе, чем в оригинальной игре. Там, судя по моему наблюдению, для эффектов Джонни использовался BillBoard с грабпасом. Достаточно элегантное и экономичное решение в плане затраты ресурсов, но честно говоря моих текущих навыков пока не хватает чтобы реализовать это. Однако, если вдруг появятся знающие люди в комментариях, и у них будет желание пообщаться со мной по этому поводу — буду искринне рад.

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

Ну и последнее — этот механизм будет сделан с помощью Шейдер Графа. По тому что во — первых sg более нагляден для представления, во — вторых менее сложен чем язык шейдеров, и в третьих — у такого шейдера на hsls всего пару строк кода, а из такого материала не удастся вытянуть целую статью!

Ну что — ж, поехали!

Глава 1: Первые шаги

Опустим шаги с импортом шейдер графа, установкой Render Pipelin«a» и разбрасыванием геймобджектов на сцене. В конечном итоге создаём простенькую композицию, на которой и будем тестировать наш эффект.

Вот так вот получилось у меня
Вот так вот получилось у меня

Первая итерация нашего механизма будет работать следующим образом: Рендерим только объекты с "гличами" в одну рендер текстуру, а все оставшиеся — в другую. В первой текстуре рисуем эффекты, а потом просто смешиваем 2 этих изображения и выводим его на экран. А теперь более конкретно:
Создаём отдельный слой, называемый Glitch.

Создание Glitch Effect«а» Джонни Сильверхенда на Unity

Помечаем им все наши объекты, которые будут «корёжиться». У нашей главной камеры в свойстве «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:

Нода Split позволяет вычленить из 4х мерного вектора любой канал, в данном случае Y.
Нода Split позволяет вычленить из 4х мерного вектора любой канал, в данном случае Y.

Двигая координаты UV мы можем смещать наше изображение. Как уже нетрудно догадаться, нам нужно просто добавить в Y какое — нибудь значение, не трогая Х. В нашем случае мы двигаем координаты целыми полосками, поэтому я заранее заготовил текстуру шума. Хотя конечно для рандомности её можно было бы и сгенерировать...

Для создания такого эффекта в Shader Graph«е» создадим ноду «UV», разделим функцией Split её на каналы, прибавим к Y каналу значение нашего шума (R канала шума будет вполне достаточно) и снова объединим значения в вектор UV, а затем укажем нашей текстуре полученный UV. Выглядеть это будет так:

По факту это продолжение графа из предыдущего скрина
По факту это продолжение графа из предыдущего скрина

Ну и результат:

Похоже на то как Вергилий в DMC разрубает экран.
Похоже на то как Вергилий в DMC разрубает экран.

Честно говоря выглядит не очень впечатляюще. Хотелось бы больше контроля. Для этого перед тем как добавлять к Y наш шум умножим его значение на какое — нибудь число, а сам наш шум затайлим по горизонтали. Оба этих значения вынесем в Property, и вот теперь мы можем дёргая эти значения получать ожидаемый результат:

Справа гифки видно, как я меняю значения

Кстати, та часть графа теперь стала вот такой:

Я всего - лишь хотел больше контроля, а граф разросся в 2 раза. Бррр...
Я всего - лишь хотел больше контроля, а граф разросся в 2 раза. Бррр...

Глава 3: Баг глубины

Знаете, я ведь не просто так ничего не ставил впереди наших объектов с гличом. Давайте попробуем поставить что — нибудь перед ботом:

Главный принцип любой хорошей призентации - умолчать об проблемах
Главный принцип любой хорошей призентации - умолчать об проблемах

Очевидно это возникает потому, что мы бездумно накладываем одно изображение на второе. Мы совсем забыли о таком понятии, как глубина изображения. На юнити из себя она представляет красный канал, в котором дальность пикселя определяется его яркостью.

Хммм... Похоже на крутой графический эффект для другого урока!
Хммм... Похоже на крутой графический эффект для другого урока!

А вот теперь я совершу каминг аут — я понятия не имею как одной камерой рендерить отдельно цвет и глубину в разные рендер текстуры. Вроде у камеры есть метод camera.setTargetBuffers, в котором по идее можно определить буферы для вывода в разные рендертекстуры, но что — то он у меня не работает.

Поэтому сегодня, и только сегодня мы добавим на сцену ещё 2 камеры — одну для глубины мира, вторую — гличей. На этот раз обойдёмся без примеров, просто не забудьте у новых рендер текстур указать Color Format как Depth.

В нашем графе добавляем соответствующие Sample Textur«ы», и в текстуру глубины глича передаём ту же UV, что и у текстуры глича.

Дальше нам поможет волшебная функция Step — она принимает у себя 2 параметра — A и B, и возвращает 0 Если А > B, и 1 — если меньше. Кароче, я всегда в ней путаюсь. А стоит просто запомнить, что:

step(a, b) = a - b > 0 ? 0 : 1

Но так как результат у нас получается немного вверх тормашками, просто проносим наш его через ноду One Minus, которая под капотом отнимает от единицы входящий параметр, и меняем альфу из ноды Lerp на наш новый результат. Выглядеть в Shader Graphe это будет следующим образом:

Кто - нибудь знает как объединить Step и One Minus в один шаг?
Кто - нибудь знает как объединить Step и One Minus в один шаг?

Ну, а в самом проекте вот так:

They see me rollin'. Rotating...

Глава 4: Баг повторения текстуры (clamping’a')

Я называю эту проблему багом длинношеев, в честь прикола из 3ей батлухи. Для его воспроизведения необходимо расположить камеру таким образом, чтобы объект с гличом выходил за пределы камеры, а потом сдвинуть объект эффектом в противоположную сторону. Полученный результат будет выглядеть вот так:

В это сложно поверить, но разница между данными скриншотами составляет 9 лет.
В это сложно поверить, но разница между данными скриншотами составляет 9 лет.

Почему так происходит? Ну, конкретно в нашем случае мы сдвигаем все пиксели по оси Y на определённое расстояние, но когда шейдёр заходит за границы текстуры, вместо того чтобы передать пустоту, он тянет самые крайние пиксели. Слава богу мы сами знаем на сколько мы смещаем UV по Y (это значение которое мы помещаем в вектор UV.Y на втором шаге), поэтому вооружившись этим знанием, а также «степами» для верхней и нижней границ, мы узнаем какие координаты, которые необходимо удалить из текущих текстур глича и глубины глича. Вот что в итоге получится:

Красными полосками я указал изменения от предыдущей итерации.
Красными полосками я указал изменения от предыдущей итерации.

Заключение

В заключении я создал анимацию, которая с помощью нехитрого скрипта устанавливает значения внутрь материала, и таким образом лишил себя необходимости каждый раз руками устанавливать это значение. Результат получился такой:

Но с HDR"ом" выглядел бы лучше...

Ну, если бы я преследовал своей целью набрать плюсов, я бы оставил здесь фотографию с милым рождественским котиком. Но вместо этого я оставлю ссылку на репозиторий, если вдруг кому — то захочется посмотреть как это выглядит вживую, не продираясь через дебри копирования нод и читания моей писанины.

Повторюсь, если в комментариях будут знающие люди, которые знают ответ на:
1) Как достать из одной камеры глубину и цвет изображения.
2) Как реализовать в юнити билборд, который рендерит всё, что за ним находится и может менять эти пиксели на ходу.
3) Заменить Step и OneMinus на одну ноду.
Да и просто обсудить настроение и планы на новый год — Буду счастлив пообщаться в комментариях :)

Ну а так — удачи всем в новом году! Оттянитесь сегодня на полную! :)

4141
5 комментариев

Комментарий недоступен

1
Ответить

Весьма интересная заметка, обязательно попробую прокачать себя в вертексах на графе, но... Вам не кажется что двигая вертексы не удастся получить 'ровные' вертикальные линии, даже если выкрутить тесселяцию на всю катушку? А уж тем более сделать разрыв между линиями.
P.S. могу быть не прав.

1
Ответить

Комментарий недоступен

1
Ответить

Glitch-эффекта

Ответить