Vulkan API — урок 22. Компиляция и использование шейдеров (+листинг)

Компиляция

Рекомендую создать папку shaders в папке проекта и хранить шейдеры в файлах shader.vert и shader.frag (vertex и fragmet соответственно) в этой папке. GLSL шейдеры не имеют обязательного расширения, но обычно используются эти два, что бы их отличать.

Содержимое shader.vert должно быть таким:

#version 450
#extension GL_ARB_separate_shader_objects : enable

out gl_PerVertex {
    vec4 gl_Position;
};

layout(location = 0) out vec3 fragColor;

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

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)
);

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

А shader.frag:

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(location = 0) in vec3 fragColor;

layout(location = 0) out vec4 outColor;

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

Теперь о самой компиляции, для которой будет использоваться программа glslangValidator.

Для Windows создадим .bat файл, и заполним его вот этим:

C:/VulkanSDK/1.0.26.0/Bin32/glslangValidator.exe -V shader.vert
C:/VulkanSDK/1.0.26.0/Bin32/glslangValidator.exe -V shader.frag
pause

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

Для Linux аналогично создаем файл, только теперь .sh, и пишем в нем

/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslangValidator -V shader.vert
/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslangValidator -V shader.frag

Проверяем директорию, и запускаем его при помощи chmod +x compile.sh.

Обе команды вызываются с флагом -V, что говорит о необходимости компилить GLSL -> SPIR-V. После запуска вы увидите, что создастся два бинарника: vert.spv and frag.spv. Имена создаются автоматически из расширений шейдеров. Но при желании вы можете и переименовать. Не пугайтесь сообщениям «missing features» во время компиляции, их можно игнорировать.

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

Загрузка шейдеров

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

#include <fstream>

...

static std::vector<char> readFile(const std::string& filename) {
    std::ifstream file(filename, std::ios::ate | std::ios::binary);

    if (!file.is_open()) {
        throw std::runtime_error("failed to open file!");
    }
}

Функция readFile будет считывать все биты из указанного файла и возвращать их в массив байт под управлением std::vector. Начнем с открытия файла используя два флага:

  • ate: Начинает чтение с конца файла
  • binary: Чтения файла как бинарного файла (избегая преобразований текста)

Преимущество чтения файла с конца – возможность по позиции чтения для определить размера файла и указать размер буфера.

size_t fileSize = (size_t) file.tellg();
std::vector<char> buffer(fileSize);

После можно вернуться к началу файла и прочитать все байты сразу:

file.seekg(0);
file.read(buffer.data(), fileSize);

В конце закроем файл (банальность, но никогда не стоит об этом забывать, сам иногда пропускаю этот момент, но не повторяйте этой ошибки) и вернем байты:

file.close();
return buffer;

И вызовем функцию  из createGraphicsPipeline, для загрузки байткода двух шейдеров:

void createGraphicsPipeline() {
    auto vertShaderCode = readFile("shaders/vert.spv");
    auto fragShaderCode = readFile("shaders/frag.spv");
}

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

Создание шейдерных модулей

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

void createShaderModule(const std::vector<char>& code, VDeleter<VkShaderModule>& shaderModule) {

}

Функция будет принимать буфер с байткодом как параметр и создавать из него VkShaderModule. Вместо возвращения непосредственно дескриптора, он запишет результат в переменную, передаваемую вторым параметром, благодаря чему гораздо проще результат работы метода обернуть в наш VDeleter.

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

VkShaderModuleCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();
createInfo.pCode = (uint32_t*) code.data();

Теперь VkShaderModule можно создать вызвав vkCreateShaderModule:

if (vkCreateShaderModule(device, &createInfo, nullptr, shaderModule.replace()) != VK_SUCCESS) {
    throw std::runtime_error("failed to create shader module!");
}

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

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

VDeleter<VkShaderMobule> vertShaderModule{device, vkDestroyShaderModule};
VDeleter<VkShaderMobule> fragShaderModule{device, vkDestroyShaderModule};

Они будут автоматически очищены по окончанию создания графического конвейера. Теперь просто вызовим функцию-помощник, написанную ранее.

createShaderModule(vertShaderCode, vertShaderModule);
createShaderModule(fragShaderCode, fragShaderModule);

Создание шейдеров

Объект  VkShaderModule – это лишь обертка для буфера байткода, не способная ничего делать сама. Шейдеры не связаны друг с другом и им не заданы цели. Присваивается модуль определенному шагу в конвейере через структуру VkPipelineShaderStageCreateInfo, которая является элементом создания конвейера.

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

VkPipelineShaderStageCreateInfo vertShaderStageInfo = {};
vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;

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

vertShaderStageInfo.module = vertShaderModule;
vertShaderStageInfo.pName = "main";

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

Изменить структуру в соответствии с fragment shader достаточно просто:

VkPipelineShaderStageCreateInfo fragShaderStageInfo = {};
fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
fragShaderStageInfo.module = fragShaderModule;
fragShaderStageInfo.pName = "main";

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

VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo};

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

И листинг.

Main Admin

6 Comments

  1. Урок 23 не отображается: «It looks like nothing was found at this location. Maybe try one of the links below or a search?»

  2. >>В конце закроем файл (банальность, но никогда не стоит об этом забывать, сам иногда пропускаю этот момент, но не повторяйте этой ошибки)
    А в чем банальность и ошибка? Деструктор ifstream автоматически закрывает открытый файл.

  3. VDeleter<VkShaberMobule> vertShaderModule{device, vkDestroyShaderModule};
    VDeleter<VkShaberMobule> fragShaderModule{device, vkDestroyShaderModule};

    Опечатка в слове VkShaderModule

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

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