深度
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
动态设置深度偏差值。