深度缓冲区
介绍
尽管目前我们的几何体都是投影到3D场景的,可是它仍然是扁平的。在本章我们会为位置增加一个Z轴,以实现3D网格。将会看到若没有经过深度排序,正方形的在定位会产生的问题。
3D几何体
修改 Vertex 结构体为位置使用3D向量,同时更新 VkVertexInputAttributeDescription 中的格式说明:
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;
}
最后更新顶点容器,添加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.5,并为这个正方形添加类似的索引:
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
};
运行程序,将会看到如下图像:
底部的正方形绘制在了上部正方形的上面,这是因为底部索引数组提供的更晚,有两种方案去解决这个问题:
- 以深度为标准由后向前排序所有绘制调用
- 使用深度缓冲区的深度测试
第一个方案通常被用于绘制有透明度的物体,但是更常用的解决片段顺序的方式是去使用深度缓冲,一个深度缓冲是附加的存储一个像素点的深度的数据区域,就像是颜色缓冲区存储着每一个点的颜色一样,每次光栅器都会生成一个片段,深度测试会检查新的深度是否比之前的深度更小,如果没有,这个新的片段会被丢弃掉,如果想法通过了深度测试,会将这个片段的新的深度值写入深度缓冲区,有可能手动操作修改这个值,就像手动控制颜色的输出一样。
#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 来配置为 Vulkan 的范围 0.0 ~ 1.0 。
深度图像和视图
与颜色附件一样,深度附件也是基于图像的.不同点是,交换链并不会自动为我们创建深度附件,我们只需要单个的深度附件,因为一个绘制操作每时每刻都只会运行一次,深度缓冲会再次需要资源三件套 图像,内存和图像视图。
VkImage depthImage;
VkImageView depthImageView;
VkDeviceMemory depthImageMemory;
新建函数 createDepthResources 去创建这些资源:
void initVulkan() {
...
createCommandPool();
createDepthResources();
createTextureImage();
...
}
...
void createDepthResources() {
}
创建深度图像相当直接,它需要与颜色附件有相同的分辨率,设置图像使用方式是作为深度缓冲区,瓦片优化,设备本地内存。唯一的问题是,如何为深度图像选择一个合适的格式,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_32_SFLOAT,因为对它的硬件支持是最通用的,但是为其他的可能性添加扩展也是很不错的,我们准备去写一个 findSupportedFormat 函数,它会接收一串待选格式列表,找出第一个适合的格式:
VkFormat findSupportedFormat(const std::vector<VkFormat>& candidates, VkImageTiling tiling, VkFormatFeatureFlags features){
}
支持的格式依赖于瓦片的格式及使用的方式,所以我们必须也将这些参数包含进来,支持的待选格式可以通过函数 vkGetPhysicalDeviceFormatProperties 查询到:
for(VkFormat format : candicates){
VkFormatProperties props;
vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &props);
}//end for each
VkFormatProperties 结构体包含三个字段
- linearTilingFeatures : 支持线性瓦片
- optimalTilingFeatures : 支持硬件最优瓦片布局
- bufferFeatures : 支持缓冲区
只有前两个参数是与我们检测的依赖tilingMode 的方式有关系:
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_ ,所有的备选格式都包含了深度组件,不过最后的两个还包含了模板组件,我们目前不会使用到它,但我们再实现图像的布局转换时,需要考虑到它。增加一个简单的函数用于查询当前的格式是否包含深度组件。
bool hasStencilComponent(VkFormat format){
return format == VK_FORMAT_D32_SFLOAT_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT;
}
在 createDepthResources 中调用 findDepthFormat 去找到一个合适的深度格式。
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 现在总是假定图像的subresource 总是 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_SRGB, VK_IMAGE_ASPECT_COLOR_BIT);
以上便是创建深度图像的步骤,我们并不需要为深度图像做映射或拷贝操作,因为我们使用其的方式是先清理它然后用渲染通道给它写入数据,就像颜色附件一样。
显式地转换深度图像
我们并不需要显式地转换深度附件的图像布局,因为会在渲染通道中自动完成这样的转换。然而为了完整性,我们依然会在这里处理这个转换,您可以选择跳过。
在 createDepthResources 函数的最后调用 transitionImageLayout :
transitionImageLayout(
depthImage, depthFormat,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
初始化布局时,使用未定义的布局,因为初始时并不存在深度内容,我们需要更新一些 transitionImageLayout的逻辑,以让图像正确的使用子资源(subresource aspect)
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;
}
}
尽管我们不会使用模板组件,但在深度图像中依然需要包含它。
最后添加正确的访问权限和管线阶段
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 参数需要与深度图像的格式保持一致,这一次我们并不在意深度数据的存储,因为在绘制结束时,深度数据就不再使用了,这样的设置可以让硬件做出额外的优化。 就像颜色缓冲区一样,我们并不在意之前的深度内容,所以 initialLayout 我们选择使用 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, depthAttachmen};
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;
下一步,更新 VkSubpassDependency 结构体来引用所有的附件。
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
最后我们扩展了子渲染通道的依赖.
帧缓冲
下一步是去修改帧缓冲区的创建,来到 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;
颜色缓冲区需要针对每一个交换链上的图像做出区分,但是深度图像可以被所有图像共用,因为我们已经通过信号量保证了一次只会有一个渲染通道在运行。
需要保证在创建帧缓冲区之前深度相关的资源已经创建完毕
void initVulkan() {
...
createDepthResources();
createFramebuffers();
...
}
清理值
由于目前已经有多个附件都使用了 VK_ATTACHMENT_LOAD_OP_CLEAR 选项,所以也需要指定多个清理值,来到recordCommandBuffer函数中创建一个 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,注意 clearValues 的顺序需要与定义附件的顺序相一致。
深度和模板状态
现在深度附件已经可以使用了,但是深度测试仍然需要在图形管线中手动开启,它是通过 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.depthBoundTestEnable = VK_FALSE;
depthStencil.minDepthBounds = 0.0f;
depthStencil.maxDepthBounds = 1.0f;
depthBoundTestEnable,minDepthBounds,maxDepthBounds 这三个值用于深度的边界测试。简单来说,这些值可以让你的片段深度限制在指定范围内,目前我们不使用此参数。
depthStencil.stencilTestEnable = VK_FALSE;
depthStencil.front = {};
depthStencil.back = {};
最后三个参数配置模板缓冲区的操作,目前教程中并不使用。如果想使用这些操作,必须确保图像中包含了模板组件。
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();
createDepthResources();
createFramebuffers();
}
在清理函数中也需要清除掉之前分配的深度资源。
void cleanupSwapChain() {
vkDestroyImageView(device, depthImageView, nullptr);
vkDestroyImage(device, depthImage, nullptr);
vkFreeMemory(device, depthImageMemory, nullptr);
...
}
现在你的应用程序可以渲染出正确的3D几何体了,我们将在下一章绘制出一个带纹理的模型。