Link

在第 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(最大深度)处清除深度缓冲区,并将其添加到渲染通道初始化信息的清除值中。

如果您现在执行应用程序,并且一切顺利,您应该会看到一个非常漂亮的旋转猴头。

triangle

下一步: 场景管理