圖形管線之旅 Part6

原文:《A trip through the Graphics Pipeline 2011》
翻譯:往昔之劍
 
轉載請註明出處
 
歡迎回來。此次咱們去看看三角形的光柵化。但在光柵化三角形以前,咱們須要執行三角形設置,而且在設置三角形以前,我還要解釋一下爲了什麼作的準備,咱們來聊聊三角形硬件光柵化算法。
 
如何畫一個三角形
 
首先,給很熟悉這部分並本身寫過優化的軟紋理映射的人一點小提示:三角形光柵器一次要處理一堆東西:跟蹤三角形的形狀,插值出座標u和v(對於透視矯正映射,是u/z,v/z和1/z),執行Z緩衝測試(對於透視矯正映射,能夠用1/z緩衝替代),而後處理實際的紋理(還有着色),以上步驟都在一個安排好可用寄存器的大循環裏。在硬件中,這些東西都被打包成很整齊的小模塊,這便於設計以及獨立測試。硬件中的「三角形光柵器」是告訴你三角形覆蓋了哪些像素的塊;某些狀況下,它也會給出三角形中這些像素的重心座標。但僅此而已。不只沒有給出u和v, 甚至沒有1/z。固然也沒有紋理和着色,但經過使用專用的紋理和shader單元這些都不是個事。
 
其次,若是你寫過本身的三角形映射器,你就可能會用過像Chris Hecker這種透視紋理映射的增量式掃描線光柵算法。在沒有SIMD單元的處理器上這是一個很是好的方法,可是它對於擁有高速SIMD單元的現代處理器並不很適合,對於硬件甚至更糟糕。就像是放在角落裏的過期了的遊戲主機,如今根本沒人感興趣了。就比如是三角形光柵器對於屏幕底部和右側的邊的保護帶裁剪很是快,而對於頂部和左側的邊就沒這麼快了。只是打個比方而已。
 
那麼,對硬件來講這個算法到底哪裏很差?首先,它確實是經過逐條掃描線來光柵化三角形。當進行像素着色時就出現了問題,咱們想要光柵器輸出成組的2x2個像素點(所謂的「 方塊quads」——不要與「四邊形quad」圖元相混淆,quad圖元在管線中被分解爲一對三角形)。由於咱們不只要並行的運行兩個「實例instances」,還要從他們各自的掃描線上的第一個像素開始繪製,它們可能離的很遠而致使不能很好的生成咱們想要的2x2的塊,這就是掃描線算法的尷尬之處。並且很難高效的並行化,在x和y的方向上不對稱——這意味着畫一個寬8像素高100像素的三角形與一個寬100像素高8像素的三角程度是大相徑庭的。如今得讓「x」和「y」的步進「循環」一樣快來避免瓶頸——但咱們要是在「y」的步進上執行全部工做,那「x」的循環就不重要了!這就有點麻煩了。
 
更好的方法
 
在1988年Pineda的  論文裏提到了一個很是簡單(對硬件更加友好)的渲染三角形的方法。這個方法能夠歸結爲兩句話:到直線的符號距離能夠經過2D點積來計算(相乘再相加)——就像到平面的符號距離能夠經過3D點擊來計算同樣。以及三角形本質上能夠被定義爲三條邊正確側面上的全部點的集合。因此只用遍歷全部像素的座標而且測試他們是否在三角形裏就好了。這就是最基本的算法。
 
注意,好比當咱們移動一個像素到右邊,咱們在X上加上一個數並同時保持Y不變。咱們的邊的公式有以下形式:
a,b,c是三角形常量,因此對於X+1就是:
換句話說,一旦獲得邊的公式在已知點上的值,對於鄰接像素的值僅做一些相加就可得出。還要注意,這很容易並行化:好比像AMD的硬件一次能夠光柵化8x8=64像素(或是Xbox360,參考《Real-time Rendering》第三版)。你只用計算  其中 。一次計算每一個三角形(和邊)並保存在寄存器中。而後只需 計算左上角的三邊公式,執行8x8次並行相加咱們計算過的常量,來光柵化一個8x8的像素塊,以後測試結果符號位來斷定每一個8x8像素是在邊的內部仍是外部。這樣計算三條邊,很是快,一個8x8的 三角形光柵塊很適合並行化的方式,而且除了作大量的整數加法操做就沒什麼更復雜的了!這就是爲何在上一部分裏要對齊到定點(fixed-point)網格——這樣咱們就能夠在這用整數運算了。整數累加器比浮點運算單元可簡單多了。固然咱們能夠選擇累加器的寬度來恰好支持咱們想要的視口大小,有足夠的子像素精度,以及大概2~4倍的合適尺寸的保護帶。
 
順帶一提,這裏還有一個棘手點,就是填充規則:你須要保證任何一對共用一條邊的三角形,共用的邊附近沒有像素被漏掉或者被光柵化兩次。D3D和OpenGL都使用所謂的「左上角」填充規則;具體細節在各自的用戶手冊上有解釋。我就不在這裏贅述了,不過要注意這種整數光柵器,在三角形設置過程當中從一些邊的常數項中減去1。使其保證不會出現問題—— 相比較,Chris在他的文章中的作法就適用這項工做了。兩種方法結合起來就很棒了。
 
仍然存在一個問題:咱們如何找到要測試哪些8x8的像素塊呢?Pineda提出了兩種策略:1)只掃描整個三角形包圍盒,或者2)一個更聰明的方案:一旦沒有命中任何三角形採樣點,就中止反覆了。好吧,若是一次只測試一點像素點是沒有問題的。可是咱們如今要處理8x8個像素!同時執行64次並行相加,最後卻發現沒命中任何像素,太浪費了。因此,千萬別這麼幹。
 
咱們這裏須要的是更多的層級
 
我剛纔講的是適合光柵器工做 (實際輸出的採樣量)的方式。爲了不像素級上的多餘工做,咱們應該在它以前添加另外一個光柵器,這個光柵器不把三角形 光柵化成像素,只是將8x8像素塊分紅tiles(McCormack和McNamara的  論文中有一些詳細內容,以及Greene的「  Hierarchical Polygon Tiling with Coverage Masks」的結論中用到了這個想法)。光柵化邊的方程到覆蓋的tile的工做很相似於光柵化像素;咱們要作的是按照邊的方程計算整個tile的上下邊界;由於方程是線性的,因此極值是在tile的邊界上——實際上,能夠循環4個拐角點,從公式中a和b因數的符號能夠判斷出是哪一個拐角。底部的線相比之下計算量就很小了,也須要一樣的層級——一些並行的整數累加器。若是要估算tile一個拐角的邊方程,不如傳到細粒度光柵器中執行:每一個8x8的塊須要一個參考值,還記得嗎?
 
因此要先執行一次粗粒度光柵化來獲得可能被三角形覆蓋的tiles,這個光柵器能夠作的小一點(8x8都足夠用了),它不須要速度很是快(由於它只用來執行每一個8x8的塊),在這個層次,找到空的塊的開銷是比較小的。
 
能夠參考Greene的論文和Mike Abrash的 《Rasterization on Larrabee》,實現一個完整的層級光柵器。但對於硬件光柵器來講:實際上增長了一些對小三角形的處理工做(除非你能夠跳過層次級別,但硬件數據流不是那樣設計的),若是三角形很是大,要作大量的光柵化工做。這種架構下生成像素位置很是快,比Shader單元的處理速度要快。
 
然而,實際的問題不是處理大三角形:它們對於任何算法都頗有效(固然包括掃描線光柵算法)。主要的問題在於小三角形。假若有一堆生成0或1個可見像素的小三角形,也須要執行三角形設置(立刻就要講到了),對於8x8的塊至少要執行一步粗粒度光柵化和一步細粒度光柵化。小三角形很容易執行三角形設置,以及粗粒度光柵化邊界。
 
須要注意的是,這種算法對於薄片形(又長又窄的三角形)是開銷很大的——你得遍歷大量的tiles,卻只能獲得不多的覆蓋像素。因此這種狀況很是慢,要儘量的避免。
 
三角形設置階段都作了什麼?
 
我已經講過了三角形的光柵化算法,在三角形設置過程當中,僅須要看一下每條邊使用的常量:
 
  • 邊方程中的三角形三條邊a, b, c。
  • 以前提到的一些派生值;若是不是要加上另外一個值的話,通常不會將8x8的矩陣所有存儲進硬件裏。最好的方法就是隻在硬件中計算,使用進位保留累加器(又名3:2 reducer,我以前寫到過)來減小單獨的和公式計算,而後完成常規加法。
  • 參考獲取tile的四個角的方法來獲取邊方程的上下邊界作粗粒度光柵化。
  • 在第一個粗粒度光柵化的參考點上,邊方程的初始值(調整填充規則)。
 
……這些就是三角形設置階段要作的計算。它能夠歸結爲用於邊方程的幾個大整數的乘法計算,以及它們的初始賦值,一些步進值的乘法計算,還有一些低開銷的其它邏輯計算。
 
其它光柵化問題和像素輸出
 
有一件事到目前尚未提,那就是裁剪矩形(scissor rect)。這只是一個屏幕對齊的矩形掩碼像素。光柵器不會生成矩形以外的像素。這至關容易實現——粗粒度光柵器能夠直接拒毫不與scissor rect重疊的tiles,而且細粒度光柵器 將經過「光柵化」的scissor rect的覆蓋像素掩碼進行AND邏輯與運算(此處的「光柵化」指的是逐行逐列的整數比較,以及一些位的AND運算)。
 
還有一個問題是多重抗鋸齒。如今最大的挑戰是須要測試每一個像素的多個採樣點——DX11中硬件須要至少支持8x MSAA。注意,每一個像素中的採樣位置不是在規則的網格里(這對於近似水平或近似垂直的邊效果很很差),但大多數方向的邊均可以獲得不錯的結果。這些不規則的採樣位置是掃描線光柵算法的致命點(這是不使用它們的另外一個緣由!),但卻很容易支持Pineda-style算法:即在三角形設置階段計算每一個邊上的一些偏移量,而後對每一個像素上的這些偏移量進行並行 相加和測試符號,來替代只計算一個點的方法。
 
好比說4x MSAA,在一個8x8的光柵器上能夠作兩件事情:能夠將每一個採樣點看成是一個特別的「像素」,它表示有效的tile大小是4x4個實際的屏幕像素,細粒度光柵格中的每一個塊有2x2個位置對應一個「像素」,或者能夠用8x8個實際像素運行4次。8x8彷佛有點大了,我假設AMD是這種工做方式,其它的MSAA也都差很少。
 
不管如何,咱們如今獲得了一個細粒度的光柵器,它能夠給出每一個塊上的8x8塊的位置加上覆蓋區域的掩碼。很是好,不過這只是故事的一半——當今的硬件在執行pixel shader以前還要執行early Z和hierarchica Z測試,實際的光柵化與Z處理過程是交織在一塊兒的。但最好分開來說;因此在下一部分裏,將會講多種Z處理過程,Z比較,以及一些三角形設置——就是咱們剛剛將的光柵化設置,但還有多個Z和像素着色的內插值,它們也須要在以前進行設置。
 
注意事項
 
我把一些我認爲有表明性的光柵化算法聯繫到了一塊兒(這些在網上都有資料)。還有一些我沒嘗試過的算法都在這給出了介紹;恐怕這塊內容寫的有點複雜了。
 
本文假設爲使用高端PC硬件平臺。在大多數領域,特別是移動/嵌入式中,被稱爲tile渲染器,屏幕被分紅若干tiles單獨渲染。這和我講過的8x8tile光柵化有所不一樣。基於tile的渲染器還至少須要一個很是粗粒度的光柵化階段,它會預先找到被每一個三角形覆蓋的大塊的tile;這個階段一般被稱爲「裝箱(Binning)」。基於tile的渲染器的工做方式有所不一樣,它相比「後排序(sort-last)」架構有不一樣的設計參數。講完D3D11的管線,我有可能會用一到兩篇文章講一下基於tile的渲染器(若是感興趣的話),可是如今先忽略它們,好比在經常使用的智能手機上的PowerVR芯片,它的處理方式是有些不一樣的。
 
在8x8的塊中(其它尺寸的塊也有一樣的問題),當三角形小於必定尺寸或者是不合適的比例時,須要作大量的光柵化工做,而且在處理過程當中會獲得很糟糕的效果。我很想告訴給你一個神奇的易於並行化的算法,不過我不知道,一些硬件廠商也作的不是很好。因此就目前而言,這些都是硬件光柵化的難題。或許將來會有一個不錯的解決方案。
 
我講到的「邊方程的下邊界」適合於粗粒度光柵化,可是在某些狀況下會出現錯誤(即須要在不覆蓋任何像素的塊中執行細粒度光柵化)。是有技巧減小這種狀況的,但檢測這些特殊狀況比起在不覆蓋任何像素的塊中執行光柵化每每開銷更大。這也是一種權衡。
 
在光柵化過程當中用到的塊一般都是固定在一個網格上的(下一篇會講的更詳細)。若是一個三角形覆蓋的兩個像素跨過了兩個tile,就得光柵化兩個8x8的塊。這是很是低效的。
 
以上內容看似簡單,但並不完美,實際的三角形光柵化是達不到理論峯值的(理論上老是假設全部的塊都被所有填充)。請記住這一點。
相關文章
相關標籤/搜索