Найти тему
Пикабу

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

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

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

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

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

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

.
.

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

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

-3

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

-4

Теперь берем камеру сцены и в блоке 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, если картинка будет засвеченной. У меня вышло так:

-5

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

-6

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

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

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

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

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

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

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

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

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

-7

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

https://github.com/rikovmike/LimitPaletteRaw

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