steps3D - Tutorials - Vulkan. Часть 5

Vulkan. Рендеринг треугольника

"You useed to be much more muchier

You've lost your muchness".

The Mad Hatter

В отличии от OpenGL, где вы сразу получаете пригодный для рендеринга фреймбуфер, у Vulkan нет фреймбуфера по умолчанию. Если вы хотите осуществить рендеринг в окно, то вам необходимо самому об этом позаботиться (а это будет совсем не просто).

Соответствующая часть зависит от платформы и поэтому вынесена в ряд расширений. Часть этой работы возьмет на себя библиотека GLFW, но довольно большой объем работы нам придется проделать самим.

Необходимо вручную создать фреймбуфер (а обычно не один, а несколько для поддержки двойной и тройной буферизации) и объект swap chain. Этот объект отвечает за список изображений, в которые можно осуществлять рендеринг и которые можно показывать в окне. При этом у нас всегда есть изображение/фреймбуфер, готовый для рендеринга, и есть изображение, готовое для показа.

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

Создание изображения (VkImage) и VkImageView

Еще одним отличием OpenGL от Vulkan является то, что необходимо явно создать все необходимые для фреймбуфера изображения. Мы начнем с создания простого изображения, которому в Vulkan соответствует непрозрачный тип VkImage. Для создания его служит функция vkCreateImage (и функции vDestroyImage для его уничтожения). Все входные передаются через поля структуры VkImageCreateInfo.

    // create image for rendering into
VkImageCreateInfo imageInfo = {};
VkImage           image     = VK_NULL_HANDLE;

imageInfo.sType         = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType     = VK_IMAGE_TYPE_2D;
imageInfo.extent.width  = 512;
imageInfo.extent.height = 512;
imageInfo.extent.depth  = 1;
imageInfo.mipLevels     = 1;
imageInfo.arrayLayers   = 1;
imageInfo.format        = VK_FORMAT_R8G8B8A8_UNORM;
imageInfo.tiling        = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage         = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |  VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
imageInfo.samples       = VK_SAMPLE_COUNT_1_BIT;
imageInfo.sharingMode   = VK_SHARING_MODE_EXCLUSIVE;

if ( vkCreateImage ( device, &imageInfo, nullptr, &image ) != VK_SUCCESS ) 
    fatal () << "Image: Cannot create image" << std::endl;

В поле sType содержится идентификатор структуры (VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO). В поле imageType задается тип создаваемого изображения, в нашем случае это будет VK_IMAGE_TYPE_2D. Размер изображения задается в поле extentwidth и height). В mipLevels задается требуемое число слоев в mipmap-пирамиде, а в arrayLayers число слоев (для случая когда у нас кубическая текстура или текстурный массив). Поле format задает формат изображения (мы будем использовать значение VK_FORMAT_R8G8B8A8_UNORM, а поле samples содержит число сэмплов для текстуры с мультисэмплингом (в нашем случае это VK_SAMPLE_COUNT_1_BIT).

Гораздо более интересными являются поля tiling и initialLayout. Если поле format задает строение отдельного тексела, то мы обычно считаем, что дальше все текселы лежат в линейном порядке (один за другим сева направо по строкам). На самом деле обычно это не так. Подобное размещение данных в памяти удобно для CPU, но для GPU оно приводит к замедлению работы. Поэтому обычно изображение хранится в памяти GPU в каком-то специальном внутреннем представлении, обычно оптимизированном под конкретную задачу.

Параметр tiling (принимающий всего два возможных значения - VK_IMAGE_TILING_OPTIMAL и VK_IMAGE_TILING_LINEAR) как раз и отвечает за то, как надо размещать текселы в памяти, традиционным (линейным) способом или же оптимизированным под задачи GPU. В случае значения VK_IMAGE_TILING_LINEAR текселы размещаются в памяти линейным образом, один за другим слева направо строка за строкой. Это очень удобно для чтения или записи со стороны CPU, но будет крайне неэффективным при рендеринге. Второй вариант - VK_IMAGE_TILING_OPTIMAL - задает использование какого-то другого, оптимизированного под работу со стороны GPU, размещения в памяти. Обратите внимание, что обычно изображения с tiling равным VK_IMAGE_TILING_LINEAR поддерживают очень небольшое число операций над ними и работа с ними обычно происходит заметно медленнее, чем для оптимизированного.

Кроме tiling у изображения есть также memory layout, т.е. раскладка в памяти для конкретного применения. Если tiling для изображения нельзя изменить - оно задается всего один раз при создании изображения и таким остается все время, то memory layout можно (и нужно) переводить из одного состояние в другое. Каждый memory layout соответствует какому-то конкретному применению и для него выполняет одну базовую задачу - снижение bandwidth. Так для изображений, в которые производится рендеринг, довольно часто применяются различные способы сжатия данных.

Так, если мы используем изображение для рендеринга, то тогда оно должны иметь memory layout равный VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, если мы хотим показать его в окне, то используется memory layout равный VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, а если мы хотим читать из него в шейдере, то VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL. И в поле initialLayout мы как раз и задаем начальное значение для memory layout.

Далее нужно выделить память GPU для этого изображения и прикрепить ее к нему при помощи вызова vkBindImageMemory.

VkMemoryRequirements memRequirements;

vkGetImageMemoryRequirements ( device, image, &memRequirements );

    // allocate memory with required properties
VkMemoryAllocateInfo    allocInfo  = {};
VkDeviceMemory          memory     = VK_NULL_HANDLE;
VkMemoryPropertyFlags   properties = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;

allocInfo.sType                = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize       = memRequirements.size;
allocInfo.memoryTypeIndex      = findMemoryType ( physicalDevice, memRequirements.memoryTypeBits, properties );

if ( vkAllocateMemory ( device, &allocInfo, nullptr, &memory ) != VK_SUCCESS )
    fatal () << "Cannot allocate memory for buffer" << std::endl;

vkBindImageMemory ( device, image, memory, 0 );

Вся работа с изображением идет не через VkImage, а через его вид (view) - VkImageView. Его нам тоже нужно создать, соответствующий код приводится ниже.

     // create imageview for the image
VkImageView             imageView = VK_NULL_HANDLE;
VkImageViewCreateInfo   viewInfo  = {};
        
viewInfo.sType                           = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image                           = image;
viewInfo.viewType                        = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format                          = VK_FORMAT_R8G8B8A8_UNORM;
viewInfo.subresourceRange.aspectMask     = VK_IMAGE_ASPECT_COLOR_BIT;
viewInfo.subresourceRange.baseMipLevel   = 0;
viewInfo.subresourceRange.levelCount     = 1;
viewInfo.subresourceRange.baseArrayLayer = 0;
viewInfo.subresourceRange.layerCount     = 1;

if ( vkCreateImageView ( device, &viewInfo, nullptr, &imageView ) != VK_SUCCESS )
    fatal () << "failed to create texture image view!" << std::endl;

Создание фреймбуфера и прохода рендеринга

Следующим нашим шагом будет создание на основе изображения и его вида фреймбуфера, где наше изображение (точнее, его вид) будет выступать в качестве цветового подключения (color attachment).

Мы начнем с создания фреймбуфера и первым шагом будем создание прохода рендеринга (render pass). Проход содержит в себе массив подключений (цветовых и глубины) и массив подпроходов рендеринга (subpass). На самом деле подобные структуры в первую очередь нужны именно для мобильных платформ (тайлового рендеринга) - именно там они могут принести пользу.

Каждое цветовое (и не только цветовое) подключение задается экземпляром структуры VkAttachmentDescription. Обратите внимание на его поля loadOp, storeOp, stencilLoadOp> и stencilStoreOp. Поля *loadOp задают операции инициализации для соответствующего подключения. У нас в качестве операции инициализации будет выступать VK_ATTACHMENT_LOAD_OP_CLEAR, задающая очистку заданным в графическом конвейере значением. Если мы зададим в качестве операции инициализации значение VK_ATTACHMENT_LOAD_OP_DONT_CARE, то это говорит о том, что о инициализация не нужна. Поля *storeOp задают операции записи. Аналогично задания в качестве операции записи VK_ATTACHMENT_STORE_OP_DONT_CARE говорит о том, что содержимое этого подключения далее нам не будет нужно.

    // create renderpass for framebuffer creation
VkAttachmentDescription colorAttachment{};

colorAttachment.format         = VK_FORMAT_R8G8B8A8_UNORM;
colorAttachment.samples        = VK_SAMPLE_COUNT_1_BIT;
colorAttachment.loadOp         = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp        = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.stencilLoadOp  = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colorAttachment.initialLayout  = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachment.finalLayout    = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

VkAttachmentReference colorAttachmentRef = {};

colorAttachmentRef.attachment = 0;
colorAttachmentRef.layout     = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

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

     // create renderpass for framebuffer creation
VkSubpassDescription subpass = {};

subpass.pipelineBindPoint    = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments    = &colorAttachmentRef;

После этого мы готовы создать сам проход рендеринга - объект типа VkRenderPass:

// create renderpass for framebuffer creation
VkRenderPass            renderPass;
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 )
    fatal () << "failed to create render pass!" << std::endl;

Сам код для создания фреймбуфера прост и прямолинеен - мы просто ссылаемся на ранее созданные объекты и задаем размер фреймбуфера:

    // create framebuffer for rendering into
VkFramebuffer           framebuffer     = VK_NULL_HANDLE;
VkImageView             attachments []  = { imageView };
VkFramebufferCreateInfo framebufferInfo = {};
            
framebufferInfo.sType           = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebufferInfo.renderPass      = renderPass;
framebufferInfo.attachmentCount = 1;
framebufferInfo.pAttachments    = attachments;
framebufferInfo.width           = 512;
framebufferInfo.height          = 512;
framebufferInfo.layers          = 1;

if ( vkCreateFramebuffer ( device, &framebufferInfo, nullptr, &framebuffer ) != VK_SUCCESS )
    fatal () << "failed to create framebuffer!" << std::endl;

Создание графического конвейера

Теперь мы можем приступить и к созданию графического конвейера - объекта типа VkPipeline. Для его создания используется специальная инициализирующая структура - VkPipelineCreateInfo. На самом деле данная структура содержит в себе ссылки на ряд вспомогательных структур, задающих отдельных групп свойств графического конвейера. Фактически мы должны задать в VkPipelineCreateInfo все свойства и параметры графического конвейера (которые в OpenGL задаются большим числом различных команд, но обычно мы все их вместе никогда не задаем).

Задание шейдеров

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

    // load shaders from .spv files
VkShaderModule              vertShader = VK_NULL_HANDLE;
Data                        vertSource ( "shaders/simple.vert.spv" );
VkShaderModuleCreateInfo    moduleCreateInfo = {};

moduleCreateInfo.sType    = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
moduleCreateInfo.codeSize = vertSource.getLength ();
moduleCreateInfo.pCode    = reinterpret_cast<const uint32_t*>( vertSource.getPtr () );

if ( vkCreateShaderModule ( device, &moduleCreateInfo, nullptr, &vertShader ) != VK_SUCCESS )
    fatal () << "Failed to create shader module! " << std::endl;

VkShaderModule              fragShader = VK_NULL_HANDLE;
Data                        fragSource ( "shaders/simple.frag.spv" );

moduleCreateInfo.sType    = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
moduleCreateInfo.codeSize = fragSource.getLength ();
moduleCreateInfo.pCode    = reinterpret_cast<const uint32_t*>( fragSource.getPtr () );

if ( vkCreateShaderModule ( device, &moduleCreateInfo, nullptr, &fragShader ) != VK_SUCCESS )
    fatal () << "Failed to create shader module! " << std::endl;

Как и ранее для каждого шейдера мы заводим свой элемент в структуре VkPipelineShaderStageCreateInfo.

VkPipelineShaderStageCreateInfo vertShaderStageInfo = {};
VkPipelineShaderStageCreateInfo fragShaderStageInfo = {};

vertShaderStageInfo.sType  = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vertShaderStageInfo.stage  = VK_SHADER_STAGE_VERTEX_BIT;
vertShaderStageInfo.module = vertShader;
vertShaderStageInfo.pName  = "main";
fragShaderStageInfo.sType  = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fragShaderStageInfo.stage  = VK_SHADER_STAGE_FRAGMENT_BIT;
fragShaderStageInfo.module = fragShader;
fragShaderStageInfo.pName  = "main";

VkPipelineShaderStageCreateInfo shaderStages[] = { vertShaderStageInfo, fragShaderStageInfo };

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

#version 450

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

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

#version 450

layout ( location = 0 ) out vec4 color;

void main() 
{
    color = vec4 ( 0.0, 1.0, 0.0, 1.0 );        // green
}

Задание типа входных примитивов

Следующей структурой, описывающей графический конвейер, которую мы рассмотрим, будет VkPipelineInputAssemblyCreateInfo. Она описывает блок сборки примитивов. Помимо стандартный полей (sType и pNext) она содержит также следующие поля - flags, topology и primitiveRestartEnable. Поле flags зарезервировано для дальнейшего использования. Поле topology задает тип входных примитивов (VK_PRIMITIVE_TOPOLOGY_POINT_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_LINE_WITH_ADJACENCY, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY, VK_PRIMITIVE_TOPOLOGY_PATCH_LIST).

Поле primitiveRestartEnable позволяет использовать специальное значение индекса как маркер начала нового примитива. Это работает только для рендеринга с использованием индексов и этим значением (в зависимости от типа индекса) является 0xFF, 0xFFFF, 0xFFFFFFFF.

Vertex Input

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

     // setup vertex assembly
    VkPipelineInputAssemblyStateCreateInfo  inputAssembly = {};

    inputAssembly.sType                  = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
    inputAssembly.topology               = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
    inputAssembly.primitiveRestartEnable = VK_FALSE;

Задание области вывода (view) и области отсечения (scissor)

Также мы должны задать область вывода (viewport) и область отсечения (scissor). Для задания области вывода служит структура VkViewport, а для задания области отсечения - структура VkRect2D. Все данные по области вывода и области отсечения задаются в графическом конвейере при помощи структуры VkPipelineViewportStateCreateInfo.

VkViewport  viewport = {};
VkRect2D    scissor  = {};

viewport.x            = 0.0f;
viewport.y            = 0.0f;
viewport.width        = 512.0f;
viewport.height       = 512.0f;
viewport.minDepth     = 0.0f;
viewport.maxDepth     = 1.0f;
scissor.offset        = {0, 0};
scissor.extent.width  = 512;
scissor.extent.height = 512;

VkPipelineViewportStateCreateInfo   viewportState = {};

viewportState.sType         = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports    = &viewport;
viewportState.scissorCount  = 1;
viewportState.pScissors     = &scissor;

Задание растеризации

Далее мы задаем состояние растеризатора, для чего служит структура VkPipelineRasterizationStateCreateInfo. Наиболее важными ее полями являются polygonMode (режим вывода полигонов, может принимать значения VK_POLYGON_MODE_FILL, V_POLYGON_MODE_LINE, VK_POLYGON_MODE_POINT), lineWidth (толщина линии, обратите внимание, что задается числом типа float), cullMode (задает отсечение примитивов по их ориентации, допустимые значения VK_CULL_MODE_FRONT_BIT, VK_CULL_MODE_BACKK_BIT, VK_CULL_MODE_FRONT_AND_BACK), frontFace (задает какие грани будут считаться лицевыми, возможными значениями являются VK_FRONT_FACE_COUNTER_CLOCKWISEm VK_FRONT_FACE_CLOCKWISE). Также при помощи этой структуры можно задавать depth bias.

    // setup rasterizer
VkPipelineRasterizationStateCreateInfo  rasterizer = {};

rasterizer.sType                   = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizer.depthClampEnable        = VK_FALSE;
rasterizer.polygonMode             = VK_POLYGON_MODE_FILL;
rasterizer.lineWidth               = 1.0f;
rasterizer.cullMode                = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace               = VK_FRONT_FACE_CLOCKWISE;
rasterizer.depthBiasEnable         = VK_FALSE;
rasterizer.depthBiasConstantFactor = 0.0f;          // Optional
rasterizer.depthBiasClamp          = 0.0f;          // Optional
rasterizer.depthBiasSlopeFactor    = 0.0f;          // Optional

Задание мультисэмплинга

Далее при помощи структуры VkPipelineMultisampleStateCreateInfo задаются параметры мультисэмпинга.

    // setup multisampling
VkPipelineMultisampleStateCreateInfo    multisampling = {};

multisampling.sType                 = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampling.sampleShadingEnable   = VK_FALSE;
multisampling.rasterizationSamples  = VK_SAMPLE_COUNT_1_BIT;
multisampling.minSampleShading      = 1.0f;         // Optional
multisampling.pSampleMask           = nullptr;      // Optional
multisampling.alphaToCoverageEnable = VK_FALSE;     // Optional
multisampling.alphaToOneEnable      = VK_FALSE;     // Optional

Задание смешивания цветов

Теперь нам нужно задать параметры смешивания цветов (alpha blending). Для этого нам понадобятся структуры VkPipelineColorBlendingAttachmentState и VkPipelineColorBlendStateCreateInfo. Использование сразу двух структур связано с тем, что у нас может быть несколько цветовых подключений, в которые мы осуществляем рендеринг, и для каждого из них мы можем задать свой режим смешивания цветов. Структура VkPipelineColorBlendingAttachmentState как раз и служит для задания смешивания цветов для одного цветового подключения.

Ее поле blendEnable задает включено ли смешивание цветов для данного цветового подключения (VK_TRUE) или нет (VK_FALSE). Еще четыре поля (srcColorBlendFactor, dstColorBlendFactor, srcAlphaBlendFactor, dstAlphaBlendFactor) принимают значения типа VkBlendFactor. Они задают закон вычисления коэффициентов смешивания для цвета и альфы и принимают следующие значения - VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_SRC_COLOR, VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR, VK_BLEND_FACTOR_DST_COLOR, VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_FACTOR_DST_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA, VK_BLEND_FACTOR_CONSTANT_COLOR, VK_BLEND_FACTOR_CONSTANT_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR, VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA, VK_BLEND_FACTOR_ALPHA_SATURATE, VK_BLEND_FACTOR_SRC1_COLOR, VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR, VK_BLEND_FACTOR_SRC1_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA. Эти значения полностью аналогичны своим эквивалентам в OpenGL.

Поле colorBlendOp задает используемую для смешивания цветов операцию. Возможными значениями для этого поля являются - VK_BLEND_OP_ADD, VK_BLEND_OP_SUBTRACT, VK_BLEND_OP_REVERSE_SUBTRACT, VK_BLEND_OP_MIN, VK_BLEND_OP_MAX.

И наконец поле colorWriteMask это просто битовая маска, позволяющая задать в какие из каналов RGBA разрешена запись.

    // setup color blending
VkPipelineColorBlendAttachmentState colorBlendAttachment = {};

colorBlendAttachment.colorWriteMask      = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | 
                                           VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
colorBlendAttachment.blendEnable         = VK_FALSE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE;     // Optional
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO;    // Optional
colorBlendAttachment.colorBlendOp        = VK_BLEND_OP_ADD;         // Optional
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;     // Optional
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;    // Optional
colorBlendAttachment.alphaBlendOp        = VK_BLEND_OP_ADD;         // Optional

Полное описание смешивания цветов в конвейере задается в структуре VkPipelineColorBlendStateCreateInfo. Поля logicOpEnable и loginOp позволяют задать применение побитовых операций при записи в фреймбуфер (точнее, в цветовые подключения) вместо обычной записи. В blendConstants находится массив из четырех элементов типа float, задающих константный RGBA цвет для смешивания. В полях attachmentCount и pAttachments содержится массив указателей на структуры VkPipelineColorBlendingAttachmentState по одному для каждого используемого цветового подключения.

VkPipelineColorBlendStateCreateInfo colorBlending = {};

colorBlending.sType             = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.logicOpEnable     = VK_FALSE;
colorBlending.logicOp           = VK_LOGIC_OP_COPY;             // Optional
colorBlending.attachmentCount   = 1;
colorBlending.pAttachments      = &colorBlendAttachment;
colorBlending.blendConstants[0] = 0.0f;                         // Optional
colorBlending.blendConstants[1] = 0.0f;                         // Optional
colorBlending.blendConstants[2] = 0.0f;                         // Optional
colorBlending.blendConstants[3] = 0.0f;                         // Optional

Создание pipeline layout

Теперь, как и в случае с вычислительным шейдером, необходимо создать pipeline layout, описывающий передаваемые ресурсы - множество set layout-ов и push-констант. В нашем случае у конвейера нет ни того, ни другого.

    // setup pipeline layout
VkPipelineLayout            pipelineLayout;
VkPipelineLayoutCreateInfo  pipelineLayoutInfo = {};

pipelineLayoutInfo.sType                  = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount         = 0;              // Optional
pipelineLayoutInfo.pSetLayouts            = nullptr;        // Optional
pipelineLayoutInfo.pushConstantRangeCount = 0;              // Optional
pipelineLayoutInfo.pPushConstantRanges    = nullptr;        // Optional

if ( vkCreatePipelineLayout ( device, &pipelineLayoutInfo, nullptr, &pipelineLayout ) != VK_SUCCESS )
    fatal () << "failed to create pipeline layout!" << std::endl;

Собираем все свойства конвейера вместе

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

VkGraphicsPipelineCreateInfo    pipelineInfo = {};
VkPipeline                      pipeline;

pipelineInfo.sType               = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount          = 2;
pipelineInfo.pStages             = shaderStages;
pipelineInfo.pVertexInputState   = &vertexInputInfo;
pipelineInfo.pInputAssemblyState = &inputAssembly;
pipelineInfo.pViewportState      = &viewportState;
pipelineInfo.pRasterizationState = &rasterizer;
pipelineInfo.pMultisampleState   = &multisampling;
pipelineInfo.pDepthStencilState  = nullptr;             // Optional
pipelineInfo.pColorBlendState    = &colorBlending;
//pipelineInfo.pDynamicState     = &dynamicState;
pipelineInfo.layout              = pipelineLayout;
pipelineInfo.renderPass          = renderPass;
pipelineInfo.subpass             = 0;

if ( vkCreateGraphicsPipelines ( device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline ) != VK_SUCCESS )
    fatal () << "failed to create graphics pipeline!" << std::endl;

Создание и подготовка командного буфера

После создания графического конвейера необходимо создать и заполнить командами командный буфер. Отличиями от ранее рассмотренного случая вычислительного шейдера является использование команд vkCmdBeginRenderPass и vkCmdEndRenderPass для обозначения прохода рендеринга (он будет у нас всего один) и использование команды vkCmdDraw для рендеринга одного треугольника. Обратите внимание, что для каждого подключения к фреймбуферу нам необходимо задать свое значение для его очистки (в правильном порядке).

VkCommandBuffer             commandBuffer;
VkCommandBufferAllocateInfo cbAllocInfo = {};

cbAllocInfo.sType              = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
cbAllocInfo.commandPool        = commandPool;
cbAllocInfo.level              = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
cbAllocInfo.commandBufferCount = 1;

if ( vkAllocateCommandBuffers ( device, &cbAllocInfo, &commandBuffer ) != VK_SUCCESS )
    fatal () << "failed to allocate command buffers!" << std::endl;
    
VkCommandBufferBeginInfo    beginInfo = {};

beginInfo.sType            = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags            = 0;             // Optional
beginInfo.pInheritanceInfo = nullptr;       // Optional

if ( vkBeginCommandBuffer ( commandBuffer, &beginInfo ) != VK_SUCCESS )
    fatal () << "failed to begin recording command buffer!" << std::endl;
    
VkRenderPassBeginInfo   rpInfo = {};
VkClearValue            clearColor     = {{{0.0f, 0.0f, 0.0f, 1.0f}}};

rpInfo.sType             = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
rpInfo.renderPass        = renderPass;
rpInfo.framebuffer       = framebuffer;
rpInfo.renderArea.offset = { 0, 0 };
rpInfo.renderArea.extent = { 512, 512 };
rpInfo.clearValueCount   = 1;
rpInfo.pClearValues      = &clearColor;

vkCmdBeginRenderPass ( commandBuffer, &rpInfo, VK_SUBPASS_CONTENTS_INLINE );
vkCmdBindPipeline    ( commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline );
vkCmdDraw            ( commandBuffer, 3, 1, 0, 0 );
vkCmdEndRenderPass   ( commandBuffer );

if ( vkEndCommandBuffer ( commandBuffer ) != VK_SUCCESS )
    fatal () << "failed to record command buffer!" << std::endl;

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

После этого мы можем вызывать функцию saveScreenshot для сохранения результатов рендеринга в файл в формате PNG и освободить все ранее выделенные ресурсы. Работу функции saveScreenshot мы не будем сейчас разбирать, чтобы не залезать в детали работы с изображениями.

VkFence                 fence             = VK_NULL_HANDLE;
VkFenceCreateInfo       fenceCreateInfo   = {};

fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;

if ( vkCreateFence ( device, &fenceCreateInfo, nullptr, &fence ) != VK_SUCCESS )
    fatal () << "Semaphore: error creating" << std::endl;

VkSubmitInfo    submitInfo = {};

submitInfo.sType                = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.waitSemaphoreCount   = 0;
submitInfo.pWaitSemaphores      = nullptr;
submitInfo.pWaitDstStageMask    = 0;
submitInfo.commandBufferCount   = 1;
submitInfo.pCommandBuffers      = &commandBuffer;
submitInfo.signalSemaphoreCount = 0;
submitInfo.pSignalSemaphores    = nullptr;

vkResetFences ( device, 1, &fence );        

if ( vkQueueSubmit ( graphicsQueue, 1, &submitInfo, fence ) != VK_SUCCESS )
    fatal () << "failed to submit command buffer" << std::endl;

    // wait till results will be ready 
if ( vkWaitForFences ( device, 1, &fence, VK_TRUE, DEFAULT_FENCE_TIMEOUT  ) != VK_SUCCESS )
    fatal () << "Waiting for fence failed" << Log::endl;

saveScreenshot ( device, physicalDevice,  graphicsQueue, commandPool, image, 512, 512 );

    // free all allocated resources
vkFreeCommandBuffers         ( device, commandPool, 1, &commandBuffer );
vkDestroyFence               ( device, fence,          nullptr );
vkDestroyFramebuffer         ( device, framebuffer,    nullptr );
vkDestroyImageView           ( device, imageView,      nullptr );
vkDestroyImage               ( device, image,          nullptr );
vkDestroyRenderPass          ( device, renderPass,     nullptr );
vkFreeMemory                 ( device, memory,         nullptr );
vkDestroyPipeline            ( device, pipeline,       nullptr );
vkDestroyPipelineLayout      ( device, pipelineLayout, nullptr );
vkDestroyShaderModule        ( device, vertShader,     nullptr );
vkDestroyShaderModule        ( device, fragShader,     nullptr );
vkDestroyCommandPool         ( device, commandPool,    nullptr);
vkDestroyDevice              ( device,                 nullptr );

destroyDebugUtilsMessengerEXT ( instance, debugMessenger, nullptr );

vkDestroyInstance ( instance, nullptr );

Код к этой статье (исходный код на С++ и проект для cmake) можно скачать по этой ссылке.