Я создал этот шейдер VHS-видео в Unreal в рамках первого шейдер-челленджа для Discord-канала Technically Speaking. Темой челленджа стало «Ретро», а я экспериментировал с идеями FMV-игры, поэтому решил соединить эти две темы.


Интересующиеся исходниками могут посмотреть файлы проекта здесь. Распакуйте эти папки и скопируйте их в папку содержимого вашего проекта. Можете свободно задавать мне вопросы в комментариях к оригиналу статьи или в twitter.

Видео


Видеотекстура


Начнём с импортирования видеотекстуры и создания простого материала без освещения, который будет её использовать. Затем создадим блюпринт, который при запуске уровня будет открывать импортированный источник мультимедиа.

Инструкции по импорту, настройке и использованию видеотекстуры на уровне можно найти на странице документации Unreal, поэтому я не буду здесь об этом рассказывать.


Видеотекстура на сфере

Видео представляет собой видеоклип в стиле «хоррор», который я записал на телефон, рассматривая самые жуткие предметы, нашедшиеся у меня дома. Качество видео неважно, ведь мы несколько раз уменьшим его размер; к тому же, низкое качество делает его более олдскульным!

Размытие


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

Здесь мы немного сжульничаем — вместо написания собственной функции, воспользуемся нодом SpiralBlur-Texture. Если он не сможет взять в качестве входящих данных сэмпл внешней текстуры, просто возьмите внутренний нод Custom и используйте его напрямую в своём графе.


Нод Spiral Blur


Логика размытия, использующая нод Custom из Spiral Blur

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


Скалярные параметры размытия в экземпляре материала

Размытие выглядит здорово, но мы не хотим, чтобы камера была расфокусирована постоянно! Чтобы это исправить, мы подвергнем исходное изображение и размытое изображение линейной интерполяции (lerp), используя для переключения между ними оператор деления с остатком.

Деление с остатком (нод Fmod в Unreal) возвращает остаток от деления. Остатком от деления 4 на 2 будет 0, а от деления 5 на 2 — единица.


Нод Fmod

Получая остаток от деления Time на скалярный параметр, мы создаём неравномерное колебание значений. При этом создаётся более естественный внешний вид, чем при волновой функции. Округляя результат, мы создаём резкий переход, что хорошо подходит картинке VHS.


Разность между остатком от деления (modulo) и синусоидой.

Теперь можно использовать скалярный параметр для управления тем, насколько часто мы хотим видеть каждую из версий изображения. Значения наподобие 2 часто будут давать результат 0, а значения типа 5, скорее всего, будут иметь остаток, поэтому результатом будет 1 или более. Понаблюдать за изменениями результатов можно на показанном выше gif.


Я поместил обычное изображение в слот A, а размытое изображение — в слот B, а затем, поскольку размытое изображение отображалось чаще, чем обычное, вставил минус, чтобы обратить значения. По большей мере я вставил это ради красоты графа, так что вы можете пропустить этот момент или изменить порядок входящих данных в своём графе.


Два сэмпла плюс lerp между ними.


Материал на сфере; деление с остатком производится на 2, чтобы эффект было проще увидеть.

Тёмные участки, появляющиеся на показанном выше gif — это места, где остаток является отрицательным числом. Лично мне кажется, что это улучшает эффект, но если вы хотите от них избавиться, то для этого можно использовать нод Saturate перед Lerp. (Saturate выполняет ограничение в интервале 0-1 за одну инструкцию АЛУ, а нод Clamp на некоторых типах оборудования может быть представлен несколькими инструкциями.)

Манипуляции с UV


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

Разрешение


Разрешение VHS составляет 333?430, поэтому наше видео должно это передавать. Я объяснил методику снижения разрешения в предыдущем посте, однако она была реализована на hlsl, поэтому для Unreal мы повторим всё заново.

Создадим скалярный параметр или константу для разрешения по X и Y, присвоив им значения 333 и 430. Соединим их, создав значение float2, а затем округлим вниз (floor).

Умножим координаты нашей текстуры на него, чтобы создать сетку изображения. После создания сетки округляем её вниз, а затем разделим на разрешение. При этом тайлинг снизится, но из-за округления вниз он будет ограничен сеткой, которую мы создали изначально.


Этапы вычислений для понижения разрешения.


Ноды для снижения разрешения.

Затем это можно использовать в качестве входящих данных UV для видеотекстуры.


Шейдер с разными разрешениями.

Растяжение


Следующим мы добавим возникающее время от времени растяжение изображения. Оно реализуется умножением разрешения по Y на lerp между синусоидой и 1.

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

Далее умножим это на 0.5 и прибавим 0.5. Так мы перенесём волну из пространства -1 -> 1 в пространство 0 -> 1 ( -1 * 0.5 = – 0. 5, + 0.5 = 0, тогда 1 * 0.5 = 0.5, + 0.5 = 1).

Наконец, умножим это на ещё один скалярный параметр для контроля над силой синусоиды.


Для интерполятора мы используем ту же логику деления с остатком, что и для размытия. Создадим новый скалярный параметр, определяющий частоту появления растянутой версии, поделим с остатком на время, а затем округлим значение.


Хроматическая аберрация


Последним изменением UV будет хроматическая аберрация. Это эффект, при котором мы получаем красный и синий цвета по краям изображения из-за смещения каналов друг относительно друга.

Чтобы его реализовать, мы заменим один сэмпл неразмытой версии текстуры тремя новыми сэмплами, два из которых будут смещены.

Возьмём UV, которые мы создали как результат смены разрешения, и добавим к ним для смещения скалярный параметр. Выполним Append этого значения с 1 по оси Y, а затем подадим на вход в качестве UV первой текстуры.

Для второй текстуры возьмём UV без изменений.

Для третьей умножим параметр смещения на -1, затем прибавим её на UV и выполним append с 1 по Y.

Сделав это, возьмём R из первой текстуры, G из второй и B из третьей, а потом объединим их для создания готового цвета. После чего его можно использовать для интерполяции с размытием.


Структура нодов для хроматической аберрации.


Хроматическая аберрация на сфере.

Эффекты изображения


Теперь, закончив экспериментировать с UV и базовым изображением, можно начать добавлять статические помехи и иные эффекты изображения, которые сделают шейдер похожим на видео в стиле VHS.

Для этих эффектов нам понадобится текстура, в каналах которой будут храниться растровые строки, блок белого цвета и дата/время. Рекомендую поместить дату/время в канал G, растровые строки в R, а блок белого в B, чтобы использовать преимущества разницы качества сжатия каналов.


Моя текстура — делайте как я сказал, а не как сделал! Блок в канале G был плохим выбором.

Если вы работаете в Photoshop, то для создания эффекта растровых строк рекомендую использовать дискретную кисть (scatter brush), затем размытие в движении (motion blur), а потом добавить шум.

Модификации


Первое, что мы сделаем — возьмём выходные данные lerp между размытым и обычным видео и добавим модификации. Добавьте ноды power и multiply, а затем создайте для них параметры. Power позволяет нам регулировать гамму изображения, а multiply придаёт оттенок цвета.



Материал на сфере со значительно усиленной гаммой.

Зерно плёнки


Следующим мы добавим эффект зернистости. Он будет умножаться на выходные данные описанных выше модификаций.

Вместо текстуры возьмём нод симплексного шума. Мы будем выполнять линейную интерполяцию между шумом и единицей минус шум, чтобы позиция видимого шума менялась. (При умножении будут видны только чёрные области.)

Интерполятор — это округлённая синусоида, поэтому для скорости добавим Time, умноженное на новый скалярный параметр, а затем получим синус этого значения. Умножим на 0.5 и прибавим 0.5, как мы делали это выше, а затем округлим, чтобы получать значения только -1, 0 или 1.

После этого добавим ещё один параметр для силы, обратим его с помощью минуса, а затем добавим к lerp. Мы обратили его, потому что зерно становится сильнее, когда в нём меньше белого, но для пользователя это нелогично и мы делаем входящие данные более чистыми. Затем этот результат можно умножить на выходные данные модификаций.



Материал на сфере с добавленной зернистостью — при снижении гаммы эффект увидеть проще.


Выходные данные эффекта зернистости.

Растровые строки


Следующим нам нужно добавить растровые строки. Именно здесь нам пригодится текстура, которую мы создавали выше. Возьмём канал белого блока текстуры, умножим его на константу float 2 со значениями 1, 75, чтобы создать тонкие линии. Затем используем нод Panner, чтобы сместить это по оси Y. Я оставил их как константы, но вы можете добавить параметры для скорости и тайлинга! Умножим это на зерно и модификации.


Ноды для растровых строк.


Работа почти закончена!

Статические помехи


Следующим эффектом будут статические помехи. Это длинные статические полосы, придающие ощущение реальной VHS-записи.

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

Для координат мы создадим синусоиду, которая будет изменять масштаб текстуры. Умножим Time на скалярный параметр скорости, а затем получим синус этого значения, и умножим его на ещё один параметр, чтобы получить амплитуду. Я назвал амплитуду «Variation», потом что она изменяет величину, а значит степень отличия текстуры от оригинала. Умножим её на координату текстуры, и мы получим растровую строку, меняющую свой масштаб!

Мы хотим, чтобы скорость изменялась по y, но не по x, поэтому добавим нод Append с константой 0 по x. Умножим волну на новый параметр скорости (не тот же, что скорость масштабирования — при согласованности помехи становятся менее явными), а затем поместим результат в слот y. Это даст нам колеблющуюся скорость.

Добавим сэмпл текстуры к умножению на зерно и растровые строки.


Добавленные к эффекту линии статических помех.

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


Ноды эффекта статических помех.


Менее частые статические помехи благодаря делению с остатком.

Оверлей


Последнее, что мы сделаем — добавим оверлей с датой/временем. Просто возьмём канал даты/времени из текстуры и добавим к предыдущим эффектам.

После этого можно добавить всему эффекту множитель, чтобы создать более яркое свечение.


Ноды оверлея.


Параметры


И на этом мы закончили! Вот скриншот, параметров и настроек, которые я использовал при записи показанного в начале статьи видео.


Производительность


GPU


Хотя эффект не создавался с учётом производительности и не рассчитан на игры, я рекомендую вам разбираться, как шейдеры влияют на время работы и память GPU!

Здесь довольно мало инструкций и сэмплов, даже несмотря на количество операций сэмплирования видеотекстуры.

Судя по статистике, обработка занимает примерно 0,3 мс во время выполнения, что, по моему мнению, вполне приемлемо, даже если видео не было основным элементом сцены. Если бы мы использовали эффект в игре, стремящейся к 60fps, то при 0,3 мс я бы, пожалуй, пошёл на какую-нибудь экономию.


Память


Видеотекстуры невозможно стримить как обычные текстуры. Для них можно генерировать mip-текстуры, но они не будут обладать таким же поведением, поэтому всегда нужно считать, что видео будет загружаться.


В моём приложении без потоковой передачи из памяти этот эффект в сцене занимал 130 МБ. Это огромный объём памяти для одной текстуры. Если она будет единственным важным элементом в сцене, то это нормально, но если она используется на большом уровне, то это может стать проблемой.

Если вы хотите использовать эту технику в игре, то существует пара способов сделать её менее затратной:

  • Сделайте видео короче — моё видео было довольно долгим
  • Сделайте меньше размеры видео — я просто использовал стандартное разрешение своего телефона, а затем изменил его шейдером, а вы можете сделать исходное видео более низкого разрешения.
  • Если вы вообще не можете позволить себе использовать видео, то создайте атлас текстур из фотографий и по очереди переключайте их в шейдере.

Вот и всё! Благодарю за чтение, развлекайтесь, создавая страшные VHS-эффекты — в конце концов, скоро Хэллоуин!