Vulkan API — урок 55. Буфер глубины (Depth buffering или Z-buffering)

Итак, для проблемы предыдущего урока есть два способа решения:

  • Подавать вершины в соответствующем порядке – от глубинных до верхних.
  • Использовать тестирование глубины с буфером глубины (для аналогов можно так же встретить термин «Z-буферизация»).

Первый вариант в основном используется для отрисовки прозрачных объектов, потому как тяжело просчитать независимую от порядка прозрачность. Способ понятный и простой в понимании, но вот дописывать для этого десятки строк кода далеко не всегда актуально, ведь уже имеется готовое решение. Проблема упорядочивания фрагментов по глубине гораздо чаще решается с использованием буфера глубины. Так что такое буфер глубины? Это дополнительное вложение, которое хранит глубину каждой позиции, так же как вложение цвета (color attachment) хранит цвет каждой позиции. Каждый раз, когда растеризатор создает фрагмент, тест глубины будет проверять новый фрагмент, находится ли новый фрагмент ближе, чем предыдущий. Если это не так, тогда новый фрагмент отбрасывается. Фрагмент, прошедший отбор, записывает свою глубину в буфер. Можно манипулировать этим значением из fragment shader, так же, как и можно управлять цветом.

Перспективная проекция матрицы, сгенерированная GLM, будет использовать уровень глубины OpenGL от -1.0 до 1.0, используем определение GLM_FORCE_DEPTH_ZERO_TO_ONE для перевода в формат Vulkan’а 0.0 — 1.0.

#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

Вложение глубины (depth attachment) основывается на изображении, так же как и вложение цвета. Разница в том, что swap chain не будет для нас автоматически создавать изображения глубины (depth images). нам нужно только одно подобное изображение, т.к. только одна операция отрисовки будет запущена в один момент времени. Изображение снова потребует трех ресурсов: изображения (image), памяти (memory) и отображаемого изображения (image view).

VDeleter<VkImage> depthImage{device, vkDestroyImage};
VDeleter<VkDeviceMemory> depthImageMemory{device, vkFreeMemory};
VDeleter<VkImageView> depthImageView{device, vkDestroyImageView};

Создадим новую функцию, где и займемся данными ресурсами:

void initVulkan() {
    ...
    createCommandPool();
    createDepthResources();
    createTextureImage();
    ...
}

...

void createDepthResources() {

}

Далее все достаточно банально. Изображение должно иметь то же разрешение, что и вложение цвета (color attachment), определяемое swapChainExtent, флаги соответственно VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT (флаг ранее не встречался, но о его назначении можно догадаться из названия), VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT. Непонятным остается только правильный формат. Формат в VK_FORMAT_ должен содержать компонент глубины (depth), который будет обозначен буквой D соответственно _D??_, но какой именно?

В отличии от изображения текстуры, конкретный формат не потребуется, т.к. непосредственного доступа к текселям из программы не будет. Он просто должен иметь достаточную точность, в реальных приложениях 24 бита должно быть вполне достаточно. Но есть несколько возможных форматов.

  • VK_FORMAT_D32_SFLOAT: И так понятно
  • VK_FORMAT_D32_SFLOAT_S8_UINT: 32-битный знаковый флоат для глубины и 8-битный для трафарета (stencil)
  • VK_FORMAT_D24_UNORM_S8_UINT: 24-битный знаковый флоат для глубины и 8-битный для трафарета (stencil)

Stencil используется в соответствующем буфере, который может комбинироваться с проверкой глубины.

Можно было бы просто выбрать формат VK_FORMAT_D32_SFLOAT, он наиболее распространен. Но лучше будет написать функцию findSupportedFormat, которая принимает список форматов в порядке от наиболее желательного, и проверять, поддерживается ли он. Стоит помнить, что вот прям в этот момент может появиться новый формат, или это случится завтра, или вдруг формат не будет поддерживаться конкретной железячкой, потому программе нужно придавать максимальную гибкость.

VkFormat findSupportedFormat(const std::vector& candidates, VkImageTiling tiling, VkFormatFeatureFlags features) {

}

Поддержка формата зависит от режима управления (tiling mode) и использования. Для запроса поддержки формата имеется функция vkGetPhysicalDeviceFormatProperties:

for (VkFormat format : candidates) {
    VkFormatProperties props;
    vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &props);
}

Структура VkFormatProperties содержит три поля:

  • linearTilingFeatures: Поддерживаемые форматы для линейного tiling’a.
  • optimalTilingFeatures: Поддерживаемые форматы для оптимального tiling’a.
  • bufferFeatures: Для буферов.

В текущей ситуации понадобятся лишь первые два, вот пример, как должна выполняться проверка:

if (tiling == VK_IMAGE_TILING_LINEAR && (props.linearTilingFeatures & features) == features) {
    return format;
} else if (tiling == VK_IMAGE_TILING_OPTIMAL && (props.optimalTilingFeatures & features) == features) {
    return format;
}

Нужно так же учесть случай, когда ни один из форматов не будет поддерживаться. На данный момент просто выдадим исключение:

VkFormat findSupportedFormat(const std::vector& candidates, VkImageTiling tiling, VkFormatFeatureFlags features) {
    for (VkFormat format : candidates) {
        VkFormatProperties props;
        vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &props);

        if (tiling == VK_IMAGE_TILING_LINEAR && (props.linearTilingFeatures & features) == features) {
            return format;
        } else if (tiling == VK_IMAGE_TILING_OPTIMAL && (props.optimalTilingFeatures & features) == features) {
            return format;
        }
    }

    throw std::runtime_error("failed to find supported format!");
}

Эту функцию будем вызывать из другой, findDepthFormat, мало ли какие условия еще добавятся. Ну а пока жестко зададим все форматы и обуславливающие выбор компоненты:

VkFormat findDepthFormat() {
    return findSupportedFormat(
        {VK_FORMAT_D32_SFLOAT, VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D24_UNORM_S8_UINT},
        VK_IMAGE_TILING_OPTIMAL,
        VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT
    );
}

Сейчас используется флаг VK_FORMAT_FEATURE_, взамен VK_IMAGE_USAGE_. Второй и третий формат так же содержат трафарет, который использовать не будем, но его стоит принимать в расчет во время изменения формата (layout transitions). Добавим простою вспомогательную функцию, говорящую нам, что выбранный формат содержит stencil:

bool hasStencilComponent(VkFormat format) {
    return format == VK_FORMAT_D32_SFLOAT_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT;
}

Создадим новую функцию, которая будет искать depthFormat и создавать соответствующие изображение (createImage), образ (createImageView) и обеспечивать изменение формата (transitionImageLayout):

void createDepthResources() {
    VkFormat depthFormat = findDepthFormat();

    createImage(swapChainExtent.width, swapChainExtent.height, depthFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory);
    createImageView(depthImage, depthFormat, VK_IMAGE_ASPECT_DEPTH_BIT, depthImageView);

    transitionImageLayout(depthImage, depthFormat, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
}

Однако, функция createImageView на данный момент предполагает, что подресурс всегда VK_IMAGE_ASPECT_COLOR_BIT, так что нужно изменить это поле:

void createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags, VDeleter& imageView) {
    ...
    viewInfo.subresourceRange.aspectMask = aspectFlags;
    ...
}

Так же нужно обновить все вызовы этой функции

createImageView(swapChainImages[i], swapChainImageFormat, VK_IMAGE_ASPECT_COLOR_BIT, swapChainImageViews[i]);
...
createImageView(depthImage, depthFormat, VK_IMAGE_ASPECT_DEPTH_BIT, depthImageView);
...
createImageView(textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT, textureImageView);

Т.е. нам не нужно использовать map операции или копировать его, т.к. мы не собираемся очищать его в начале прохода рендера, как вложение цвета. Однако, по прежнему нужно держать его в подходящем к ситуации формате. Это можно было бы сделать в проходе рендера, как вложение цвета, но все же будем использовать барьер конвейера, т.к. изменение понадобится только лишь раз (но может захотите использовать первый вариант, это возможно).

Неопределенный макет может быть использован в качестве исходного макета, т.к. содержимое нас не интересует. Нужно немного доработать логику transitionImageLayout, для правильного использования подресурсов:

if (newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) {
    barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;

    if (hasStencilComponent(format)) {
        barrier.subresourceRange.aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
    }
} else {
    barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
}

Не смотря на то, что stencil использоваться не будет, но учитывая написанный ранее код, мы должны добавить его в логику.

Ну и корректировка как такового выбора:

if (oldLayout == VK_IMAGE_LAYOUT_PREINITIALIZED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
    barrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT;
    barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
} else if (oldLayout == VK_IMAGE_LAYOUT_PREINITIALIZED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
    barrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT;
    barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
} else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
    barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
} else if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) {
    barrier.srcAccessMask = 0;
    barrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
} else {
    throw std::invalid_argument("unsupported layout transition!");
}

Main Admin

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

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