ASCII‑art Шейдер на Unity для новичков
Шейдера в юнити — это именно та часть, которой пугают джунов на собеседованиях. Однако если потратить немного времени становится ясно, что вся эта шейдерная магия на самом деле, как и любой программный код вашего проекта может быть слеплена из говна и палок. И сейчас я постараюсь личным примером вам это доказать.
Совсем недавно вышла статья в разделе геймдева, которая должна была нам объяснить как добиться такой магии:
Однако ничего кроме сложных слов из - разряда Luminance, Lut текстура и pikabu я в ней не нашёл. Она и побудила меня попробовать написать шейдер, пусть и откладывал это дело я наверное месяц.
Реализация данного шейдера будет состоять из пары шагов:
1) Разбивание изображения камеры на монолитные прямоугольники равного размера.
2) Замещение этих прямоугольников текстурами символов, подбираемых в зависимости от цвета.
Руки чешутся написать пару строк кода, так что давайте начнём с подготовки. Для охвата наибольшей аудитории эта часть будет расписана максимально подробно:
Подготовка к работе
Для начала создадим простой Unlit Shader, и удалим оттуда всё лишнее (связанное с туманом). А также вместо return col для начала вернём свой кастомный цвет float(0, 0, 0, 1). Получаем на выходе:
Обратите внимание на блок Properties - сюда мы будем кидать поля, блок над функцией v2f vert - здесь мы будем инициализировать поля, а также на функцию fixed4 frag - в ней мы с вами будем срать кодом.
Несмотря на то, что шейдер у нас уже готов, надо заставить камеру его как то рендерить. Для этого создаём скрипт - AsciiCamera, и переопределяем у него метод OnRenderImage, заставляя нашу камеру использовать кастомный материал. Кодом это будет выглядеть так:
Теперь создаём материал, и устанавливаем ему наш шейдер. На самой же сцене вешаем наш AsciiCamera скрипт на камеру, и линкуем материал внутрь монобеха. Если вы всё сделали правильно, то вы увидете, что камера рендерит чёрный экран. Ну разве не прекрасно?
Когда с приготовлениями покончено, можно в функции frag раскомментировать правильный ретурн, и убрать строку с возвратом float4(0, 0, 0, 1) куда подальше.
Шаг 1
Итак, мы подошли к первому шагу. Нам необходимо разбить исходное изображение:
На пиксели. Выставить видеоряд в 144р так сказать.
Для этого нам подойдёт функция frag из шейдера. Видите код:
Здесь наш шейдер берёт конкретный пиксель экрана. Uv - это по факту координата пикселя, а _MainTex - текстура, в данном случае наше изображение с камеры. Следует учесть, что uv - это относительная величина. Т.е. самый левый верхний пиксель имеет координату (0,0), а самый нижний правый - (1,1).
Давайте введём в шейдер такие понятия, как ширина и высота. В блоке properties шейдера, прямо под строкой "_MainTex ("Texture", 2D) = "white" {}" добавим вот такой код:
Цифры хоть и похожие на правдивые, таковыми не являются, ведь на вашем мониторе скорее всего другое разрешение. Поэтому, чтобы наш шейдер всегда работал с правильной диагональю монитора, в функции апдейта монобеха AsciiCamera запихнём принудительную установку этих значений внутрь материала:
А также над функцией шейдера v2f vert добавим 2 соответствующих поля:
Вообще говоря куда вы их поместите не принципиально, важно только чтобы они были в блоке Pass, и имели такое же имя и тип как в Property.
Теперь создайте для шейдера 2 параметра, ширины и высоты ячейки (CellWidth и CellHeight), на этот раз сами. Когда всё будет готово, вернёмся наконец к нашей функции frag. По факту нам нужно разбить всё изображение на блоки, и сделать, чтобы каждый блок был монолитного цвета. Это очень важно. Цветом блока я решил выбрать центральный пиксель. Код будет выглядеть вот так:
В результате мы получаем такую картину:
Шаг 2
Теперь нам необходимо заменить каждый блок из исходного изображения на символ соответсвующего цвета из второй текстуры. Немного повозившись в фотошопе я быстренько нарисовал себе таблицу символов 10х3.
Добавим в шейдер текстуру с символами (AddTex("Texture", 2D) = "white" {} внутри Property, и sampler2D AddTex в Pass'e').
Символ, который будет рисоваться вместо блока будет определяться следующим образом - суммируем rgb цвета исходного блока, и остаток от деления на 10 - это индекс по X символа, а на 3 - это Y замещаемого символа. В конечном итоге наша функция frag должна превратиться вот в такого небольшого монстра:
Результат? Ну, он немного стрёмный:
Всё потому что необходимо подбирать замещаемые символы не абы как, а в соотвествии с палитрой цветов исходного изображения. Например точка соответсвует самому тёмному символу, а решётка - самому светлому.
Немного перерисовав символы у меня получилось добиться такого результата:
На этом всё, пойду играть в Апекс. Ведь это у меня получается лучше чем написание шейдеров и статей. А вам желаю хорошо провести остаток этого воскресенья!
Upd: Залил улучшенную версию на гитхаб: