Серия «Ретро-рендер мечты»

12

Создание шейдера для Unity с эффектом дрожания вершин в стиле PlayStation 1

Дрожь Вершин. Работает на Unity 6.

Дрожь Вершин. Работает на Unity 6.

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

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

В предыдущей статье мы реализовали довольно неплохой эффект старого рендера из ранних 90х. Однако, ощущения от него будут неполными, если мы не введем другие характерные для того времени ограничения. Нет-нет, 10 фпс и имитацию слабых компьютеров вводить не будем, но постараемся ввести один значимый для ретро-рендера эффект. Возможно он не является частью общей парадигмы, особенно в области рендеринга на РС, но добавит особую атмосферу. Речь идет, конечно же, об эффекте дрожания вершин, характерный для Playstation 1.

Эффект «дрожания вершин» в PlayStation 1, был следствием ограниченной точности расчетов, использовавшихся на этом железе. Поскольку PlayStation 1 использовала фиксированную точку для вычисления координат вершин, это приводило к визуальным артефактам, когда позиции вершин «дрожали» при перемещении камеры или объектов. Мы, как и в прошлый раз, попробуем воссоздать данный эффект только с помощью шейдера в Unity, без использования дополнительных скриптов на C#.

Основная идея простая — нужно будет обойти все доступные шейдеру вершины и округлить их позиции относительно проекционных координат .

Основы шейдера

Напишем заготовку для шейдера, в которой укажем будущие параметры для материалы и объявим структуры и переменные.

Здесь мы задаём два параметра для будущего материала: _MainTex — текстура и _JitterAmount — слайдер для точной настройки силы эффекта дрожания.

Еще нам, конечно же, понадобятся две структуры:

  • appdata — данные вершин, передаваемые в вершинный шейдер из Unity

  • v2f — данные, передаваемые из вершинного шейдера в пиксельный шейдер и, в конечном итоге, в графический конвейер

Добавим функцию округления позиций вершин. Мы применяем округление только к clipPos.xy, то есть к координатам, которые уже находятся в проекционных координатах (после всех трансформаций объекта). Это даст эффект «дрожания», как на PlayStation 1, когда камера или объект движется. Параметр clipPos будет передаваться в функцию, как и параметр jitterAmount, который позволяет контролировать, насколько сильным будет эффект дрожания.

Округление в clip space означает, что каждая вершина будет «привязана» к определенным фиксированным позициям на экране. При движении камеры или объекта из-за этого квантования вершины будут перескакивать с одного положения на другое, создавая желаемый эффект «дрожания».

Начинаем колдовать

Пишем «вершинный» блок шейдера.

Здесь мы сначала получаем clipPos функцией UnityObjectToClipPos для нашей вершины. Затем получаем новую позицию вершины через вызов написанной ранее функции JitterVertex, которой передаем полученный clipPos и наш параметр _JitterAmount из инспектора.
Возвращаем новые данные вершины. UV-координаты оставляем без изменений.

Параметр _JitterAmount нужно держать в пределах от 0.01 до 0.1, этого достаточно для заметного эффекта. Однако, никто не запрещает эксперименты — все ваших руках.

Осталось только добавить фрагментную (пиксельную) часть, в которой мы просто отдаем текстуру, как есть.

Полный код шейдера лучше посмотреть в репозитории. Выкладывать картинку сюда было бы издевательством )

Настройка текстуры

Шейдер использует стандартную текстуру, которую можно задать в Unity через инспектор. Текстура рендерится через стандартную функцию tex2D, которая отображает текстуру по UV-координатам, переданным из вершинного шейдера. С помощью бегунка _JitterAmount плавно регулируем эффект дрожания. В зависимости от проекта, свойства самой текстуры настраиваются индивидуально, но стилистически, конечно же, желательно выключить фильтрацию и использовать текстуры с небольшим разрешением, вроде 64х64 или 128х128.

На будущее

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

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

Ссылка на github: https://github.com/rikovmike/YetAnotherVertexJitter

Показать полностью 5
20

Еще один demake–шейдер или рубка палитры с плеча в Unity

Для начала сразу оговорюсь – да, я в курсе, что подобного днища на маркетах Юнити и прочих систем овердофига и публика уже изрядно подустала от подобных сверхфишек. Но если есть чем поделиться – почему бы и не поделиться? В конце концов, если это пригодится хотя бы одному человеку – значит это было не зря. Ссылка на гитхаб - в конце поста.


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

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

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


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

Итак, создадим чистый проект в Unity. Так как я хочу стилизовать картинку под 4 цвета cga-палиры, 3D будет не особо уместно использовать для демонстрации (но это не значит, что в 3Д проектах этого нельзя делать – все в руках создателей). Выбираю шаблон 2D (URP).

Для затравки и тестов возьмем из интернетов какую-нибудь крутую картинку. Я нашел фото кибертянки из трейлера фильма Cyberbride, рейтинг которого 2,7 на imdb. Идеально.

Импортируем ее в проект, сразу закидываем на сцену без каких-либо настроек. Теперь нам необходимо подготовить «железную» основу нашего будущего рендера, прежде чем мы начнем использовать шейдер. Создаем рендер-текстуру размером 320х200. Отключаем обязательно фильтрацию – олдскул все-таки.

Теперь берем камеру сцены и в блоке Output в параметр Output texture подкидываем нашу рендер-текстуру. В окне вывода Game сразу появится дурацкая надпись, что нет камер, рендерящих в экран. Жмем три вертикальные точки справа вверху и снимаем галочку с «Warn If No Cameras Rendering».


Теперь добавим на сцену Canvas. В блоке “Canvas Scaler“ в параметре UI Scale Mode выставим ему «Scale with Screen Size», а в Reference Resolution – наши 320х200. Добавим к этому Canvas дочерним элементом Raw Image. Ему в texture забрасываем нашу рендер-текстуру. И накидываем на него компонент Aspect Ratio Filter, у которого выставляем Mode в “Fit In Parent”, а сам Aspect Ratio ставим 1.6 (что соответствует 320х200).


Остался один необязательный момент, но для приятных ощущений лучше его сделать. Во вкладке Game, там, где красуется Free Aspect, добавим разрешение 640х400. Картинка станет приятнее, да и эффект удвоения на наших экранах все же выглядит поинтереснее. (если у вас 2к или 4к монитор, возможно лучше добавить утроенное, а то и учетверенное разрешение предпросмотра)


Все. База готова. Она стандартная, она обычная. Ничего нового нет. Можно конечно рендерить и в более высоком разрешении, но мне теплее старые добрые жесткие пиксели.


Если видим, что рендерится все так как надо, то можно подключать шейдер. Импортируем сам шейдер, картинку дизеринга и картинку с палитрой. У обоих картинок нужно выключить сжатие и фильтрацию, а у дизеринга нужно выставить Repeat в параметре Wrap Mode.

Создаем новый материал, указываем ему шейдер Rikovmike/LimitPaletteRaw, закидываем рендер-текстуру, палитру и дизер-картинку в соответствующие им параметры параметры и кидаем этот материал на RawImage, которая у нас в Canvas (чистую текстуру с него можно уже и убрать).


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


Ну и на самом спрайте можно подкрутить яркость в параметре Color, если картинка будет засвеченной. У меня вышло так:

То же, но с палитрой sweetie16 с сайта lospec.com:

Теперь немного о том, как работает шейдер.


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


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


Далее следует блок обработки дизеринга. Зона дизеринга определяется особым условием – насколько «недолетел» проверяемый цвет до нужного, то есть какая финальная дистанция была рассчитана в момент окончательного утверждения выбранного цвета палитры. Дистанция в принципе варьируется от 0 до 1, так как все компоненты цвета (альфу мы тут не учитываем) изменяются в тех же пределах. Нетрудно выяснить и степень «недолета» цвета до нужного. Все эти непонятные недолеты превращаются в шашечки. Уровень недолетаемости определяется бегунком Dither Treshold, который по умолчанию равен 0.5. То есть если недолет был почти в половину цветового пространства – значит точно дизерить.


А вот цвета перехода определяются проще – для этого введены дополнительные переменные, хранящие «предыдущий» проверяемый в палитре цвет. И дальше в блоке обработки дизеринга сначала выбирается из текстуры дизеринга текущий по координатам сетки дизеринга пиксель. Если он белый, то рисуется найденный цвет, если он черный – рисуется предыдущий по дальности цвет.


Подход может быть и спорный, но дает достаточно правдоподобный эффект и работает на любых палитрах. Главное подобрать правильный Dither Treshold.


А теперь немного о параметре количества цветов.

Да, суть в том, что он используется в цикле обхода текстуры палитры попиксельно. И да, вместо него можно приспособить значение из _TexelSize текстуры палитры. Но мне показалось, что удобно все равно иметь в руках инструмент ручного ограничения палитры. В бОльшую сторону ничего плохого не случится, в меньшую же – есть интересные эффекты обрезки палитры. В любом случае, можно попробовать избавиться от этого параметра и взять количество цветов из ширины текстуры палитры.


Отмечу, что пробовал подсовывать под стандартные демки Unity, с небольшими твиками по свету выходило неплохо, например, в Lost Crypt:

Скачать все необходимое (шейдер, дизер и палитру) можно тут:


https://github.com/rikovmike/LimitPaletteRaw


Теоретически, шейдер можно приспособить не только к Юнити, но я слишком ленив для проверки этого. Исходник шейдера я постарался разбавить комментариями, насколько смог :)

Показать полностью 6
Отличная работа, все прочитано!