Найти в Дзене
Игровая ниша

Часть 1. Введение в GLSL и ShaderToy

Написание шейдеров может быть похоже на магию и вызывать непонимание. Можно найти множество примеров кода который демонстрирует невероятные эффекты, но очень сложно найти описание того как это работает. Это руководство поможет восполнить это пробел с помощью ShaderToy - бесплатного инструмента, который позволяет запускать код шейдера прямо в браузере. Изучение написания графических шейдеров - это освоение возможностей графического процессора (GPU) с его тысячами ядер, работающих параллельно. Этот вид программирования требует особого подхода, но стоит потраченного на него времени. Практически всё то что мы видим на экране монитора так или иначе работает на основе кода, написанного для GPU - от реалистичных эффектов в передовых ААА-играх до 2D-эффектов и симуляций жидкости. ShaderToy - это веб-платформа, которая позволяет создавать и делиться фрагментными шейдерами (fragment shaders) прямо в браузере. Ссылка на ShaderToy Написание шейдеров может восприниматься как чёрная магия и нередко
Оглавление

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

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

Практически всё то что мы видим на экране монитора так или иначе работает на основе кода, написанного для GPU - от реалистичных эффектов в передовых ААА-играх до 2D-эффектов и симуляций жидкости.

ShaderToy

ShaderToy - это веб-платформа, которая позволяет создавать и делиться фрагментными шейдерами (fragment shaders) прямо в браузере.

Ссылка на ShaderToy

Цель руководства

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

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

Что такое шейдер?

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

Шейдеры пишутся на специализированном языке шейдеров. Не беспокойтесь, вам не придется осваивать совершенно новый язык - мы будем использовать GLSL (OpenGL Shading Language), который основан на синтаксисе языка C. Существует множество языков шейдеров для различных платформ, но поскольку все они адаптированы для выполнения на графическом процессоре, они имеют значительное сходство между собой.

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

Приступим к работе!

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

После нажатия кнопки New вы должны увидеть следующее:

Интерфейс может выглядеть несколько иначе, если вы не выполнили вход в систему.
Интерфейс может выглядеть несколько иначе, если вы не выполнили вход в систему.

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

Что происходит?

Сейчас я объясню принцип работы шейдеров одним предложением. Готовы?

Единственная задача шейдера - вернуть четыре числа: r, g, b и a.

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

Учитывая это, давайте попробуем окрасить весь экран в красный цвет. Значения rgba (красный, зеленый, синий и "альфа-канал", определяющий прозрачность) варьируются от 0 до 1, поэтому нам достаточно вернуть r,g,b,a = 1,0,0,1. В ShaderToy итоговый цвет пикселя должен быть сохранен в переменной fragColor.

-3

Поздравляю! Это ваш первый рабочий шейдер!

Задача: можно ли изменить его цвет на сплошной серый?

vec4 — это просто тип данных, поэтому мы могли бы объявить наш цвет как переменную, например так:

-4

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

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

Входные данные шейдера

Пиксельный шейдер передает несколько переменных, которые мы можем использовать. Самая полезная для нас — это fragCoord, которая содержит координаты x и y (а также z, если вы работаете в 3D) пикселя. Давайте попробуем сделать все пиксели на левой половине экрана черными, а на правой половине — красными:

-5
Результат выполнения код
Результат выполнения код

Примечание: Для любого типа vec4 вы можно получить доступ к его компонентам через obj.x, obj.y, obj.z и obj.w или через obj.r, obj.g, obj.b, obj.a. Эти обращения эквивалентны; это просто удобный способ именования компонентов, чтобы сделать код более читаемым.

Видите ли вы проблему в коде выше? Попробуйте нажать кнопку развернуть на весь экран (Go fullscreen) в правом нижнем углу окна предварительного просмотра.

Кнопка Развернуть на весь экран (Go fullscreen)
Кнопка Развернуть на весь экран (Go fullscreen)

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

Если что-то не является встроенной переменной, вы можете передать эту информацию с CPU (вашей основной программы) на GPU (ваш шейдер). ShaderToy делает это за нас. Вы можете увидеть все переменные, передаваемые в шейдер, во вкладке Shader Inputs. Переменные, передаваемые таким образом с CPU на GPU, в GLSL называются uniform.

Входные значения для шейдера
Входные значения для шейдера

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

-9

Если попробуете увеличить окно в этот раз, цвета по-прежнему будут идеально разделять экран пополам.

От разделения к градиенту

Превратить код выше в градиент достаточно просто. Значения цвета варьируются от 0 до 1, и наши координаты теперь тоже изменяются от 0 до 1.

-10
Результат выполнения кода
Результат выполнения кода

Готово!

Задача: Можете ли преобразовать это в вертикальный градиент? А как насчет диагонального? Или градиента с несколькими цветами? Если поэкспериментируете с этим достаточно, то заметите, что верхний левый угол имеет координаты (0,1), а не (0,0). Это важно помнить.

Создание изображений

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

При программировании на обычной платформе нам потребовалось бы отправлять наше изображение (или текстуру) на графический процессор в качестве uniform-переменной, аналогично тому, как мы отправляли разрешение экрана. ShaderToy берет эту задачу на себя. В нижней части интерфейса расположены четыре входных канала:

Четыре входных канала ShaderToy.
Четыре входных канала ShaderToy.

Нажмите на iChannel0 и выберите любую текстуру (изображение), которая вам нравится.

После этого изображение будет передано вашему шейдеру. Однако есть одна проблема: здесь нет функции DrawImage(). Помните, единственное, что может делать пиксельный шейдер — это изменять цвет каждого пикселя.

Итак, если мы можем возвращать только цвет, как же мы нарисуем текстуру на экране? Нам нужно как-то сопоставить текущий пиксель, обрабатываемый нашим шейдером, с соответствующим пикселем на текстуре.

В зависимости от того, где на экране находится (0,0), вам может потребоваться перевернуть ось Y, чтобы правильно отобразить текстуру. На момент написания статьи ShaderToy был обновлен, чтобы его начало было вверху слева, поэтому нет необходимости что-либо переворачивать.
В зависимости от того, где на экране находится (0,0), вам может потребоваться перевернуть ось Y, чтобы правильно отобразить текстуру. На момент написания статьи ShaderToy был обновлен, чтобы его начало было вверху слева, поэтому нет необходимости что-либо переворачивать.

Можем сделать это с помощью функции texture(textureData, coordinates), которая принимает данные текстуры и пару координат (x, y) в качестве входных параметров и возвращает цвет текстуры в этих координатах в виде vec4.

Можно сопоставить координаты с экраном любым удобным способом. Например, можно отобразить всю текстуру на четверти экрана (пропуская пиксели, фактически уменьшая масштаб) или нарисовать только часть текстуры.

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

-14

Итак, у нас есть первое изображение!

-15

Теперь, когда вы правильно получаете данные из текстуры, вы можете манипулировать ими как угодно! Можно растягивать и масштабировать текстуру или изменять её цвета.

Давайте попробуем изменить её с помощью градиента, как мы делали выше:

texColor.b = xy.x;

-16

Поздравляю, вы только что создали свой первый эффект постобработки!

Задание: Можете ли вы написать шейдер, который превратит изображение в черно-белое? Обратите внимание, что хотя это статичное изображение, то, что вы видите перед собой, происходит в реальном времени. Вы можете убедиться в этом сами, заменив статичное изображение на видео: нажмите снова на вход iChannel0 и выберите одно из видео.

Добавим движения

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

iGlobalTime — это постоянно увеличивающаяся переменная; мы можем использовать её как основу для создания периодических эффектов. Давайте немного поэкспериментируем с цветами:

-17

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

Задание: Можете ли вы создать шейдер, который будет переключать изображение туда-обратно между черно-белым и полноцветным режимами?

Примечание об отладке шейдеров

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

Заключение

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

Одна вещь, о которой я не упомянул в этом руководстве - это Вершинные шейдеры (Vertex Shaders). Они пишутся на том же языке, но выполняются для каждой вершины вместо пикселя, и возвращают как позицию, так и цвет. Вершинные шейдеры обычно отвечают за проецирование 3D-сцены на экран (что встроено в большинство графических конвейеров). Пиксельные шейдеры отвечают за многие продвинутые эффекты, которые мы видим, поэтому они и являются нашим основным фокусом.

На этом все для данного руководства! Я буду очень признателен за ваши комментарии и вопросы.