D3D11中的MSAA

effulgent發表於2013-08-29

    這兩年我的工作都轉到了D3D11,目前新出硬體幾乎全部支援此標準,加上D3D11介面清晰,概念直觀,等到windows7普及,想必未來都是D3D11的天下。最近時間較空,我陸續開始寫些基礎文章,希望對新學者有所幫助。但文章純屬我自己隨意寫寫,錯誤肯定很多,請大家多多包涵。 

    所謂MSAA,就是讓一個畫素可以同時儲存多個顏色,而最終的顯示結果由多個顏色重建而成。具體儲存顏色的數量由DXGI_SAMPLE_DESC中的Count來決定,其中的Quality則一般用來給硬體設計廠商作為非常規發揮的餘地,比如NVDIA CSAA開啟方式就是用Quality某些值來實現的。在D3D9中MSAA中即使一個畫素被分成多個子片段來光柵化,但實際上覆蓋此畫素的每個三角形依然只執行一次pixel shader,子片段的位置只用來決定各種頂點屬性的插值位置,以及進行覆蓋率評定,這就是MSAA相比SSAA的最大不同之處,MSAA只會增加被多個三角形同時非完全覆蓋時的計算率,而且不管其覆蓋率有多高,每個三角形都只執行一次Pixel shader,並將Pixel shader返回的值存入相關覆蓋子畫素。需要注意的是,這裡存在兩個細節問題,一是Pixel shader輸入的值如何插值而來(插值位置和插值演算法);二是子畫素到底位於畫素中的何處,個數如何決定,是否每個子畫素都對應一個color/z/stencil儲存,如何將所有這些儲存的子合成為最後的結果。對於第一個問題,就牽涉到MSAA光柵化規則的問題,注意如果沒有任何特殊設定,對應畫素Fragment的屬性插值操作都將在畫素中心位置上執行,MSAA中進行覆蓋率評定時,很有可能三角形並未覆蓋到畫素中心位置,這就牽扯到一個外部插值(extrapolate)的問題,即插值位置根本就不在三角形內,很顯然這樣插值出來的屬性結果是錯誤的,為了解決這個問題,D3D引入一個配置“centroid sample”,指定在rasterize時,相關屬性進行插值的位置必須位於三角形與畫素相交區域內,這個通常這個位置取在某個被覆蓋的子畫素位置,但並不保證永遠不變,可能和具體硬體設計還有關,D3D11 reference rasterizer選擇centroid sample位置的具體演算法,可參考D3D文件。在D3D11中想要開啟centroid sample,只需在對應pixel shader input attribute上加上centroid modifier即可;屬性的插值演算法,就是如何用三個頂點attribute值,以及中間點A的位置,使用某種演算法插值出attribute在中心點A上的值,D3D中最常用的就是帶透視矯正的線性插值(linear),所有attribute預設都使用此演算法插值(linear modifier),當然D3D11還提供其他幾種插值方式:nointerpolation(就是不插值,使用三角形中第一個頂點的屬性值作為Fragment屬性值),noperspective(不帶透視矯正的線性插值,只使用螢幕2D座標位置進行插值計算),sample(在每個子畫素位置進行插值)。這些modifier可以加在PS input attribute前面,不過使用起來還是有些限制和規則,比如centroid、sample明顯只能在MSAA模式下才能起作用,因為普通模式下不存在非中心覆蓋和子畫素位置問題;而centroid很顯然也不能同nointerpolation一起使用,更多資訊還請參考DX文件,畢竟知道這些背後原理後,更好記憶和理解這些限制。現在討論第二個問題,子畫素分佈在畫素區域中的位置是因硬體設計而變的,D3D標準並沒有規定具體分佈的位置,而個數按道理上來講就是DXGI_SAMPLE_DESC種的count所變數指定。是否每個子畫素都會在RT surface上有相應的儲存位置(color/z/stencil),這個就有點懸了,畢竟這個是要增加硬體成本的事,而且D3D標準也沒強制,硬體廠商說:OK,我可以給你指定的覆蓋點數,我也可以把這些點的位置進行精心設計分佈,但我不一定會給每個點都分配實際的儲存位置。比如CSAA就將子畫素數和實際儲存數分開來了,以此來節省儲存和頻寬,CSAA和16x實際上只有4個儲存位置(但它確實有16個子畫素),16個子畫素(覆蓋率判斷)如何分享4個儲存位置呢?答案是硬體設計有關。最後一個問題,每個畫素中儲存的多個值如何重建為最終結果?答案還是硬體設計有關,但我們可以自己resolve(http://mynameismjp.wordpress.com/2012/10/28/msaa-resolve-filters/)。

    在D3D11中是可以指定pixel shader進行per-sample excution的,這個和D3D9完全不同,在pixel shader input中指定SV_SampleIndex屬性或為屬性指定sample modifier都會開啟pixel shader逐子畫素執行(這個在CSAA中就有點問題了,因為CSAA並不為每個子畫素分配獨立的儲存)。MSAA並不由Pipeline中的一個stage完成,而牽涉到rasterization、pixel shader、output merger三個stage,D3D11對MSAA的操作進行了空前的增強,可以獲取sample index, coverage mask, sub pixel value, 以及pixel shader新支援的UAV,綜合這些我們可以完成一些很特別的演算法。需要注意的是用centroid sample或per sample execution後會帶來一個問題,就是GPU的某些地方的導數計算可能有誤,比如ddx ddy以及texture lod計算,因為三角形邊緣畫素的取樣位置會被偏置到某個sample的位置,而不再是畫素中心,這樣2x2畫素中,變數相差之後的值就不再是基於單位的螢幕空間座標了,這樣在三角形邊緣的畫素上計算變數的導數就會出現跳躍起伏,這樣會使ddx ddy的結果產生異常,所以要麼你能容忍或解決這個問題,要麼就不要在centroid sample的屬性上進行導數計算。

    pixel shader輸出Z會給MSAA帶來一些麻煩。如果pixel shader沒有開啟per sample exctution,但卻輸出了SV_Depth,這就產生一個問題,本來每個子畫素在depth stencil buffer中都會輸出各自獨立的Z值,此Z值為光柵化時插值產生,因此每個子畫素都有一個正確的Z值,但如果pixel shader人工輸出了Z,而這個pixel shader只執行一次,這樣被此三角形覆蓋的所有子畫素的Z值都將是這個單一值,此值為畫素中心的Z值(沒有開啟centroid sample的情況下),這就會導致一個問題,所有先繪製了更近三角形的邊緣畫素都可能失去或產生錯誤的抗鋸齒效果!(特別是在三角形連續交界處)請看下圖,繪製順序為紅、藍、綠,這些幾何體的pixel shader都輸出了SV_Depth。請注意某些邊緣已經失去了抗鋸齒效果。

另外D3D10引入一個新的概念ALPHA-TO-COVERAGE,以及一個SV_Coverage的pixel shader輸出變數。註定要把MSAA玩出花來了!以8x的MSAA為例,在z/stencil/color buffer上每個畫素均有8個子畫素,如果開啟了ALPHA-TO-COVERAGE,pixel shader輸出的ALPHA值會被轉為一個8階的值,表示此Fragment在畫素上的mask,這個主要是用來解決Alpha Test邊緣鋸齒問題,其原理就是將光柵化階段產生的MASK A,AND ALPHA轉化的MASK B,AND SV_Coverage MASK C。看下面的例子,三塊完全重疊的面片,開啟Alpha-To-Coverage,並且都輸出0.5的Alpha值,從近到遠分別為紅、綠、藍,發現完全不會有互相半透的效果,原因很簡單,本例開的是8x msaa,0.5的ALPHA會被GPU轉化為00001111B的MASK,紅綠藍三個Mesh都輸出相同MASK的話,子畫素的值會被最近的Mesh覆蓋掉。

我們修改下輸出的Alpha值,紅色0.25,綠色0.5,藍色0.75,當紅綠藍視距從近到遠排列時,輸出結果如下:

很簡單,因為紅色的MASK為00000011B,綠色為00001111B,藍色為00111111B,互相重疊的部分,近的顏色將佔據MASK相對應的子畫素,較遠的會被覆蓋掉。如果我們再反過來看,讓紅綠藍視距變為從遠到近排列,結果就變成這樣了:

原因大家可以自己分析。綜上,Alpha-To-Coverage註定是個悲催的OIT技術!

更多MSAA資料

http://mynameismjp.wordpress.com/2012/10/24/msaa-overview/

相關文章