Vulkan. Руководство разработчика. Layout дескрипторов и буфер

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


Добрый день! Я продолжаю выкладывать на Хабр переводы Vulkan Tutorial на русский язык (оригинал руководства можно посмотреть здесь).

Сегодня я перехожу к следующему разделу — Uniform buffers, и начну я с перевода статьи, которая называется Descriptor layout and buffer.

Содержание
1. Вступление

2. Краткий обзор

3. Настройка окружения

4. Рисуем треугольник

  1. Подготовка к работе
    • Базовый код
    • Экземпляр (instance)
    • Слои валидации
    • Физические устройства и семейства очередей
    • Логическое устройство и очереди
  2. Отображение на экране
    • Window surface
    • Swap chain
    • Image views
  3. Графический конвейер (pipeline)
    • Вступление
    • Шейдерные модули
    • Непрограммируемые стадии конвейера
    • Проходы рендера (Render passes)
    • Заключение
  4. Отрисовка
    • Фреймбуферы
    • Буферы команд
    • Рендеринг и отображение на экране
  5. Пересоздание swap chain

5. Вершинные буферы

  1. Описание входных данных вершин
  2. Создание вершинного буфера
  3. Промежуточный буфер
  4. Индексный буфер

6. Uniform-буферы

  1. Layout дескрипторов и буфер
  2. Пул дескрипторов и сеты дескрипторов

7. Текстурирование

  1. Изображения
  2. Image view и image sampler
  3. Комбинированный image sampler

8. Буфер глубины

9. Загрузка моделей

10. Создание мип-карт

11. Multisampling

FAQ

Политика конфиденциальности


Layout дескрипторов и буфер


  • Вступление
  • Вершинный шейдер
  • Layout дескрипторов
  • Uniform-буфер
  • Обновление uniform-данных


Вступление


Мы можем передавать произвольные атрибуты вершинному шейдеру, но как быть с глобальными переменными? С этой главы мы переходим к трехмерной графике, и для этого нам потребуются матричные преобразования. Мы будем использовать 3 матрицы: model — матрица преобразования из локальных координат в мировые, view — для преобразования из мировых координат во view пространство, и proj — проекционная матрица. Мы могли бы записать уже преобразованные координаты в вершинный буфер, но для этого пришлось бы выделять лишнюю память и постоянно обновлять данные.

Чтобы решить эту проблему в Vulkan, нужно использовать дескрипторы ресурсов. Дескрипторы предоставляют шейдерам доступ к таким ресурсам, как буферы и images. Мы создадим буфер, содержащий матрицы преобразования, и предоставим вершинному шейдеру доступ к ним через дескриптор. Использование дескрипторов состоит из трех этапов:

  • Указание layout-а дескрипторов во время создания конвейера
  • Выделение набора дескрипторов из пула дескрипторов
  • Привязка (bind) набора дескрипторов во время рендеринга

Есть два вида объектов для работы с дескрипторами ресурсов: layout-ы дескрипторов и сеты дескрипторов (descriptor set). Layout определяет типы ресурсов, к которым будет осуществляться доступ из конвейера, точно так же, как render pass определяет типы attachment-ов, с которыми он работает. Сет дескрипторов определяет, какие именно буферы или images нужно привязать к дескрипторам, точно так же, как фреймбуфер определяет, какие именно images использовать для работы с render pass-ом. Именно сеты привязываются к командам рисования, как вершинные буферы или фреймбуферы.

Есть разные типы дескрипторов, но в этой главе мы будем работать с объектами uniform-буферов (uniform buffer objects или UBO). В дальнейшем мы рассмотрим и другие типы дескрипторов, но принцип работы у них один.

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

struct UniformBufferObject {
    glm::mat4 model;
    glm::mat4 view;
    glm::mat4 proj;
};

Затем мы можем скопировать эти данные в VkBuffer и получить к ним доступ из вершинного шейдера с помощью дескриптора uniform-буфера:

layout(binding = 0) uniform UniformBufferObject {
    mat4 model;
    mat4 view;
    mat4 proj;
} ubo;

void main() {
    gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);
    fragColor = inColor;
}

Мы постоянно будем обновлять матрицы model, view и proj, чтобы прямоугольник, который мы получили в предыдущей главе, вращался в 3D.

Вершинный шейдер


Изменим вершинный шейдер, чтобы подключить uniform-буфер. Вероятно, вы уже знакомы с MVP преобразованиями. Если нет, см. ссылку.

#version 450

layout(binding = 0) uniform UniformBufferObject {
    mat4 model;
    mat4 view;
    mat4 proj;
} ubo;

layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec3 inColor;

layout(location = 0) out vec3 fragColor;

void main() {
    gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);
    fragColor = inColor;
}

Обратите внимание, что порядок объявлений uniform, in и out не имеет значения. Директива binding аналогична директиве location для атрибутов. Мы укажем на нее в layout-е дескрипторов. Строка с вычислением gl_Position изменена, чтобы использовать матрицы для преобразования координат. В отличие от двумерных треугольников, последний компонент координат в пространстве отсечения может быть не равен 1, что приведет к делению при преобразовании в конечные нормализованные экранные координаты устройства. Это используется в перспективной проекции в качестве перспективного деления и необходимо для того, чтобы более близкие объекты выглядели больше, чем объекты вдалеке.

Layout дескрипторов


Наш следующий шаг — описать используемый в шейдере uniform buffer на стороне C ++.

struct UniformBufferObject {
    glm::mat4 model;
    glm::mat4 view;
    glm::mat4 proj;
};

Мы дублируем описание буфера из шейдера с помощью соответствующих типов GLM. Данные в матрицах бинарно совместимы с тем, как ожидает шейдер, поэтому в дальнейшем мы можем просто скопировать UniformBufferObject в VkBuffer с помощью memcpy.

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

void initVulkan() {
    ...
    createDescriptorSetLayout();
    createGraphicsPipeline();
    ...
}

...

void createDescriptorSetLayout() {

}

Каждая привязка должна быть описана с помощью структуры VkDescriptorSetLayoutBinding.

void createDescriptorSetLayout() {
    VkDescriptorSetLayoutBinding uboLayoutBinding{};
    uboLayoutBinding.binding = 0;
    uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
    uboLayoutBinding.descriptorCount = 1;
}

Первые два поля определяют binding, используемый в шейдере, и тип дескриптора, который представляет собой uniform-буфер. Переменная шейдера может представлять собой массив uniform-буферов, а descriptorCount указывает количество значений в массиве.
Это можно использовать, например, для указания трансформаций для каждой кости скелета в скелетной анимации. Наше MVP преобразование находится в единственном uniform-буфере, поэтому в descriptorCount мы укажем 1.

uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;

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

uboLayoutBinding.pImmutableSamplers = nullptr; // Optional

Поле pImmutableSamplers актуально только для дескрипторов, связанных с чтением из текстур. Их мы рассмотрим позже. Здесь можно оставить значение по умолчанию.

Все привязки дескрипторов объединены в один объект VkDescriptorSetLayout. Определим новый член класса перед pipelineLayout:

VkDescriptorSetLayout descriptorSetLayout;
VkPipelineLayout pipelineLayout;

Затем мы можем создать его с помощью vkCreateDescriptorSetLayout. Эта функция принимает довольно простую структуру VkDescriptorSetLayoutCreateInfo, описывающую массив привязок:

VkDescriptorSetLayoutCreateInfo layoutInfo{};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = 1;
layoutInfo.pBindings = &uboLayoutBinding;

if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) {
    throw std::runtime_error("failed to create descriptor set layout!");
}

Мы должны указать на только что созданный layout при создании конвейера, чтобы сообщить Vulkan, какие дескрипторы будут использоваться шейдерами. Layout-ы дескрипторов указываются в объекте layout-а конвейера. Изменим VkPipelineLayoutCreateInfo:

VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;

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

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

void cleanup() {
    cleanupSwapChain();

    vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);

    ...
}


Uniform-буфер


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

Сразу несколько кадров могут обрабатываться одновременно (например, один кадр рендерится, другой подготавливается к рендеру). Мы не должны допускать ситуации, когда данные из буфера считываются видеокартой и обновляются процессором в один и тот же момент, поэтому нам необходимо использовать несколько буферов. Мы можем использовать по одному uniform-буферу на кадр или на image из swap chain. Но поскольку мы ссылаемся на uniform-буферы из буфера команд, то лучше использовать свой uniform-буфер для каждого image.

Для этого добавим новые члены класса для uniformBuffers и uniformBuffersMemory:

VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory;

std::vector<VkBuffer> uniformBuffers;
std::vector<VkDeviceMemory> uniformBuffersMemory;

Аналогичным образом создадим новую функцию createUniformBuffers, которая вызывается после createIndexBuffer и выделяет буферы:

void initVulkan() {
    ...
    createVertexBuffer();
    createIndexBuffer();
    createUniformBuffers();
    ...
}

...

void createUniformBuffers() {
    VkDeviceSize bufferSize = sizeof(UniformBufferObject);

    uniformBuffers.resize(swapChainImages.size());
    uniformBuffersMemory.resize(swapChainImages.size());

    for (size_t i = 0; i < swapChainImages.size(); i++) {
        createBuffer(bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, uniformBuffers[i], uniformBuffersMemory[i]);
    }
}

Мы напишем отдельную функцию, которая будет обновлять буфер на каждом кадре, поэтому vkMapMemory здесь не используется. Uniform-данные будут использоваться для всех вызовов отрисовки, поэтому буфер, содержащий их, должен быть уничтожен только после того, как мы закончим рендеринг. Поскольку он также зависит от количества images в swap chain, которые могут измениться после пересоздания swap chain, мы удалим его в cleanupSwapChain:

void cleanupSwapChain() {
    ...

    for (size_t i = 0; i < swapChainImages.size(); i++) {
        vkDestroyBuffer(device, uniformBuffers[i], nullptr);
        vkFreeMemory(device, uniformBuffersMemory[i], nullptr);
    }
}

И пересоздадим в recreateSwapChain:

void recreateSwapChain() {
    ...

    createFramebuffers();
    createUniformBuffers();
    createCommandBuffers();
}


Обновление uniform-данных


Создадим новую функцию updateUniformBuffer и вызовем ее из drawFrame сразу после того, как узнаем, какой image будет получен из swap chain:

void drawFrame() {
    ...

    uint32_t imageIndex;
    VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);

    ...

    updateUniformBuffer(imageIndex);

    VkSubmitInfo submitInfo{};
    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;

    ...
}

...

void updateUniformBuffer(uint32_t currentImage) {

}

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

#define GLM_FORCE_RADIANS
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include <chrono>

Заголовочный файл glm/gtc/matrix_transform.hpp предоставляет функции, которые можно использовать для генерирования model-преобразований, таких как glm :: rotate, view-преобразований, таких как glm :: lookAt, и projection-преобразований, таких как glm::perspective. Определение GLM_FORCE_RADIANS необходимо, чтобы такие функции, как glm :: rotate, использовали радианы в качестве аргументов во избежание любой возможной путаницы.

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

void updateUniformBuffer(uint32_t currentImage) {
    static auto startTime = std::chrono::high_resolution_clock::now();

    auto currentTime = std::chrono::high_resolution_clock::now();
    float time = std::chrono::duration<float, std::chrono::seconds::period>(currentTime - startTime).count();
}

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

Теперь вычислим model, view и proj матрицы. Трансформация в мировую систему координат (model) – это просто вращение вокруг оси Z с использованием переменной time:

UniformBufferObject ubo{};
ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));

В качестве параметров функция glm :: rotate принимает существующее преобразование, угол вращения и ось вращения. Конструктор glm :: mat4 (1.0f) возвращает единичную матрицу. Использование угла вращения time * glm :: radians (90.0f) позволяет осуществлять вращение на 90 градусов в секунду.

ubo.view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f));

Для view-преобразования я решил взглянуть на геометрию сверху под углом в 45 градусов. В качестве параметров функция glm :: lookAt принимает точку, в которой находится камера (eye position), точку, на которую смотрит камера (center position) и направление «вверх» для камеры (up axis).

ubo.proj = glm::perspective(glm::radians(45.0f), swapChainExtent.width / (float) swapChainExtent.height, 0.1f, 10.0f);

Я решил использовать перспективную проекцию с вертикальным полем зрения в 45 градусов. Остальные параметры — это отношение сторон, ближняя плоскость отсечения и дальняя плоскость отсечения. Важно использовать текущее разрешение swap chain, чтобы рассчитать отношения сторон с учетом новой ширины и высоты после изменения размера окна.

ubo.proj[1][1] *= -1;

Изначально GLM был разработан для OpenGL, где ось Y в пространстве отсечения имеет противоположное направление. Самый простой способ компенсировать это — инвертировать знак соответствующего поля в матрице проекции. Если этого не сделать, изображение будет рендериться вверх ногами.

Теперь все матрицы вычислены, поэтому мы можем скопировать данные в текущий uniform-буфер.

void* data;
vkMapMemory(device, uniformBuffersMemory[currentImage], 0, sizeof(ubo), 0, &data);
    memcpy(data, &ubo, sizeof(ubo));
vkUnmapMemory(device, uniformBuffersMemory[currentImage]);

Такой способ использования UBO — не самый эффективный для передачи постоянно меняющихся значений шейдеру. Для передачи небольших буферов данных эффективнее будет использовать push-константы. Мы рассмотрим их в следующих главах.

В следующей главе мы рассмотрим сеты дескрипторов, которые привяжут (bind) VkBuffers к дескрипторам uniform-буферов, чтобы шейдер мог получить доступ к данным преобразований.

Код С++ / Вершинный шейдер / Фрагментный шейдер
Источник: https://habr.com/ru/post/582838/


Интересные статьи

Интересные статьи

Это вторая часть из 2 частей Практического руководства по HashiCorp Consul. Предыдущая часть была в первую очередь сосредоточена на понимании проблем, которые решает Consul, и на том, к...
Многие компании в определенный момент приходят к тому, что ряд процессов в бизнесе нужно автоматизировать, чтобы не потерять свое место под солнцем и своих заказчиков. Поэтому все...
Примечание от переводчика: это первая часть монументальной (на самом деле монументальной) статьи о Vim и его возможностях от разработчика из Миннеаполиса и автора проекта PostgREST Джо begriffs Н...
Если в вашей компании хотя бы два сотрудника, отвечающих за работу со сделками в Битрикс24, рано или поздно возникает вопрос распределения лидов между ними.
Эта публикация написана после неоднократных обращений как клиентов, так и (к горести моей) партнеров. Темы обращений были разные, но причиной в итоге оказывался один и тот же сценарий, реализу...