Мой опыт — создание шейдера обратного фи-феномена в Unity

Все кто страдает фоточувствительной эпилепсией или боится программного кода или математических формул эта статья не для вас!

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

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

Мой опыт — создание шейдера обратного фи-феномена в Unity

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

Шейдеры в Unity — это небольшие программы, написанные на специальном языке программирования, называемом GLSL (или HLSL для DirectX). Они выполняются на графическом процессоре (GPU) и используются для определения внешнего вида и отображения объектов на экране.

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

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

Пришло время начать все сначала. Мы возвращаемся к самому первому скриншоту, предоставленному автором этой иллюзии. Зная, как в Unity формируется значение цвета с помощью RGB, мы немного дополняем рисунок. В Unity для задания цвета используются значения каждого из цветов R, G и B в диапазоне от 0 до 1 — это будет ось Y, в то время как ось X будет отвечать за время.

И вот здесь наступает момент истинного удовлетворения — момент, когда мы вспоминаем школьную математику и осознаем, что все эти синусы и косинусы изучались не зря! После некоторого размышления мы приходим к следующим формулам:

y = 0.5 cos(0.5π*x)+0.5$ — значения Green
y = 0.5 cos(0.5π*(x+1))+0.5$ — Значения Red
y = 0.5 cos(0.5π*(x-2))+0.5$ — Значения Blue

И дописываем в код следующие функции:

float RColor(float x)
{
return 0.5f * Mathf.Cos(0.5f * Mathf.PI * (x + 1)) + 0.5f;
}

float GColor(float x)
{
return 0.5f * Mathf.Cos(0.5f * Mathf.PI * (x)) + 0.5f;
}

float BColor(float x)
{
return 0.5f * Mathf.Cos(0.5f * Mathf.PI * (x — 1)) + 0.5f;
}

И задавать цвет будем соответственно через RGB:

Color mainColor = new Color(RColor(hue), GColor(hue), BColor(hue), 1);

Ну все, теперь то точно заработает! Пуск…

В правильном спектре синий канал должен быть в противофазе красному, как-то так:

Меняем функцию для Синего канала на y = 0.5 cos(0.5π*(x-1))+0..

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

Вау! Но, как я упоминал в самом начале, моя цель — создать шейдер, а не C# скрипт. Конечно, результаты уже впечатляют, но давайте наконец объединим все это вместе! За эти пару дней, проведенных в компании с ChatGPT, я значительно продвинулся в понимании шейдеров и теперь готов собрать этого франкенштейна.

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

Properties
{
[MaterialToggle] PixelSnap («Pixel snap», Float) = 0
_Speed («Speed», Range(0,20)) = 8.5
_Radius («Radius», Range(0,5)) = 0.02
_Angle («Angle», Range(0,360)) = 0.0
_ColorOffset («Color Offset», Range(0,1)) = 0.5
}

В нашем шейдере мы будем рисовать все за три прохода (пасса) — основной спрайт и два его дубликата по отдельности. Давайте рассмотрим пример отрисовки одного из пассов:

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

Для начала возьмем переменную float4 _Offset. Это вектор из четырех значений с плавающей запятой, который мы будем использовать как хранилище для координат X и Y.

Находим координату X через косинус, а Y через синус:

X=cos(α)∗RX=cos(α)∗R

Y=sin(α)∗RY=sin(α)∗R

Где α — это значение угла поворота нашей иллюзии, а R — радиус отклонения дубликатов спрайта.

v2f OUT; // Объявляем структуру v2f, которая будет использоваться для передачи данных из вершинного шейдера во фрагментный шейдер.

_Offset = float4(cos(radians(_Angle))*_Radius, sin(radians(_Angle))*_Radius,0,0);
// Вычисляем смещение для каждой вершины на основе заданного угла (_Angle) и радиуса (_Radius).
// Это делается путем преобразования угла из градусов в радианы и применения функций cos и sin для получения x и y компонентов смещения.
// Результат сохраняется в переменной _Offset.

OUT.vertex = UnityObjectToClipPos(IN.vertex + _Offset);
// Добавляем вычисленное смещение к позиции каждой вершины (IN.vertex) и преобразуем ее из пространства объекта в пространство отсечения с помощью функции UnityObjectToClipPos.
// Пространство отсечения — это координатное пространство, в котором производится окончательное отсечение геометрии перед растеризацией.
// Результат сохраняется в OUT.vertex, который затем передается во фрагментный шейдер.

Далее устанавливаем цвет и координаты для нашего спрайта

OUT.texcoord = IN.texcoord;
// Копируем текстурные координаты из входных данных вершины (IN.texcoord) в выходные данные вершины (OUT.texcoord).
// Текстурные координаты используются для определения, как текстура должна быть отображена на геометрии.

OUT.color = IN.color * fixed4(_Red, _Green, _Blue, 1.0);
// Вычисляем цвет каждой вершины, умножая входной цвет (IN.color) на вектор цвета (fixed4(_Red, _Green, _Blue, 1.0)).
// Это позволяет нам контролировать интенсивность каждого из каналов цвета (красного, зеленого и синего) независимо.

#ifdef PIXELSNAP_ON
OUT.vertex = UnityPixelSnap (OUT.vertex);
#endif
// Если определено PIXELSNAP_ON, мы применяем функцию UnityPixelSnap к позиции вершины (OUT.vertex).
// Это обеспечивает, что вершины будут выровнены по пикселям, что может помочь предотвратить артефакты рендеринга, особенно при работе с 2D-графикой.

return OUT;
// Возвращаем выходные данные вершины (OUT), которые затем будут использоваться во фрагментном шейдере.

Здесь мы задаем цвет через каналы RGB по тем же самым формулам, которые уже реализовывали в C#

fixed4 frag(v2f IN) : SV_Target
{
// Получаем цвет пикселя из текстуры (_MainTex) в соответствии с текстурными координатами (IN.texcoord) и умножаем его на цвет вершины (IN.color)
fixed4 c = tex2D(_MainTex, IN.texcoord) * IN.color;

// Меняем красный (r), зеленый (g) и синий (b) каналы цвета пикселя, используя функцию косинуса для создания эффекта цветового смещения
c.r = 0.5f * cos(0.5f * 3.14159f * (_ColorOffset + _Speed *_Time.y + 1)) + 0.5f;
c.g = 0.5f * cos(0.5f * 3.14159f * (_ColorOffset + _Speed * _Time.y)) + 0.5f;
c.b = 0.5f * cos(0.5f * 3.14159f * (_ColorOffset +_Speed * _Time.y — 1)) + 0.5f;

// Умножаем RGB-каналы на альфа-канал, чтобы учесть прозрачность пикселя
c.rgb *= c.a;

// Возвращаем итоговый цвет пикселя
return c;
}

Вот и все, детская задачака со звездочкой успешно решена! Поздравляю вас, и, конечно, поздравляю себя!

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

Я считаю, что этот шейдер может прекрасно вписаться в игры в стиле shmup или топ-даун шутеры, например «The Binding of Isaac». Он также может добавить сложности играм, вдохновленным «Geometry Dash». Но в качестве дополнительного элемента, добавляющего сложности игровому процессу, он, безусловно, может найти свое применение.

Какие у вас мысли на этот счет? В каких играх, по вашему мнению, такие оптические иллюзии будут уместны? И готовы ли вы принять вызов и окунуться в игру с такими элементами?

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

P.S: Я уже доработал шейдер и в нем теперь можно выбирать режим работы (смещение, увеличение, уменьшение объекта)

 

Источник

unity, мой, обратного, опыт, создание, фифеномена, шейдера

Читайте также