Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
"You useed to be much more muchierYou've lost your muchness".
The Mad Hatter
В отличии от OpenGL, где вы сразу получаете пригодный для рендеринга фреймбуфер, у Vulkan нет фреймбуфера по умолчанию. Если вы хотите осуществить рендеринг в окно, то вам необходимо самому об этом позаботиться (а это будет совсем не просто).
Соответствующая часть зависит от платформы и поэтому вынесена в ряд расширений. Часть этой работы возьмет на себя библиотека GLFW, но довольно большой объем работы нам придется проделать самим.
Необходимо вручную создать фреймбуфер (а обычно не один, а несколько для поддержки двойной и тройной буферизации) и объект swap chain. Этот объект отвечает за список изображений, в которые можно осуществлять рендеринг и которые можно показывать в окне. При этом у нас всегда есть изображение/фреймбуфер, готовый для рендеринга, и есть изображение, готовое для показа.
Сейчас мы рассмотрим гораздо более простой вариант. Мы сами создадим изображение и по нему создадим фреймбуфер, в который можно будет осуществить рендеринг. Также мы создадим объект-конвейер, но на этот раз это будет графический конвейер и вы сами сможете увидеть насколько он сложнее вычислительного.
Еще одним отличием 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
.
Размер изображения задается в поле extent
(в width
и 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 задаются большим числом различных команд,
но обычно мы все их вместе никогда не задаем).
// 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
.
Еще одной структурой, которую нужно заполнить, является 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;
Также мы должны задать область вывода (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, описывающий передаваемые ресурсы - множество 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) можно скачать по этой ссылке.