本文档是对Vulkan Tutorial教程的翻译,非官方翻译,仅供vulkan爱好者参考学习。如果有翻译错误,请留言指出,或者联系占航(hangliebe@163.com)进行修正,感谢。建议英文好的朋友直接读原文档。

关注我的微博主页

2022-02-20 v1.0

多重采样

简介

我们的程序现在可以为纹理加载多层次的细节,这修复了在渲染离观众较远的物体时出现的假象。现在的图像平滑了许多,然而仔细观察,你会发现在绘制的几何图形的边缘有锯齿状的图案。在我们早期的一个程序中,当我们渲染一个四边形时,这一点尤其明显:

img

这种不受欢迎的效果被称为 “混叠”,它是由于可用于渲染的像素数量有限造成的。由于没有无限分辨率的显示器,它在某种程度上总是可见的。有很多方法可以解决这个问题,在这一章中,我们将重点讨论其中一个比较流行的方法。多重采样抗锯齿 (MSAA)。

在普通的渲染中,像素的颜色是根据单个采样点确定的,在大多数情况下,这个采样点就是屏幕上目标像素的中心。如果绘制的线条有一部分穿过某个像素点,但没有覆盖到采样点,那么这个像素点就会留下空白,导致锯齿状的 “阶梯”效果。

img

MSAA所做的是,它使用每个像素的多个采样点(因此而得名)来确定其最终颜色。正如人们所期望的那样,更多的样本会带来更好的结果,但是它的计算成本也更高。

img

在我们的实施中,我们将着重于使用最大的可用样本数。根据你的应用,这不一定是最好的方法,如果最终结果能满足你的质量要求,为了更高的性能而使用更少的样本可能更好。

获得可用的样本数

让我们首先确定我们的硬件可以使用多少个样本。大多数现代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之前调用该函数:

void initVulkan() {
    ...
    createColorResources();
    createDepthResources();
    ...
}

现在我们有了一个多采样的颜色缓冲器,是时候处理深度问题了。修改createDepthResources并更新深度缓冲器使用的样本数:

void createDepthResources() {
    ...
    createImage(swapChainExtent.width, swapChainExtent.height, 1, msaaSamples, depthFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory);
    ...
}

我们现在已经创建了几个新的Vulkan资源,所以我们不要忘记在必要时释放它们:

void cleanupSwapChain() {
    vkDestroyImageView(device, colorImageView, nullptr);
    vkDestroyImage(device, colorImage, nullptr);
    vkFreeMemory(device, colorImageMemory, nullptr);
    ...
}

并更新recreateSwapChain,以便在窗口调整大小时,可以以正确的分辨率重新创建新的彩色图像。:

void recreateSwapChain() {
    ...
    createGraphicsPipeline();
    createColorResources();
    createDepthResources();
    ...
}

我们已经完成了最初的MSAA设置,现在我们需要开始在我们的图形管道、帧缓冲器、渲染通道中使用这种新资源,并看到结果!

添加新的附件

让我们先来处理一下渲染通道的问题。修改createRenderPass并更新颜色和深度附件创建信息结构:

void createRenderPass() {
    ...
    colorAttachment.samples = msaaSamples;
    colorAttachment.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    ...
    depthAttachment.samples = msaaSamples;
    ...

你会注意到,我们把最终布局从VK_IMAGE_LAYOUT_PRESENT_SRC_KHR改为VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL。这是因为多采样图像不能直接呈现。我们首先需要将它们解析为普通图像。这个要求并不适用于深度缓冲区,因为它不会在任何时候被呈现。因此,我们将不得不为颜色添加一个新的附件,这是一个所谓的解析附件:

    ...
    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并将新的图像视图添加到列表中:

void createFramebuffers() {
        ...
        std::array<VkImageView, 3> attachments = {
            colorImageView,
            depthImageView,
            swapChainImageViews[i]
        };
        ...
}

最后,通过修改createGraphicsPipeline,告诉新创建的管道使用一个以上的样本:

void createGraphicsPipeline() {
    ...
    multisampling.rasterizationSamples = msaaSamples;
    ...
}

现在运行你的程序,你应该看到以下内容:

img

就像mipmapping一样,差别可能不会马上显现出来。仔细观察,你会发现边缘不再那么参差不齐,整个图像与原始图像相比似乎更平滑了一些。

img

当近距离观察其中一个边缘时,这种差异更加明显:

img

质量改进

我们目前的MSAA实现有一定的局限性,可能会影响到更多细节场景下的输出图像质量。例如,我们目前没有解决由着色器混叠引起的潜在问题,也就是说,MSAA只平滑了几何体的边缘,而没有平滑内部填充。这可能会导致这样一种情况:你在屏幕上得到一个平滑的多边形,但如果所应用的纹理包含高对比度的颜色,它仍然会看起来有异化。解决这个问题的一个方法是启用Sample Shading,这将进一步提高图像质量,尽管要付出额外的性能代价:

void createLogicalDevice() {
    ...
    deviceFeatures.sampleRateShading = VK_TRUE; // enable sample shading feature for the device
    ...
}

void createGraphicsPipeline() {
    ...
    multisampling.sampleShadingEnable = VK_TRUE; // enable sample shading in the pipeline
    multisampling.minSampleShading = .2f; // min fraction for sample shading; closer to one is smoother
    ...
}

在这个例子中,我们将禁用样本阴影,但在某些情况下,质量的改善可能是明显的:

img

结论

虽然花了很多功夫才走到这一步,但现在你终于有了一个很好的Vulkan程序的基础。你现在拥有的Vulkan基本原理的知识应该足以开始探索更多的功能,比如。

  • 推送常量
  • 实例渲染
  • 动态uniforms
  • 独立的图像和采样器描述符
  • 管线缓存
  • 多线程的命令缓冲区生成
  • 多个子通道
  • 计算着色器

目前的程序可以在很多方面进行扩展,比如增加Blinn-Phong照明、后处理效果和阴影映射。你应该能够从其他API的教程中了解到这些效果是如何工作的,因为尽管Vulkan是明确的,但许多概念的工作原理还是一样的。

C++ code / Vertex shader / Fragment shader

留言板