深度

术语 depthVulkan 规范的多个地方使用。本章旨在概述 Vulkan 中使用的各种“深度”术语。需要一些 3D 图形的基本知识才能充分理解本章内容。

虽然模板与深度密切相关,但本章的目的不是涵盖 API 名称之外的内容

图形管线

“深度”的概念仅用于 Vulkan 中的 图形管线,并且在提交绘制调用之前不会生效。

VkGraphicsPipelineCreateInfo 中,有许多与 depth 相关的不同值可以控制。一些状态甚至是动态的

深度格式

有几种不同的深度格式,实现可能在 Vulkan 中公开支持。

对于从深度图像进行的读取,仅需要支持通过采样或 blit 操作读取 VK_FORMAT_D16_UNORMVK_FORMAT_D32_SFLOAT

对于向深度图像进行的写入,需要支持 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 中,它只是一个 VkImage/VkImageViewVkFramebuffer 可以在绘制时引用它。创建 VkRenderPass 时,pDepthStencilAttachment 值指向帧缓冲区中的深度附件。

为了使用 pDepthStencilAttachment,必须使用 VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT 创建后备的 VkImage

当执行图像屏障或清除等需要 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;

如果不确定在应用程序中使用早期片段测试还是晚期片段测试,请同时使用两者。

清除

最好始终在传递开始时使用设置为 VK_ATTACHMENT_LOAD_OP_CLEARloadOp 来清除深度缓冲区,但也可以使用 vkCmdClearDepthStencilImage 在渲染传递之外清除深度图像。

清除时,请注意 VkClearValue 是一个联合,应设置 VkClearDepthStencilValue depthStencil 而不是颜色清除值。

光栅化前

在图形管线中,有一系列 光栅化前着色器阶段生成要光栅化的图元。在到达光栅化步骤之前,最后一个光栅化前阶段的最终 vec4 位置 (gl_Position) 将通过 固定函数顶点后处理

下面概述了在光栅化之前发生的各种坐标名称和操作。

depth_coordinates_flow

图元裁剪

除非使用 VK_EXT_depth_clip_enable 中的 depthClipEnable,否则如果图元位于视锥体之外,则始终会发生裁剪。在 Vulkan 中,深度裁剪表示为:

0 <= Zc <= Wc

当计算归一化设备坐标 (NDC) 时,任何超出 [0, 1] 范围的值都会被裁剪。

以下是一些示例,其中 ZdZc/Wc 的结果:

  • 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 中,视锥体表示为:

-Wc <= Zc <= Wc

任何超出 [-1, 1] 范围的值都会被裁剪。

添加了 VK_EXT_depth_clip_control 扩展,以便在 Vulkan 上高效地分层 OpenGL。在创建 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;

视口变换

视口变换是将归一化设备坐标转换为帧缓冲区坐标的变换,该变换基于视口矩形和深度范围。

管道中正在使用的视口列表由 VkPipelineViewportStateCreateInfo::pViewports 表示,而 VkPipelineViewportStateCreateInfo::viewportCount 设置正在使用的视口数量。如果未启用 VkPhysicalDeviceFeatures::multiViewport,则只能有 1 个视口。

可以使用 VK_DYNAMIC_STATE_VIEWPORT 或来自 VK_EXT_extended_dynamic_stateVK_DYNAMIC_STATE_VIEWPORT_WITH_COUNT_EXT 动态设置视口值。

深度范围

每个视口都包含一个 VkViewport::minDepthVkViewport::maxDepth 值,这些值设置视口的“深度范围”。

尽管名称如此,minDepth 可以小于、等于或大于 maxDepth

minDepthmaxDepth 被限制在 0.01.0 之间(包括端点)。如果启用了 VK_EXT_depth_range_unrestricted,则此限制将被解除。

帧缓冲区深度坐标 Zf 表示为:

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

  • Oz = minDepth

  • Pz = maxDepth - minDepth

光栅化

深度偏移

可以通过为多边形计算的单个值来偏移由多边形的光栅化生成的所有片段的深度值。如果在绘制时 VkPipelineRasterizationStateCreateInfo::depthBiasEnableVK_FALSE,则不应用深度偏移。

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

需要支持 VkPhysicalDeviceFeatures::depthBiasClamp 功能,否则 VkPipelineRasterizationStateCreateInfo::depthBiasClamp 必须为 0.0f

可以使用 VK_DYNAMIC_STATE_DEPTH_BIAS 或来自 VK_EXT_extended_dynamic_state2VK_DYNAMIC_STATE_DEPTH_BIAS_ENABLE_EXT 动态设置深度偏移值。

后光栅化

片段着色器

内置输入 FragCoord 是帧缓冲区坐标。Z 分量是图元的插值深度值。如果着色器不写入 FragDepth,则此 Z 分量值将被写入 FragDepth。如果着色器动态写入 FragDepth,则必须声明 DepthReplacing 执行模式(这在 glslang 等工具中完成)。

FragDepthFragCoord 在 GLSL 中分别是 gl_FragDepthgl_FragCoord

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

保守深度

DepthGreaterDepthLessDepthUnchanged 执行模式允许对 依赖于在片段之前运行的早期深度测试的实现进行可能的优化。这可以通过使用适当的布局限定符声明 gl_FragDepth 在 GLSL 中轻松完成。

// 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,则会丢弃该片段。

解析深度缓冲区

可以使用 VK_KHR_depth_stencil_resolve 扩展(在 1.2 中提升为 Vulkan 核心)在子通道中解析多重采样的深度/模板附件,方式与颜色附件类似。

深度边界

需要支持 VkPhysicalDeviceFeatures::depthBounds 功能。

如果使用 VkPipelineDepthStencilStateCreateInfo::depthBoundsTestEnable,则会检查深度附件中的每个 Za 是否在 VkPipelineDepthStencilStateCreateInfo::minDepthBoundsVkPipelineDepthStencilStateCreateInfo::maxDepthBounds 设置的范围内。如果值不在边界内,则将覆盖掩码设置为零。

深度边界值可以使用 动态 状态设置,通过 VK_DYNAMIC_STATE_DEPTH_BOUNDS 或来自 VK_EXT_extended_dynamic_stateVK_DYNAMIC_STATE_DEPTH_BOUNDS_TEST_ENABLE_EXT 实现。

深度测试

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

以下是深度测试的高级概述。

depth_test

深度比较操作

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 | 测试失败

depthTestEnabledepthCompareOp 值可以使用 动态 状态设置,通过来自 VK_EXT_extended_dynamic_stateVK_DYNAMIC_STATE_DEPTH_TEST_ENABLE_EXTVK_DYNAMIC_STATE_DEPTH_COMPARE_OP_EXT 实现。

深度缓冲区写入

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

depthWriteEnable 值可以使用 动态 状态设置,通过来自 VK_EXT_extended_dynamic_stateVK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE_EXT 实现。

深度钳制

需要支持 VkPhysicalDeviceFeatures::depthClamp 功能。

在深度测试之前,如果启用 VkPipelineRasterizationStateCreateInfo::depthClampEnable,则在将样本的 ZfZa 进行比较之前,Zf 将被钳制到 [min(n,f), max(n,f)],其中 nf 分别是该片段使用的视口的 minDepthmaxDepth 深度范围值。