[译]Vulkan教程(33)多重采样


[译]Vulkan教程(33)多重采样

Multisampling 多重采样

Introduction 入门

Our program can now load multiple levels of detail for textures which fixes artifacts when rendering objects far away from the viewer. The image is now a lot smoother, however on closer inspection you will notice jagged saw-like patterns along the edges of drawn geometric shapes. This is especially visible in one of our early programs when we rendered a quad:

我们的程序现在加载了多层LOD的纹理,它修复了对象远离观察者时的锯齿问题。现在的图像平滑得多了,但是对于靠近观察者的物体,你仍旧会观察到锯齿形的边缘-在几何体上。这在我们早起的一个程序上十分显眼when我们渲染一个四边形:

This undesired effect is called "aliasing" and it's a result of a limited numbers of pixels that are available for rendering. Since there are no displays out there with unlimited resolution, it will be always visible to some extent. There's a number of ways to fix this and in this chapter we'll focus on one of the more popular ones: Multisample anti-aliasing (MSAA).

这个讨厌的效果被称为“锯齿”,它是由于可供渲染的像素数量不足导致的结果。由于没有无限解析度的显示器,总会看到一些。有若干方法可以解决这个问题,本章我们关注于流行的一个:Multisample anti-aliasing(MSAA)。

In ordinary rendering, the pixel color is determined based on a single sample point which in most cases is the center of the target pixel on screen. If part of the drawn line passes through a certain pixel but doesn't cover the sample point, that pixel will be left blank, leading to the jagged "staircase" effect.

在普通的渲染里,像素压缩呢由单一采样点决定,大多数时候这是屏幕上像素的中心位置。如果绘制的线部分经过某个像素,但是不覆盖采样点,那个像素就会是空白,导致锯齿状的“楼梯”效果。

What MSAA does is it uses multiple sample points per pixel (hence the name) to determine its final color. As one might expect, more samples lead to better results, however it is also more computationally expensive.

MSAA所做的是,对每个像素使用多个采样点(如名字所指),依次决定它的最终颜色。如你所料,采样越多,效果越好,但是也消耗更多计算资源。

In our implementation, we will focus on using the maximum available sample count. Depending on your application this may not always be the best approach and it might be better to use less samples for the sake of higher performance if the final result meets your quality demands.

在我们的实现里,我们关注使用最大可用采样量。根据你的app的不同,这可能不是最好的方法,如果最终渲染质量已经达标了,也许使用少一点的采样更好,为了性能嘛。

Getting available sample count 获取可用采样量

Let's start off by determining how many samples our hardware can use. Most modern GPUs support at least 8 samples but this number is not guaranteed to be the same everywhere. We'll keep track of it by adding a new class member:

首先,我们查查我们的硬件能用多少采样量。大多数现代GPU支持最少8个采样量,但是并非所有地方都是这样。我们添加一个新成员来记录它:

...
VkSampleCountFlagBits msaaSamples = VK_SAMPLE_COUNT_1_BIT;
...

By default we'll be using only one sample per pixel which is equivalent to no multisampling, in which case the final image will remain unchanged. The exact maximum number of samples can be extracted from VkPhysicalDeviceProperties associated with our selected physical device. We're using a depth buffer, so we have to take into account the sample count for both color and depth - the lower number will be the maximum we can support. Add a function that will fetch this information for us:

默认的,我们让每个像素只有1个采样点,这等于没有多重采样,此时最后图像不变。准确的采样点的最大值,可以从与我们选择的物理设备关联的VkPhysicalDeviceProperties 中提取到。我们在使用深度缓存,所以我们必须考虑颜色和深度的采样数量-其中的较小值会是我们支持的最大值。添加函数that为我们提取这个信息:

 1 VkSampleCountFlagBits getMaxUsableSampleCount() {
 2     VkPhysicalDeviceProperties physicalDeviceProperties;
 3     vkGetPhysicalDeviceProperties(physicalDevice, &physicalDeviceProperties);
 4  
 5     VkSampleCountFlags counts = std::min(physicalDeviceProperties.limits.framebufferColorSampleCounts, physicalDeviceProperties.limits.framebufferDepthSampleCounts);
 6     if (counts & VK_SAMPLE_COUNT_64_BIT) { return VK_SAMPLE_COUNT_64_BIT; }
 7     if (counts & VK_SAMPLE_COUNT_32_BIT) { return VK_SAMPLE_COUNT_32_BIT; }
 8     if (counts & VK_SAMPLE_COUNT_16_BIT) { return VK_SAMPLE_COUNT_16_BIT; }
 9     if (counts & VK_SAMPLE_COUNT_8_BIT) { return VK_SAMPLE_COUNT_8_BIT; }
10     if (counts & VK_SAMPLE_COUNT_4_BIT) { return VK_SAMPLE_COUNT_4_BIT; }
11     if (counts & VK_SAMPLE_COUNT_2_BIT) { return VK_SAMPLE_COUNT_2_BIT; }
12  
13     return VK_SAMPLE_COUNT_1_BIT;
14 }

We will now use this function to set the msaaSamples variable during the physical device selection process. For this, we have to slightly modify the pickPhysicalDevice function:

我们现在要用这个函数来设置msaaSamples 变量-在物理设备选择期间。为此,我们必须稍微修改pickPhysicalDevice 函数:

void pickPhysicalDevice() {
    ...
    for (const auto& device : devices) {
        if (isDeviceSuitable(device)) {
            physicalDevice = device;
            msaaSamples = getMaxUsableSampleCount();
            break;
        }
    }
    ...
}

Setting up a render target 设置渲染目标

In MSAA, each pixel is sampled in an offscreen buffer which is then rendered to the screen. This new buffer is slightly different from regular images we've been rendering to - they have to be able to store more than one sample per pixel. Once a multisampled buffer is created, it has to be resolved to the default framebuffer (which stores only a single sample per pixel). This is why we have to create an additional render target and modify our current drawing process. We only need one render target since only one drawing operation is active at a time, just like with the depth buffer. Add the following class members:

在MSAA,每个像素都在离屏buffer上采样,然后被选人到屏幕上。这个新buffer与我们之前渲染到的常规image有点不同——它必须能够保存超过1个采样点每像素。一旦一个多采样的buffer被创建,它必须被resolve到默认帧缓存(which仅保存1个单采样每像素)。这就是为什么,我们必须创建一个额外的渲染目标,修改我们当前的绘制过程。我们只需要1个渲染目标,因为同一时间只有1个绘制操作,就像深度缓存那样。添加下述类成员:

...
VkImage colorImage;
VkDeviceMemory colorImageMemory;
VkImageView colorImageView;
...

This new image will have to store the desired number of samples per pixel, so we need to pass this number to VkImageCreateInfo during the image creation process. Modify the createImage function by adding a numSamples parameter:

这个新image必须保存要求的采样量每像素,所以我们需要在image创建过程期间传入这个数量到VkImageCreateInfo 。修改createImage 函数by添加一个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;
    ...

For now, update all calls to this function using VK_SAMPLE_COUNT_1_BIT - we will be replacing this with proper values as we progress with implementation:

目前,使用VK_SAMPLE_COUNT_1_BIT 更新所有对此函数的调用——随着我们逐步的实现,我们会用合适的值替换这个值:

1 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);
2 ...
3 createImage(texWidth, texHeight, mipLevels, VK_SAMPLE_COUNT_1_BIT, VK_FORMAT_R8G8B8A8_UNORM, 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);

We will now create a multisampled color buffer. Add a createColorResources function and note that we're using msaaSamples here as a function parameter to createImage. We're also using only one mip level, since this is enforced by the Vulkan specification in case of images with more than one sample per pixel. Also, this color buffer doesn't need mipmaps since it's not going to be used as a texture:

现在我们要创建一个多采样的颜色buffer。添加createColorResources 函数,注意,我们这里要用msaaSamples 作为createImage函数的参数。我们也只使用1个mip层,因为这被Vulkan说明书强制规定,在多采样每像素的image上。而且,这个颜色buffer不需要mipmap,因为它不会被用作纹理:

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);
 
    transitionImageLayout(colorImage, colorFormat, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, 1);
}

For consistency, call the function right before createDepthResources:

为保持一致,在createDepthResources之前调用这个函数:

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

You may notice that the newly created color image uses a transition path from VK_IMAGE_LAYOUT_UNDEFINED to VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL which is a new case for us to handle. Let's update transitionImageLayout function to take this into account:

你可能注意到,新创建的颜色image使用了一个从VK_IMAGE_LAYOUT_UNDEFINED 到VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL 的变换路径which对我们是一个要处理的新情况。让我们更新transitionImageLayout 函数to考虑这个情况:

 1 void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout, uint32_t mipLevels) {
 2     ...
 3     else if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
 4         barrier.srcAccessMask = 0;
 5         barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
 6         sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
 7         destinationStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
 8     }
 9     else {
10         throw std::invalid_argument("unsupported layout transition!");
11     }
12     ...
13 }

Now that we have a multisampled color buffer in place it's time to take care of depth. Modify createDepthResources and update the number of samples used by the depth buffer:

既然我们有多采样的颜色buffer了,是时候关心一下深度了。修改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);
    ...
}

We have now created a couple of new Vulkan resources, so let's not forget to release them when necessary:

我们现在已经创建了一些新Vulkan资源,所以别忘了在需要的时候释放它们:

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

And update the recreateSwapChain so that the new color image can be recreated in the correct resolution when the window is resized:

更新recreateSwapChain ,这样新颜色image就可以以正确的解析度创建when窗口resize:

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

We made it past the initial MSAA setup, now we need to start using this new resource in our graphics pipeline, framebuffer, render pass and see the results!

我们度过了MSAA的设置阶段,现在我们需要开始使用这个新资源到我们的图形管道、帧缓存、render pass,并看看结果!

Adding new attachments 添加新附件

Let's take care of the render pass first. Modify createRenderPass and update color and depth attachment creation info structs:

我们先关心一下render pass。修改createRenderPass ,更新颜色和深度附件的创建结构体:

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

You'll notice that we have changed the finalLayout from VK_IMAGE_LAYOUT_PRESENT_SRC_KHR to VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL. That's because multisampled images cannot be presented directly. We first need to resolve them to a regular image. This requirement does not apply to the depth buffer, since it won't be presented at any point. Therefore we will have to add only one new attachment for color which is a so-called resolve attachment:

你会注意到,我们将finalLayout从VK_IMAGE_LAYOUT_PRESENT_SRC_KHR 修改为VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL。这是因为多采样image不能被直接呈现。我们首先需要将它们解析为一个常规的image。这个要求不应用到深度缓存,因为它不会被呈现到任何地方。因此我们必须添加一个新颜色附件which被称为解析附件:

    ...
    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;
    ...

The render pass now has to be instructed to resolve multisampled color image into regular attachment. Create a new attachment reference that will point to the color buffer which will serve as the resolve target:

现在render pass已经被指示去解析多采样颜色image为常规附件了。创建新附件引用,其指向会被用作解析的目标的颜色buffer:

    ...
    VkAttachmentReference colorAttachmentResolveRef = {};
    colorAttachmentResolveRef.attachment = 2;
    colorAttachmentResolveRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    ...

Set the pResolveAttachments subpass struct member to point to the newly created attachment reference. This is enough to let the render pass define a multisample resolve operation which will let us render the image to screen:

设置pResolveAttachments 子pass结构体成员to指向新创建的附件引用。这足够让render pass定义一个多采样解析操作which会让我们选人image到屏幕:

    ...
    subpass.pResolveAttachments = &colorAttachmentResolveRef;
    ...

Now update render pass info struct with the new color attachment:

现在更新render pass信息结构体with新颜色附件:

    ...
    std::array3> attachments = {colorAttachment, depthAttachment, colorAttachmentResolve};
    ...

With the render pass in place, modify createFrameBuffers and add the new image view to the list:

随着render pass就位,修改createFrameBuffers ,添加新image视图到列表:

void createFrameBuffers() {
        ...
        std::array3> attachments = {
            colorImageView,
            depthImageView,
            swapChainImageViews[i]
        };
        ...
}

Finally, tell the newly created pipeline to use more than one sample by modifying createGraphicsPipeline:

最后,告诉新创建的管道to使用超过1个采样by修改createGraphicsPipeline

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

Now run your program and you should see the following:

现在运行你的程序,你应当看到下图:

Just like with mipmapping, the difference may not be apparent straight away. On a closer look you'll notice that the edges on the roof are not as jagged anymore and the whole image seems a bit smoother compared to the original.

就像mipmap一样,区别不能直接的看出来。靠近点观察,你会发现房顶的边缘不再锯齿了,整个图像看起来更光滑了一点,与原图相比。

The difference is more noticable when looking up close at one of the edges:

区别更显眼when靠近观察边缘:

Quality improvements 质量提升

There are certain limitations of our current MSAA implementation which may impact the quality of the output image in more detailed scenes. For example, we're currently not solving potential problems caused by shader aliasing, i.e. MSAA only smoothens out the edges of geometry but not the interior filling. This may lead to a situation when you get a smooth polygon rendered on screen but the applied texture will still look aliased if it contains high contrasting colors. One way to approach this problem is to enable Sample Shading which will improve the image quality even further, though at an additional performance cost:

我们当前的MSAA实现有一些限制which在更精细的场景中可能影响输出图像的质量。例如,我们现在没有解决潜在的shader产生的锯齿问题,即MSAA只对几何体的边缘进行平滑,但没有对内部填充进行平滑。这可能导致一种情况,即你得到平滑的多边形,但是在高对比度的颜色下,贴图仍旧有锯齿。一种解决方案是启用Sample Shadingwhich会更加提升image质量,只是性能会更加受损:

 1  
 2 void createLogicalDevice() {
 3     ...
 4     deviceFeatures.sampleRateShading = VK_TRUE; // enable sample shading feature for the device
 5     ...
 6 }
 7  
 8 void createGraphicsPipeline() {
 9     ...
10     multisampling.sampleShadingEnable = VK_TRUE; // enable sample shading in the pipeline
11     multisampling.minSampleShading = .2f; // min fraction for sample shading; closer to one is smoother
12     ...
13 }

In this example we'll leave sample shading disabled but in certain scenarios the quality improvement may be noticeable:

本例中我们禁用采样着色,但是在某些场景中质量提升会很显眼:

Conclusion 总结

It has taken a lot of work to get to this point, but now you finally have a good base for a Vulkan program. The knowledge of the basic principles of Vulkan that you now possess should be sufficient to start exploring more of the features, like:

你做了很多工作才到达这里,但现在你终于有了一个Vulkan程序的良好基础。你现在拥有的Vulkan基本原则的知识应当足够你开始探索更多特性了,例如:

  • Push constants
  • Instanced rendering 实例化渲染
  • Dynamic uniforms 动态uniform
  • Separate images and sampler descriptors 独立image和采样描述符
  • Pipeline cache 管道缓存
  • Multi-threaded command buffer generation 多线程命令buffer生成
  • Multiple subpasses 多个子pass
  • Compute shaders 计算shader

The current program can be extended in many ways, like adding Blinn-Phong lighting, post-processing effects and shadow mapping. You should be able to learn how these effects work from tutorials for other APIs, because despite Vulkan's explicitness, many concepts still work the same.

当前的程序可以用多种方式扩展,例如添加Blinn-Phong光照、后处理效果和阴影映射。你应当可以在其他API的教程中学习这些效果如何工作,因为尽管Vulkan的显式化风格,许多概念的工作方式还是相同的。

C++ code / Vertex shader / Fragment shader

  • Previous

 

  • Next

 【译者注:这是本系列教程最后一篇。经过翻译这33篇,我对Vulkan的各个知识点有了第一印象,接下来就可以反复阅读思考,融会贯通了。】