你們好,本文學習MSAA以及在WebGPU中的實現。html
上一篇博文
WebGPU學習(二): 學習「繪製一個三角形」示例git
下一篇博文
WebGPU學習(四):Alpha To Coveragegithub
MSAA(多重採樣抗鋸齒),是硬件實現的抗鋸齒技術web
參考深刻剖析MSAA :canvas
具體到實時渲染領域中,走樣有如下三種:
1.幾何體走樣(幾何物體的邊緣有鋸齒),幾何走樣因爲對幾何邊緣採樣不足致使。
2.着色走樣,因爲對着色器中着色公式(渲染方程)採樣不足致使。比較明顯的現象就是高光閃爍。
3.時間走樣,主要是對高速運動的物體採樣不足致使。好比遊戲中播放的動畫發生跳變等。ide
這裏討論幾何體走樣。
函數
如上圖所示,咱們要繪製一個三角形。它由三個頂點組成,紅線範圍內的三角形是片元primitive覆蓋的區域。
primitive會被光柵化爲fragment,而一個fragment最終對應屏幕上的一個像素,如圖中的小方塊所示。學習
gpu會根據像素中心的採樣點是否被pritimive覆蓋來判斷是否生成該fragment和執行對應的fragment shader。動畫
圖中紅色的點是被覆蓋的採樣點,它所在的像素會被渲染。ui
下圖是最終渲染的結果,咱們看到三角形邊緣產生了鋸齒:
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
有下面幾個要點:
目前我沒找到查詢的方法,但至少支持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.
dictionary GPURenderPipelineDescriptor : GPUPipelineDescriptorBase { ... unsigned long sampleCount = 1; ... };
dictionary GPUTextureDescriptor : GPUObjectDescriptorBase { ... unsigned long sampleCount = 1; ... };
咱們在WebGPU 規範中看到render pipeline descriptor和texture descriptor能夠設置sampleCount。
在「解析」步驟中,須要從新採樣到指定的分辨率:
過採樣的信號必須從新採樣到指定的分辨率,這樣咱們才能夠顯示它
因此咱們應該設置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來看下。
打開webgpu-samplers->helloTriangleMSAA.ts文件。
代碼基本上與咱們上篇文章學習的webgpu-samplers->helloTriangle.ts差很少,
const sampleCount = 4; const pipeline = device.createRenderPipeline({ ... sampleCount, });
這裏設置了sample count爲4
const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [{ attachment: attachment, resolveTarget: swapChain.getCurrentTexture().createView(), ... }], };
該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
swapChain.getCurrentTexture()得到的texture的大小是與屏幕相同,因此它包含了屏幕分辨率的信息。
helloTriangleMSAA.ts
Investigation: Multisampled Render Targets and Resolve Operations