Vulkan教程 - 22 深度緩衝

現在創建的幾何對象是投影到3D中的,但是還是完全的平面。本章我們添加一個Z座標來爲3D網格做準備。我們會使用這個第三個座標來放置一個正方形在我們當前正方形之上,從而引出不進行深度排序存在的問題。

修改Vertex結構體來爲位置使用3D向量,並更新format:

glm::vec3 pos;
...
attributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT;

接着,更新頂點着色器來接收和轉換3D座標以作爲輸入,不要忘記重新編譯着色器。

layout(location = 0) in vec3 inPosition;
...
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0);

最後更新vertices容器來包含Z座標:

const std::vector<Vertex> vertices = {
    {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}},
    {{0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}},
    {{0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}},
    {{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {1.0f, 1.0f}}
};

現在運行代碼會發現這和原來的一模一樣,是時候添加一些幾何圖形讓場景更有趣了,並且要搞出我們本章要解決的哪個問題。複製頂點來定義一個正方形的位置,其中Z座標改爲-0.5f:

const std::vector<Vertex> vertices = {
    {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}},
    {{0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}},
    {{0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}},
    {{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {1.0f, 1.0f}},

    {{-0.5f, -0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}},
    {{0.5f, -0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}},
    {{0.5f, 0.5f, -0.5f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}},
    {{-0.5f, 0.5f, -0.5f}, {1.0f, 1.0f, 1.0f}, {1.0f, 1.0f}}
};

const std::vector<uint16_t> indices = {
    0, 1, 2, 2, 3, 0,
    4, 5, 6, 6, 7, 4
};

對應圖形爲:

現在運行程序,得到類似埃舍爾插畫一樣的效果。它的問題是較低的正方形繪製在了較高的正方形的上方,僅僅是因爲它在頂點數組中比較靠後。有兩種方式來處理該問題:

將所有繪製調用通過深度從後向前排序;

在深度緩衝中使用深度測試。

第一個方法通常用於繪製透明對象,因爲順序無關的透明是比較難解決的。但是,通過深度進行片段排序的問題通常是用深度緩衝解決的。深度緩衝是一個額外的附件,能存儲每個位置的深度,就和顏色附件存儲每個位置的顏色那樣。光柵化器每次產生一個片段,深度測試會檢查新的片段是否比之前的更近。如果不是的話,新的片段會被丟棄。傳輸深度測試的片段將它自己的深度寫入到深度緩衝中。可以從片段着色器操作該值,就像你可以操作顏色輸出一樣。

#define GLM_FORCE_DEPTH_ZERO_TO_ONE

這樣得到的效果:

GLM生成的透視投影矩陣會使用OpenGL的深度範圍,默認就是-1.0到1.0。我們要配置使用Vulkan的範圍0.0到1.0,所以要用GLM_FORCE_DEPTH_ZERO_TO_ONE。

深度附件是基於圖像的,就和顏色附件一樣。不同之處是交換鏈不會自動創建深度圖像。我們只需要一個深度圖就行,因爲每次就運行一個繪製命令。深度圖會要求提供三個資源:圖像、內存和圖像視圖。

VkImage depthImage;
VkDeviceMemory depthImageMemory;
VkImageView depthImageView;

創建一個新的方法createDepthResources來建立這些資源,就在initVulkan建立命令池之後。

創建深度圖是比較直白的。它要和顏色附件有一樣的分辨率,也就是交換鏈大小定義的,以及適用於深度附件的圖像用法,有最優的平鋪和設備本地內存性能。只有一個問題了:深度圖的正確格式是什麼?它的格式必須包含一個深度組件,也就是VK_FORMAT_中帶_D??_的。

不像是貼圖圖像,我們不需要特定格式,因爲我們不會直接從程序訪問紋素。它僅僅需要一個較好的精度即可,真實世界中的程序起碼要24位。有幾個格式能滿足該要求:

VK_FORMAT_D32_SFLOAT:32位浮點深度;

VK_FORMAT_D32_SFLOAT_S8_UINT:32位有符號浮點數深度和8位模板組件;

VK_FORMAT_D24_UNORM_S8_UINT:24位浮點深度和8位模板組件。

模板組件用於模板測試,是可以和深度測試結合在一起的另一個附加測試。

我們可以選擇VK_FORMAT_D32_SFLOAT格式,因爲它是被廣泛支持的,但是添加一些靈活性也是不錯的。我們打算寫一個方法findSupportedFormat,接收一組候選格式,順序是從最想要的直到最不太想要的,然後看哪個先被支持:

VkFormat findSupportFormat(const std::vector<VkFormat>& candiates, VkImageTiling tiling,
    VkFormatFeatureFlags features) {

}

格式是否支持依賴於平鋪模式和用法,所以我們必須包含這些作爲參數。我們可以用vkGetPhysicalDeviceFormatProperties查詢是否支持某個格式:

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

VkFormatProperties包含以下三個字段:

linearTilingFeatures:支持線性平鋪;

optimalTilingFeatures:支持最優平鋪;

bufferFeatures:支持緩衝。

這裏就前兩個有用,我們檢查的那個則依賴於方法的tiling參數:

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

如果所有候選格式都不能用,我們就返回一個特殊值或者拋出異常。

我們會使用該方法創建一個findDepthFormat助手方法來選擇有深度組件,支持用作深度附件的格式:

VkFormat findDepthFormat() {
    return findSupportFormat({ 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_。所有這些候選格式都包含深度組件,但是後面倆也包含了模板組件。我們現在不用但是在有這些格式的圖像上執行佈局轉移要考慮。添加一個簡單的助手方法,告訴我們選中的深度格式是否包含模板組件:

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

現在從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);
depthImageView = createImageView(depthImage, depthFormat);

但是,createImageView方法當前認爲子資源都是VK_IMAGE_ASPECT_COLOR_BIT,所以要將該字段變成一個參數:

VkImageView createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags) {
    ...
    viewInfo.subresourceRange.aspectMask = aspectFlags;
    ...
}

然後將調用該方法的地方都修改正確:

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

這就是創建深度圖的所有內容了。我們不用將它映射或從另一個圖像向它拷貝內容,因爲我們會在渲染通道一開始的時候清空它,就和顏色附件那樣。但是它仍然需要轉移到一個適合深度附件用途的佈局。我們可以在渲染通道中做這些,就和顏色附件一樣,但是這裏我還是選擇使用管線屏障來做因爲轉移就只要做一次即可:

transitionImageLayout(depthImage, depthFormat, VK_IMAGE_LAYOUT_UNDEFINED,
    VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);

未定義的佈局可以作爲初始佈局,因爲當前深度圖像內容是什麼沒有關係。我們需要更新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;
}

儘管我們不用模板組件,我們還是要把它包含在深度圖像的佈局轉移中。

最終,添加正確的訪問掩碼和管線階段:

if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
    barrier.srcAccessMask = 0;
    barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;

    sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
    destinationStage = VK_PIPELINE_STAGE_TRANSFER_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;

    sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
    destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_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;

    sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
    destinationStage = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
} else {
    throw std::invalid_argument("unsupported layout transition!");
}

深度緩衝會被讀取來執行深度測試以查看片段是否可見,當新的片段繪製後會被寫入。讀取發生在VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT階段,寫入發生在VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT階段。你應該選取匹配這些特定操作的最早的管線階段,以便它準備好用作深度附件。

我們現在打算修改createRenderPass來包含一個深度附件。首先指定VkAttachmentDescription:

VkAttachmentDescription depthAttachment = {};
depthAttachment.format = findDepthFormat();
depthAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
depthAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
depthAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
depthAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
depthAttachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

format應該和深度圖的一樣,這一次我們不關心存儲深度數據,因爲繪製完成後就不用它了。這可能也會讓硬件更好進行優化。就和顏色緩衝一樣,我們不關心之前的深度內容,所以初始佈局使用VK_IMAGE_LAYOUT_UNDEFINED。

VkAttachmentReference depthAttachmentRef = {};
depthAttachmentRef.attachment = 1;
depthAttachmentRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

爲第一個子通道添加一個到該附件的引用:

VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;
subpass.pDepthStencilAttachment = &depthAttachmentRef;

和顏色附件不一樣的是,子通道只能使用一個深度(模板)附件,在多緩衝上進行深度測試是沒有意義的。

std::array<VkAttachmentDescription, 2> attachments = { colorAttachment, depthAttachment };
VkRenderPassCreateInfo renderPassInfo = {};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
renderPassInfo.pAttachments = attachments.data();
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;
renderPassInfo.dependencyCount = 1;
renderPassInfo.pDependencies = &dependency;

最終修改VkRenderPassCreateInfo來引用兩個附件。

下一步是修改幀緩衝創建來綁定深度圖像到深度附件中。到createFramebuffers中指定深度圖像視圖作爲第二個附件:

std::array<VkImageView, 2> attachments = { swapChainImageViews[i], depthImageView };

VkFramebufferCreateInfo framebufferInfo = {};
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebufferInfo.renderPass = renderPass;
framebufferInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
framebufferInfo.pAttachments = attachments.data();
framebufferInfo.width = swapChainExtent.width;
framebufferInfo.height = swapChainExtent.height;
framebufferInfo.layers = 1;

顏色附件對於每個交換鏈圖像都會不一樣,但是同一個深度圖可以用於所有圖像,因爲由於我們設置了信號量,同一個時間下只有一個子通道在運行。

你還需要移動該調用到createFramebuffers以確保它在深度圖像視圖創建後調用:

void initVulkan() {
    ...
    createDepthResources();
    createFramebuffers();
    ...
}

我們現在有多個VK_ATTACHMENT_LOAD_OP_CLEAR類型的附件,我們也需要指定多個清除值。到createCommandBuffers並創建VkClearValue結構體:

std::array<VkClearValue, 2> clearValues = {};
clearValues[0].color = { 0.0f, 0.0f, 0.0f, 1.0f };
clearValues[1].depthStencil = { 1.0f, 0 };
renderPassInfo.clearValueCount = static_cast<uint32_t>(clearValues.size());
renderPassInfo.pClearValues = clearValues.data();

Vulkan中深度緩衝的深度範圍是0.0到1.0,1.0在很遠的視圖平面,0.0在近處的視圖平面。深度緩衝中每個點的初始值應該是可能的最遠深度值,也就是1.0。

深度附件現在能使用了,但是深度測試還要在管線中啓用纔行。通過VkPipelineDepthStencilStateCreateInfo結構體配置:

VkPipelineDepthStencilStateCreateInfo depthStencil = {};
depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
depthStencil.depthTestEnable = VK_TRUE;
depthStencil.depthWriteEnable = VK_TRUE;

depthTestEnable字段指定了新片段的深度是否應該和深度緩衝的比較來確定它們是否該被丟棄。depthWriteEnable字段指定傳遞深度測試的片段的新深度是否要寫入到深度緩衝。這對於繪製透明對象很有用。它們應該和之前渲染的不透明對象比較,但是不會導致透明對象不會繪製。

depthStencil.depthCompareOp = VK_COMPARE_OP_LESS;

depthCompareOp字段指定了要進行的比較操作是否保留還是丟棄片段。我們堅持用較低深度等於較近的傳統,所以新片段的深度應該較小。

depthStencil.depthBoundsTestEnable = VK_FALSE;
depthStencil.minDepthBounds = 0.0f;  // Optional
depthStencil.maxDepthBounds = 1.0f;  // Optional

depthBoundsTestEnable,minDepthBounds和maxDepthBounds字段用於可選深度範圍檢測。基本上,這會讓你能只保留特定深度方位內的片段,我們這裏不用。

depthStencil.stencilTestEnable = VK_FALSE;
depthStencil.front = {};  // Optional
depthStencil.back = {};  // Optional

最後三個字段配置模板緩衝操作,這裏也不用。如果你要用這些操作,需要確保深度/模板圖像的格式包含模板組件。

pipelineInfo.pDepthStencilState = &depthStencil;

更新VkGraphicsPipelineCreateInfo結構體引用深度模板狀態。如果渲染通道包含深度模板附件,深度模板狀態一定要指定纔行。

如果你現在運行程序,你應該能看到幾何片段都正確排序了。

當窗口大小改變的時候,深度緩衝的分辨率應該改變以匹配新的顏色附件分辨率。擴展recreateSwapChain方法來重建深度資源:

void recreateSwapChain() {
    int width = 0, height = 0;
    while (width == 0 || height == 0) {
        glfwGetFramebufferSize(window, &width, &height);
        glfwWaitEvents();
    }

    vkDeviceWaitIdle(device);
    cleanupSwapChain();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createDepthResources();
    createFramebuffers();
    createUniformBuffers();
    createDescriptorPool();
    createDescriptorSets();
    createCommandBuffers();
}

清理操作在交換鏈清理方法中:

void cleanupSwapChain() {
    vkDestroyImageView(device, depthImageView, nullptr);
    vkDestroyImage(device, depthImage, nullptr);
    vkFreeMemory(device, depthImageMemory, nullptr);
    ...
}

恭喜你,你的程序終於可以繪製任意3D幾何體並能夠有正確的視覺效果了。下一章我們會繪製一個貼圖模型。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章