1. 程式人生 > >[譯]Vulkan教程(33)多重取樣

[譯]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::array<VkAttachmentDescription, 3> 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::array<VkImageView, 3> 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的各個知識點有了第一印象,接下來就可以反覆閱讀思考,融會貫通了。】