深度
depth
被称为深度,本文简单介绍一下 Vulkan 使用的各种“深度”场景,了解本章内容需要一些 3D 图形的基本知识。
模板与深度密切相关,本章内容咱不介绍模板。
图形管线
在Vulkan 中只有图形管线会用到“深度”概念,并且只在提交绘制任务时才会生效。
VkGraphicsPipelineCreateInfo
里有许多与depth
相关的可控制项,有些控制状态甚至是动态设置。
深度格式
有几种不同的深度格式。
对于读取深度图像,只有VK_FORMAT_D16_UNORM
和VK_FORMAT_D32_SFLOAT
必须支持通过采样或blit操作读取。
对于写入深度图像,只有VK_FORMAT_D16_UNORM
必须支持,此外,VK_FORMAT_X8_D24_UNORM_PACK32
、VK_FORMAT_D32_SFLOAT
、VK_FORMAT_D24_UNORM_S8_UINT
、VK_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中的深度附件。
为了使用pDepthStencilAttachment
,VkImage
必须使用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
是一个联合体,应该设置VkClearDepthStencilValue
、depthStencil
而不是颜色。
预光栅化
在图形管线中,有一套预光栅化着色器阶段,用于生成要光栅化的图元。在光栅化之前,预光栅化阶段的gl_Position
会经过固定顶点后处理流程。
下面展示了光栅化之前发生的各种坐标变化:
图元剪裁
除非关闭VK_EXT_depth_clip_enable的depthClipEnable
,否则图元在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
)
用户自定义裁剪和剔除
使用内置数组ClipDistance
和CullDistance
,预光栅化着色器阶段可以设置用户自定义的裁剪和剔除。
在最 后的预光栅化阶段,将在整个图元中进行线性插值,图元中插值距离小于0的部分将视作视锥体之外。如果ClipDistance
或CullDistance
随后被片段着色器使用,它们将包含这些线性插值值。
ClipDistance
和CullDistance
在 GLSL 中的用法是 gl_ClipDistance[]
和gl_CullDistance[]
从 OpenGL 移植
在 OpenGL 中view volume
(视锥体)表示为
-Wc <= Zc <= Wc
[-1, 1]
之外的任何东西都会被剪掉。
vulkan的VK_EXT_depth_clip_control 扩展允许复用 OpenGL的shader功能。在创建VkPipeline
时通过设置VkPipelineViewportDepthClipControlCreateInfoEXT::negativeOneToOne
为VK_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_VIEWPORT
或VK_DYNAMIC_STATE_VIEWPORT_WITH_COUNT_EXT
动态地设置视口。
深度范围
每个视口包含一个VkViewport::minDepth
和VkViewport::maxDepth
值,表示视口的“深度范围”。
minDepth
可以小于、等于或大于maxDepth
。
minDepth
和maxDepth
被限制在[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
中使用depthBiasConstantFactor
、depthBiasClamp
和depthBiasSlopeFactor
,可计算出深度偏移。
如果不支持VkPhysicalDeviceFeatures::depthBiasClamp
功能,VkPipelineRasterizationStateCreateInfo::depthBiasClamp
必须0.0f
。
可以使用VK_EXT_extended_dynamic_state2的VK_DYNAMIC_STATE_DEPTH_BIAS
或VK_DYNAMIC_STATE_DEPTH_BIAS_ENABLE_EXT
动态设置深度偏差值。
后光栅化
片元着色器
内置的FragCoord
是framebuffer坐标。Z
分量是图元的深度值。如果着色器未写入FragDepth
,则会默认写入Z
。如果着色器动态写入FragDepth
,则必须声明DepthReplacing
执行模式(在 glslang 等工具中完成)。
FragDepth
和FragCoord
在 GLSL 中的用法是gl_FragDepth
和gl_FragCoord
在SPIR-V 中使用 OpTypeImage
时,Vulkan 会忽略操作数Depth
保守深度
DepthGreater
、DepthLess
和 DepthUnchanged
执行模式可能对依赖片元阶段前运行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::minDepthBounds
和VkPipelineDepthStencilStateCreateInfo::maxDepthBounds
范围内,如果值不在范围内,则将覆盖掩码设置为零。
可以使用VK_EXT_extended_dynamic_state扩展的VK_DYNAMIC_STATE_DEPTH_BOUNDS
或VK_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_EXT
和VK_DYNAMIC_STATE_DEPTH_COMPARE_OP_EXT
动态设置depthTestEnable
和depthCompareOp
的值。
写入深度缓冲区
即使深度测试通过,如果VkPipelineDepthStencilStateCreateInfo::depthWriteEnable
设置成VK_FALSE
,也不会写深度附件。主要原因是深度测试可用于设置某些渲染技术的覆盖掩码。
可以使用VK_EXT_extended_dynamic_state扩展的VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE_EXT
动态设置depthWriteEnable
。
深度clamp
需要支持VkPhysicalDeviceFeatures::depthClamp
功能。
在深度测试之前,如果启用了VkPipelineRasterizationStateCreateInfo::depthClampEnable
,则在将样本Zf
与Za
比较之前,Zf
将限制为[min(n,f), max(n,f)]
,n
和f
分别是此片元视口的minDepth
和maxDepth
深度范围。