D3D11中的MSAA


    這兩年我的工作都轉到了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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM