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几何体并能够有正确的视觉效果了。下一章我们会绘制一个贴图模型。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片