目录
Explicitly transitioning the depth image
Introduction
到目前为止,我们使用的几何体被投影到3D中,但它仍然是完全平坦的。在本章中,我们将向该位置添加一个Z坐标,以准备三维网格。我们将使用第三个坐标在当前正方形上放置一个正方形,以查看当几何体未按深度排序时出现的问题。
3D geometry
更改Vertex结构以使用位置的3D矢量,并更新相应VkVertexInputAttributeDescription中的格式:
struct Vertex {
glm::vec3 pos;
glm::vec3 color;
glm::vec2 texCoord;
...
static std::array<VkVertexInputAttributeDescription, 3> getAttributeDescriptions() {
std::array<VkVertexInputAttributeDescription, 3> attributeDescriptions{};
attributeDescriptions[0].binding = 0;
attributeDescriptions[0].location = 0;
attributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT;
attributeDescriptions[0].offset = offsetof(Vertex, pos);
...
}
};
接下来,更新顶点着色器以接受并变换3D坐标作为输入。不要忘记以后重新编译它!
layout(location = 0) in vec3 inPosition;
...
void main() {
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0);
fragColor = inColor;
fragTexCoord = inTexCoord;
}
最后,更新顶点容器以包含Z坐标:
const std::vector<Vertex> vertices = {
{{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}},
{{0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}},
{{0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}},
{{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}}
};
如果您现在运行应用程序,那么您将看到与以前完全相同的结果。现在是时候添加一些额外的几何图形以使场景更有趣,并演示我们将在本章中解决的问题了。复制顶点以定义当前顶点正下方的正方形的位置,如下所示:
使用-0.5f的Z坐标,并为额外的正方形添加适当的索引:
const std::vector<Vertex> vertices = {
{{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}},
{{0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}},
{{0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}},
{{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}},
{{-0.5f, -0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}},
{{0.5f, -0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}},
{{0.5f, 0.5f, -0.5f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}},
{{-0.5f, 0.5f, -0.5f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}}
};
const std::vector<uint16_t> indices = {
0, 1, 2, 2, 3, 0,
4, 5, 6, 6, 7, 4
};
现在运行程序,您将看到类似于Escher插图的内容:
问题是,下部正方形的碎片被绘制在上部正方形的碎片之上,这仅仅是因为它在索引数组中出现得较晚。有两种方法可以解决此问题:
- 从后到前按深度对所有绘图调用进行排序
- 使用深度缓冲区进行深度测试
第一种方法通常用于绘制透明对象,因为与顺序无关的透明度是一个难以解决的挑战。然而,按深度排序碎片的问题通常使用深度缓冲区来解决。深度缓冲区是存储每个位置的深度的附加附件,就像颜色附件存储每个位置颜色一样。每当光栅化器生成片段时,深度测试将检查新片段是否比前一片段更接近。如果不是,则丢弃新片段。通过深度测试的片段将自己的深度写入深度缓冲区。可以从片段着色器操纵该值,就像可以操纵颜色输出一样。
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
默认情况下,GLM生成的透视投影矩阵将使用-1.0到1.0的OpenGL深度范围。我们需要使用GLM_FORCE_DEPTH_ZERO_TO_ONE定义将其配置为使用0.0到1.0的Vulkan范围。
Depth image and view
深度附件基于图像,就像颜色附件一样。不同的是,交换链不会自动为我们创建深度图像。我们只需要一个深度图像,因为一次只运行一个绘制操作。深度图像将再次需要三重资源:图像、内存和图像视图。
VkImage depthImage;
VkDeviceMemory depthImageMemory;
VkImageView depthImageView;
创建一个新函数createDepthResources来设置这些资源:
void initVulkan() {
...
createCommandPool();
createDepthResources();
createTextureImage();
...
}
...
void createDepthResources() {
}
创建深度图像非常简单。它应该具有与颜色附件相同的分辨率,由交换链范围、适合深度附件的图像使用、最佳平铺和设备本地内存定义。唯一的问题是:深度图像的正确格式是什么?格式必须包含深度分量,由_D??_表示在VK_FORMAT_。
与纹理图像不同,我们不一定需要特定的格式,因为我们不会直接从程序访问纹理像素。它只需要具有合理的精度,在现实应用中,至少24位是常见的。有几种格式符合此要求:
- VK_FORMAT_D32_SFLOAT:32位浮点深度
- VK_FORMAT_D32_SFLOAT_S8_UINT:深度和8位模板组件的32位有符号浮点
- VK_FORMAT_D24_UNORM_S8_UINT:深度和8位模板组件的24位浮点
模板组件用于模板测试,这是一种可以与深度测试相结合的附加测试。我们将在以后的章节中讨论这一点。
我们可以简单地使用VK_FORMAT_D32_SFLOAT格式,因为对它的支持非常普遍(请参阅硬件数据库),但在可能的情况下为我们的应用程序添加一些额外的灵活性是很好的。我们将编写一个函数findSupportedFormat,它按照从最理想到最不理想的顺序获取候选格式列表,并检查第一个支持的格式:
VkFormat findSupportedFormat(const std::vector<VkFormat>& candidates, VkImageTiling tiling, VkFormatFeatureFlags features) {
}
格式的支持取决于平铺模式和用法,因此我们还必须将其作为参数。可以使用vkGetPhysicalDeviceFormatProperties函数查询对格式的支持:
for (VkFormat format : candidates) {
VkFormatProperties props;
vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &props);
}
VkFormatProperties结构包含三个字段:
- linearTilingFeatures:线性平铺支持的用例
- optimalTilingFeatures:最佳平铺支持的用例
- bufferFeatures:缓冲区支持的用例
这里只有前两个相关,我们检查的一个取决于函数的平铺参数:
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;
}
如果没有一种候选格式支持所需的用法,那么我们可以返回一个特殊值,也可以简单地抛出一个异常:
我们现在将使用此函数来创建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中查找深度格式:
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函数当前假定子源始终是VK_IMAGE_ASPECT_COLOR_BIT,因此我们需要将该字段转换为参数:
VkImageView createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags) {
...
viewInfo.subresourceRange.aspectMask = 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_SRGB, VK_IMAGE_ASPECT_COLOR_BIT);
这就是创建深度图像的方法。我们不需要映射它或将另一个图像复制到它,因为我们将在渲染过程开始时清除它,就像颜色附件一样。
Explicitly transitioning the depth image
我们不需要显式地将图像的布局转换为深度附件,因为我们将在渲染过程中处理这一点。如果你愿意,可以跳过它。
调用createDepthResources函数末尾的transitionImageLayout,如下所示:
transitionImageLayout(depthImage, depthFormat, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
未定义的布局可以用作初始布局,因为没有重要的现有深度图像内容。我们需要更新transitionImageLayout中的一些逻辑,以使用正确的子资源方面:
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!");
}
Render pass
现在我们将修改createRenderPass以包含深度附件。首先指定VkAttachmentDescription:
VkAttachmentDescription depthAttachment{};
depthAttachment.format = findDepthFormat();
depthAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
depthAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
depthAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
depthAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
depthAttachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
格式应与深度图像本身相同。这次我们不关心存储深度数据(storeOp),因为绘制完成后将不会使用它。这可以允许硬件执行额外的优化。就像颜色缓冲区一样,我们不关心之前的深度内容,因此我们可以使用VK_IMAGE_LAYOUT_UNDEFINED作为initialLayout。
VkAttachmentReference depthAttachmentRef{};
depthAttachmentRef.attachment = 1;
depthAttachmentRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
为第一个(也是唯一一个)子路径添加对附件的引用:
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;
subpass.pDepthStencilAttachment = &depthAttachmentRef;
与彩色附件不同,子过程只能使用单个深度(+模板)附件。在多个缓冲区上进行深度测试实际上没有任何意义。
std::array<VkAttachmentDescription, 2> attachments = {colorAttachment, depthAttachment};
VkRenderPassCreateInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
renderPassInfo.pAttachments = attachments.data();
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;
renderPassInfo.dependencyCount = 1;
renderPassInfo.pDependencies = &dependency;
接下来,更新VkSubpassDependency结构以引用两个附件。
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
最后,我们需要扩展子路径依赖关系,以确保深度图像的转换与作为其加载操作的一部分被清除之间没有冲突。深度图像是在早期片段测试管道阶段首先访问的,因为我们有一个清除的加载操作,所以我们应该为写入指定访问掩码。
Framebuffer
下一步是修改帧缓冲区创建以将深度图像绑定到深度附件。转到createFramebuffers并将深度图像视图指定为第二个附件:
std::array<VkImageView, 2> attachments = {
swapChainImageViews[i],
depthImageView
};
VkFramebufferCreateInfo framebufferInfo{};
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebufferInfo.renderPass = renderPass;
framebufferInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
framebufferInfo.pAttachments = attachments.data();
framebufferInfo.width = swapChainExtent.width;
framebufferInfo.height = swapChainExtent.height;
framebufferInfo.layers = 1;
每个交换链图像的颜色附件都不同,但所有交换链图像都可以使用相同的深度图像,因为由于我们的信号量,在同一时间只有一个子路径在运行。
您还需要移动对createFramebuffers的调用,以确保在实际创建深度图像视图后调用该调用:
void initVulkan() {
...
createDepthResources();
createFramebuffers();
...
}
Clear values
因为我们现在有多个带有VK_ATTACHMENT_LOAD_OP_CLEAR的附件,所以我们还需要指定多个清除值。转到recordCommandBuffer并创建VkClearValue结构的数组:
std::array<VkClearValue, 2> clearValues{};
clearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
clearValues[1].depthStencil = {1.0f, 0};
renderPassInfo.clearValueCount = static_cast<uint32_t>(clearValues.size());
renderPassInfo.pClearValues = clearValues.data();
在Vulkan中,深度缓冲区的深度范围为0.0到1.0,其中1.0位于远视图平面,0.0位于近视图平面。深度缓冲区中每个点的初始值应为可能的最远深度,即1.0。
注意clearValues的顺序应该与附件的顺序相同。
Depth and stencil state
深度附件现在可以使用了,但深度测试仍需要在图形管道中启用。它通过VkPipelineDepthStencilStateCreateInfo结构进行配置:
VkPipelineDepthStencilStateCreateInfo depthStencil{};
depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
depthStencil.depthTestEnable = VK_TRUE;
depthStencil.depthWriteEnable = VK_TRUE;
depthTestEnable字段指定是否应该将新片段的深度与深度缓冲区进行比较,以确定是否应该丢弃它们。depthWriteEnable字段指定是否应该将通过深度测试的片段的新深度写入深度缓冲区。
depthStencil.depthCompareOp = VK_COMPARE_OP_LESS;
depthCompareOp字段指定为保留或丢弃片段而执行的比较。我们坚持较低深度=较近的惯例,因此新碎片的深度应该较小。
depthStencil.depthBoundsTestEnable = VK_FALSE;
depthStencil.minDepthBounds = 0.0f; // Optional
depthStencil.maxDepthBounds = 1.0f; // Optional
depthBoundsTestEnable、minDepthBounds和maxDepthBound字段用于可选的深度绑定测试。基本上,这允许您只保留落在指定深度范围内的碎片。我们不会使用此功能。
depthStencil.stencilTestEnable = VK_FALSE;
depthStencil.front = {}; // Optional
depthStencil.back = {}; // Optional
最后三个字段配置模板缓冲区操作,我们也不会在本教程中使用。如果要使用这些操作,则必须确保深度/模具图像的格式包含模具组件。
pipelineInfo.pDepthStencilState = &depthStencil;
更新VkGraphicsPipelineCreateInfo结构以引用我们刚刚填充的深度模具状态。如果渲染过程包含深度模具附件,则必须始终指定深度模具状态。
如果现在运行程序,则应该看到几何体的片段现在已正确排序:
Handling window resize
当调整窗口大小以匹配新的颜色附件分辨率时,深度缓冲区的分辨率应更改。在这种情况下,扩展recreateSwapChain函数以重新创建深度资源:
void recreateSwapChain() {
int width = 0, height = 0;
while (width == 0 || height == 0) {
glfwGetFramebufferSize(window, &width, &height);
glfwWaitEvents();
}
vkDeviceWaitIdle(device);
cleanupSwapChain();
createSwapChain();
createImageViews();
createDepthResources();
createFramebuffers();
}
清理操作应在交换链清理函数中进行:
void cleanupSwapChain() {
vkDestroyImageView(device, depthImageView, nullptr);
vkDestroyImage(device, depthImage, nullptr);
vkFreeMemory(device, depthImageMemory, nullptr);
...
}
恭喜您,您的应用程序现在终于可以渲染任意3D几何体并使其看起来正确。我们将在下一章通过绘制纹理模型来尝试这一点!