Шейдер “эффект Матрицы”

Шейдер “эффект Матрицы”

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

Исходный шейдер: https://www.shadertoy.com/view/ldccW4

Мой репозиторий Github с кодом: https://github.com/IRCSS/MatrixVFX

В этом посте я сначала рассмотрю реализацию в Shader Toy и объясню строки, чтобы сделать более понятными некоторые из стандартных приемов, а затем очень кратко объясню реализацию в Unity.

Функция Main

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

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

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
 fragColor = vec4(text(fragCoord)*rain(fragCoord),1.0);
}

Эффект дождя выступает своего рода маской, которая скрывает текст. Математически это делается посредством умножения: когда функция дождя возвращает 0, данный пиксель будет черным. Если убрать текстовую часть и показать только дождь, мы увидим следующее:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
 vec3 rain = rain(fragCoord);
 fragColor = vec4(rain,1.0);
}

Я рассмотрю функцию дождя позже. Но это ничто иное, как просто перемещающиеся вниз колонки.

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

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
 float text =text(fragCoord);
 fragColor = vec4(0,text,0.0,1.0);}

Функция Text

Эта функция отвечает за визуализацию символов. Вот все тело функции:

float text(vec2 fragCoord)
{
 vec2 uv    = mod(fragCoord.xy, 16.)*.0625;
 vec2 block = fragCoord*.0625 — uv;
      uv    = uv*.8+.1; // немного увеличить символы
      uv   += floor(texture(iChannel1,              block/iChannelResolution[1].xy + iTime*.002).xy * 16.); // сделать символы случайными
      uv   *= .0625; // вернуть к диапазону 0-1
      uv.x  = -uv.x; // развернуть символы горизонтально
 return texture(iChannel0, uv).r;
}

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

Первые два строки создают структуру сетки с ячейками. Каждая ячейка содержит символ. Это очень распространенный прием для шейдеров — разделить пространство на повторяющуюся сетку, которая используется для создания повторяющихся эффектов. Больше об этом читайте в Book of Shaders.

Обычно функция fract или mod используется для получения положения ячейки в системе координат. Их результат можно брать как UV координаты для сэмплирования текстуры. Функция floor используется чтобы получить уникальный ID для каждой ячейки. В данном случае взят еще один способ получить уникальный ID, который объяснен ниже.  

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

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

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
 vec2 uv = mod(fragCoord.xy, 16.)*.0625;
 fragColor = vec4(uv.xy,0.,1.0);
}
Координаты, в которых реализован эффект Матрицы

Математически это эквивалентно fract.

vec2 uv = fract(fragCoord.xy /16.);

FragCoord здесь не нормализованы, поэтому они находятся в диапазоне от 0 до любой ширины / высоты холста в пикселях. Вот почему fract(uv/16) будет создавать повторяющиеся тайлы каждые 16 пикселей.

В исходной строке 0,0625 это 1/16. Я предпочитаю fract исключительно потому, что этому я научился в первую очередь и так больше привык. У вас могут быть свои причины использовать тот или иной способ.

Вторая строка этой функции вычисляет уникальный ID каждого блока.

vec2 block = fragCoord*.0625 — uv;

Идея проста, если мне удастся хорошо ее объяснить. Если вы разделите число A на B, вы получите два компонента вместе в одном числе. Первый — сколько B помещается в A без какого-либо частичного остатка, второй — частичный остаток. И вместе они составляют A/B. Например, 7 яблок разделить на 3 это 2 яблока плюс 1 в остатке:

3*2 + 1 = 7 или

(2.0 + 0.33…)*3 = 2.33… *3 = 7

Здесь:

A = 7, B = 3, A/B_WholeNumber = 2 (целочисленный результат), A/B_Fraction (остаток) = 0.33… и A/B = 2.33

Очень простая, но мощная математика для создания повторяющихся систем координат. Во второй строке функции UV является дробной составляющей и блокирует целочисленную составляющую. Обратите внимание, что в приведенных выше обозначениях вы всегда можете получить одно из двух чисел. Если у вас есть A / B и A / B_Fraction, вы можете получить компонент wholeNumber, выполнив вычитание одного из другого.

A/B_WholeNumber = A/B -A/B_Fraction

Именно это и происходит во второй строке, где рассчитывается блок. Чтобы убедиться, что вы это понимаете, запустите пример для пикселя. Какие числа вы получите, если запустите этот код для пикселя (24, 0)?

Надеюсь, вы поняли. Используя эти два компонента, мы можем теперь выбрать случайную букву из текстуры шрифта. Текстура шрифта представляет собой сетку 16×16, состоящую из букв. Чтобы получить случайную букву, нам нужно выбрать случайную ячейку из этой сетки 16×16. Имея генератор случайных чисел, мы можем передать ID блока, который мы создали выше, и использовать это случайное число, чтобы получить случайное смещение к случайной сетке.

Именно это делает следующая строка:

uv += floor(texture(iChannel1, block/iChannelResolution[1].xy + iTime*.002).xy * 16.); // сделать символы случайными

IChannel1 содержит текстуру шума. Автор делит его на разрешение текстуры, так что каждый блок в сетке соответствует одному пикселю текстуры шума. Это помогает избежать таких артефактов, как наложение спектров, и наилучшим образом использует текстуру шума. Дополнение iTime — это часть, отвечающая за смену букв. Если вы уберете его, у вас все равно будет эффект дождя, но на фоне из статических букв.

Как видите, это случайное значение затем добавляется к дробной части, которую мы вычислили ранее. Поскольку случайное значение находится между 0 и 1, мы умножаем его на 16, чтобы получить случайное смещение от 0 до 16 в компонентах x и y. Функция floor должна обеспечить, чтобы смещения были только целочисленными и одинаковыми для всех пикселей одной сетки.

После этой операции UV принимает значение между 0 и 16. Поскольку текстура шрифта имеет диапазон UV 0-1, для возврата к таким координатам мы делим UV на 16.

uv *= .0625; // вернуть к диапазону 0-1

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

return texture(iChannel0, uv).r;

Я реализовал ту же самую функцию с моим собственным синтаксисом и комментариями в Unity. Можете взглянуть для лучшего понимания.

float text(float2 coord)
{
	float uv = frac (coord.xy/ 16.); // Получение дробной части блока, это uv карта для блока bloc1
	float2 block = floor(coord.xy/ 16.); // Получение ID для блока. Первый блок (0,0), крайний правый (1,0), крайний верхний (0,1)
		uv = uv * 0.7 + .1; //Небольшое увеличение каждого блока, чтобы символы были крупнее
	float2 rand = tex2D(_white_noise, block.xy/float2(512.,512.)).xy; // 512 это ширина текстуры шума. Деление гарантирует, что каждый блок сэмплирует в точности 1 пиксель из текстуры шума
		rand = floor(rand*16.); // Каждое случайное значение используется блоком чтобы сэмплировать одну из 16 колонок текстуры символов. Случайное смещение выбирает символ, а белый шум - то, что меняет ее.
		uv += rand; // Случайно сгенерированная текстура имеет разные значения и uv каналы. Это гарантирует, что из нее будет выбираться один случайный элемент.
		uv *= 0.0625; // До сих пор значения uv были в пределах 0-16. Чтобы сэмплировать текстуру символов, нужно привести ее к 0-1. Поэтому делим на 16.
		uv.x = -uv.x;
	return tex2D(_font_texture, uv).r;
}

Функция Rain

Главная задача функции rain — формировать колонны, которые движутся сверху вниз. Вот все тело функции:

vec3 rain(vec2 fragCoord)
{
 fragCoord.x -= mod(fragCoord.x, 16.);

 float offset = sin(fragCoord.x*15.);
 float speed  = cos(fragCoord.x*3.)*.3+.7;
 
 float y  = fract(fragCoord.y/iResolution.y + iTime*speed + offset);
 return vec3(.1,1,.35) / (y*20.);
}

Давайте проанализируем функцию с конца. Если выводить только упрощенную версию последней строки:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
 float   y = fract(fragCoord.y/iResolution.y );
 fragColor.xyz = vec3(.1,1,.35) / (y*20.);
}
Красивый зеленый градиент для символов

Мы увидим градиент сверху вниз. FragCoord.y, разделенная на разрешение экрана, нормализует float y таким образом, чтобы он имел значение 1 вверху и 0 внизу. Умножение его на большое число и деление на него окончательного цвета приведет к тому, что верхняя часть станет черной, а нижняя будет иметь большое значение.

Если вы добавите в функцию fract время, градиент станет перемещаться. Это уже намного ближе к желаемому результату.

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
 float y  = fract(fragCoord.y/iResolution.y + iTime*0.5 );
 fragColor.xyz = vec3(.1,1,.35) / (y*20.);
}

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

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

Упражнение в цвете

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

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

Если хотите посмотреть реализацию, вот мой эффект разноцветной матрицы.

Применение в сцене

Как мы могли бы использовать это в реальном продакшене? Этот эффект работает в пиксельном шейдере 2D поверхности. Если хотите применить его к мешу, у вас есть несколько вариантов.

Я использую меш от Global Digital Heritage с лицензией CC на некоммерческое использование.

Чтобы применить этот эффект к 3D топологии, вам потребуется параметризация сетки, которая описывала бы ее поверхность как 2D координаты. Самый распространенный способ сделать это — развернуть UV.

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

Но если вы хотите применить его к каждому мешу в игре и не хотите создавать сложный конвейер генерации ресурсов для параметризации сеток определенными способами, вы можете использовать трипланарную проекцию. Я не буду вдаваться в подробности о том, что такое трипланар и как его использовать, по этой теме есть МНОГО хороших статей. Примеры взяты у cat like coding, Ben Golous и Ronja.

Вот моя реализация проецирования и эффекта матрицы. Строки обильно прокомментированы, поэтому вам будет легко следить за мыслью.

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

Эффект Матрицы от Shahriar Shahrabi

Как обычно, спасибо за чтение. Вы можете следовать за мной в Твиттере: IRCSS

Источники

Исходный шейдер в shadertoy: https://www.shadertoy.com/view/ldccW4

Статья про трипланарное проецирование в Catlike Codings: https://catlikecoding.com/unity/tutorials/advanced-rendering/triplanar-mapping/

Статья про трипланар у Бена Голуса: https://medium.com/@bgolus/normal-mapping-for-a-triplanar-shader-10bf39dca05a

Пост Ronja про трипланар: https://www.ronja-tutorials.com/2018/05/11/triplanar-mapping.html

Оригинал статьи на английском: https://shahriyarshahrabi.medium.com/shader-studies-matrix-effect-3d2ead3a84c5

Понравилась статья? Поделиться с друзьями:
Автор natalya
Переводит для Вас самые интересные статьи про разработку игр. По образованию физик-программист. Техническими переводами начала подрабатывать еще на старших курсах и постепенно это переросло в основное занятие. Интересуется гуманитарными технологиями, пробует себя в журналистике.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *