Depth
The term depth
is used in various spots in the Vulkan Spec. This chapter is aimed to give an overview of the various "depth" terminology used in Vulkan. Some basic knowledge of 3D graphics is needed to get the most out of this chapter.
While stencil is closely related depth, this chapter does not aim to cover it outside the realm of API names |
Graphics Pipeline
The concept of "depth" is only used for graphics pipelines in Vulkan and doesn’t take effect until a draw call is submitted.
Inside the VkGraphicsPipelineCreateInfo
there are many different values related to depth
that can be controlled. Some states are even dynamic as well.
Depth Formats
There are a few different depth formats and an implementation may expose support for in Vulkan.
For reading from a depth image only VK_FORMAT_D16_UNORM
and VK_FORMAT_D32_SFLOAT
are required to support being read via sampling or blit operations.
For writing to a depth image VK_FORMAT_D16_UNORM
is required to be supported. From here at least one of (VK_FORMAT_X8_D24_UNORM_PACK32
or VK_FORMAT_D32_SFLOAT
) and (VK_FORMAT_D24_UNORM_S8_UINT
or VK_FORMAT_D32_SFLOAT_S8_UINT
) must also be supported. This will involve some extra logic when trying to find which format to use if both the depth and stencil are needed in the same format.
// 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
Depth Buffer as a VkImage
The term "depth buffer" is used a lot when talking about graphics, but in Vulkan, it is just a VkImage
/VkImageView
that a VkFramebuffer
can reference at draw time. When creating a VkRenderPass
the pDepthStencilAttachment
value points to the depth attachment in the framebuffer.
In order to use pDepthStencilAttachment
the backing VkImage
must have been created with VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT
.
When performing operations such as image barriers or clearing where the VkImageAspectFlags
is required, the VK_IMAGE_ASPECT_DEPTH_BIT
is used to reference the depth memory.
Layout
When selecting the VkImageLayout
there are some layouts that allow for both reading and writing to the image:
-
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
-
VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL
-
VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL
as well as layouts that allow for only reading to the image:
-
VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
-
VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL
-
VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL
When doing the layout transition make sure to set the proper depth access masks needed for both reading and writing the depth image.
// 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;
If unsure to use only early or late fragment tests for your application, use both. |
Clearing
It is always better to clear a depth buffer at the start of the pass with loadOp
set to VK_ATTACHMENT_LOAD_OP_CLEAR
, but depth images can also be cleared outside a render pass using vkCmdClearDepthStencilImage
.
When clearing, notice that VkClearValue
is a union and VkClearDepthStencilValue depthStencil
should be set instead of the color clear value.
Pre-rasterization
In the graphics pipeline, there are a series of pre-rasterization shader stages that generate primitives to be rasterized. Before reaching the rasterization step, the final vec4
position (gl_Position
) of the last pre-rasterization stage runs through Fixed-Function Vertex Post-Processing.
The following gives a high level overview of the various coordinates name and operations that occur before rasterization.
Primitive Clipping
Clipping always occurs, unless using the depthClipEnable
from VK_EXT_depth_clip_enable, if the primitive is outside the view volume
. In Vulkan, this is expressed for depth as
0 <= Zc <= Wc
When the normalized device coordinates (NDC) are calculated, anything outside of [0, 1]
is clipped.
A few examples where Zd
is the result of Zc
/Wc
:
-
vec4(1.0, 1.0, 2.0, 2.0)
- not clipped (Zd
==1.0
) -
vec4(1.0, 1.0, 0.0, 2.0)
- not clipped (Zd
==0.0
) -
vec4(1.0, 1.0, -1.0, 2.0)
- clipped (Zd
==-0.5
) -
vec4(1.0, 1.0, -1.0, -2.0)
- not clipped (Zd
==0.5
)
User defined clipping and culling
Using ClipDistance
and CullDistance
built-in arrays the pre-rasterization shader stages can set user defined clipping and culling.
In the last pre-rasterization shader stage, these values will be linearly interpolated across the primitive and the portion of the primitive with interpolated distances less than 0
will be considered outside the clip volume. If ClipDistance
or CullDistance
are then used by a fragment shader, they contain these linearly interpolated values.
|
Porting from OpenGL
In OpenGL the view volume
is expressed as
-Wc <= Zc <= Wc
and anything outside of [-1, 1]
is clipped.
The VK_EXT_depth_clip_control extension was added to allow efficient layering of OpenGL over Vulkan. By setting the VkPipelineViewportDepthClipControlCreateInfoEXT::negativeOneToOne
to VK_TRUE
when creating the VkPipeline
it will use the OpenGL [-1, 1]
view volume.
If VK_EXT_depth_clip_control
is not available, the workaround currently is to perform the conversion in the pre-rasterization shader
// [-1,1] to [0,1]
position.z = (position.z + position.w) * 0.5;
Viewport Transformation
The viewport transformation is a transformation from normalized device coordinates to framebuffer coordinates, based on a viewport rectangle and depth range.
The list of viewports being used in the pipeline is expressed by VkPipelineViewportStateCreateInfo::pViewports
and VkPipelineViewportStateCreateInfo::viewportCount
sets the number of viewports being used. If VkPhysicalDeviceFeatures::multiViewport
is not enabled, there must only be 1 viewport.
The viewport value can be set dynamically using |
Depth Range
Each viewport holds a VkViewport::minDepth
and VkViewport::maxDepth
value which sets the "depth range" for the viewport.
Despite their names, |
The minDepth
and maxDepth
are restricted to be set inclusively between 0.0
and 1.0
. If the VK_EXT_depth_range_unrestricted is enabled, this restriction goes away.
The framebuffer depth coordinate Zf
is represented as:
Zf = Pz * Zd + Oz
-
Zd
=Zc
/Wc
(see Primitive Clipping) -
Oz
=minDepth
-
Pz
=maxDepth
-minDepth
Rasterization
Depth Bias
The depth values of all fragments generated by the rasterization of a polygon can be offset by a single value that is computed for that polygon. If VkPipelineRasterizationStateCreateInfo::depthBiasEnable
is VK_FALSE
at draw time, no depth bias is applied.
Using the depthBiasConstantFactor
, depthBiasClamp
, and depthBiasSlopeFactor
in VkPipelineRasterizationStateCreateInfo
the depth bias can be calculated.
Requires the |
The depth bias values can be set dynamically using |
Post-rasterization
Fragment Shader
The input built-in FragCoord
is the framebuffer coordinate. The Z
component is the interpolated depth value of the primitive. This Z
component value will be written to FragDepth
if the shader doesn’t write to it. If the shader dynamically writes to FragDepth
, the DepthReplacing
Execution Mode must be declared (This is done in tools such as glslang).
|
When using |
Conservative depth
The DepthGreater
, DepthLess
, and DepthUnchanged
Execution Mode allow for a possible optimization for implementations that relies on an early depth test to be run before the fragment. This can be easily done in GLSL by declaring gl_FragDepth
with the proper layout qualifier.
// 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;
Violating the condition yields undefined behavior.
Per-sample processing and coverage mask
The following post-rasterization occurs as a "per-sample" operation. This means when doing multisampling with a color attachment, any "depth buffer" VkImage
used as well must also have been created with the same VkSampleCountFlagBits
value.
Each fragment has a coverage mask based on which samples within that fragment are determined to be within the area of the primitive that generated the fragment. If a fragment operation results in all bits of the coverage mask being 0
, the fragment is discarded.
Resolving depth buffer
It is possible in Vulkan using the VK_KHR_depth_stencil_resolve extension (promoted to Vulkan core in 1.2) to resolve multisampled depth/stencil attachments in a subpass in a similar manner as for color attachments.
Depth Bounds
Requires the |
If VkPipelineDepthStencilStateCreateInfo::depthBoundsTestEnable
is used to take each Za
in the depth attachment and check if it is within the range set by VkPipelineDepthStencilStateCreateInfo::minDepthBounds
and VkPipelineDepthStencilStateCreateInfo::maxDepthBounds
. If the value is not within the bounds, the coverage mask is set to zero.
The depth bound values can be set dynamically using |
Depth Test
The depth test compares the framebuffer depth coordinate Zf
with the depth value Za
in the depth attachment. If the test fails, the fragment is discarded. If the test passes, the depth attachment will be updated with the fragment’s output depth. The VkPipelineDepthStencilStateCreateInfo::depthTestEnable
is used to enable/disable the test in the pipeline.
The following gives a high level overview of the depth test.
Depth Compare Operation
The VkPipelineDepthStencilStateCreateInfo::depthCompareOp
provides the comparison function used for the depth test.
An example where depthCompareOp
== VK_COMPARE_OP_LESS
(Zf
< Za
)
-
Zf
= 1.0 |Za
= 2.0 | test passes -
Zf
= 1.0 |Za
= 1.0 | test fails -
Zf
= 1.0 |Za
= 0.0 | test fails
The |
Depth Buffer Writes
Even if the depth test passes, if VkPipelineDepthStencilStateCreateInfo::depthWriteEnable
is set to VK_FALSE
it will not write the value out to the depth attachment. The main reason for this is because the depth test itself will set the coverage mask which can be used for certain render techniques.
The |
Depth Clamping
Requires the |
Prior to the depth test, if VkPipelineRasterizationStateCreateInfo::depthClampEnable
is enabled, before the sample’s Zf
is compared to Za
, Zf
is clamped to [min(n,f), max(n,f)]
, where n
and f
are the minDepth
and maxDepth
depth range values of the viewport used by this fragment, respectively.