Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Меня зовут Александра, я работаю в IT-компании CG Tribe в Ижевске и занимаюсь переводом Vulkan Tutorial на русский язык (ссылка на источник — vulkan-tutorial.com).
Сегодня хочу поделиться переводом заключительных глав раздела, посвященного графическому конвейеру (Graphics pipeline basics), — Render passes и Conclusion.
Прежде чем завершить создание графического конвейера нужно сообщить Vulkan, какие буферы (attachments) будут использоваться во время рендеринга. Необходимо указать, сколько будет буферов цвета, буферов глубины и сэмплов для каждого буфера. Также нужно указать, как должно обрабатываться содержимое буферов во время рендеринга. Вся эта информация обернута в объект прохода рендера (render pass), для которого мы создадим новую функцию
Мы используем только один цветовой буфер, представленный одним из images в swap chain.
Формат цветового буфера (поле
Мы будем использовать
Для
Нам нужно вывести отрендеренный треугольник на экран, поэтому перейдем к операции сохранения:
Текстуры и фреймбуферы в Vulkan — это объекты VkImage с определенным форматом пикселей, однако layout пикселей в памяти может меняться в зависимости от того, что вы хотите сделать с image.
Вот некоторые из наиболее распространенных layout-ов:
Более подробно мы обсудим эту тему в главе, посвященной текстурированию. Сейчас нам важно, чтобы images были переведены в layouts, подходящие для дальнейших операций.
В
Один проход рендера может состоять из множества подпроходов (subpasses). Подпроходы — это последовательные операции рендеринга, зависящие от содержимого фреймбуферов в предыдущих проходах. К ним относятся, например, эффекты постобработки, применяемые друг за другом. Если объединить их в один проход рендера, Vulkan сможет перегруппировать операции для лучшего сохранения пропускной способности памяти и большей производительности (прим. переводчика: видимо, имеется в виду тайловый рендеринг). Однако мы будем использовать для нашего треугольника только один подпроход.
Каждый подпроход ссылается на один или несколько attachment-ов. Эти отсылки представляют собой структуры VkAttachmentReference:
В поле
Подпроход описывается с помощью структуры VkSubpassDescription:
Мы должны явно указать, что это графический подпроход, поскольку не исключено, что в будущем Vulkan может поддерживать вычислительные подпроходы. После этого укажем ссылку на цветовой буфер:
Директива
Подпроход может ссылаться на следующие типы буферов:
Теперь, когда буфер и подпроход, ссылающийся на этот буфер, описаны, мы можем создать сам проход рендера. Перед
Теперь создадим объект прохода рендера. Для этого заполним структуру VkRenderPassCreateInfo массивом буферов и подпроходами рендера. Обратите внимание, объекты VkAttachmentReference используют индексы из этого массива (прим. переводчика: видимо, имеется в виду массив
Мы будем ссылаться на проход рендера на протяжении всего жизненного цикла программы, поэтому его нужно очистить в самом конце:
Мы проделали большую работу, и осталось лишь собрать все воедино, чтобы наконец-то создать графический конвейер!
C++ code / Vertex shader / Fragment shader
Теперь мы можем объединить все структуры и объекты, чтобы создать графический конвейер!
Давайте вспомним, какие объекты у нас уже есть:
Все эти объекты полностью определяют функционал графического конвейера. С ними можно начать заполнение структуры VkGraphicsPipelineCreateInfo. Сделаем это в конце функции
Начнем с указателя на массив структур VkPipelineShaderStageCreateInfo.
Затем заполним указатели на все структуры, описывающие непрограммируемые стадии конвейера.
После этого укажем layout конвейера, который является дескриптором Vulkan, а не указателем на структуру.
В конце сделаем ссылку на проход (render pass) и номер подпрохода (subpass), который используется в создаваемом пайплайне. Во время рендера можно использовать и другие объекты прохода, но они должны быть совместимы с нашим
Остались два параметра —
Прежде чем завершить создание конвейера создадим член класса для хранения объекта VkPipeline:
И наконец создадим графический конвейер:
Функция vkCreateGraphicsPipelines содержит больше параметров, чем обычная функция создания объектов в Vulkan. За один вызов она позволяет создать несколько объектов VkPipeline из массива структур VkGraphicsPipelineCreateInfo.
Параметр VkPipelineCache необязательный, поэтому мы передаем
Графический конвейер понадобится для всех операций рисования, поэтому он должен быть уничтожен в самом конце:
Теперь запустим программу, чтобы убедиться, что конвейер создан успешно! Совсем скоро мы сможем увидеть результат нашей работы на экране. В следующих главах мы создадим фреймбуферы на основе images из swap chain и подготовим команды рисования.
C++ code / Vertex shader / Fragment shader
Сегодня хочу поделиться переводом заключительных глав раздела, посвященного графическому конвейеру (Graphics pipeline basics), — Render passes и Conclusion.
Содержание
1. Вступление
2. Краткий обзор
3. Настройка окружения
4. Рисуем треугольник
5. Буферы вершин
6. Uniform-буферы
7. Текстурирование
8. Буфер глубины
9. Загрузка моделей
10. Создание мип-карт
11. Multisampling
FAQ
Политика конфиденциальности
2. Краткий обзор
3. Настройка окружения
4. Рисуем треугольник
- Подготовка к работе
- Базовый код
- Экземпляр (instance)
- Слои валидации
- Физические устройства и семейства очередей
- Логическое устройство и очереди
- Отображение на экране
- Window surface
- Swap chain
- Image views
- Графический конвейер (pipeline)
- Вступление
- Шейдерные модули
- Непрограммируемые этапы
- Проходы рендера
- Заключение
- Отрисовка
- Повторное создание цепочки показа
5. Буферы вершин
- Описание
- Создание буфера вершин
- Staging буфер
- Буфер индексов
6. Uniform-буферы
- Дескриптор layout и буфера
- Дескриптор пула и sets
7. Текстурирование
- Изображения
- Image view и image sampler
- Комбинированный image sampler
8. Буфер глубины
9. Загрузка моделей
10. Создание мип-карт
11. Multisampling
FAQ
Политика конфиденциальности
Проходы рендера
- Подготовка
- Настройка буферов (attachments)
- Подпроходы (subpasses)
- Проход рендера (render pass)
Подготовка
Прежде чем завершить создание графического конвейера нужно сообщить Vulkan, какие буферы (attachments) будут использоваться во время рендеринга. Необходимо указать, сколько будет буферов цвета, буферов глубины и сэмплов для каждого буфера. Также нужно указать, как должно обрабатываться содержимое буферов во время рендеринга. Вся эта информация обернута в объект прохода рендера (render pass), для которого мы создадим новую функцию
createRenderPass
. Вызовем эту функцию из initVulkan
перед createGraphicsPipeline
.void initVulkan() {
createInstance();
setupDebugMessenger();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapChain();
createImageViews();
createRenderPass();
createGraphicsPipeline();
}
...
void createRenderPass() {
}
Настройка буферов (attachments)
Мы используем только один цветовой буфер, представленный одним из images в swap chain.
void createRenderPass() {
VkAttachmentDescription colorAttachment{};
colorAttachment.format = swapChainImageFormat;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
}
Формат цветового буфера (поле
format
) должен соответствовать формату image из swap chain, и поскольку мы пока не задействуем мультисэмплинг, нам понадобится только 1 сэмпл.colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
loadOp
и storeOp
указывают, что делать с данными буфера перед рендерингом и после него. Для loadOp
возможны следующие значения:VK_ATTACHMENT_LOAD_OP_LOAD
: буфер будет содержать те данные, которые были помещены в него до этого прохода (например, во время предыдущего прохода)VK_ATTACHMENT_LOAD_OP_CLEAR
: буфер очищается в начале прохода рендераVK_ATTACHMENT_LOAD_OP_DONT_CARE
: содержимое буфера не определено; для нас оно не имеет значения
Мы будем использовать
VK_ATTACHMENT_LOAD_OP_CLEAR
, чтобы заполнить фреймбуфер черным цветом перед отрисовкой нового фрейма. Для
storeOp
возможны только два значения:VK_ATTACHMENT_STORE_OP_STORE
: содержимое буфера сохраняется в память для дальнейшего использованияVK_ATTACHMENT_STORE_OP_DONT_CARE
: после рендеринга буфер больше не используется, и его содержимое не имеет значения
Нам нужно вывести отрендеренный треугольник на экран, поэтому перейдем к операции сохранения:
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
loadOp
и storeOp
применяются к буферам цвета и глубины. Для буфера трафарета используются поля stencilLoadOp
/stencilStoreOp
. Мы не используем буфер трафарета, поэтому результаты загрузки и сохранения нас не интересуют.colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
Текстуры и фреймбуферы в Vulkan — это объекты VkImage с определенным форматом пикселей, однако layout пикселей в памяти может меняться в зависимости от того, что вы хотите сделать с image.
Вот некоторые из наиболее распространенных layout-ов:
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
: images используются в качестве цветового буфераVK_IMAGE_LAYOUT_PRESENT_SRC_KHR
: images используются для показа на экранеVK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
: image принимает данные во время операций копирования
Более подробно мы обсудим эту тему в главе, посвященной текстурированию. Сейчас нам важно, чтобы images были переведены в layouts, подходящие для дальнейших операций.
В
initialLayout
указывается layout, в котором будет image перед началом прохода рендера. В finalLayout
указывается layout, в который image будет автоматически переведен после завершения прохода рендера. Значение VK_IMAGE_LAYOUT_UNDEFINED
в поле initialLayout
обозначает, что нас не интересует предыдущий layout, в котором был image. Использование этого значения не гарантирует сохранение содержимого image, но это и не важно, поскольку мы все равно очистим его. После рендеринга нам нужно вывести наш image на экран, поэтому в поле finalLayout
укажем VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
.Подпроходы (subpasses)
Один проход рендера может состоять из множества подпроходов (subpasses). Подпроходы — это последовательные операции рендеринга, зависящие от содержимого фреймбуферов в предыдущих проходах. К ним относятся, например, эффекты постобработки, применяемые друг за другом. Если объединить их в один проход рендера, Vulkan сможет перегруппировать операции для лучшего сохранения пропускной способности памяти и большей производительности (прим. переводчика: видимо, имеется в виду тайловый рендеринг). Однако мы будем использовать для нашего треугольника только один подпроход.
Каждый подпроход ссылается на один или несколько attachment-ов. Эти отсылки представляют собой структуры VkAttachmentReference:
VkAttachmentReference colorAttachmentRef{};
colorAttachmentRef.attachment = 0;
colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
В поле
attachment
указывается порядковый номер буфера в массиве, на который ссылается подпроход. Наш массив состоит только из одного буфера VkAttachmentDescription, его индекс равен 0
. В поле layout
мы указываем layout буфера во время подпрохода, ссылающегося на этот буфер. Vulkan автоматически переведет буфер в этот layout, когда начнется подпроход. Мы используем attachment в качестве буфера цвета, и layout VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
обеспечит нам самую высокую производительность.Подпроход описывается с помощью структуры VkSubpassDescription:
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
Мы должны явно указать, что это графический подпроход, поскольку не исключено, что в будущем Vulkan может поддерживать вычислительные подпроходы. После этого укажем ссылку на цветовой буфер:
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;
Директива
layout(location = 0) out vec4 outColor
ссылается именно на порядковый номер буфера в массиве subpass.pColorAttachments
.Подпроход может ссылаться на следующие типы буферов:
pInputAttachments
: буферы, содержимое которых читается из шейдераpResolveAttachments
: буферы, которые используются для цветовых буферов с мультисэмплингомpDepthStencilAttachment
: буферы глубины и трафаретаpPreserveAttachments
: буферы, которые не используются в текущем подпроходе, но данные которых должны быть сохранены
Проход рендера (render pass)
Теперь, когда буфер и подпроход, ссылающийся на этот буфер, описаны, мы можем создать сам проход рендера. Перед
pipelineLayout
создадим новую переменную-член класса для хранения объекта VkRenderPass:VkRenderPass renderPass;
VkPipelineLayout pipelineLayout;
Теперь создадим объект прохода рендера. Для этого заполним структуру VkRenderPassCreateInfo массивом буферов и подпроходами рендера. Обратите внимание, объекты VkAttachmentReference используют индексы из этого массива (прим. переводчика: видимо, имеется в виду массив
renderPassInfo.pAttachments
). VkRenderPassCreateInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassInfo.attachmentCount = 1;
renderPassInfo.pAttachments = &colorAttachment;
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;
if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) {
throw std::runtime_error("failed to create render pass!");
}
Мы будем ссылаться на проход рендера на протяжении всего жизненного цикла программы, поэтому его нужно очистить в самом конце:
void cleanup() {
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
vkDestroyRenderPass(device, renderPass, nullptr);
...
}
Мы проделали большую работу, и осталось лишь собрать все воедино, чтобы наконец-то создать графический конвейер!
C++ code / Vertex shader / Fragment shader
Заключение
Теперь мы можем объединить все структуры и объекты, чтобы создать графический конвейер!
Давайте вспомним, какие объекты у нас уже есть:
- Шейдеры: шейдерные модули, определяющие функционал программируемых стадий конвейера
- Непрограммируемые стадии: структуры, описывающие работу конвейера на непрограммируемых стадиях, таких как input assembler, растеризатор, вьюпорт и функция смешивания цветов
- Layout конвейера: описание uniform-переменных и push-констант, которые используются конвейером и которые могут обновляться динамически
- Проход рендера (render pass): описания буферов (attachments), в которые будет производиться рендер
Все эти объекты полностью определяют функционал графического конвейера. С ними можно начать заполнение структуры VkGraphicsPipelineCreateInfo. Сделаем это в конце функции
createGraphicsPipeline
, но перед вызовами vkDestroyShaderModule, поскольку шейдерные модули будут использоваться во время создания конвейера.VkGraphicsPipelineCreateInfo pipelineInfo{};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount = 2;
pipelineInfo.pStages = shaderStages;
Начнем с указателя на массив структур VkPipelineShaderStageCreateInfo.
pipelineInfo.pVertexInputState = &vertexInputInfo;
pipelineInfo.pInputAssemblyState = &inputAssembly;
pipelineInfo.pViewportState = &viewportState;
pipelineInfo.pRasterizationState = &rasterizer;
pipelineInfo.pMultisampleState = &multisampling;
pipelineInfo.pDepthStencilState = nullptr; // Optional
pipelineInfo.pColorBlendState = &colorBlending;
pipelineInfo.pDynamicState = nullptr; // Optional
Затем заполним указатели на все структуры, описывающие непрограммируемые стадии конвейера.
pipelineInfo.layout = pipelineLayout;
После этого укажем layout конвейера, который является дескриптором Vulkan, а не указателем на структуру.
pipelineInfo.renderPass = renderPass;
pipelineInfo.subpass = 0;
В конце сделаем ссылку на проход (render pass) и номер подпрохода (subpass), который используется в создаваемом пайплайне. Во время рендера можно использовать и другие объекты прохода, но они должны быть совместимы с нашим
renderPass
. Требования к совместимости вы можете найти здесь, однако в руководстве мы будем использовать только один проход. pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // Optional
pipelineInfo.basePipelineIndex = -1; // Optional
Остались два параметра —
basePipelineHandle
и basePipelineIndex
. Vulkan позволяет создать производный графический конвейер из существующего конвейера. Суть в том, что создание производного конвейера не требует больших затрат, поскольку большинство функций берется из родительского конвейера. Также переключение между дочерними конвейерами одного родителя осуществляется намного быстрее. В поле basePipelineHandle
вы можете указать дескриптор существующего конвейера, либо сделать отсылку к другому конвейеру, который будет создан по индексу, в поле basePipelineIndex
. У нас только один конвейер, поэтому укажем VK_NULL_HANDLE
и невалидный порядковый номер. Эти значения используются только в том случае, если в VkGraphicsPipelineCreateInfo в поле flags
указано VK_PIPELINE_CREATE_DERIVATIVE_BIT
.Прежде чем завершить создание конвейера создадим член класса для хранения объекта VkPipeline:
VkPipeline graphicsPipeline;
И наконец создадим графический конвейер:
if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) {
throw std::runtime_error("failed to create graphics pipeline!");
}
Функция vkCreateGraphicsPipelines содержит больше параметров, чем обычная функция создания объектов в Vulkan. За один вызов она позволяет создать несколько объектов VkPipeline из массива структур VkGraphicsPipelineCreateInfo.
Параметр VkPipelineCache необязательный, поэтому мы передаем
VK_NULL_HANDLE
. Кэш конвейера может использоваться для хранения и повторного использования данных, связанных с созданием конвейера. Он совместно используется множеством вызовов vkCreateGraphicsPipelines и даже может быть сохранен на диск для переиспользования при последующих запусках программы. Впоследствии это может значительно ускорить процесс создания конвейера. Мы еще вернемся к этой теме в главе, посвященной кэшу конвейера.Графический конвейер понадобится для всех операций рисования, поэтому он должен быть уничтожен в самом конце:
void cleanup() {
vkDestroyPipeline(device, graphicsPipeline, nullptr);
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
...
}
Теперь запустим программу, чтобы убедиться, что конвейер создан успешно! Совсем скоро мы сможем увидеть результат нашей работы на экране. В следующих главах мы создадим фреймбуферы на основе images из swap chain и подготовим команды рисования.
C++ code / Vertex shader / Fragment shader