Vulkan Learning — 8. Multipass Rendering

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

Начнем с рисунка, который показывает, какие части нашей объектной модели будут затронуты, если мы перейдем от однопроходной отрисовки к многопроходной (Рис. 1).

Рис. 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’ами.

vk::RenderPass CreateRenderPassMultipass(
    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’ов, сколько у нас изображений в свопчейне).

// THE FOLLOWING IS PSEUDOCODE!

// 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> CreateFramebuffers()
{
    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-ого пайплайна, опуская ненужные или повторяющиеся детали:

inline vk::Pipeline CreateGraphicsPipelinePostprocessing(
    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:

vk::DescriptorSetLayout CreateInputAttachmentsDescriptorSetLayout()
{
    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-ого пайплайна у нас будет такой:

vk::DescriptorSetLayout inputAttachmentsDescriptorSetLayout
    = 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.

// THE FOLLOWING IS PSEUDOCODE!

// 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’ов, это может быть похоже на цикл:

void RecordCommands(
    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’ами запись команд будет выглядеть так:

void RecordCommands(
    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-ого пайплайна:

// VERTEX SHADER

#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 шейдере), причем треугольник должен накрывать собою весь экран — исходя из этого нужно выбрать координаты его вершин.

// FRAGMENT SHADER

#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().

На этом о многопроходном рендеринге всё.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *