Vulkan API — урок 21. Написание шейдеров

В отличии от других API, код шейдеров в Vulkan’е в формате байт-кода, и отличаетя от человеко-ориентированного синтаксиса на подобии  GLSL и HLSL. Формат этого байт-кода – SPIR-V, разрабатывался он для использования как с Vulkan’ом, так и OpenCL (оба API от Khronos). Этот формат может быть использован для написания графических и вычислительных шейдеров, но мы сфокусируемся на шейдерах, используемых в Vulkan’е для графических целей.

Преимущество использования байт-кода в том, что компиляторы написанные производителями GPU переводят байт-код в нативный код со значительно меньшими нагрузками. Время показало, что некоторые производители достаточно неоднозначно понимали стандарты связанные с человеко-ориентированным синтаксисом на вроди GLSL. Если вам случится писать не тривиальные шейдеры для GPU одного из таких производителей, тогда вы рискуете получить ошибку на продукции других производителей, но даже не это самое страшное, ведь может в итоге запуститься что-то совсем непонятное из-за ошибок компилятора. А вот с прямолинейным байт-кодом на подобии SPIR-V такое произойдет наврядли.

Однако, это не означает, что нам необходимо писать этот байткод руками. Khronos предоставили собственный, независимый от разработчиков железа, компилятор для перевода GLSL в SPIR-V. Этот компилятор разработан для проверки кода ваших шейдеров на соответствие стандартам и производит SPIR-V, который вы можете поставлять вместе с программой. Вы можете так же включить этот компилятор как библиотеку для получения SPIR-V во время работы программы, но мы этого делать не будем. Компилятор уже включен в LunarG SDK (glslangValidator.exe), так что ничего дополнительного качать не нужно.

GLSL – язык для программирования шейдеров основанный на С. Программы написанные на нем имеют main функцию которая вызывается для каждого объекта. Вместо использования входящих параметров и возвращаемых значений, GLSL использует глобальные переменные для ввода и вывода. Язык включает множество фич для помощи программам работающим с графикой, на подобии дополнительных функций и типов данных, например для работы с векторами и матрицами (векторного произведения, произведения матриц и т.п.). Тип векторов зовется vec с числом означающим количество элементов. К примеру, 3D позиции будут храниться в vec3. Это позволяет получить доступ к отдельным через компонентам члены на подобии .x, и это также позволяет создавать новые вектора из множества компонентов в то же время. Например выражение vec3(1.0, 2.0, 3.0).xy приведет к  vec2. Конструкторы векторов могут так же принимать комбинации векторных объектов и скалярных значений – vec3 может быть построен при помощи vec3(vec2(1.0, 2.0), 3.0).

В предыдущем уроке упоминалось, что нам нужно написать vertex shader и fragment shader для вывода треугольника на экран. Далее в этом уроке будет описан код GLSL для них и перевод его в байт-код SPIR-V.

Vertex shader (Вершинный шейдер)

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

Клип координаты – это однородные координаты, которые отображают буфер кадра в системе координат от [-1, 1] до [-1, 1], но на изображении это думаю будет понятнее:

clip

Если вы ранее работали с компьютерной графикой, то это вам уже должно быть знакомо. Если вы использовали ранее OpenGL, тогда обратите внимание, что координаты Y «перевернуты». Координаты Z теперь используют тот же диапазон, что и Direct3D, от 0 до 1.

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

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

#version 450
#extension GL_ARB_separate_shader_objects : enable

out gl_PerVertex {
    vec4 gl_Position;
};

vec2 positions[3] = vec2[](
    vec2(0.0, -0.5),
    vec2(0.5, 0.5),
    vec2(-0.5, 0.5)
);

void main() {
    gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
}

Функция main вызывается для каждой вершины, встроенная переменная gl_VertexIndex содержит индекс текущей вершины. Обычно это индекс в буфере вершин, но в нашем случае это будет индекс в жестко заданном массиве вершин. Позиция каждой вершины извлекается из константного массива в самом шейдере и в сочетании с заглушками для элементов z и w для получения позиции в клип координатах. Встроенная переменная gl_Position в качестве выходного параметра. Требуется расширение GL_ARB_separate_shader_objects для работы шейдоров.

Fragment shader (Пиксельный шейдер)

Треугольник формируется на основе позиций из vertex shader заполняет область на экране фрагментами. Fragment shader  вызывается для этих фрагментов, он рассчитает цвет и глубину для фреймбуфера ( или фреймбуферов). Простейший vragment shader, который выводит красный цвет для всего треугольника выглядит следующим образом:

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(1.0, 0.0, 0.0, 1.0);
}

Функция main вызывается для каждого фрагмента как и функция main vertex shader вызывается для каждой вершины. Цвета  в GLSL – 4-х компонентные векторы с R,G,B и Альфа каналами со значениями в промежутке [0,1]. В отличии от gl_Position, в vertex shader не существует встроенной переменной для вывода цвета для текущего фрагмента. Вы указываете свою собственную переменную для каждого фреймбуфера, где модификатор layout(location = 0) определяет индекс фреймбуфера. Красный цвет записывается в переменную outColor, которая ссылается на первый (только лишь) фреймбуфер с индексом 0.

Цвета вершин

Но красный треугольник не очень красиво, так что сделаем его вершины разных цветов:

triangle_coordinates_colors

Сделаем несколько изменений в обоих шейдерах для получения такой красоты).

Во первых – нам нужно указать определенный цвет для каждой из вершин. Vertex shader теперь должен содержатьс массив с цветами аналогичный позициям.

vec3 colors[3] = vec3[](
    vec3(1.0, 0.0, 0.0),
    vec3(0.0, 1.0, 0.0),
    vec3(0.0, 0.0, 1.0)
);

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

layout(location = 0) out vec3 fragColor;

void main() {
    gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
    fragColor = colors[gl_VertexIndex];
}

Далее добавим соответсвующий вход в fragment shader’е:

layout(location = 0) in vec3 fragColor;

void main() {
    outColor = vec4(fragColor, 1.0);
}

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

Main Admin

3 Comments

  1. И все-таки не могли бы вы описать метод компиляции кода при вызове библиотеки,у меня некий сумбур по этому вопросу… Я использую язык cg для написания шейдеров в unity и не знаю, интерпретирует ли его компилятор вулкана вообще?

    • Извиняюсь, помочь не смогу, т.к. ни разу не использовал Unity и языка cg. Вулкан использует SPIR-V, который в уроках получается посредством компиляции из GLSL, попробуйте найти способ перевести cg в SPIR-V.

  2. я немного порыл по этому вопросу:cg является обрезанным клоном hlsl,и в то же время компилирует(unity) его под все платформы включая linux с glsl. В конце концов под вулкан он компилит и сам(используя ключевое слово),как оказалось. Спасибо за ответ.

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

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