Шейдеры GLSL [ч.1 - Fragment Shaders]

Шейдеры GLSL [ч.1 - Fragment Shaders]

Версия(и) Minecraft
любая
Доброго времени суток, уважаемые о/

Данная серия туториалов будет посвящена созданию собственных шейдеров на основе языка openGL Shading Language, или GLSL.

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

Язык GLSL чем-то похож на Java, чем-то - на Си, так что с синтаксисом проблем возникнуть не должно.
Первая часть будет посвящена Фрагментным Шейдерам. Что это такое объясню ниже.


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

Сложно, тёмно и не особо понятно, да? Давайте попробуем упростить вышесказанное и всё таки дать ответ на главный вопрос "что же делают фрагментные шейдеры? ". Готовы?
В двух словах: определяют цвет
Всё. Это действительно всё, что может сделать этот тип шейдера - определить и вернуть цвет одного-единственного полученного пикселя. Но не спешите закрывать урок. Даже с этим, казалось бы, небольшим функционалом можно делать просто умопомрачительные эффекты. Как? Читайте дальше.

На входе фрагментный шейдер действительно получает лишь один пиксель, точнее, его координаты, x и y. А на выходе выдаёт цвет в формате RGBA (красный, зелёный, синий и прозрачность).

Итак, рассмотрим примерный вид программы, которые у вас всех будут в итоге получаться:
C-like:
#version N

uniform type globalVar1;

void main() {
    // Координата текущего пикселя
    vec2 pixelCoord = gl_FragCoord.xy;
    // Цвет текущего пикселя
    vec4 pixelColor = vec4(0.0);

    /**
     * Любые вычисления и изменение цвета выходного пикселя
     *
     */

    // Присвоение выходного результата
    gl_FragColor = pixelColor;
}

Типы переменных:
В GLSL есть практически все стандартные примитивные типы: bool (не boolean!), int, uint (unsigned int, т.е. неотрицательное число), float и double.

В добавок к ним есть контейнеры - векторы: vec2, vec3, vec4 (на 2, 3 и 4 значения соответственно) и матрицы: mat2, mat3, mat4 (2x2, 3x3 и 4x4 соответственно). Стандартый тип переменных в них - float. Если нужен, например, вектор int'ов на три компонента, вначале нужно дописать i - ivec3 (соответственно b - bool, u - uint, d - double).

Грубо говоря, это одно- и двумерные массивы, и доступ к их элементам можно получить через стандартный оператор индексирования - []. Но для нашего удобства был разработан метод swizzling, позволяющий получить доступ к элементам по их идентификаторам (xyzw, rgba или stpq, использующиеся для координат, цветов и точек текстуры соответственно (но не обязательно так)), допустим:
C-like:
// инициализация:
vec4 v = vec4(0.0); // все компоненты равны, т.е. 0.0, 0.0, 0.0, 0.0
vec2 vect = vec2(0.5, 0.7); // два компонента: 0.5 и 0.7
vec4 result = vec4(vect, 0.0, 0.0); // четыре: 0.5, 0.7 (из первого вектора), 0.0, 0.0
vec4 otherResult = vec4(result.xyz, 1.0); // 0.5, 0.7, 0.0, 1.0

// доступ и операции:
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.abr;
vec4 otherVec = someVec.xyyx + anotherVec.sttp;

Модификатор uniform - это переменные, передаваемые в шейдер извне (из программы, в которой использовался шейдер). Самый распространённый uniform - time, то есть время. Он используется для анимации, ведь мало кому интересно смотреть на статичные шейдеры, которые проще нарисовать текстурой в фотошопе, чем учить новый язык.

Модификаторы in и out - это входные и выходные данные. Например, переменная gl_FragColor из кода выше как раз таки является переменной с модификатором out, встроенной в GLSL, её тип - vec4. Она используется для извлечения цвета пикселя из шейдера.

"Аннотация" #version указывает версию GLSL, которую вы хотите использовать. Она всегда должна находится на самом верху. Вот небольшая справка соответствия версий OpenGL и GLSL:
(записывать без точки)
2.0 - 1.10
2.1 - 1.20
3.0 - 1.30
3.1 - 1.40
3.2 - 1.50

Начиная с OpenGL 3.3 версии GLSL соответствуют версиям OpenGL, так что, допустим, GL 44 использует GLSL 440
*Источник*

Функции:
В GLSL есть множество встроенных функций, очень похожих на их аналоги из библиотеки Math:
sin(a), cos(a), tan(a); min(a, b), max(a, b) - синус, косинус, тангенс углов; минимальное и максимальное значения.
normalize(a) - возвращает нормализованный (с длинной равной 1) вектор a
length
(a) - возвращает длину вектора a
distance
(a, b) - расстояние между двумя точками (векторами a и b)
И так далее. Полный список можно найти вот тут - *тык* (для OpenGL 4)

Практика:
С теорией покончили, настало время попрактиковаться!
Для этого есть множество удобных и не очень программ и сайтов, лично я при обучении использовал Shader Edit for Android - бесплатная программа из Play Market'а (нет, это не реклама и мне за неё не платили). Для компьютеров можно использовать ShaderToy, но убедитесь, что ваш браузер поддерживает WebGL, иначе ничего не выйдет.

Итак, давайте для начала покрасим весь экран в один цвет, скажем, зелёный.
Для этого нам понадобится всего одна строка в нашей main функции:
gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0); - мы создаём вектор на 4 компонента, отвечающий за цвет текущего пикселя (RGBA), присваивая зелёному цвету значение 1.0 (это максимально допустимое значение. Если вам сложно рассчитать нужное число, используйте конструкцию "n/255" чтобы превратить числа из диапазона 0-255 в 0.0-1.0), а так же устанавливая насыщенность цвета на максимум:
8abceEqBV8g.jpg
Теперь давайте превратим сплошной цвет в градиент. Для этого "нормализуем" координаты нашего пикселя по размеру экрана, то есть поделим его координаты на размер окна:
vec2 xy = gl_FragCoord.xy / resolution.xy;
И используем координату y для выбора цвета:
gl_FragColor = vec4(0.0, xy.y, 0.0, 1.0);
jF1uZA1XTRU.jpg
Сможете перевернуть градиент, чтобы чёрный был сверху? А справа? А слева внизу?
rH9eF_1PeJw.jpg
Теперь давайте попробуем вставить изображение. В ShaderToy для этого используются каналы ниже; в Shader Edit нужно нажать на три точки справа вверху и в меню "Add uniform/texture" выбрать и добавить нужный sampler2D, нажав на +, например:
uniform sampler2D img;
Далее, используя функцию texture[2D] мы получаем цвет в нужной нам точке текстуры и возвращаем его программе:
gl_FragColor = texture2D(img, gl_FragCoord.xy)
smxoy1mf_cY.jpg
Вы можете заметить, что экран окрасился в один цвет, совсем не похожий на вашу картинку.
Что же делать? Попробуйте нормализовать координаты (поделить на разрешение экрана):
gl_FragColor = texture2D(img, gl_FragCoord.xy/resolution)
rvQcEtJuhvk.jpg
Теперь давайте попробуем поменять цвета на картинке, скажем, наложив градиент жёлтый -> синий.
Для этого просто сделаем синий цвет зависимым от какой-то координаты:
Код:
vec2 xy = gl_FragCoord / resolution;
vec4 col = texture(img, xy);
col.b = distance(vec2(0.5), xy);
gl_FragColor = col;
wJcXulwExIw.jpg

Теперь фотографируем всё подряд и пропускаем через этот шейдер :D
Попробуйте сделать чёрно-белый фильтр. А сепию? Осветление/затемнение?
vgBp_SthqPk.jpg

Ну вот и всё.
Для закрепления материала попробуйте создать радугу, имитацию водной поверхности (с волнами) или телевизионные помехи. Не забывайте использовать встроенные в Shader Edit и ShaderToy uniform'ы.

Послесловие:
Вершинный шейдер выполняется ДЛЯ КАЖДОГО пикселя на экране, а таких пикселей может быть миллионы и миллиарды, так что вот пара советов по оптимизации:
- Чем меньше условных операторов, тем лучше
- Старайтесь экономить память, т.е. не создавать переменных, без которых можно обойтись

Следующая часть: Шейдеры GLSL [ч.2 - Добавление в игру]
Всего хорошего!





Автор
AlexSoсol
Просмотры
10,072
Первый выпуск
Обновление
Оценка
4.29 звёзд 7 оценок

Другие ресурсы пользователя AlexSoсol

Последние рецензии

Хорошее, мотивирующее начало, основные шаги хорошо описаны.
Но упущены некоторые важные детали:
Как в первой, так и во второй части практически ничего не сказано о юниформах
Ничего не сказано о различных именах входных переменных в shadertoy и lwjgl
Показано, как обрабатывать ошибки, но нет инфы о часто возникающих ошибках
Туториал полезный и всё по делу, но где вставка этого в майн.
Мало информации. Вершинный шейдер выполняется для каждой ВЕРШИНЫ, фрагментный шейдер выполняется для каждого ПИКСЕЛЯ.
Как это использовать в практике, лучше показывай в майне. Все таки тут мкмоддинг)
AlexSoсol
AlexSoсol
До этого ещё нужно описать вершинные шейдеры. Вы пока учитесь создавать такие, имхо, так проще :)
Миллиарды пикселей на экране это круто, хочу экранчик такого разрешения
AlexSoсol
AlexSoсol
Ну, я утрирую, но и мысль так лучше дойдёт :D
Подробно, понятно, нужно
Сверху