跳到主要内容

深度

depth被称为深度,本文简单介绍一下 Vulkan 使用的各种“深度”场景,了解本章内容需要一些 3D 图形的基本知识。

提示

模板与深度密切相关,本章内容咱不介绍模板。

图形管线

在Vulkan 中只有图形管线会用到“深度”概念,并且只在提交绘制任务时才会生效。

VkGraphicsPipelineCreateInfo里有许多与depth相关的可控制项,有些控制状态甚至是动态设置

深度格式

有几种不同的深度格式。

对于读取深度图像,只有VK_FORMAT_D16_UNORMVK_FORMAT_D32_SFLOAT必须支持通过采样或blit操作读取。

对于写入深度图像,只有VK_FORMAT_D16_UNORM必须支持,此外,VK_FORMAT_X8_D24_UNORM_PACK32VK_FORMAT_D32_SFLOATVK_FORMAT_D24_UNORM_S8_UINTVK_FORMAT_D32_SFLOAT_S8_UINT至少得支持一个。如果深度和模板都需要,那么需要在查找格式时添加额外的判断:

// Example of query logic
VkFormatProperties properties;

vkGetPhysicalDeviceFormatProperties(physicalDevice, VK_FORMAT_D24_UNORM_S8_UINT, &properties);
bool d24s8_support = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT);

vkGetPhysicalDeviceFormatProperties(physicalDevice, VK_FORMAT_D32_SFLOAT_S8_UINT, &properties);
bool d32s8_support = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT);

assert(d24s8_support | d32s8_support); // will always support at least one

深度缓冲区作为 VkImage

术语“深度缓冲区”在图形领域使用非常广泛,但在 Vulkan 中,它只是一个可以在绘制时被VkFramebuffer引用的VkImage/VkImageView。创建VkRenderPass时,pDepthStencilAttachment指向framebuffer中的深度附件。

为了使用pDepthStencilAttachmentVkImage必须使用VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT标志创建。

在执行图像barrier或清除操作时,VkImageAspectFlags需设置VK_IMAGE_ASPECT_DEPTH_BIT用于引用深度图像。

布局

选择VkImageLayout时,这些布局同时支持读写:

  • VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
  • VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL
  • VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL

这些布局支持只读:

  • VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
  • VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL
  • VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL

进行布局转换时,请确保读写深度图像所需的访问掩码设置正确。

// Example of going from undefined layout to a depth attachment to be read and written to

// Core Vulkan example
srcAccessMask = 0;
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 | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;

// VK_KHR_synchronization2
srcAccessMask = VK_ACCESS_2_NONE_KHR;
dstAccessMask = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT_KHR | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT_KHR;
sourceStage = VK_PIPELINE_STAGE_2_NONE_KHR;
destinationStage = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT_KHR | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT_KHR;
提示

如果不确定应用程序使用early-z还是late-z测试,那就两个都使用。

清除

最好在第一个pass将loadOp设置为VK_ATTACHMENT_LOAD_OP_CLEAR来清除深度缓冲区,也可以在渲染流程外使用vkCmdClearDepthStencilImage清除深度图像。

清除时,要注意VkClearValue是一个联合体,应该设置VkClearDepthStencilValuedepthStencil而不是颜色。

预光栅化

在图形管线中,有一套预光栅化着色器阶段,用于生成要光栅化的图元。在光栅化之前,预光栅化阶段的gl_Position会经过固定顶点后处理流程

下面展示了光栅化之前发生的各种坐标变化:

深度坐标流

图元剪裁

除非关闭VK_EXT_depth_clip_enabledepthClipEnable,否则图元在view volume (视锥体)之外的部分始终会被裁剪。在 Vulkan 中,深度表示为:

0 <= Zc <= Wc

当计算标准化设备坐标 (NDC) 时,[0, 1]坐标之外的任何东西都会被剪掉。

以下几个示例显示了Zc/Wc结果Zd是否裁剪的情况:

  • vec4(1.0, 1.0, 2.0, 2.0)- 未剪辑 ( Zd== 1.0)
  • vec4(1.0, 1.0, 0.0, 2.0)- 未剪辑 ( Zd== 0.0)
  • vec4(1.0, 1.0, -1.0, 2.0)- 剪辑 ( Zd== -0.5)
  • vec4(1.0, 1.0, -1.0, -2.0)- 未剪辑 ( Zd== 0.5)

用户自定义裁剪和剔除

使用内置数组ClipDistanceCullDistance预光栅化着色器阶段可以设置用户自定义的裁剪和剔除

在最后的预光栅化阶段,将在整个图元中进行线性插值,图元中插值距离小于0的部分将视作视锥体之外。如果ClipDistanceCullDistance随后被片段着色器使用,它们将包含这些线性插值值。

提示

ClipDistanceCullDistance在 GLSL 中的用法是 gl_ClipDistance[]gl_CullDistance[]

从 OpenGL 移植

在 OpenGL 中view volume(视锥体)表示为

-Wc <= Zc <= Wc

[-1, 1]之外的任何东西都会被剪掉。

vulkan的VK_EXT_depth_clip_control 扩展允许复用 OpenGL的shader功能。在创建VkPipeline时通过设置VkPipelineViewportDepthClipControlCreateInfoEXT::negativeOneToOneVK_TRUE,将使用 OpenGL [-1, 1]的视锥体。

如果VK_EXT_depth_clip_control不可用,规避方法是在预光栅化着色器中转换:

// [-1,1] to [0,1]
position.z = (position.z + position.w) * 0.5;

视口变换

视口变换指的是基于视口矩形和深度范围,将NDC坐标转换到framebuffer坐标。

管线中使用的视口通过VkPipelineViewportStateCreateInfo::pViewports设置,同时设置VkPipelineViewportStateCreateInfo::viewportCount为使用的视口数量。如果VkPhysicalDeviceFeatures::multiViewport未启用,只能有1个视口。

提示

可以使用VK_EXT_extended_dynamic_state扩展的VK_DYNAMIC_STATE_VIEWPORTVK_DYNAMIC_STATE_VIEWPORT_WITH_COUNT_EXT动态地设置视口。

深度范围

每个视口包含一个VkViewport::minDepthVkViewport::maxDepth值,表示视口的“深度范围”。

提示

minDepth可以小于、等于或大于maxDepth

minDepthmaxDepth被限制在[0.0,1.0]之间,如果启用了VK_EXT_depth_range_unrestricted扩展,则无限制。

framebuffer深度坐标Zf表示为:

Zf = Pz * Zd + Oz
  • Zd= Zc/ Wc(参见图元剪裁
  • Oz=minDepth
  • Pz= maxDepth-minDepth

光栅化

深度偏移

多边形光栅化生成的所有片元深度都可以进行偏移,如果在绘制时VkPipelineRasterizationStateCreateInfo::depthBiasEnable设置为VK_FALSE,则深度偏移功能不生效。

VkPipelineRasterizationStateCreateInfo中使用depthBiasConstantFactordepthBiasClampdepthBiasSlopeFactor可计算出深度偏移

提示

如果不支持VkPhysicalDeviceFeatures::depthBiasClamp功能,VkPipelineRasterizationStateCreateInfo::depthBiasClamp必须0.0f

提示

可以使用VK_EXT_extended_dynamic_state2VK_DYNAMIC_STATE_DEPTH_BIASVK_DYNAMIC_STATE_DEPTH_BIAS_ENABLE_EXT动态设置深度偏差值。

后光栅化

片元着色器

内置的FragCoord是framebuffer坐标。Z分量是图元的深度值。如果着色器未写入FragDepth,则会默认写入Z。如果着色器动态写入FragDepth,则必须声明DepthReplacing执行模式(在 glslang 等工具中完成)。

提示

FragDepthFragCoord在 GLSL 中的用法是gl_FragDepthgl_FragCoord

提示

在SPIR-V 中使用 OpTypeImage时,Vulkan 会忽略操作数Depth

保守深度

DepthGreaterDepthLessDepthUnchanged 执行模式可能对依赖片元阶段前运行early-z test的驱动实现会有优化,这可以在 GLSL 中使用适当的 layout 限定符声明 gl_FragDepth 来实现:

DepthGreater`、`DepthLess`和执行模式允许对[依赖于在片段之前运行的早期深度测试的](https://registry.khronos.org/OpenGL/extensions/ARB/ARB_conservative_depth.txt)`DepthUnchanged`实现进行可能的优化。这可以在 GLSL 中通过使用适当的布局限定符声明轻松完成。`gl_FragDepth
// assume it may be modified in any way
layout(depth_any) out float gl_FragDepth;

// assume it may be modified such that its value will only increase
layout(depth_greater) out float gl_FragDepth;

// assume it may be modified such that its value will only decrease
layout(depth_less) out float gl_FragDepth;

// assume it will not be modified
layout(depth_unchanged) out float gl_FragDepth;

不遵循这些条件可能会导致未定义的行为。

逐像素处理和覆盖掩码

光栅化视为“逐像素”操作,意味着对颜色附件进行多重采样时,使用的“深度缓冲区”VkImage也必须使用相同的VkSampleCountFlagBits值创建。

每个片元都有一个覆盖掩码,根据该掩码可确定该样本是否位于片元的基图元区域内。如果片元覆盖掩码的所有位都是0,则丢弃该片元。

resolve深度缓冲区

在 Vulkan 中可以使用VK_KHR_depth_stencil_resolve扩展(在 1.2 中提升为 Vulkan 核心扩展),用与颜色附件类似的方式,来resolve一个subpass中的多重采样深度/模板附件。

深度边界

提示

需要VkPhysicalDeviceFeatures::depthBounds来支持该功能。

VkPipelineDepthStencilStateCreateInfo::depthBoundsTestEnable用于获取深度附件中的Za并检查它是否在VkPipelineDepthStencilStateCreateInfo::minDepthBoundsVkPipelineDepthStencilStateCreateInfo::maxDepthBounds范围内,如果值不在范围内,则将覆盖掩码设置为零。

提示

可以使用VK_EXT_extended_dynamic_state扩展的VK_DYNAMIC_STATE_DEPTH_BOUNDSVK_DYNAMIC_STATE_DEPTH_BOUNDS_TEST_ENABLE_EXT动态设置深度边界值。

深度测试

深度测试将framebuffer深度坐标Zf与深度附件的深度值Za进行比较,如果测试失败,则丢弃该片元。如果测试通过,则深度附件将使用片元的输出深度进行更新。VkPipelineDepthStencilStateCreateInfo::depthTestEnable用于在管线中启用/禁用深度测试。

下面是深度测试的流程概览:

深度测试

深度比较操作

VkPipelineDepthStencilStateCreateInfo::depthCompareOp是深度测试的比较函数。

depthCompareOp== VK_COMPARE_OP_LESS( Zf< Za)的示例

  • Zf= 1.0 | Za= 2.0 | 测试通过
  • Zf= 1.0 | Za= 1.0 | 测试失败
  • Zf= 1.0 | Za= 0.0 | 测试失败
提示

可以使用VK_EXT_extended_dynamic_state扩展的VK_DYNAMIC_STATE_DEPTH_TEST_ENABLE_EXTVK_DYNAMIC_STATE_DEPTH_COMPARE_OP_EXT动态设置depthTestEnabledepthCompareOp的值。

写入深度缓冲区

即使深度测试通过,如果VkPipelineDepthStencilStateCreateInfo::depthWriteEnable设置成VK_FALSE,也不会写深度附件。主要原因是深度测试可用于设置某些渲染技术的覆盖掩码

提示

可以使用VK_EXT_extended_dynamic_state扩展的VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE_EXT动态设置depthWriteEnable

深度clamp

提示

需要支持VkPhysicalDeviceFeatures::depthClamp功能。

在深度测试之前,如果启用了VkPipelineRasterizationStateCreateInfo::depthClampEnable,则在将样本ZfZa比较之前,Zf将限制为[min(n,f), max(n,f)]nf分别是此片元视口的minDepthmaxDepth深度范围。