Optimize Triangle Mesh Vertex
僅供個人學習使用,請勿轉載,勿用於任何商業用途。
對同一個triangle mesh,用TriangleList還是TriangleStrip渲染比較快,快在哪裡?大部分人都會不加思索的認為strip比較快,原因在於strip有更好的緩衝。這樣的回答其實是完全錯誤的。對以相同方式排列的mesh來說,strip唯一的優點只在於需要的索引比較少,對n個三角形只需要n+2個索引,而list需要n*3個。雖然較少的資料意味著更低的頻寬佔用,但實際情況下,卻對渲染影響不大,無論list和strip,索引本身的資料量就不大(與vertex相比),因此微小的優勢幾乎很難察覺出來。
那麼對同一個mesh,不同的渲染方法會不會有差別,如果有,差別在哪裡呢?答案顯然是肯定的,否則就不會有這篇文章的存在。真正決定一個mesh渲染效率的是其索引相片順序或者說三角形排列順序,優秀的相片順序可以明顯減少頂點和像素處理的操作。
這就要從硬體的構架上說起了,顯卡上除了眾所周知的顯存外,在GPU晶片上還有一塊非常快的緩衝,根據資源類型的不同,這塊緩衝又分為紋理緩衝,頂點緩衝等等。這裡我們主要關心頂點緩衝。是nvidia G80構架之前的GPU流水線:
可以看到頂點緩衝有兩塊(vCache),一塊稱為pre-T&L cache另一塊稱為Post-T&L Cache。Pre-cache中儲存了待處理的頂點,post cache中儲存了經過變換(也就是Vertex shader之後)的頂點。正是由於這兩塊緩衝的存在,才讓最佳化成為了可能。兩塊cache都是FIFO的隊列,假設要渲染如所示的三角形mesh:
頂點索引為:
1,2,15|15,2,16|2,3,16|16,3,17 ………… 15,16,29|29,16,30………………..
vcache的狀態如下:
1,2,15 ---> 1,2,15,16 ---> 1,2,15,16,3----> 1,2,15,16,3,17 ---> …………--->xx,xx,xx,15,16,29 ----> xx,xx,15,16,29,30--->…………….
同一個頂點只會在vcache中出現一次,當渲染第二個三角形時,頂點15和2已經在pre-cache中, GPU不需要對他們做進一步尋找,只需請求把頂點16載入到pre-cache中,降低了頻寬需求,減小了vertex fetch的延遲。這還不是最重要的,更大的加速在於當GPU準備處理頂點15和2時,發現這2個頂點已經存在於post-cache中,於是它會完全跳過vertex shader的計算,直接使用post-cache中的結果!通過對封閉mesh的分析可以發現通常一個頂點會被5~6個三角形共用(比中的頂點16),這意味著在最理想的索引方式下,這個頂點只會被處理一次,當然最糟的情況則是6次,兩者之間有巨大的差別。由於vcache容量的限制,可以看到當渲染第二排三角形時,頂點15,16已經被彈出了緩衝,因此GPU將進行重複計算。
我們已經知道了vcache的原理,如何充分利用他計算出最優的三角形索引順序呢?以上面的規則三角形網格為例,理想的索引應該是這樣的:
1,2,3|4,5,6|7,8,9|10,11,12|13,14,14| + |1,2,15|15,2,16|2,3,16|………………………
為了方便討論,我把索引寫成了2段。如果你細心的話就會發現第一段實際上是一系列退化三角形,他們並不會產生任何圖形,但恰好把第一排的頂點全部放入了緩衝中。當渲染實際三角形時,每一排的頂點將依次載入到vcache中:
vcache state after degenerate triangle:1,2,3,4,5,6,7,8,9,10,11,12,13,14;
vcache state after first line of triangle:1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23……….29;
vcache state after second line:15,16,17,18,19,20,21……….29,30,31,32……..44
顯然,這就是最理想的索引順序,所有頂點都被完全複用。不過實際情況要複雜的多,顯然,這裡假設vcache至少能容納28個頂點,此外這個演算法嚴重依賴於vcache大小,最後,這個演算法只對規則mesh比較有效(非常適合於地形J)。對於第一個問題,在dx9上可以通過IDirect3DQuery9查詢VCACHE獲得實際vcache的大小,不過ati的卡從來不支援這個查詢。對於dx10來說,在beta的時候有一個類似查詢函數,可惜在後面的版本中刪除了。不過我們可以確定的是對於nvidia Geforce 4~7系列的顯卡,vcache大小至少為24,8系列至少為32。而對於ati來說,除dx10層級的卡以外,大部分的vcache只有14:( 。顯然,對於vcache只有24的情況來說,上面網格的索引計算就變得有些複雜了,一種可行的方法是把網格分為兩個子列來渲染,對於複雜模型來說,前人們已經發明了很多優秀的演算法Hoppe的研究成果產生了dx中optimizeMesh這樣的函數,Forsyth還發明出了一種不依賴於vcache大小的演算法。最後由於triangle list索引本身的靈活性,更容易排列出最優的三角形順序。
上面只討論了mesh最佳化的一方面,稱為頂點級最佳化,還有一種近年來伴隨early-z出現的像素級最佳化。不過由於這種演算法本身比較複雜,這裡只介紹基本原理。對於同一個mesh來說,假設有a,b兩個朝向相同,但是相互遮擋的面,假設a擋住b,那麼在渲染時,如果先渲染a,就可以讓b所產生的像素被early-z剔除,避免無用的pixel shader計算。
頂點最佳化的作用和意義是明顯的,可以在基本不修改現有程式的情況下,得到顯著性能提升。那麼我們應該如果最佳化呢?幸運的是已經有這樣的最佳化工具了,前面提到的OptimizeMesh以及nvidia的NvTriStrip工具都實現了頂點級的最佳化,而ati的Tootle工具不但實現了頂點最佳化,還採用了一些更先進的演算法進行模型遮擋最佳化。
最後需要說明的是上面的討論大部分基於dx9層級的硬體構架,對於dx10層級的顯卡來說,由於統一構架帶來的優點,硬體本身有更好的緩衝機制,但無論如何,在模型預先處理過程中,最佳化頂點都是非常值得的。回到開頭的問題,其實無論list還是strip,索引儲存方式本身並不會帶來很大區別,而索引本身的組織方式才是關鍵,由於list更容易最佳化,所以對現代硬體來說,最佳化之後的list通常更快。