目录
26 三维几何图形
深度图像和视图
27 显式转换深度图像
渲染通道
帧缓冲区
清除值
深度和模版状态
处理窗口调整大小
26 三维几何图形
到目前为止,我们所处理的几何体是投射到三维的,但它仍然是完全平面的。在这一章中,我们要给位置添加一个Z坐标,为三维网格做准备。我们将使用这第三个坐标在当前的正方形上放置一个正方形,看看几何体不按深度排序时出现的问题。
改变 “顶点”结构以使用三维矢量作为位置,并更新相应的VkVertexInputAttributeDescription中的format
。
attributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT;
更新顶点着色器以接受和转换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;
}
//更新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,并为额外的正方形添加适当的索引:
{{-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
//由GLM生成的透视投影矩阵将使用OpenGL深度范围,默认为-1.0到1.0。我们需要使用
//GLM_FORCE_DEPTH_ZERO_TO_ONE'定义将其配置为使用Vulkan的0.0’到`1.0’范围。
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
深度图像和视图
深度附件是基于图像的,就像颜色附件一样。不同的是,互换链不会自动为我们创建深度图像。我们只需要一个深度图像,因为一次只运行一个绘制操作。深度图像将再次需要三方面的资源:图像、内存和图像视图。
void createDepthResources() {
//创建一个深度图像是相当直接的。它应该具有与颜色附件相同的分辨率,由交换链的范围定义,
//适合于深度附件的图像使用,最佳的平铺和设备本地内存
}
与纹理图像不同,我们不一定需要一个特定的格式,因为我们不会直接从程序中访问纹理。它只需要有一个合理的精度,在现实世界的应用中,至少有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
,它按照从最理想到最不理想的顺序接收一个候选格式列表,并检查哪个是第一个被支持的:
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!");
}
VkFormatProperties 结构包含三个字段。
linearTilingFeatures
:支持线性铺排的使用情况optimalTilingFeatures
: 支持最优铺排的使用情况bufferFeatures
: 支持缓冲区的用例
我们现在要用这个函数来创建一个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
);
}
//添加一个简单的辅助函数,告诉我们所选择的深度格式是否包含模版组件:
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);
...
27 显式转换深度图像
我们不需要将图像的布局显式转换为深度附件,因为我们将在渲染过程中处理这一点。
在 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!");
}
渲染通道
我们现在要修改createRenderPass
以包括一个深度附件。首先指定VkAttachmentDescription:
VkAttachmentDescription depthAttachment{};
depthAttachment.format = findDepthFormat();
//format应该与深度图像本身相同。这次我们不关心存储深度数据(storeOp
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;
//我们不关心之前的深度内容,所以我们可以使用VK_IMAGE_LAYOUT_UNDEFINED作为initialLayout。
depthAttachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkAttachmentReference depthAttachmentRef{};
depthAttachmentRef.attachment = 1;
depthAttachmentRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
为第一个(也是唯一的)子通道添加一个对附件的引用:
subpass.pColorAttachments = &colorAttachmentRef;
subpass.pDepthStencilAttachment = &depthAttachmentRef;
与颜色附件不同,一个子通道只能使用一个深度(+stencil)附件。在多个缓冲区上做深度测试其实没有任何意义。
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;
//更新VkRenderPassCreateInfo结构以引用两个附件。
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();
...
}
清除值
因为我们现在有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();
//注意clearValues的顺序应该与你的附件的顺序相同。
在Vulkan中,深度缓冲区的深度范围是0.0
到1.0
,其中1.0
位于远视平面,0.0
位于近视平面。深度缓冲区中每一点的初始值应该是最远的深度,也就是`1.0’。
深度和模版状态
深度附件现在已经可以使用了,但深度测试仍需要在图形管道中启用。它是通过VkPipelineDepthStencilStateCreateInfo结构配置的:
VkPipelineDepthStencilStateCreateInfo depthStencil{};
depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
//depthTestEnable字段指定是否应将新片段的深度与深度缓冲区进行比较,看它们是否应被丢弃。
//depthWriteEnable字段指定是否应该将通过深度测试的新片段的深度实际写入深度缓冲区。
depthStencil.depthTestEnable = VK_TRUE;
depthStencil.depthWriteEnable = VK_TRUE;
//为保留或丢弃片段而进行的比较
depthStencil.depthCompareOp = VK_COMPARE_OP_LESS;
//可选的深度绑定测试。基本上,这允许你只保留落在指定深度范围内的片段
depthStencil.depthBoundsTestEnable = VK_FALSE;
depthStencil.minDepthBounds = 0.0f; // Optional
depthStencil.maxDepthBounds = 1.0f; // Optional。
//
depthStencil.stencilTestEnable = VK_FALSE;
depthStencil.front = {}; // Optional
depthStencil.back = {}; // Optional
//最后三个字段配置了 stencil buffer 的操作
pipelineInfo.pDepthStencilState = &depthStencil;
如果你现在运行你的程序,那么你应该看到几何体的碎片现在已经正确排序了:
处理窗口调整大小
深度缓冲区的分辨率应该在窗口调整大小时改变,以匹配新的颜色附件分辨率。在这种情况下,扩展recreateSwapChain
函数来重新创建深度资源。
void recreateSwapChain() {
int width = 0, height = 0;
while (width == 0 || height == 0) {
glfwGetFramebufferSize(window, &width, &height);
glfwWaitEvents();
}
清理操作应该发生在交换链清理功能中:
void cleanupSwapChain() {
vkDestroyImageView(device, depthImageView, nullptr);
vkDestroyImage(device, depthImage, nullptr);
vkFreeMemory(device, depthImageMemory, nullptr);
...
}