深度图像和视图
深度附着和颜色附着一样都是基于图像对象。区别是,交换链不会自动地为我们创建深度附着使用的深度图像对象。我们需要自己创建深度图像对象。使用深度图像需要图像、内存和图像视图对象这三种资源。
VkImage depthImage;
VkDeviceMemory depthImageMemory;
VkImageView depthImageView;
添加一个叫做createDepthResources的函数来配置深度图像需要的资源:
void initVulkan() {
...
createCommandPool();
createDepthResources();
createTextureImage();
...
}
...
void createDepthResources() {
}
深度图像的创建方法相当简单。设置和颜色附着完全一样的图像分辨率,以及用于深度附着的使用标记,优化的tiling模式和设备内存。对于深度图像,只需要使用一个颜色通道。
和纹理图像不同,深度图像不需要特定的图像数据格式,这是因为实际上我们不需要直接访问深度图像。只需要保证深度数据能够有一个合理的精度即可,通常这一合理精度是至少为深度数据提供24位的位宽,下面这些值满足24位的需求:
-
VK_FORMAT_D32_SFLOAT:32位浮点深度值
-
VK_FORMAT_D32_SFLOAT_S8_UINT:32位浮点深度值和8位模板值
-
VK_FORMAT_D24_UNORM_S8_UINT:24位浮点深度值和8位模板值
模板值用于模板测试,我们会在之后的章节介绍它。
我们可以直接使用VK_FORMAT_D32_SFLOAT作为图像数据格式,目前这一图像数据格式已经被广泛支持。但我们最好还是对设备是否支持这一图像数据格式进行检测。为此我们可以编写一个findSupportedFormat函数查找一个既符合我们需求又被设备支持的图像数据格式:
VkFormat findSupportedFormat(const std::vector<VkFormat>& candidates, VkImageTiling tiling, VkFormatFeatureFlags features) {
}
可用的深度图像数据格式还依赖于tiling模式和使用标记,所以我们需要将这些信息作为函数参数,通过调用vkGetPhysicalDeviceFormatProperties函数查询可用的深度图像数据格式:
for (VkFormat format : candidates) {
VkFormatProperties props;
vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &props);
}
上面代码中的VkFormatProperties结构体包含了下面这些成员变量:
-
linearTilingFeatures:数据格式支持线性tiling模式
-
optimalTilingFeatures:数据格式支持优化tiling模式
-
bufferFeatures:数据格式支持缓冲
目前我们只用到上面前两个成员变量,通过它们对应的tiling模式检测数据格式是否被支持:
if (tiling == VK_IMAGE_TILING_LINEAR && (props.linearTilingFeatures & features) == features) {
return format;
} else if (tiling == VK_IMAGE_TILING_OPTIMAL && (props.optimalTilingFeatures & features) == features) {
return format;
}
如果没有合适的格式可用,我们可以返回一个特殊值或直接抛出一个异常:
VkFormat findSupportedFormat(const std::vector<VkFormat>& candidates, VkImageTiling tiling, VkFormatFeatureFlags features) {
for (VkFormat format : candidates) {
VkFormatProperties props;
vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &props);
if (tiling == VK_IMAGE_TILING_LINEAR && (props.linearTilingFeatures & features) == features)
{
return format;
} else if (tiling == VK_IMAGE_TILING_OPTIMAL && (props.optimalTilingFeatures & features) == features) {
return format;
}
}
throw std::runtime_error("failed to find supported format!");
}
我们使用findSupportedFormat函数创建一个叫做findDepthFormat的辅助函数来查找适合作为深度附着的图像数据格式:
VkFormat findDepthFormat() {
return findSupportedFormat( {VK_FORMAT_D32_SFLOAT, VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D24_UNORM_S8_UINT}, VK_IMAGE_TILING_OPTIMAL, VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT);
}
需要注意这里使用的是VK_FORMAT_FEATURE_ 类的标记而不是VK_IMAGE_USAGE_ 类标记,这一类标记的格式都包含了深度颜色通道,有一些还包含了模板颜色通道,但在这里,我们没有用到模板颜色通道。但如果使用的格式包含模板颜色通道,在进行图像布局变换时就需要考虑这一点。这里我们添加一个可以用于检测格式是否包含模板颜色通道的函数:
bool hasStencilComponent(VkFormat format) {
return format == VK_FORMAT_D32_SFLOAT_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT;
}
我们在createDepthResources函数中调用findDepthFormat函数来查找一个可用的深度图像数据格式:
VkFormat depthFormat = findDepthFormat();
至此,我们已经具有足够的信息来创建深度图像对象,可以开始调用createImage和createImageView函数来创建图像资源:
createImage(swapChainExtent.width, swapChainExtent.height, depthFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory);
depthImageView = createImageView(depthImage, depthFormat);
但我们的createImageView函数目前假设aspectFlags的设置总是VK_IMAGE_ASPECT_COLOR_BIT,不符合我们的需求,我们需要进行修改将其作为一个函数参数:
VkImageView createImageView(VkImage image, VkFormat format,
VkImageAspectFlags aspectFlags) {
...
viewInfo.subresourceRange.aspectMask = aspectFlags;
...
}
更新所有使用createImageView函数的地方,使用正确的aspectFlags标记:
swapChainImageViews[i] = createImageView(swapChainImages[i],
swapChainImageFormat, VK_IMAGE_ASPECT_COLOR_BIT);
...
depthImageView = createImageView(depthImage, depthFormat, VK_IMAGE_ASPECT_DEPTH_BIT);
...
textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT);
接着,就是创建深度图像。通常,我们在渲染流程开始后首先会清除深度附着中的数据,不需要复制数据到深度图像中。我们需要对图像布局进行变换,来让它适合作为深度附着使用。由于这一图像变换只需要进行一次,这里我们使用管线障碍来同步图像变换:
transitionImageLayout(depthImage, depthFormat, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
因为我们不需要深度图像之前的数据,所以我们使用VK_IMAGE_LAYOUT_UNDEFINED作为深度图像的初始布局。现在我们需要修改transitionImageLayout函数的部分代码来使用正确的subresource aspect:
if (newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) {
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
if (hasStencilComponent(format)) {
barrier.subresourceRange.aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
}
} else {
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
}
尽管这里我们没有使用模板颜色通道,我们还是要对它进行变换处理。
最后,设置正确的访问掩码和管线阶段。
if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
barrier.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
} else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
} else if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) {
barrier.srcAccessMask = 0;
barrier.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;
} else {
throw std::invalid_argument("unsupported layout transition!");
}
深度缓冲数据会在进行深度测试时被读取,用来检测片段是否可以覆盖之前的片段。这一读取过程发生在VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT,如果片段可以覆盖之前的片段,新的深度缓冲数据会在VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT写入。我们应该使用与要进行的操作相匹配的管线阶段进行同步操作。