目录
30 多重采样
获得可用的样本数
设置一个渲染目标
添加新的附件
30 多重采样
我们的程序现在可以为纹理加载多层次的细节,这修复了在渲染离观众较远的物体时出现的假象。现在的图像平滑了许多,然而仔细观察,你会发现在绘制的几何图形的边缘有锯齿状的图案。在我们早期的一个程序中,当我们渲染一个四边形时,这一点尤其明显:
在普通的渲染中,像素的颜色是根据单个采样点确定的,在大多数情况下,这个采样点就是屏幕上目标像素的中心。如果绘制的线条有一部分穿过某个像素点,但没有覆盖到采样点,那么这个像素点就会留下空白,导致锯齿状的 “阶梯”效果。
MSAA所做的是,它使用每个像素的多个采样点(因此而得名)来确定其最终颜色。正如人们所期望的那样,更多的样本会带来更好的结果,但是它的计算成本也更高。
获得可用的样本数
让我们首先确定我们的硬件可以使用多少个样本。大多数现代GPU至少支持8个样本,但这个数字不能保证在任何地方都是一样的。我们将通过添加一个新的类成员来跟踪它:
...
VkSampleCountFlagBits msaaSamples = VK_SAMPLE_COUNT_1_BIT;
...//。准确的最大样本数可以从与我们选定的物理设备相关的VkPhysicalDeviceProperties中提取。
//我们必须考虑到颜色和深度的样本数。两者都支持的最高采样数将是我们能支持的最大限度。
//添加一个函数,为我们获取这些信息:VkSampleCountFlagBits getMaxUsableSampleCount() {VkPhysicalDeviceProperties physicalDeviceProperties;vkGetPhysicalDeviceProperties(physicalDevice, &physicalDeviceProperties);VkSampleCountFlags counts = physicalDeviceProperties.limits.framebufferColorSampleCounts & physicalDeviceProperties.limits.framebufferDepthSampleCounts;if (counts & VK_SAMPLE_COUNT_64_BIT) { return VK_SAMPLE_COUNT_64_BIT; }if (counts & VK_SAMPLE_COUNT_32_BIT) { return VK_SAMPLE_COUNT_32_BIT; }if (counts & VK_SAMPLE_COUNT_16_BIT) { return VK_SAMPLE_COUNT_16_BIT; }if (counts & VK_SAMPLE_COUNT_8_BIT) { return VK_SAMPLE_COUNT_8_BIT; }if (counts & VK_SAMPLE_COUNT_4_BIT) { return VK_SAMPLE_COUNT_4_BIT; }if (counts & VK_SAMPLE_COUNT_2_BIT) { return VK_SAMPLE_COUNT_2_BIT; }return VK_SAMPLE_COUNT_1_BIT;
}
现在我们将在物理设备选择过程中使用这个函数来设置msaaSamples
变量。为此,我们必须稍微修改pickPhysicalDevice
函数:
void pickPhysicalDevice() {...for (const auto& device : devices) {if (isDeviceSuitable(device)) {physicalDevice = device;msaaSamples = getMaxUsableSampleCount();break;}}...
}
设置一个渲染目标
在MSAA中,每个像素在屏幕外的缓冲区中被采样,然后被渲染到屏幕上。这个新的缓冲区与我们一直在渲染的普通图像略有不同–它们必须能够存储每个像素的一个以上的样本。一旦多采样缓冲区被创建,它就必须被解析为默认的帧缓冲区(每个像素只存储一个样本)。
//必须创建一个额外的渲染目标并修改我们当前的绘图过程。我们只需要一个渲染目标,因为每次只有一个绘制操作是活动的,就像深度缓冲器一样。添加以下类成员:...
VkImage colorImage;
VkDeviceMemory colorImageMemory;
VkImageView colorImageView;
...//这个新图像将必须存储每个像素所需的样本数,所以我们需要在图像创建过程中把这个数字传给VkImageCreateInfo。修改createImage函数,增加一个numSamples参数:void createImage(uint32_t width, uint32_t height, uint32_t mipLevels, VkSampleCountFlagBits numSamples, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) {...imageInfo.samples = numSamples;...//使用`VK_SAMPLE_COUNT_1_BIT’更新对该函数的所有调用 - 我们将在实施过程中用适当的值替换它:createImage(swapChainExtent.width, swapChainExtent.height, 1, VK_SAMPLE_COUNT_1_BIT, depthFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory);
...
createImage(texWidth, texHeight, mipLevels, VK_SAMPLE_COUNT_1_BIT, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory);
我们现在将创建一个多采样的颜色缓冲区。添加一个createColorResources
函数,注意我们在这里使用msaaSamples
作为createImage
的一个函数参数。我们也只使用一个mip级别,因为这是Vulkan规范在每个像素有一个以上的样本的情况下强制执行的。另外,这个颜色缓冲区不需要mipmaps,因为它不会被用作纹理:
void createColorResources() {VkFormat colorFormat = swapChainImageFormat;createImage(swapChainExtent.width, swapChainExtent.height, 1, msaaSamples, colorFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, colorImage, colorImageMemory);colorImageView = createImageView(colorImage, colorFormat, VK_IMAGE_ASPECT_COLOR_BIT, 1);
}//在createDepthResources之前调用该函数://我们现在已经创建了几个新的Vulkan资源,所以我们不要忘记在必要时释放它们:void cleanupSwapChain() {vkDestroyImageView(device, colorImageView, nullptr);vkDestroyImage(device, colorImage, nullptr);vkFreeMemory(device, colorImageMemory, nullptr);...
}
添加新的附件
让我们先来处理一下渲染通道的问题。修改createRenderPass
并更新颜色和深度附件创建信息结构:
void createRenderPass() {...colorAttachment.samples = msaaSamples;colorAttachment.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
//你会注意到,我们把最终布局从VK_IMAGE_LAYOUT_PRESENT_SRC_KHR改为VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL。
//这是因为多采样图像不能直接呈现。我们首先需要将它们解析为普通图像。
//这个要求并不适用于深度缓冲区,因为它不会在任何时候被呈现。
//因此,我们将不得不为颜色添加一个新的附件,这是一个所谓的解析附件:...depthAttachment.samples = msaaSamples;...VkAttachmentDescription colorAttachmentResolve{};colorAttachmentResolve.format = swapChainImageFormat;colorAttachmentResolve.samples = VK_SAMPLE_COUNT_1_BIT;colorAttachmentResolve.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;colorAttachmentResolve.storeOp = VK_ATTACHMENT_STORE_OP_STORE;colorAttachmentResolve.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;colorAttachmentResolve.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;colorAttachmentResolve.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;colorAttachmentResolve.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;...//现在必须指示渲染通道将多采样的彩色图像解析为常规附件。
//创建一个新的附件引用,它将指向作为解析目标的颜色缓冲区:...VkAttachmentReference colorAttachmentResolveRef{};colorAttachmentResolveRef.attachment = 2;colorAttachmentResolveRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;//设置pResolveAttachments子通道结构成员,指向新创建的附件引用。subpass.pResolveAttachments = &colorAttachmentResolveRef;...
现在用新的颜色附件更新渲染通道信息结构:
...std::array<VkAttachmentDescription, 3> attachments = {colorAttachment, depthAttachment, colorAttachmentResolve};...//渲染通道到位后,修改createFramebuffers并将新的图像视图添加到列表中:
std::array<VkImageView, 3> attachments = {colorImageView,depthImageView,swapChainImageViews[i]};//修改createGraphicsPipeline,告诉新创建的管道使用一个以上的样本:void createGraphicsPipeline() {...multisampling.rasterizationSamples = msaaSamples;...
}