大家好,本文學習MSAA以及在WebGPU中的實現。
上一篇博文
WebGPU學習(二): 學習“繪制一個三角形”示例
下一篇博文
WebGPU學習(四):Alpha To Coverage
學習MSAA
介紹
MSAA(多重采樣抗鋸齒),是硬件實現的抗鋸齒技術
動機
參考深入剖析MSAA :
具體到實時渲染領域中,走樣有以下三種:
1.幾何體走樣(幾何物體的邊緣有鋸齒),幾何走樣由於對幾何邊緣采樣不足導致。
2.着色走樣,由於對着色器中着色公式(渲染方程)采樣不足導致。比較明顯的現象就是高光閃爍。
3.時間走樣,主要是對高速運動的物體采樣不足導致。比如游戲中播放的動畫發生跳變等。
這里討論幾何體走樣。
如上圖所示,我們要繪制一個三角形。它由三個頂點組成,紅線范圍內的三角形是片元primitive覆蓋的區域。
primitive會被光柵化為fragment,而一個fragment最終對應屏幕上的一個像素,如圖中的小方塊所示。
gpu會根據像素中心的采樣點是否被pritimive覆蓋來判斷是否生成該fragment和執行對應的fragment shader。
圖中紅色的點是被覆蓋的采樣點,它所在的像素會被渲染。
下圖是最終渲染的結果,我們看到三角形邊緣產生了鋸齒:
原理
MSAA通過增加采樣點來減輕幾何體走樣。
它包括4個步驟:
1.針對采樣點進行覆蓋檢測
2.每個被覆蓋的fragment執行一次fragment shader
3.針對采樣點進行深度檢測和模版檢測
4.解析(resolve)
下面以4X MSAA為例(每個像素有4個采樣點),說明每個步驟:
1.針對采樣點進行覆蓋檢測
gpu會計算每個fragment的coverage(覆蓋率),從而得知對應像素的每個采樣點是否被覆蓋的信息。
coverage相關知識可以參考WebGPU學習(四):Alpha To Coverage -> 學習Alpha To Coverage -> 原理
這里為了簡化,我們只考慮通過“檢測每個像素有哪些采樣點被primitive覆蓋”來計算coverager:
如上圖所示,藍色的采樣點是在三角形中,是被覆蓋的采樣點。
2.每個被覆蓋的fragment執行一次fragment shader
如果一個像素至少有一個采樣點被覆蓋,那么會執行一次它對應的fragment(注意,只執行一次哈,不是執行4次)(它所有的輸入varying變量都是針對其像素中心點而言的,所以計算的輸出的顏色始終是針對該柵格化出的像素中心點而言的),輸出的顏色保存在color buffer中(覆蓋的采樣點都要保存同一個輸出的顏色)
3.針對采樣點進行深度檢測和模版檢測
所有采樣點的深度值和模版值都要存到depth buffer和stencil buffer中(無論是否被覆蓋)。
被覆蓋的采樣點會進行深度檢測和模版檢測,通過了的采樣點會進入“解析”步驟。
那為什么要保存所有采樣點的深度和模版值了(包括沒有被覆蓋的)?因為它們在深度檢測和模版檢測階段決定所在的fragment是否被丟棄:
那是因為之后需要每個sample(采樣點)都執行一下depth-test,以確定整個fragment是否要流向(通往緩沖區輸出的)流水線下一階段——只有當全部fragment-sample的Depth-Test都Fail掉的時候,才決定拋棄掉這個fragment(蒙版值stencil也是這樣的,每個sample都得進行Stencil-Test)。
4.解析
什么是解析?
根據深入剖析MSAA 的說法:
像超采樣一樣,過采樣的信號必須重新采樣到指定的分辨率,這樣我們才可以顯示它。
這個過程叫解析(resolving)。
根據亂彈紀錄II:Alpha To Coverage 的說法:
在把所有像素輸出到渲染緩沖區前執行Resolve以生成單一像素值。
。。。。。。
也該是時候談到一直說的“計算輸出的顏色”是怎么一回事了。MultiSample的Resolve階段,如果是屏幕輸出的話這個階段會發生在設備的屏幕輸出直前;如果是FBO輸出,則是發生在把這個Multisample-FBO映射到非multisample的FBO(或屏幕)的時候(見[多重采樣(MultiSample)下的FBO反鋸齒] )。Resolve,說白了就是把MultiSample的存儲區域的數據,根據一定法則映射到可以用於顯示的Buffer上了(這里的輸出緩沖區包括了Color、Depth或還有Stencil這幾個)。Depth和Stencil前面已經提及了法則了,Color方面其實也簡單,一般的顯卡的默認處理就是把sample的color取平均了。注意,因為depth-test等Test以及Coverage mask的影響下,有些sample是不參與計算的(被摒棄),例如4XMSAA下上面的0101,就只有兩個sample,又已知各sample都對應的只是同一個顏色值,所以輸出的顏色 = 2 * fragment color / 4 = 0.5 * fragment color。也就是是說該fragemnt最終顯示到屏幕(或Non-MS-FBO)上是fragment shader計算出的color值的一半——這不僅是顏色亮度減半還包括真·透明度值的減半。
我的理解:
“解析”是把每個像素經過上述步驟得到的采樣點的顏色值,取平均值,得到這個像素的顏色值。
如上圖右邊所示,像素的2個采樣點進入了“解析”,最終該像素的顏色值為 0.5(2/4) * 原始顏色值
經過上述所有步驟后,最終的渲染結果如下:
總結
MSAA能減輕幾何體走樣,但會增加color buffer、depth buffer、stencil buffer開銷。
參考資料
深入剖析MSAA
亂彈紀錄II:Alpha To Coverage
Anti Aliasing
WebGPU實現MSAA
有下面幾個要點:
- 能夠查詢最大的采樣個數sample count
目前我沒找到查詢的方法,但至少支持4個采樣點
參考 Investigation: Multisampled Render Targets and Resolve Operations:
We can say that 4xMSAA is guaranteed on all WebGPU implementations, and we need to provide APIs for queries on whether we can create a multisampled texture with given format and sample count.
- 設置sample count
dictionary GPURenderPipelineDescriptor : GPUPipelineDescriptorBase {
...
unsigned long sampleCount = 1;
...
};
dictionary GPUTextureDescriptor : GPUObjectDescriptorBase {
...
unsigned long sampleCount = 1;
...
};
我們在WebGPU 規范中看到render pipeline descriptor和texture descriptor可以設置sampleCount。
- 設置resolveTarget
在“解析”步驟中,需要重新采樣到指定的分辨率:
過采樣的信號必須重新采樣到指定的分辨率,這樣我們才可以顯示它
所以我們應該設置color的resolveTarget(depth、stencil不支持resolveTarget),它包含“分辨率”的信息。
我們來看下WebGPU 規范:
dictionary GPURenderPassColorAttachmentDescriptor {
required GPUTextureView attachment;
GPUTextureView resolveTarget;
required (GPULoadOp or GPUColor) loadValue;
GPUStoreOp storeOp = "store";
};
resolveTarget在render pass colorAttachment descriptor中設置,它的類型是GPUTextureView。
而GPUTextureView是從GPUTexture得來的,我們來看下GPUTexture的descriptor的定義:
dictionary GPUExtent3DDict {
required unsigned long width;
required unsigned long height;
required unsigned long depth;
};
typedef (sequence<unsigned long> or GPUExtent3DDict) GPUExtent3D;
dictionary GPUTextureDescriptor : GPUObjectDescriptorBase {
...
required GPUExtent3D size;
...
};
GPUTextureDescriptor的size屬性有width和height屬性,只要texture對應了屏幕大小,我們就能獲得屏幕“分辨率”的信息(depth設為1,因為分辨率只有寬、高,沒有深度)。
實現sample
我們對應到sample來看下。
打開webgpu-samplers->helloTriangleMSAA.ts文件。
代碼基本上與我們上篇文章學習的webgpu-samplers->helloTriangle.ts差不多,
我們看下創建render pipeline代碼
const sampleCount = 4;
const pipeline = device.createRenderPipeline({
...
sampleCount,
});
這里設置了sample count為4
我們看下frame函數->render pass descrptor代碼
const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: [{
attachment: attachment,
resolveTarget: swapChain.getCurrentTexture().createView(),
...
}],
};
- 設置attachment為多重采樣的texture的view
該texture的創建代碼為:
const texture = device.createTexture({
size: {
width: canvas.width,
height: canvas.height,
depth: 1,
},
sampleCount,
format: swapChainFormat,
usage: GPUTextureUsage.OUTPUT_ATTACHMENT,
});
const attachment = texture.createView();
注意:texture的sampleCount應該與render pipeline的sampleCount一樣,都是4
- 設置resolveTarget為swapChain對應的view
swapChain.getCurrentTexture()獲得的texture的大小是與屏幕相同,所以它包含了屏幕分辨率的信息。
參考資料
helloTriangleMSAA.ts
Investigation: Multisampled Render Targets and Resolve Operations