Students Save 30%! Learn & create with unlimited courses & creative assets Students Save 30%! Save Now
Advertisement
  1. Game Development
  2. Shaders
Gamedevelopment

Введение в программирование шейдеров для начинающих

by
Difficulty:IntermediateLength:LongLanguages:
This post is part of a series called A Beginner's Guide to Coding Graphics Shaders.
A Beginner's Guide to Coding Graphics Shaders: Part 2

Russian (Pусский) translation by Anton Voronin (you can also view the original English article)

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

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

Сцена из Minecraft: до (сверху) и после (внизу) применения шейдеров.

Цели данного урока

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

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

Итак, что же такое вообще шейдер?

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

Шейдеры пишутся на особом языке программирования. Однако не стоит волноваться. Вам не придется учить с нуля совершенно новый язык, поскольку для написания шейдеров мы будем использовать GLSL, синтаксис которого очень похож на другие C-подобные языки. (На самом деле, существует достаточно большое количество языков для написания шейдеров, однако, ввиду того, что все они предназначены для выполнения на GPU, они все практически одинаковые)

Итак, приступим!

Для данного урока мы будем использовать ShaderToy. Этот ресурс позволяет писать шейдеры прямо в вашем браузере, не утруждая себя никакими дополнительными настройками. (Однако для использования ShaderToy вам понадобится браузер с поддержкой WebGL, поскольку именно он используется для рендеринга.) Регистрация не обязательна, однако она позволит сохранить свой код на будущее в профиле.

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

Нажав на New Shader, вы должны увидеть вот такое окно:

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

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

Итак, что же здесь происходит?

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

Основная задача шейдера - предоставить на выходе четыре числа, выражаемые буквами r, g, b , a.

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

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

Поздравляю! Вот вы и написали свой первый работающий шейдер!

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

vec4 - это тип данных (четырехмерный вектор), поэтому мы могли бы записать цвет в переменную типа vec4 и передать ее значение во fragColor:

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

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

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

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

Прим.: К компонентам любой переменной типа vec4 можно обращаться с помощью obj.x, obj.yobj.z иobj.w, либо obj.robj.g,obj.bobj.a. Эти наборы компонентов ничем не отличаются - это просто разные способы представления одних и тех же данных для улучшения читабельности кода (например, если вы используете переменную vec4 obj для обозначения цветов, другие люди, увидев в коде обозначение obj.r, сразу это поймут).

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

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

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

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

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

Переходим к градиенту.

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

Вуаля!

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

Если вы уделите модификациям и  тестированию этого кода достаточно времени, то должны догадаться, что верхний левый угол экрана имеет координаты(0,1) вместо ожидаемых (0,0). Очень важно помнить об этом при написании шейдеров.

Рисуем картинки

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

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

Каналы для входящих данных ShaderToy.

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

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

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

Также вспомним, что левый верхний пиксель экрана имеет координаты (0,1), в то время как левый верхний пиксель текстуры имеет координаты (0,0), поэтому прежде всего нам необходимо перевернуть ось y.

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

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

Сейчас нам важно просто увидеть изображение на экране, поэтому будем соотносить все пиксели один к одному:

Итак, вот и наш первый шейдер с изображением!

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

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

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

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

Обратите внимание, что то, что вы видите на экране, хоть и является статичным изображением, на самом деле вычисляется непрерывно в настоящем времени. Это можно увидеть, если вместо изображения обрабатывать видео: нажмите на кнопку iChannel0 снова и выберите один из видеофайлов.

Оживим наш шейдер

До этого все эффекты, которые мы создавали, были статичными. На самом деле, используя те данные, которые дает ShaderToy, можно создавать гораздо более интересные вещи. iGlobalTime - это переменная, которая постоянно увеличивается со временем. Ее можно использовать в качестве параметра для создания периодических эффектов. Давайте попробуем поиграть с цветами:

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

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

Небольшая заметка по отладке шейдеров

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

Заключение

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

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

Финальная задача: Сможете ли вы написать шейдер, который удаляет зеленый экран из видео на ShaderToy и вставляет вместо него другое видео на заднем плане?

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

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.