本文档是对Vulkan Tutorial教程的翻译,非官方翻译,仅供vulkan爱好者参考学习。如果有翻译错误,请留言指出,或者联系占航(hangliebe@163.com)进行修正,感谢。建议英文好的朋友直接读原文档。
关注我的微博主页
2022-02-20 v1.0
到目前为止,我们所处理的几何体是投射到三维的,但它仍然是完全平面的。在这一章中,我们要给位置添加一个Z坐标,为三维网格做准备。我们将使用这第三个坐标在当前的正方形上放置一个正方形,看看几何体不按深度排序时出现的问题。
改变 “顶点”结构以使用三维矢量作为位置,并更新相应的VkVertexInputAttributeDescription
中的format
。
struct Vertex {
::vec3 pos;
glm::vec3 color;
glm::vec2 texCoord;
glm
...
static std::array<VkVertexInputAttributeDescription, 3> getAttributeDescriptions() {
std::array<VkVertexInputAttributeDescription, 3> attributeDescriptions{};
[0].binding = 0;
attributeDescriptions[0].location = 0;
attributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT;
attributeDescriptions[0].offset = offsetof(Vertex, pos);
attributeDescriptions
...
}
};
接下来,更新顶点着色器以接受和转换3D坐标作为输入。之后别忘了重新编译!
layout(location = 0) in vec3 inPosition;
...
void main() {
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0);
= inColor;
fragColor = inTexCoord;
fragTexCoord }
最后,更新vertices
容器以包括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}}
};
如果你现在运行你的应用程序,那么你应该看到和之前完全一样的结果。现在是时候添加一些额外的几何图形来使场景更加有趣,并演示我们在本章中要解决的问题。复制顶点来定义当前正方形下面的位置,就像这样。
使用Z坐标为-0.5f
,并为额外的正方形添加适当的索引:
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
};
现在运行你的程序,你会看到类似于埃舍尔插图的东西。
问题是,下层正方形的碎片被画在上层正方形的碎片之上,仅仅是因为它在索引数组中排在后面。有两种方法可以解决这个问题。
第一种方法通常用于绘制透明对象,因为与顺序无关的透明是一个难以解决的难题。然而,使用深度缓冲器更普遍地解决了按深度排序片段的问题。深度缓冲区是一个额外的附件,它存储每个位置的深度,就像颜色附件存储每个位置的颜色一样。每次光栅化器产生一个片断时,深度测试将检查新片断是否比前一个片断更接近。如果不是,那么新的片段就会被丢弃。通过深度测试的片段会把它自己的深度写到深度缓冲区中。可以从片段着色器中操纵这个值,就像你可以操纵颜色输出一样。
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
由GLM生成的透视投影矩阵将使用OpenGL深度范围,默认为-1.0
到1.0
。我们需要使用GLM_FORCE_DEPTH_ZERO_TO_ONE'定义将其配置为使用Vulkan的
0.0’到`1.0’范围。
深度附件是基于图像的,就像颜色附件一样。不同的是,互换链不会自动为我们创建深度图像。我们只需要一个深度图像,因为一次只运行一个绘制操作。深度图像将再次需要三方面的资源:图像、内存和图像视图。
;
VkImage depthImage;
VkDeviceMemory depthImageMemory; VkImageView depthImageView
创建一个新的函数createDepthResources
来设置这些资源。
void initVulkan() {
...
();
createCommandPool();
createDepthResources();
createTextureImage...
}
...
void createDepthResources() {
}
创建一个深度图像是相当直接的。它应该具有与颜色附件相同的分辨率,由交换链的范围定义,适合于深度附件的图像使用,最佳的平铺和设备本地内存。唯一的问题是:深度图像的正确格式是什么?该格式必须包含一个深度成分,在VK_FORMAT_
中用D??_
表示。
与纹理图像不同,我们不一定需要一个特定的格式,因为我们不会直接从程序中访问纹理。它只需要有一个合理的精度,在现实世界的应用中,至少有24比特是常见的。有几种格式可以满足这个要求。
vk_format_d32_sfloat
。用于深度的32位浮点数vk_format_d32_sfloat_s8_uint
:
32位带符号的浮点数,用于深度和8位模版组件VK_FORMAT_D24_UNORM_S8_UINT
:用于深度和8位网板组件的24位浮点数。模板组件用于模版测试,这是一个额外的测试,可以与深度测试相结合。我们将在以后的章节中研究这个问题。
我们可以简单地采用VK_FORMAT_D32_SFLOAT
格式,因为对它的支持是非常普遍的(见硬件数据库),但在可能的情况下,为我们的应用增加一些额外的灵活性也是不错的。我们要写一个函数findSupportedFormat
,它按照从最理想到最不理想的顺序接收一个候选格式列表,并检查哪个是第一个被支持的:
(const std::vector<VkFormat>& candidates, VkImageTiling tiling, VkFormatFeatureFlags features) {
VkFormat findSupportedFormat
}
对一个格式的支持取决于平铺模式和用途,所以我们也必须把这些作为参数。可以使用vkGetPhysicalDeviceFormatProperties
函数来查询格式的支持情况:
for (VkFormat format : candidates) {
;
VkFormatProperties props(physicalDevice, format, &props);
vkGetPhysicalDeviceFormatProperties}
VkFormatProperties
结构包含三个字段。
linearTilingFeatures
:支持线性铺排的使用情况optimalTilingFeatures
: 支持最优铺排的使用情况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;
}
如果没有一个候选格式支持所需的用法,那么我们可以返回一个特殊的值或者直接抛出一个异常。:
(const std::vector<VkFormat>& candidates, VkImageTiling tiling, VkFormatFeatureFlags features) {
VkFormat findSupportedFormatfor (VkFormat format : candidates) {
;
VkFormatProperties props(physicalDevice, format, &props);
vkGetPhysicalDeviceFormatProperties
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!");
}
我们现在要用这个函数来创建一个findDepthFormat
辅助函数,以选择一个有深度组件的格式,支持作为深度附件使用:
() {
VkFormat findDepthFormatreturn 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
现在我们有了调用createImage
和createImageView
辅助函数的所有必要信息。:
(swapChainExtent.width, swapChainExtent.height, depthFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory);
createImage= createImageView(depthImage, depthFormat); depthImageView
然而,createImageView
函数目前假定子资源总是VK_IMAGE_ASPECT_COLOR_BIT
,所以我们需要把这个字段变成一个参数:
(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags) {
VkImageView createImageView...
.subresourceRange.aspectMask = aspectFlags;
viewInfo...
}
更新对该函数的所有调用,以使用正确的方面:
[i] = createImageView(swapChainImages[i], swapChainImageFormat, VK_IMAGE_ASPECT_COLOR_BIT);
swapChainImageViews...
= createImageView(depthImage, depthFormat, VK_IMAGE_ASPECT_DEPTH_BIT);
depthImageView ...
= createImageView(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_ASPECT_COLOR_BIT); textureImageView
这就是创建深度图像的方法。我们不需要对它进行映射或复制另一个图像,因为我们要像颜色附件一样在渲染通道开始时清除它。
我们不需要将图像的布局显式转换为深度附件,因为我们将在渲染过程中处理这一点。但是,为了完整起见,我仍将在本节中描述该过程。如果你愿意,你可以跳过它。
在 createDepthResources
函数的末尾调用
transitionImageLayout
,如下所示:
(depthImage, depthFormat, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); transitionImageLayout
未定义的布局可以用作初始布局,因为没有重要的现有深度图像内容。我们需要更新
transitionImageLayout
中的一些逻辑以使用正确的子资源方面:
if (newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) {
.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
barrier
if (hasStencilComponent(format)) {
.subresourceRange.aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
barrier}
} else {
.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
barrier}
虽然我们没有使用模板组件,但我们确实需要将它包含在深度图像的布局转换中。
最后,添加正确的访问掩码和管线阶段:
if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier
= VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
destinationStage } else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
barrier
= VK_PIPELINE_STAGE_TRANSFER_BIT;
sourceStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
destinationStage } else if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) {
.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
barrier
= VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
sourceStage = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
destinationStage } else {
throw std::invalid_argument("unsupported layout transition!");
}
深度缓冲区将被读取以执行深度测试,看一个片段是否可见,并且当一个新的片段被绘制时将被写入。读取发生在VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT
阶段,写入发生在VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT
。你应该挑选与指定操作相匹配的最早的流水线阶段,以便在需要的时候作为深度附件准备使用。
我们现在要修改createRenderPass
以包括一个深度附件。首先指定VkAttachmentDescription
:
{};
VkAttachmentDescription 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; depthAttachment
format
应该与深度图像本身相同。这次我们不关心存储深度数据(storeOp
),因为它在绘制完成后将不会被使用。这可能允许硬件进行额外的优化。就像颜色缓冲区一样,我们不关心之前的深度内容,所以我们可以使用VK_IMAGE_LAYOUT_UNDEFINED
作为initialLayout
。
{};
VkAttachmentReference depthAttachmentRef.attachment = 1;
depthAttachmentRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; depthAttachmentRef
为第一个(也是唯一的)子通道添加一个对附件的引用:
{};
VkSubpassDescription subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;
subpass.pDepthStencilAttachment = &depthAttachmentRef; subpass
与颜色附件不同,一个子通道只能使用一个深度(+stencil)附件。在多个缓冲区上做深度测试其实没有任何意义。
std::array<VkAttachmentDescription, 2> attachments = {colorAttachment, depthAttachment};
{};
VkRenderPassCreateInfo 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; renderPassInfo
接下来,更新VkRenderPassCreateInfo
结构以引用两个附件。
.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; dependency
最后,我们需要扩展我们的子通道依赖,以确保深度图像的过渡和它作为其加载操作的一部分被清除之间没有冲突。深度图像在早期片段测试管道阶段被首次访问,由于我们有一个clears的加载操作,我们应该指定写入的访问掩码。
下一步是修改framebuffer的创建,将深度图像绑定到深度附件上。进入createFramebuffers
并指定深度图像视图为第二个附件:
std::array<VkImageView, 2> attachments = {
[i],
swapChainImageViews
depthImageView};
{};
VkFramebufferCreateInfo 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; framebufferInfo
T每一个交换链图像的颜色附件都不一样,但同一个深度图像可以被所有的图像使用,因为由于我们的信号器,只有一个子通道在同一时间运行。
你还需要移动对createFramebuffers
的调用,以确保它是在深度图像视图真正被创建后调用的:
void initVulkan() {
...
();
createDepthResources();
createFramebuffers...
}
因为我们现在有VK_ATTACHMENT_LOAD_OP_CLEAR
的多个附件,我们也需要指定多个清除值。转到createCommandBuffers
并创建一个VkClearValue
结构的数组:
std::array<VkClearValue, 2> clearValues{};
[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
clearValues[1].depthStencil = {1.0f, 0};
clearValues
.clearValueCount = static_cast<uint32_t>(clearValues.size());
renderPassInfo.pClearValues = clearValues.data(); renderPassInfo
在Vulkan中,深度缓冲区的深度范围是0.0
到1.0
,其中1.0
位于远视平面,0.0
位于近视平面。深度缓冲区中每一点的初始值应该是最远的深度,也就是`1.0’。
注意clearValues
的顺序应该与你的附件的顺序相同。
深度附件现在已经可以使用了,但深度测试仍需要在图形管道中启用。它是通过VkPipelineDepthStencilStateCreateInfo
结构配置的:
{};
VkPipelineDepthStencilStateCreateInfo depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
depthStencil.depthTestEnable = VK_TRUE;
depthStencil.depthWriteEnable = VK_TRUE; depthStencil
depthTestEnable
字段指定是否应将新片段的深度与深度缓冲区进行比较,看它们是否应被丢弃。depthWriteEnable
字段指定是否应该将通过深度测试的新片段的深度实际写入深度缓冲区。
.depthCompareOp = VK_COMPARE_OP_LESS; depthStencil
depthCompareOp
字段指定了为保留或丢弃片段而进行的比较。我们坚持较低的深度=较近的惯例,所以新片段的深度应该是更少。
.depthBoundsTestEnable = VK_FALSE;
depthStencil.minDepthBounds = 0.0f; // Optional
depthStencil.maxDepthBounds = 1.0f; // Optional。 depthStencil
depthBoundsTestEnable
、minDepthBounds
和maxDepthBounds
字段用于可选的深度绑定测试。基本上,这允许你只保留落在指定深度范围内的片段。我们不会使用这个功能。
.stencilTestEnable = VK_FALSE;
depthStencil.front = {}; // Optional
depthStencil.back = {}; // Optional depthStencil
最后三个字段配置了钢网缓冲区的操作,我们在本教程中也不会使用。如果你想使用这些操作,那么你必须确保深度/铅笔图像的格式包含一个钢网组件。
.pDepthStencilState = &depthStencil; pipelineInfo
更新VkGraphicsPipelineCreateInfo
结构,以引用我们刚刚填入的深度网板状态。如果渲染通道包含一个深度网板附件,必须始终指定深度网板状态。
如果你现在运行你的程序,那么你应该看到几何体的碎片现在已经正确排序了:
深度缓冲区的分辨率应该在窗口调整大小时改变,以匹配新的颜色附件分辨率。在这种情况下,扩展recreateSwapChain
函数来重新创建深度资源。
void recreateSwapChain() {
int width = 0, height = 0;
while (width == 0 || height == 0) {
(window, &width, &height);
glfwGetFramebufferSize();
glfwWaitEvents}
(device);
vkDeviceWaitIdle
();
cleanupSwapChain
();
createSwapChain();
createImageViews();
createRenderPass();
createGraphicsPipeline();
createDepthResources();
createFramebuffers();
createUniformBuffers();
createDescriptorPool();
createDescriptorSets();
createCommandBuffers}
清理操作应该发生在交换链清理功能中:
void cleanupSwapChain() {
(device, depthImageView, nullptr);
vkDestroyImageView(device, depthImage, nullptr);
vkDestroyImage(device, depthImageMemory, nullptr);
vkFreeMemory
...
}
恭喜你,你的应用程序现在终于可以渲染任意的三维几何体了,而且看起来很好。在下一章中,我们将通过绘制一个有纹理的模型来尝试这个方法!
留言板