ASCII‑art Шейдер на Unity для новичков

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

Совсем недавно вышла статья в разделе геймдева, которая должна была нам объяснить как добиться такой магии:

​

Однако ничего кроме сложных слов из - разряда Luminance, Lut текстура и pikabu я в ней не нашёл. Она и побудила меня попробовать написать шейдер, пусть и откладывал это дело я наверное месяц.

Реализация данного шейдера будет состоять из пары шагов:

1) Разбивание изображения камеры на монолитные прямоугольники равного размера.

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

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

Подготовка к работе

Для начала создадим простой Unlit Shader, и удалим оттуда всё лишнее (связанное с туманом). А также вместо return col для начала вернём свой кастомный цвет float(0, 0, 0, 1). Получаем на выходе:

Shader "Unlit/AsciiImageEffect" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); return float4(0, 0, 0, 1); //return col; } ENDCG } } }

Обратите внимание на блок Properties - сюда мы будем кидать поля, блок над функцией v2f vert - здесь мы будем инициализировать поля, а также на функцию fixed4 frag - в ней мы с вами будем срать кодом.

Несмотря на то, что шейдер у нас уже готов, надо заставить камеру его как то рендерить. Для этого создаём скрипт - AsciiCamera, и переопределяем у него метод OnRenderImage, заставляя нашу камеру использовать кастомный материал. Кодом это будет выглядеть так:

public class AsciiCamera : MonoBehaviour { public Material material; void OnRenderImage(RenderTexture source, RenderTexture destination) { Graphics.Blit(source, destination, material); } }

Теперь создаём материал, и устанавливаем ему наш шейдер. На самой же сцене вешаем наш AsciiCamera скрипт на камеру, и линкуем материал внутрь монобеха. Если вы всё сделали правильно, то вы увидете, что камера рендерит чёрный экран. Ну разве не прекрасно?

Когда с приготовлениями покончено, можно в функции frag раскомментировать правильный ретурн, и убрать строку с возвратом float4(0, 0, 0, 1) куда подальше.

Шаг 1

Итак, мы подошли к первому шагу. Нам необходимо разбить исходное изображение:

ASCII‑art Шейдер на Unity для новичков

На пиксели. Выставить видеоряд в 144р так сказать.

Для этого нам подойдёт функция frag из шейдера. Видите код:

fixed4 col = tex2D(_MainTex, i.uv);

Здесь наш шейдер берёт конкретный пиксель экрана. Uv - это по факту координата пикселя, а _MainTex - текстура, в данном случае наше изображение с камеры. Следует учесть, что uv - это относительная величина. Т.е. самый левый верхний пиксель имеет координату (0,0), а самый нижний правый - (1,1).

Давайте введём в шейдер такие понятия, как ширина и высота. В блоке properties шейдера, прямо под строкой "_MainTex ("Texture", 2D) = "white" {}" добавим вот такой код:

Width("Width", float) = 1024 Height("Height", float) = 768

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

private void Update() { material.SetFloat("Width", Screen.width); material.SetFloat("Height", Screen.height); }

А также над функцией шейдера v2f vert добавим 2 соответствующих поля:

sampler2D _MainTex; //Новые поля float Width; float Height; //Искомая функция v2f vert (appdata v)

Вообще говоря куда вы их поместите не принципиально, важно только чтобы они были в блоке Pass, и имели такое же имя и тип как в Property.

Теперь создайте для шейдера 2 параметра, ширины и высоты ячейки (CellWidth и CellHeight), на этот раз сами. Когда всё будет готово, вернёмся наконец к нашей функции frag. По факту нам нужно разбить всё изображение на блоки, и сделать, чтобы каждый блок был монолитного цвета. Это очень важно. Цветом блока я решил выбрать центральный пиксель. Код будет выглядеть вот так:

fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); //Берём номер пикселя. Функция ceil - берёт целую часть от результата float pixelX = ceil(i.uv.x * Width); float pixelY = ceil(i.uv.y * Height); //Берём оффсеты для рассчёта центрального пикселя блока float halfCellX = ceil(CellWidth / 2); float halfCellY = ceil(CellHeight / 2); //Возвращаем координату центрального элемента каждой ячейки float xColorPos = (ceil(pixelX / CellWidth) * CellWidth + halfCellX) / Width; float yColorPos = (ceil(pixelY / CellHeight) * CellHeight + halfCellY) / Height; return tex2D(_MainTex, float2(xColorPos, yColorPos)); }

В результате мы получаем такую картину:

​А также медаль за сжатие 3ей степени.
​А также медаль за сжатие 3ей степени.

Шаг 2

Теперь нам необходимо заменить каждый блок из исходного изображения на символ соответсвующего цвета из второй текстуры. Немного повозившись в фотошопе я быстренько нарисовал себе таблицу символов 10х3.

В данной таблице строка и ряд ничего не значат. Это просто набор символов.
В данной таблице строка и ряд ничего не значат. Это просто набор символов.

Добавим в шейдер текстуру с символами (AddTex("Texture", 2D) = "white" {} внутри Property, и sampler2D AddTex в Pass'e').

Символ, который будет рисоваться вместо блока будет определяться следующим образом - суммируем rgb цвета исходного блока, и остаток от деления на 10 - это индекс по X символа, а на 3 - это Y замещаемого символа. В конечном итоге наша функция frag должна превратиться вот в такого небольшого монстра:

fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); float pixelX = ceil(i.uv.x * Width); float pixelY = ceil(i.uv.y * Height); float halfCellX = ceil(CellWidth / 2); float halfCellY = ceil(CellHeight / 2); float xColorPos = (ceil(pixelX / CellWidth) * CellWidth + halfCellX) / Width; float yColorPos = (ceil(pixelY / CellHeight) * CellHeight + halfCellY) / Height; fixed4 resultCol = tex2D(_MainTex, float2(xColorPos, yColorPos)); //Cчитаем индекс пикселя внутри ячейки float pixelXIndex = (i.uv.x * Width) % CellWidth; float pixelYIndex = (i.uv.y * Height) % CellHeight; //Рассчитываем оффсеты для взятия символа из второй текстуры. Если их не брать - то замещение будет только на первый символ из таблицы float sum = resultCol.r + resultCol.g + resultCol.b; float xOffset = ceil(sum / 0.1) * 0.1; float yOffset = ceil(sum / 0.333) * 0.333; //Берём координату первого символа из таблицы и добавляем к ней оффсет float2 percentPosition = i.uv; percentPosition.x = pixelXIndex / CellWidth / 10 + xOffset; percentPosition.y = pixelYIndex / CellHeight / 3 + yOffset; //Возвращаем конкретный пиксель по указанным координатам уже со второй текстуры return tex2D(AddTex, percentPosition); }

Результат? Ну, он немного стрёмный:

ASCII‑art Шейдер на Unity для новичков

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

Немного перерисовав символы у меня получилось добиться такого результата:

На этом всё, пойду играть в Апекс. Ведь это у меня получается лучше чем написание шейдеров и статей. А вам желаю хорошо провести остаток этого воскресенья!

Upd: Залил улучшенную версию на гитхаб:

142142
39 комментариев

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

2
Ответить

Когда мне был 21 год я мечтал делать трипл А на анриле, но скоро я осознал что деньги не пахнут и начал делать мобилки на юнити...

50
Ответить

ООО!!! Спасибо огромное!!!
Опять придётся садиться ковырять немножко шейдеры в юнити =)

1
Ответить

Тоже так делаю когда появляется статья о них)

1
Ответить

Просто поковырять можно и онлайн (:

https://www.shadertoy.com/view/wt3XR4

1
Ответить

Пажжи, если может быть слеплена из говна и палок то зачем код, там же есть Shader Graph 🤔

Ответить

Shader Graph слеплен  из говна и палокпоправил 

3
Ответить