操作系統:Windows8.1
顯卡:Nivida GTX965M
開發工具:Visual Studio 2017
Introduction
到目前為止,我們所使用的幾何圖形為3D,但仍然完全扁平的。在本章節中我們添加Z坐標到3D模型數據中。我們將使用這個第三個坐標在當前平面上放置一個正方形,以查看幾何圖形沒有進行深度排序造成的問題。
3D geometry
修改 Vertex 結構體使用3D vector作為位置,並且更新對應VkVertexInputAttributeDescription的 format。
struct Vertex { glm::vec3 pos; glm::vec3 color; glm::vec2 texCoord; ... static std::array<VkVertexInputAttributeDescription, 3> getAttributeDescriptions() { std::array<VkVertexInputAttributeDescription, 3> attributeDescriptions = {}; attributeDescriptions[0].binding = 0; attributeDescriptions[0].location = 0; attributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT; attributeDescriptions[0].offset = offsetof(Vertex, pos); ... } };
下一步更新頂點着色器接受和轉換3D坐標作為輸入。別忘記重新編譯它!
layout(location = 0) in vec3 inPosition; ... void main() { gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0); fragColor = inColor; fragTexCoord = inTexCoord; }
最后,更新 vertices 容器包含 Z 坐標:
const std::vector<Vertex> vertices = { {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}}, {{0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}}, {{0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}}, {{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}} };
如果運行程序,會看到與之前同樣的結果。現在是時候添加一些額外的幾何圖形,使場景更有趣,並展示我們將在本章節中解決的問題。復制頂點以定義當前正方形的位置,如下所示:

使用Z坐標 -0.5f 並且為額外的方形添加適當的索引:
const std::vector<Vertex> vertices = { {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}}, {{0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}}, {{0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}}, {{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}}, {{-0.5f, -0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}}, {{0.5f, -0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}}, {{0.5f, 0.5f, -0.5f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}}, {{-0.5f, 0.5f, -0.5f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}} }; const std::vector<uint16_t> indices = { 0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4 };
運行程序現在會看到類似於Escher的例子:

問題是,下方正方形的片段被繪制在上方的片段上,這僅僅是因為它在索引數組中。有兩種方式解決這種問題:
- 從后面到前面深入分析所有的繪圖調用
- 使用深度緩沖區進行深度測試
第一種方法通常用於繪制透明對象,因為與順序無關的透明度是難以解決的難題。然而,通過深度排序片段的問題通常使用深度緩沖區 depth buffer 來解決。深度緩沖區是一個額外的附件,用於存儲每個頂點的深度信息,就像顏色附件存儲每個位置的顏色信息一樣。每次光柵化生成片段時,深度測試將檢查新片段是否比上一個片段更近。如果沒有,新的片段被丟棄。一個片段將深度測試的值寫入深度緩沖區。可以從片段着色器處理此值,就像可以操作顏色輸出一樣。
#define GLM_FORCE_RADIANS #define GLM_FORCE_DEPTH_ZERO_TO_ONE #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp>
借助GLM生產出的透視投影矩陣默認使用OpenGL的深度范圍,收斂在 -1.0 到 1.0。我們需要使用GLM_FORCE_DEPTH_ZERO_TO_ONE定義將其配置為使用 0.0 到 1.0 的Vulkan深度范圍。
Depth image and view
深度附件是基於圖像的,就像顏色附件。所不同的是交換鏈不會自動創建深度圖像。我們僅需要一個深度圖像,因為每次只有一個繪制操作。深度圖像再次需要申請三種資源:圖像,內存和圖像視圖。
VkImage depthImage;
VkDeviceMemory depthImageMemory;
VkImageView depthImageView;
創建createDepthResources函數來配置資源:
void initVulkan() { ... createCommandPool(); createDepthResources(); createTextureImage(); ... } ... void createDepthResources() { }
創建深度圖像非常直接。它具備與顏色附件同樣的分辨率,定義交換鏈尺寸,合理的深度圖像是否方式,最佳的平鋪和設備本地內存。唯一的問題是:對於深度圖像什么是正確的格式?format必須包含深度原件,諸如 VK_FORMAT 中的 _D??_。
不像紋理貼圖,我們不一定需要特定的格式,因為我們不會直接從程序中訪問紋素。它僅僅需要一個合理的准確性,至少24位在實際程序中是常見的。有幾種符合要求的格式:
- VK_FORMAT_D32_SFLOAT: 32-bit float depth
- VK_FORMAT_D32_SFLOAT_S8_UNIT: 32-bit signed float depth 和 8-bit stencil component
- VK_FORMAT_D32_UNORM_S8_UINT: 24-bit float depth 和 8-bit stencil component
stencil component 模版組件用於模版測試 stencil tests,這是可以與深度測試組合的附加測試。我們將在未來的章節中展開。
我們可以簡化為 VK_FORMAT_D32_SFLOAT 格式,因為它的支持是非常常見的,但是盡可能的添加一些額外的靈活性也是很好的。我們增加一個函數 findSupportedFormat 從候選格式列表中 根據期望值的降序原則,檢測第一個得到支持的格式。
VkFormat findSupportedFormat(const std::vector<VkFormat>& candidates, VkImageTiling tiling, VkFormatFeatureFlags features) { }
支持的格式依賴於所使用的 tiling mode平鋪模式和具體的用法,所以我們必須包含這些參數。可以使用 vkGetPhysicalDeviceFormatProperties 函數查詢格式的支持:
for (VkFormat format : candidates) { 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; }
如果沒有任何期望的格式得到支持,我們可以指定一個特殊的值或者拋出異常:
VkFormat findSupportedFormat(const std::vector<VkFormat>& 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_ 。所有的候選格式都包含深度組件,但是最后兩個也包含 stencil 組件。我們不會使用它,但是我們需要考慮到這一點,比如在這些格式的圖像布局進行變換的時候。添加一個簡單的輔助函數,告訴我們所選擇的深度格式是否包含模版組件:
bool hasStencilComponent(VkFormat format) { return format == VK_FORMAT_D32_SFLOAT_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT; }
調用函數從 createDepthResources 找到深度格式:
VkFormat depthFormat = findDepthFormat();
我們現在擁有所有必須的信息來調用我們的 createImage 和 createImageView 輔助函數:
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_FRAGMETN_TESTS_BIT 中進行寫入操作。我們應該選擇與指定操作相匹配的最早的管線階段,以便在需要時可以作為深度附件使用。
Render pass
現在修改 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 應該與深度圖像一致。這次我們不會關心存儲深度數據(storeOp),因為繪制完成后它不會在被使用。這可能允許硬件執行其他的優化。就像顏色緩沖區一樣,我們不關心之前的深度內容,所以我們可以使用 VK_IMAGE_LAYOUT_UNDEFINED作為 initialLayout。
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 結構體引用兩個附件。
Framebuffer
下一步修改幀緩沖區的創建以及將深度圖像綁定到深度附件。來到 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(); ... }
Clear values
因為我們現在有多個帶 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。
Depth and stencil state
深度附件現在已經准備好,但是深度測試仍然需要在圖形管線開啟。它通過 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
最后三個字段用於配置模版緩沖區的操作,同樣的,在本系列教程中我們也不會使用該功能。如果想使用該功能,要確保 depth/stencil 圖像的格式包含模版原件。
pipelineInfo.pDepthStencilState = &depthStencil;
更新 VkGraphicsPipelineCreateInfo 結構體引用更深度模版狀態。如果渲染通道包含深度模版附件,則必須指定深度模版狀態。
運行程序,應該可以看到幾何圖形的片元按照正確的方式排列順序。

Handling window resize
當窗口調整大小以匹配新的顏色附件分辨率時,深度緩沖區的分辨率應該進行變化。在這種情況下,擴展 rebuildSwapChain 函數來重新創建深度資源:
void recreateSwapChain() { vkDeviceWaitIdle(device); createSwapChain(); createImageViews(); createRenderPass(); createGraphicsPipeline(); createDepthResources(); createFramebuffers(); createCommandBuffers(); }
清理操作應該在交換鏈清理功能中調用:
void cleanupSwapChain() { vkDestroyImageView(device, depthImageView, nullptr); vkDestroyImage(device, depthImage, nullptr); vkFreeMemory(device, depthImageMemory, nullptr); ... }
恭喜,現在應用程序終於准備好渲染任意幾何圖形了,並看起來正確。我們將在下一章中嘗試繪制紋理模型!
項目代碼 GitHub 地址。
