Суть отрисовки в несколько проходов (multipass rendering) мы описали довольно подробно в заметке Vulkan Learning — 3. Swapchain. Depth Buffer. Render Pass. Framebuffers. Один проход (subpass) — суть запуск одного определенного пайплайна и получение на выходе некоего результата в виде фреймбуфера, который затем используется в следующем проходе (subpass’е), который запускает совсем другой пайплайн. Самый последний проход выдает нам буфер цвета, который мы отправляем в presentation engine. Ну а сейчас перейдем к техническим деталям.
Начнем с рисунка, который показывает, какие части нашей объектной модели будут затронуты, если мы перейдем от однопроходной отрисовки к многопроходной (Рис. 1).

Далее мы рассмотрим модификации в исходном коде. В примере ниже мы будем делать двухпроходную отрисовку. 1-ый проход будет заполнять некий промежуточный colorBuffer, а 2-й проход будет просто отрисовывать этот colorBuffer в swapchainImage’е. При помощи такой двухпроходной отрисовки можно реализовать разные эффекты постпроцессинга, например, сделать гамма-коррекцию изображения или даже что-то более экзотичное, например, поменять местами цветовые каналы… Таким образом, промежуточный colorBuffer становится inputAttachment’ом для 2-го прохода. Но, для разнообразия, можно также сделать depthBuffer inputAttachment’ом для 2-го прохода — тогда можно будет в эффектах постпроцессинга задействовать также информацию из буфера глубины.
VulkanSwapchain
RenderPass
Прежде всего изменениям подвергается объект RenderPass — в нем будет описан не один subpass, как раньше, а несколько (в нашем примере — два). В приведенном ниже коде создания RenderPass’а я постарался удалить мелкие детали, оставив только рыбу. Обратите внимание: attachment’ов стало три (добавился промежуточный colorBuffer, в который пишет 1-ый subpass и из которого читает 2-й subpass); посокльку subpass’ов стало два, потребовалось также описать зависимости между subpass’ами.
const vk::Device& logicalDevice,
vk::Format swapchainImageFormat,
vk::Format colorImageFormat,
vk::Format depthImageFormat)
{
// --- ATTACHMENTS ---
// Position in this array is important, because AttachmentReferences refer to it
std::array<vk::AttachmentDescription, 3> attachments
{
// 0 - Swapchain color attachment (Output of Subpass 2)
vk::AttachmentDescription
{
...
.initialLayout = vk::ImageLayout::eUndefined, // INITIAL layout informs the hardware about the layout the application "provides" the given attachment with.
.finalLayout = vk::ImageLayout::ePresentSrcKHR // The final layout is the layout the given attachment will be transitioned into (automatically) AT THE END of a render pass.
},
// 1 - Color Attachment (Output of Subpass 1, Input to Subpass 2)
vk::AttachmentDescription
{
...
.initialLayout = vk::ImageLayout::eUndefined, // INITIAL layout informs the hardware about the layout the application "provides" the given attachment with.
.finalLayout = vk::ImageLayout::eColorAttachmentOptimal // The final layout is the layout the given attachment will be transitioned into (automatically) AT THE END of a render pass.
},
// 2 - Depth attachment (Output of Subpass 1, Input to Subpass 2)
vk::AttachmentDescription
{
...
.initialLayout = vk::ImageLayout::eUndefined, // INITIAL layout informs the hardware about the layout the application "provides" the given attachment with.
.finalLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal // The final layout is the layout the given attachment will be transitioned into (automatically) AT THE END of a render pass.
}
};
// --- REFERENCES ---
// Subpass 1
vk::AttachmentReference colorAttachmentRef
{
.attachment = 1,
.layout = vk::ImageLayout::eColorAttachmentOptimal
};
// Subpass 1
vk::AttachmentReference depthAttachmentRef
{
.attachment = 2,
.layout = vk::ImageLayout::eDepthStencilAttachmentOptimal
};
// Subpass 2
vk::AttachmentReference swapchainColorAttachmentRef
{
.attachment = 0,
.layout = vk::ImageLayout::eColorAttachmentOptimal
};
std::array<vk::AttachmentReference, 2> subpass2InputAttachmentRefs
{
// color
vk::AttachmentReference
{
.attachment = 1,
.layout = vk::ImageLayout::eShaderReadOnlyOptimal
},
// depth
vk::AttachmentReference
{
.attachment = 2,
.layout = vk::ImageLayout::eShaderReadOnlyOptimal
},
};
// --- SUBPASSES ---
std::array<vk::SubpassDescription, 2> subpasses
{
// SUBPASS 1
vk::SubpassDescription
{
.colorAttachmentCount = 1,
.pColorAttachments = &colorAttachmentRef,
.pDepthStencilAttachment = &depthAttachmentRef
},
// SUBPASS 2
vk::SubpassDescription
{
.inputAttachmentCount = subpass2InputAttachmentRefs.size(),
.pInputAttachments = subpass2InputAttachmentRefs.data(),
.colorAttachmentCount = 1,
.pColorAttachments = &swapchainColorAttachmentRef,
}
};
std::array<vk::SubpassDependency, 3> subpassDependencies
{
// External --> Subpass 1
vk::SubpassDependency
{
.srcSubpass = VK_SUBPASS_EXTERNAL, // The subpass index of the 1st subpass in the dependency. Subpass index (VK_SUBPASS_EXTERNAL = special value meaning outside of renderpass).
.dstSubpass = 0, // The subpass index of the 2nd subpass in the dependency
// The synchronization scope of the 1st set of commands. What pipeline stage must have completed for the dependency
.srcStageMask = vk::PipelineStageFlagBits::eBottomOfPipe,
// The synchronization scope of the 2nd set of commands. What pipeline stage is waiting on the dependency
.dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput,
// Memory access scope of the 1st set of commands (The presentation engine only reads from the image)
.srcAccessMask = vk::AccessFlagBits::eMemoryRead,
// Memory access scope of the 2nd set of commands
.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite
},
// Subpass 1 --> Subpass 2
vk::SubpassDependency
{
.srcSubpass = 0, // The subpass index of the 1st subpass in the dependency.
.dstSubpass = 1, // The subpass index of the 2nd subpass in the dependency
// The synchronization scope of the 1st set of commands. What pipeline stage must have completed for the dependency
.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput,
// The synchronization scope of the 2nd set of commands. What pipeline stage is waiting on the dependency
.dstStageMask = vk::PipelineStageFlagBits::eFragmentShader,
// Memory access scope of the 1st set of commands (The presentation engine only reads from the image)
.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite,
// Memory access scope of the 2nd set of commands
.dstAccessMask = vk::AccessFlagBits::eShaderRead
},
// Subpass 2 --> External
vk::SubpassDependency
{
.srcSubpass = 1, // The subpass index of the 1st subpass in the dependency
.dstSubpass = VK_SUBPASS_EXTERNAL, // The subpass index of the 2nd subpass in the dependency. Subpass index (VK_SUBPASS_EXTERNAL = special value meaning outside of renderp
// The synchronization scope of the 1st set of commands. What pipeline stage must have completed for the dependency
.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput,
// The synchronization scope of the 2nd set of commands. What pipeline stage is waiting on the dependency
.dstStageMask = vk::PipelineStageFlagBits::eBottomOfPipe,
// Memory access scope of the 1st set of commands
.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite,
// Memory access scope of the 2nd set of commands (The presentation engine only reads from the image)
.dstAccessMask = vk::AccessFlagBits::eMemoryRead
},
};
// --- RENDER PASS ---
vk::RenderPassCreateInfo renderPassCreateInfo
{
.attachmentCount = attachments.size(), // Number of all different attachments (elements in pAttachments array) used during whole render pass
.pAttachments = attachments.data(), // Array specifying all attachments used in a render pass
.subpassCount = subpasses.size(), // Number of subpasses a render pass consists of
.pSubpasses = subpasses.data(), // Array with descriptions of all subpasses
.dependencyCount = subpassDependencies.size(),
.pDependencies = subpassDependencies.data()
};
return logicalDevice.createRenderPass(renderPassCreateInfo);
}
ColorBuffers
К нашим буферам SwapchinImages и DepthBuffers добавился еще один набор буферов ColorBuffers — это тот самый промежуточный цветовой буфер, в который пишет 1-ый subpass. Этих буферов столько же, сколько изображений в нашем свопчейне — причина, почему их столько — такая же как и для DepthBuffer’ов — потому что мы, вообще говоря, запускаем несколько subpass’ов параллельно (в пределе — столько параллельных subpass’ов, сколько у нас изображений в свопчейне).
// Field
std::vector<VkImage2d> m_ColorBuffers;
// Constructor
m_ColorBuffers(CreateColorBuffers())
// Destructor
m_ColorBuffers.clear();
// Function
std::vector<VkImage2d> CreateColorBuffers()
{
std::vector<VkImage2d> colorBuffers;
colorBuffers.reserve(NumberOfImages());
for (size_t i = 0; i < NumberOfImages(); i++)
{
colorBuffers.push_back(CreateColorBuffer(
m_VkGraphicsContext.PhysicalDevice(),
m_VkGraphicsContext.LogicalDevice(),
SwapChainExtent(),
vk::ImageUsageFlagBits::eInputAttachment));
}
return colorBuffers;
}
Framebuffers
Поскольку количество attachment’ов изменилось, то изменилось и количество ссылок на них во Framebuffer’ах:
{
std::vector<vk::Framebuffer> framebuffers;
framebuffers.reserve(numberOfSwapchinImages);
for (size_t i = 0; i < numberOfSwapchinImages; i++)
{
std::array<vk::ImageView, 3> attachments
{
m_SwapchainImages[i], // 0 (position is important - it should match that of the render pass)
m_ColorBuffers[i].ImageView(), // 1 (position is important - it should match that of the render pass)
m_DepthBuffers[i].ImageView() // 2 (position is important - it should match that of the render pass)
};
vk::FramebufferCreateInfo framebufferCreateInfo
{
...
.attachmentCount = attachments.size(),
.pAttachments = attachments.data(), // List of attachments (1:1 with Render Pass)
...
};
framebuffers.push_back(
m_VkGraphicsContext.LogicalDevice().createFramebuffer(framebufferCreateInfo));
}
return framebuffers;
}
GraphicsPipelines
Поскольку у нас теперь два subpass’а, теперь у нас должно быть и два pipeline’а. Один пайплайн будет, как обычно, отрисовывать трехмерные модели, а второй — отрисовывать то, что нарисовал первый пайплайн, но накладывая какие-то эффекты постпроцессинга. Поскольку 1-ый пайплайн никак не изменился, я приведу здесь только код для создания 2-ого пайплайна, опуская ненужные или повторяющиеся детали:
const vk::Device& logicalDevice,
const vk::PipelineLayout& pipelineLayout,
const vk::RenderPass& renderPass,
const vk::Extent2D& swapchainExtent,
const std::string& vertexShaderFile,
const std::string& fragmentShaderFile)
{
// Read in SPIR-V code of shaders
...
// Build shader modules
...
// -- VERTEX INPUT --
// No vertex input
vk::PipelineVertexInputStateCreateInfo vInputStateCreateInfo
{
.vertexBindingDescriptionCount = 0,
.pVertexBindingDescriptions = nullptr,
.vertexAttributeDescriptionCount = 0,
.pVertexAttributeDescriptions = nullptr,
};
// -- INPUT ASSEMBLY --
// Describes two things: what kind of geometry will be drawn
// from the vertices and if primitive restart should be enabled
vk::PipelineInputAssemblyStateCreateInfo inputAssemblyCreateInfo
{
.topology = vk::PrimitiveTopology::eTriangleList, // type of primitives to be assembled from vertices
.primitiveRestartEnable = VK_FALSE // relevant for strips and fans (to start new strip or fan)
};
// -- VIEWPORT & SCISSOR --
...
// -- DYNAMIC STATE --
...
// -- RASTERIZER --
...
// -- BLENDING --
...
// -- DEPTH STENCIL TESTING --
// No writing to depth buffer
vk::PipelineDepthStencilStateCreateInfo depthStencil
{
.depthWriteEnable = VK_FALSE
};
// -- GRAPHICS PIPELINE CREATION --
vk::GraphicsPipelineCreateInfo pipelineInfo
{
...
.pVertexInputState = &vInputStateCreateInfo,
.pInputAssemblyState = &inputAssemblyCreateInfo,
.pDepthStencilState = &depthStencil,
...
};
vk::Pipeline pipeline = logicalDevice.createGraphicsPipeline(
vk::PipelineCache(), pipelineInfo).value;
// Destroy shader modules because they are no longer needed
...
return pipeline;
}
InputAttachmentDescriptorSetLayout
Надо заметить, что у нашего 2-го пайплайна, который отрисовывает эффекты постпроцессинга, иной pipelineLayout, чем у 1-ого. Никаких pushConstants, никаких samplerDescriptorSetLayout’ов, зато появился inputAttachmentsDescriptorSetLayout:
{
std::array<vk::DescriptorSetLayoutBinding, 2> layoutBindings
{
// Color input binding
vk::DescriptorSetLayoutBinding
{
.binding = 0, // Binding index in the shader
.descriptorType = vk::DescriptorType::eInputAttachment,
.descriptorCount = 1, // It is possible for the shader variable to represent an array of uniform buffer objects, and descriptorCount specifies the number of values in the array
.stageFlags = vk::ShaderStageFlagBits::eFragment, // In which shader stages the descriptor is going to be referenced
},
// Depth input binding
vk::DescriptorSetLayoutBinding
{
.binding = 1, // Binding index in the shader
.descriptorType = vk::DescriptorType::eInputAttachment,
.descriptorCount = 1, // It is possible for the shader variable to represent an array of uniform buffer objects, and descriptorCount specifies the number of values in the array
.stageFlags = vk::ShaderStageFlagBits::eFragment, // In which shader stages the descriptor is going to be referenced
}
};
return m_LogicalDevice.createDescriptorSetLayout(
vk::DescriptorSetLayoutCreateInfo
{
.bindingCount = layoutBindings.size(),
.pBindings = layoutBindings.data()
});
}
Соответственно, pipelineLayout для 2-ого пайплайна у нас будет такой:
= CreateInputAttachmentsDescriptorSetLayout(logicalDevice);
vk::PipelineLayout pipelineLayout = logicalDevice.createPipelineLayout(
vk::PipelineLayoutCreateInfo
{
.setLayoutCount = 1, // the number of different descriptor set layouts inside the shader program
.pSetLayouts = &inputAttachmentsDescriptorSetLayout // pointer to an array of descriptor set layouts
});
VkModelRenderer
InputAttachmentDescriptorSets
Итак, как уже говорилось, в нашем деле появился новый персонаж — промежуточный colorBuffer, в который пишет 1-ый subpass и из которого читает 2-й subpass. Для 1-ого subpass’а промежуточный colorBuffer является colorAttachment’ом, и этот факт отражен в соответствующей структуре vk::SubpassDescription, а ссылка на сам colorBuffer записана в структуре vk::Framebuffer. Для 2-ого subpass’а промежуточный colorBuffer является inputAttachment’ом, этот факт отражен в соответствующей структуре vk::SubpassDescription, но ссылка на сам colorBuffer должна быть записана в дескрипторе (inputAttachmentDescriptor), layout которого мы только что описали (см. функцию CreateInputAttachmentsDescriptorSetLayout). Таки надо создать этот дескриптор, а точнее descriptorSet, а еще точнее набор из numberOfSwapchainImages descriptorSet’ов. Для этого может быть удобно создать отдельный DescriptorPool.
// Fields
vk::DescriptorPool m_InputAttachmentsDescriptorPool;
std::vector<vk::DescriptorSet> m_InputAttachmentsDescriptorSets;
// Constructor
m_InputAttachmentsDescriptorPool(CreateInputAttachmentsDescriptorPool()),
m_InputAttachmentsDescriptorSets(CreateInputAttachmentsDescriptorSets())
// Destructor
logicalDevice.destroyDescriptorPool(m_InputAttachmentsDescriptorPool);
// Functions
vk::DescriptorPool CreateInputAttachmentsDescriptorPool()
{
std::array<vk::DescriptorPoolSize, 2> poolSizes
{
// Color input attachment
vk::DescriptorPoolSize
{
.type = vk::DescriptorType::eInputAttachment, // descriptor type (combined Image descriptor + Sampler descriptor)
.descriptorCount = m_ColorBuffers.size() // the number of descriptors of the specified type which can be allocated in total from the pool
},
// Depth input attachment
vk::DescriptorPoolSize
{
.type = vk::DescriptorType::eInputAttachment, // descriptor type (combined Image descriptor + Sampler descriptor)
.descriptorCount = m_DepthBuffers.size() // the number of descriptors of the specified type which can be allocated in total from the pool
},
};
vk::DescriptorPoolCreateInfo poolInfo
{
.maxSets = numberOfSwapchainImages, // the maximum number of descriptor sets that may be allocated from the pool
.poolSizeCount = poolSizes.size(),
.pPoolSizes = poolSizes.data(),
};
return logicalDevice.createDescriptorPool(poolInfo);
}
std::vector<vk::DescriptorSet> CreateInputAttachmentsDescriptorSets()
{
// We need all the copies of the layout because the next function expects
// an array matching the number of sets.
std::vector<vk::DescriptorSetLayout> layouts(
numberOfSwapchainImages,
m_InputAttachmentsDescriptorSetLayout);
vk::DescriptorSetAllocateInfo allocInfo
{
.descriptorPool = m_InputAttachmentsDescriptorPool,
.descriptorSetCount = layouts.size(),
.pSetLayouts = layouts.data()
};
auto descriptorSets = m_VkGraphicsContext.LogicalDevice().allocateDescriptorSets(allocInfo);
// The descriptor sets have been allocated now, but the descriptors within still need to be configured...
for (size_t i = 0; i < descriptorSets.size(); i++)
{
vk::DescriptorImageInfo colorImageInfo
{
.sampler = vk::Sampler{}, // Sampler doesn't work with input attachments
.imageView = m_ColorBuffers[i].ImageView(),
.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal // Image layout when it is in use
};
vk::DescriptorImageInfo depthImageInfo
{
.sampler = vk::Sampler{}, // Sampler doesn't work with input attachments
.imageView = m_DepthBuffers[i].ImageView(),
.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal // Image layout when it is in use
};
std::array<vk::WriteDescriptorSet, 2> writes
{
// color descriptor set
vk::WriteDescriptorSet
{
.dstSet = descriptorSets[i], // Descriptor set to update
.dstBinding = 0, // Binding index inside shader
.dstArrayElement = 0, // Descriptors can be arrays, so we also need to specify
// the first index in the array that we want to update.
.descriptorCount = 1, // How many array elements you want to update
.descriptorType = vk::DescriptorType::eInputAttachment,
.pImageInfo = &colorImageInfo
},
// depth descriptor set
vk::WriteDescriptorSet
{
.dstSet = descriptorSets[i], // Descriptor set to update
.dstBinding = 1, // Binding index inside shader
.dstArrayElement = 0, // Descriptors can be arrays, so we also need to specify
// the first index in the array that we want to update.
.descriptorCount = 1, // How many array elements you want to update
.descriptorType = vk::DescriptorType::eInputAttachment,
.pImageInfo = &depthImageInfo
}
};
logicalDevice.updateDescriptorSets(
writes.size(), // the number of descriptors to write to
writes.data(), // array of WriteDescriptorSet
0, nullptr); // array of CoypDescriptorSet
}
return descriptorSets;
}
RecordCommands
И наконец — отрисовка. Т. е. запись в командный буфер команд для отрисовки. От случая с одним единственным subpass’ом она будет отличаться тем, что наборы команд для привязки входных данных для пайплайна и отрисовки будут записаны последовательно для каждого subpass’а и отделяться друг от друга эти наборы команд будут командой nextSubpass. В общем случае при большом количестве subpass’ов, это может быть похоже на цикл:
const vk::CommandBuffer& commandBuffer,
const vk::Framebuffer& framebuffer,
const vk::RenderPass& renderPass,
const vk::Extent2D& swapchainExtent,
const engine::mat4& worldToScreen)
{
commandBuffer.beginRenderPass(...);
{
for (int iSubpass = 0; i < nSubpasses; iSubpass++)
{
// Bind a pipeline for the i-th subpass
// *** BIND 3D MODELS DATA ***
// bind vertex buffer for the i-th subpass
// bind index buffer for the i-th subpass
// bind descriptor sets for the i-th subpass
// push constants for the i-th subpass
// *** DRAW ***
// draw objects for the i-th subpass
// Switch to the next subpass
if (iSubpass < nSubpasses - 1)
commandBuffer.nextSubpass(...);
}
}
commandBuffer.endRenderPass();
}
В нашем же случае с двумя subpass’ами запись команд будет выглядеть так:
uint32_t imageIndex,
const vk::CommandBuffer& commandBuffer,
const vk::Framebuffer& framebuffer,
const vk::RenderPass& renderPass,
const vk::Extent2D& swapchainExtent,
const engine::mat4& worldToScreen) override
{
...
vk::RenderPassBeginInfo renderPassBeginInfo
{
.renderPass = renderPass, // Render Pass to begin
.framebuffer = framebuffer,
...
};
RunRenderPass runRenderPass(
commandBuffer,
renderPassBeginInfo,
vk::SubpassContents::eInline);
{
commandBuffer.bindPipeline(
vk::PipelineBindPoint::eGraphics,
m_1stGraphicsPipeline);
for (size_t iModel = 0; iModel < m_Models.size(); iModel++)
{
commandBuffer.bindVertexBuffers(/*m_Models[iModel].VertexBuffer()*/);
commandBuffer.bindIndexBuffer(/*m_Models[iModel].IndexBuffer()*/);
std::array<vk::DescriptorSet, 1> descriptorSets
{
m_TextureSamplerDescriptorSets[iModel],
m_UniformDescriptorSets[iModel][imageIndex]
};
commandBuffer.bindDescriptorSets(
...
descriptorSets.size(),
descriptorSets.data(),
...);
auto mvp = m_Models[iModel].ModelToWorldMatrix();
commandBuffer.pushConstants(
...,
sizeof(mvp), // size of constant being pushed
&mvp); // pValues
// Command to execute pipeline
commandBuffer.drawIndexed(...);
}
// Start 2nd subpass
commandBuffer.nextSubpass(vk::SubpassContents::eInline);
commandBuffer.bindPipeline(
vk::PipelineBindPoint::eGraphics,
m_2ndGraphicsPipeline);
commandBuffer.bindDescriptorSets(
...
&m_InputAttachmentsDescriptorSets[imageIndex],
...);
// We are drawing just a single triangle
commandBuffer.draw(
/*vertexCount*/ 3,
/*instanceCount*/ 1,
/*firstVertex*/ 0,
/*firstInstance*/ 0);
}
}
Обратите внимание на то, что мы отрисовываем во 2-ом subpass’е. Мы отрисовываем один единственный треугольник, вершины которого для пущей простоты определены прямо в вершинном шейдере. И тут пожалуй уместно будет привести код шейдеров 2-ого пайплайна:
#version 450 core
vec2 positions[3] = vec2[]
(
vec2( 3.0, -1.0),
vec2(-1.0, -1.0),
vec2(-1.0, 3.0)
);
void main()
{
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
}
Почему нужно отрисовать именно один треугольник? Потому что никаких объектов нам отрисовывать не нужно, мы ведь уже отрисовали все трехмерные модельки в 1-ом subpass’е. А треугольник нам нужен только для того, чтобы пайплайн его растеризовал, т. е. превратил в набор пикселей (цвет каждого пикселя мы вычисляем во fragment шейдере), причем треугольник должен накрывать собою весь экран — исходя из этого нужно выбрать координаты его вершин.
#version 450 core
layout(input_attachment_index = 0, binding = 0) uniform subpassInput inputColor; // Color output from Subpass 1
layout(input_attachment_index = 1, binding = 1) uniform subpassInput inputDepth; // Depth output from Subpass 1
layout(location = 0) out vec4 color;
void main()
{
int xHalf = 1280 / 2;
if (gl_FragCoord.x > xHalf)
{
float depth = subpassLoad(inputDepth).r;
float gray = 1.0 - depth;
color = vec4(gray, gray, gray, 1.0);
}
else
color = subpassLoad(inputColor).rgba;
}
Обратите внимание на то, как fragment шейдер ссылается на inputAttachments. В директиве layout указывается два числа:
- input_attachment_index — индекс inputAttachment’а. Спецификация Vulkan говорит, что этот индекс соответствует индексу в массиве структур vk::AttachmentReference при описании subpass’а в структуре vk::SubpassDescription.
- binding -индекс binding’а, который должен соответствовать полю vk::DescriptorSetLayoutBinding::binding в массиве vk::DescriptorSetLayoutCreateInfo::pBindings при создании DescriptorSetLayout’а.
Ну и наконец, отметим, что получить значение фрагмента из inputAttachment’а можно, вызвав GLSL-функцию subpassLoad().
На этом о многопроходном рендеринге всё.