在第 1 章中创建渲染通道时,为了使代码更简洁,我们跳过了一些内容,即深度缓冲。
在 3D 图形中,为了确保您不会渲染位于其他物体后面的物体,您可以使用深度缓冲并使用 z 缓冲测试。通过将深度缓冲绑定到渲染通道,您可以启用 z 缓冲测试,这将允许正确渲染 3D 对象。
我们将稍微重构引擎周围的代码以启用此功能。
深度图像
渲染时,我们将需要使用深度图像。 以前,我们使用交换链图像作为渲染目标,但交换链不包含深度图像,深度图像必须单独创建。 我们将创建一个深度图像以匹配我们的交换链图像。
首先,我们将创建 AllocatedImage 结构体,它与我们正在使用的 AllocatedBuffer 相同,但用于图像而不是缓冲区,因为我们将使用 VMA 来分配深度图像。
在 vulkan_types.h 上,我们添加结构体
struct AllocatedImage {
VkImage _image;
VmaAllocation _allocation;
};
它与 AllocatedBuffer 完全相同,但使用 VkImage 而不是 VkBuffer。
我们现在可以将其添加到 VulkanEngine 类中以存储它
class VulkanEngine {
public:
//other code ....
VkImageView _depthImageView;
AllocatedImage _depthImage;
//the format for the depth image
VkFormat _depthFormat;
//other code ....
}
在 Vulkan 中,您不能直接使用 VkImage,VkImage 必须通过 VkImageView,其中包含有关如何处理图像的一些信息。 我们正在以类似于交换链图像的方式进行操作,但我们不会让 Vkbootstrap 库初始化它们,而是自己进行初始化。
我们将需要一个新的初始化器用于我们的 vk_initializers 文件,用于图像创建信息和图像视图创建信息,所以让我们添加它。
vk_initializers.h
namespace vkinit {
//other .....
VkImageCreateInfo image_create_info(VkFormat format, VkImageUsageFlags usageFlags, VkExtent3D extent);
VkImageViewCreateInfo imageview_create_info(VkFormat format, VkImage image, VkImageAspectFlags aspectFlags);
}
VkImageCreateInfo vkinit::image_create_info(VkFormat format, VkImageUsageFlags usageFlags, VkExtent3D extent)
{
VkImageCreateInfo info = { };
info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
info.pNext = nullptr;
info.imageType = VK_IMAGE_TYPE_2D;
info.format = format;
info.extent = extent;
info.mipLevels = 1;
info.arrayLayers = 1;
info.samples = VK_SAMPLE_COUNT_1_BIT;
info.tiling = VK_IMAGE_TILING_OPTIMAL;
info.usage = usageFlags;
return info;
}
像往常一样,正确设置了 sType 和 pNext。 imageType 保存图像具有多少维度。 1、2 或 3。因为 3d 和 1d 纹理可能是小众的,所以我们在初始化器上将其默认设置为 2d 图像。
Format 保存纹理的数据是什么,例如保存单个浮点数(用于深度),或保存颜色。 Extent 是图像的大小,以像素为单位。
MipLevels 保存图像具有的 mipmap 级别数。 因为我们在这里不使用它们,所以我们将级别保留为 1。 Array layers 用于分层纹理。 您可以创建多合一的纹理,使用图层。 分层纹理的一个示例是立方体贴图,其中您有 6 个图层,每个图层对应立方体贴图的一个面。 我们将其默认设置为 1 层,因为我们没有做立方体贴图。
Samples 控制纹理的 MSAA 行为。 这仅对渲染目标有意义,例如深度图像和您正在渲染到的图像。 我们不会在本教程中进行 MSAA,因此整个指南的样本将保持为 1 个样本。
Tiling 非常重要。 Tiling 描述了纹理的数据在 GPU 中是如何排列的。 为了提高性能,GPU 不会将图像存储为像素的 2d 数组,而是使用复杂的自定义格式,这些格式对于 GPU 品牌甚至型号都是唯一的。 VK_IMAGE_TILING_OPTIMAL
告诉 Vulkan 让驱动程序决定 GPU 如何排列图像的内存。 如果您使用 VK_IMAGE_TILING_OPTIMAL
,则除非先更改其平铺方式,否则无法从 CPU 读取数据或写入数据(可以在任何时候更改纹理的平铺方式,但这可能是一项代价高昂的操作)。 我们可能关心的另一个平铺是 VK_IMAGE_TILING_LINEAR
,它将图像存储为像素的 2d 数组。 虽然 LINEAR 平铺速度会慢得多,但它将允许 CPU 安全地写入和读取该内存。
最后一件事是 usage flags。 与缓冲区类似,图像也需要正确设置 usage flags。 正确设置 usage flags 非常重要,并且您只设置您将需要的标志,因为这将控制 GPU 如何处理图像内存。
接下来是图像视图
VkImageViewCreateInfo vkinit::imageview_create_info(VkFormat format, VkImage image, VkImageAspectFlags aspectFlags)
{
//build a image-view for the depth image to use for rendering
VkImageViewCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
info.pNext = nullptr;
info.viewType = VK_IMAGE_VIEW_TYPE_2D;
info.image = image;
info.format = format;
info.subresourceRange.baseMipLevel = 0;
info.subresourceRange.levelCount = 1;
info.subresourceRange.baseArrayLayer = 0;
info.subresourceRange.layerCount = 1;
info.subresourceRange.aspectMask = aspectFlags;
return info;
}
像往常一样,sType 和 pNext。 View Type 与图像上的 imageType 非常相似,只是它有更多选项。 虽然 imageType 保存纹理的维度,但 viewType 有更多选项,例如立方体贴图的 VK_IMAGE_VIEW_TYPE_CUBE。 在这里,我们将使其与 image_create_info 匹配,并将其硬编码为 2D 图像,因为它是最常见的情况。
image 必须指向从中创建此 imageview 的图像。 由于 imageViews “包装”图像,因此您需要指向原始图像。 format 必须与从中创建此视图的图像中的格式匹配。 格式可能不匹配,这将允许您“重新解释”格式,但这可能很难使用,而且非常小众,因此现在请确保格式将匹配。
subresourceRange 保存有关图像指向位置的信息。 这用于分层图像,其中您可能在一个图像中有多个图层,并且想要创建一个指向特定图层的 imageview。 也可以使用它来控制 mipmap 级别。 对于我们当前的用法,我们将将其默认设置为无 mipmap(mipmap 基数 0,mipmap 级别 1),并且仅 1 个纹理层。
aspectMask 类似于图像中的 usageFlags。 它与此图像的用途有关。
现在我们有了这些初始化器,我们可以创建深度图像。
为了分配深度图像并创建其 imageview,我们将把此代码添加到 init_swapchain
函数中。
void VulkanEngine::init_swapchain()
{
// other code ....
//depth image size will match the window
VkExtent3D depthImageExtent = {
_windowExtent.width,
_windowExtent.height,
1
};
//hardcoding the depth format to 32 bit float
_depthFormat = VK_FORMAT_D32_SFLOAT;
//the depth image will be an image with the format we selected and Depth Attachment usage flag
VkImageCreateInfo dimg_info = vkinit::image_create_info(_depthFormat, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, depthImageExtent);
//for the depth image, we want to allocate it from GPU local memory
VmaAllocationCreateInfo dimg_allocinfo = {};
dimg_allocinfo.usage = VMA_MEMORY_USAGE_GPU_ONLY;
dimg_allocinfo.requiredFlags = VkMemoryPropertyFlags(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
//allocate and create the image
vmaCreateImage(_allocator, &dimg_info, &dimg_allocinfo, &_depthImage._image, &_depthImage._allocation, nullptr);
//build an image-view for the depth image to use for rendering
VkImageViewCreateInfo dview_info = vkinit::imageview_create_info(_depthFormat, _depthImage._image, VK_IMAGE_ASPECT_DEPTH_BIT);
VK_CHECK(vkCreateImageView(_device, &dview_info, nullptr, &_depthImageView));
//add to deletion queues
_mainDeletionQueue.push_function([=]() {
vkDestroyImageView(_device, _depthImageView, nullptr);
vmaDestroyImage(_allocator, _depthImage._image, _depthImage._allocation);
});
}
首先,我们将深度格式硬编码为 32 位浮点数。 大多数 GPU 都支持此深度格式,因此使用它很好。 对于其他用途,或者如果您使用模板缓冲区,您可能需要选择其他格式。
对于图像本身,我们将使用深度格式创建它,使用 VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT 作为用途,并使用与窗口相同的大小。 VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT 让 Vulkan 驱动程序知道这将是用于 z 缓冲测试的深度图像。
为了分配图像,我们使用 VMA 的方式与之前的顶点缓冲区几乎相同。 但这次我们使用 VMA_MEMORY_USAGE_GPU_ONLY
来确保图像分配在快速 VRAM 上。 这对于我们正在渲染到的图像之类的内容至关重要。 渲染到存储在 CPU 内存中的图像甚至可能无法实现。 为了绝对确保 VMA 确实将图像分配到 VRAM 中,我们在 required flags 上为其赋予 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
。 这强制 VMA 库无论如何都要在 VRAM 上分配图像。 (Memory Usage 部分更像是一个提示)
我们使用 vmaCreateImage 创建图像,与我们之前所做的 vmaCreatebuffer 相同。
现在图像已分配和创建,我们需要从中创建 imageview。 对于 imageview 的图像方面,我们再次使用 VK_IMAGE_ASPECT_DEPTH_BIT
,让驱动程序知道这将用于深度测试。
图像现在已创建,所以现在我们必须将其挂钩到渲染通道中。
渲染通道上的深度目标
在 init_default_renderpass()
函数中,我们将深度目标添加到其中。
void VulkanEngine::init_default_renderpass()
{
//VkAttachmentDescription color_attachment code
VkAttachmentDescription depth_attachment = {};
// Depth attachment
depth_attachment.flags = 0;
depth_attachment.format = _depthFormat;
depth_attachment.samples = VK_SAMPLE_COUNT_1_BIT;
depth_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depth_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
depth_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depth_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
depth_attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
depth_attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkAttachmentReference depth_attachment_ref = {};
depth_attachment_ref.attachment = 1;
depth_attachment_ref.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
}
我们首先需要的是深度附件的附件描述和附件引用。
代码位于初始化颜色附件的正下方。 深度附件及其引用都是颜色附件的复制粘贴,因为它的工作方式相同,但略有变化。
depth_attachment.format = _depthFormat;
设置为我们创建深度图像的深度格式。
depth_attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
与颜色附件布局位于 COLOR_ATTACHMENT_OPTIMAL 的方式相同,我们将最终布局设置为 DEPTH_STENCIL_ATTACHMENT_OPTIONAL,因为这是一个深度模板附件,而不是颜色附件。 深度附件引用上的布局也是如此
我们需要将此附件挂钩到子通道,因此将其更改为此
//we are going to create 1 subpass, which is the minimum you can do
VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &color_attachment_ref;
//hook the depth attachment into the subpass
subpass.pDepthStencilAttachment = &depth_attachment_ref;
这将深度附件连接到我们渲染的主子通道。 我们还需要将深度附件添加到渲染通道本身的附件列表中,如下所示
//array of 2 attachments, one for the color, and other for depth
VkAttachmentDescription attachments[2] = { color_attachment,depth_attachment };
VkRenderPassCreateInfo render_pass_info = {};
render_pass_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
//2 attachments from said array
render_pass_info.attachmentCount = 2;
render_pass_info.pAttachments = &attachments[0];
render_pass_info.subpassCount = 1;
render_pass_info.pSubpasses = &subpass;
我们不仅将颜色附件存储在 pAttachments 中,还将深度附件也添加到那里。
现在我们必须调整渲染通道同步。 以前,GPU 可以同时渲染多个帧。 当使用深度缓冲区时,这是一个问题,因为当先前的帧仍在渲染到深度缓冲区时,一个帧可能会覆盖深度缓冲区。
我们保留我们已经使用的颜色附件的子通道依赖项
VkSubpassDependency dependency = {};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.srcAccessMask = 0;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
我们还添加了一个新的子通道依赖项,用于同步对深度附件的访问。
VkSubpassDependency depth_dependency = {};
depth_dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
depth_dependency.dstSubpass = 0;
depth_dependency.srcStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
depth_dependency.srcAccessMask = 0;
depth_dependency.dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
depth_dependency.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
此依赖项告诉 Vulkan,在先前的渲染通道完成使用深度附件之前,无法在渲染通道中使用深度附件。
最后,我们需要将这两个依赖项都包含在 VkRenderPassCreateInfo
中
VkSubpassDependency dependencies[2] = { dependency, depth_dependency };
//other code...
render_pass_info.dependencyCount = 2;
render_pass_info.pDependencies = &dependencies[0];
这样,渲染通道现在支持深度附件。 现在我们需要修改我们的帧缓冲区,以便它们指向深度图像。
在 init_framebuffers()
函数中,我们在创建每个帧缓冲区时连接深度图像视图。
for (int i = 0; i < swapchain_imagecount; i++) {
VkImageView attachments[2];
attachments[0] = _swapchainImageViews[i];
attachments[1] = _depthImageView;
fb_info.pAttachments = attachments;
fb_info.attachmentCount = 2;
VK_CHECK(vkCreateFramebuffer(_device, &fb_info, nullptr, &_framebuffers[i]));
}
请注意,我们如何在每个交换链帧缓冲区上使用相同的深度图像。 这是因为我们不需要在帧之间更改深度图像,我们可以简单地清除并为每个帧重复使用相同的深度图像。
深度缓冲区的渲染通道初始化现已完成,因此最后需要做的是将深度测试添加到我们的网格管线中。
我们将再次向列表中添加另一个初始化器,这次用于 VkPipelineDepthStencilStateCreateInfo
,它保存有关如何在渲染管线上使用深度测试的信息。
VkPipelineDepthStencilStateCreateInfo vkinit::depth_stencil_create_info(bool bDepthTest, bool bDepthWrite, VkCompareOp compareOp)
{
VkPipelineDepthStencilStateCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
info.pNext = nullptr;
info.depthTestEnable = bDepthTest ? VK_TRUE : VK_FALSE;
info.depthWriteEnable = bDepthWrite ? VK_TRUE : VK_FALSE;
info.depthCompareOp = bDepthTest ? compareOp : VK_COMPARE_OP_ALWAYS;
info.depthBoundsTestEnable = VK_FALSE;
info.minDepthBounds = 0.0f; // Optional
info.maxDepthBounds = 1.0f; // Optional
info.stencilTestEnable = VK_FALSE;
return info;
}
深度模板创建信息比其他初始化器稍微复杂一些,因此我们稍微抽象了一些内容。
depthTestEnable 保存我们是否应该进行任何 z 剔除。 设置为 VK_FALSE
以在所有内容之上绘制,设置为 VK_TRUE
以不在其他对象之上绘制。 depthWriteEnable 允许写入深度。 虽然 DepthTest 和 DepthWrite 在大多数情况下都为真,但在某些情况下,您可能想要执行深度写入,但不执行深度测试; 它有时用于某些特殊效果。
depthCompareOp 保存深度测试功能。 设置为 VK_COMPARE_OP_ALWAYS
以完全不进行任何深度测试。 其他常见的深度比较 OP 是 VK_COMPARE_OP_LESS
(仅当 Z < 深度缓冲区中的内容时才绘制),或 VK_COMPARE_OP_EQUAL
(仅当深度 z 匹配时才绘制)
min 和 max depth bounds 让我们限制深度测试。 如果深度超出范围,则会跳过像素。 最后,我们将不使用模板测试,因此默认情况下将其设置为 VK_FALSE。
现在我们回到 PipelineBuilder,并将深度状态添加到其中。
vk_engine.h
class PipelineBuilder {
public:
//others
VkPipelineDepthStencilStateCreateInfo _depthStencil;
}
当然,请确保在构建管线时使用它。 在 build_pipeline 上,我们将深度模板状态挂钩到 VkGraphicsPipelineCreateInfo
中
VkPipeline PipelineBuilder::build_pipeline(VkDevice device, VkRenderPass pass)
{
// other code ....
VkGraphicsPipelineCreateInfo pipelineInfo = {};
//other states
pipelineInfo.pDepthStencilState = &_depthStencil;
}
现在我们已将深度测试功能添加到管线构建器中,我们终于可以为我们的主网格管线启用深度测试。
在我们的 init_pipelines()
上,我们将默认深度测试状态添加到构建器,这将向所有着色器添加深度测试。
//default depthtesting
pipelineBuilder._depthStencil = vkinit::depth_stencil_create_info(true, true, VK_COMPARE_OP_LESS_OR_EQUAL);
//finally build the pipeline
_trianglePipeline = pipelineBuilder.build_pipeline(_device, _renderPass);
管线现在进行深度测试,因此剩下的唯一事情是确保每帧清除深度图像。
以与在开始主渲染渲染通道时清除颜色的方式相同,我们清除深度。
VulkanEngine::draw(){
// other code ....
//make a clear-color from frame number. This will flash with a 120 frame period.
VkClearValue clearValue;
float flash = abs(sin(_frameNumber / 120.f));
clearValue.color = { { 0.0f, 0.0f, flash, 1.0f } };
//clear depth at 1
VkClearValue depthClear;
depthClear.depthStencil.depth = 1.f;
//start the main renderpass.
//We will use the clear color from above, and the framebuffer of the index the swapchain gave us
VkRenderPassBeginInfo rpInfo = vkinit::renderpass_begin_info(_renderPass, _windowExtent, _framebuffers[swapchainImageIndex]);
//connect clear values
rpInfo.clearValueCount = 2;
VkClearValue clearValues[] = { clearValue, depthClear };
rpInfo.pClearValues = &clearValues[0];
vkCmdBeginRenderPass(cmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE);
//other code ...
我们将在 1.0(最大深度)处清除深度缓冲区,并将其添加到渲染通道初始化信息的清除值中。
如果您现在执行应用程序,并且一切顺利,您应该会看到一个非常漂亮的旋转猴头。
下一步: 场景管理