Unity 2017 Game Optimization 讀書筆記 Dynamic Graphics (5) Shader優化

Shader optimization

Fill Rate和 Memory Bandwidth開銷最大的地方就是Fragment Shader。開銷多大取決於Fragment Shader的複雜程度:多少紋理需要採樣,多少數學計算函數需要使用等等。GPU的並行特性意味着在線程中如果任何地方存在瓶頸,都會導致有大量fragments的渲染出現問題。

Shader編程和優化是遊戲開發中比較困難的一部分,因爲它很抽象,想要寫出高質量的Shader代碼要比寫普通的CPU的遊戲邏輯代碼差別很大。有時候還要需要些走後門的歪路子小技巧,比如提前計算數據,把數據存入紋理貼圖中。

很多開發者喜歡用可視化的工具編寫Shader,比如Shader Forge或者Amplify Shader Editor,但是它們生成的代碼有可能不是最高效的。無論我們用不用可視化工具編寫Shader,都應該來使用一些能夠優化Shader的技巧。

1.Consider using Shaders intended for mobile platforms

Unity的內置Shader中有Mobile的版本,這些版本的Shader效率更高,PC主機遊戲平臺也可以使用這些版本的Shader來減少開銷,但是要評估使用這些Shader導致的渲染效果下降是否可以接受。

2.Use small data types

數據類型越小,GPU計算更快。特別是移動平臺上。所以我們第一個方法就是將float(32位)替換成更小的類型,比如half(16位),甚至fixed(12位),越小的位數也就需要更少的計算量。

Color 數值是優化對象之一,通常減少一些精度效果上並沒有非常明顯的感知,但是也要權衡是否真的可以接受渲染上效果的下降。

要注意的是這個優化方法會根據GPU的架構甚至品牌都會有影響,優化的效果也會不盡相同,我們可能得需要進行不少測試來驗證優化效果。

3.Avoid changing precision while swizzling

Swizzling 是shader中從一個現有的vector通過我們想要的數據創建一個新的vector。一些例子比如:

用xyzw和rgba都可以,無論是個vector還是color,這種寫法都使得Shader代碼更容易理解。

在shader中進行精度的轉換是開銷很大的操作,在swizzling時進行數據精度的轉換開銷就更大了,所以我們要杜絕在在swizzling時進行數據精度的轉換,如果有這種需求,還不如一開始就使用告精度的數據。

4.Use GPU-optimized helper functions

Shader 編譯器通常會有效的優化數學運算,但是CG和Unity提供的庫函數相比自己寫的代碼大多數情況下還是效率高的多。當我們寫自定義的shader時候,可以找找庫裏有沒有已經有提供的,比如CG庫提供了abs(),step()等,Unity庫提供了 WorldSpaceViewDir()來計算攝像機朝向,Luminance()將Color轉成grayScale等。

CG庫函數:http://http.developer.nvidia.com/CgTutorial/cg_tutorial_appendix_e.html

Unity庫函數: http://docs.unity3d.com/Manual/SL-BuiltinIncludes.html.

5.Disable unnecessary features

去掉不必要的屬性可以節省性能,比如我們使用的Shader有沒有必要需要transparency, Z-writing, alpha-testing或者alpha blending?去掉這些設置或者屬性帶來的渲染效果的下降是否可以接受,如果可以的話這是一個降低Fill Rate開銷的好辦法。

6.Remove unnecessary input data

刪除掉Shader中沒用到的輸入數據,因爲它們會需要從內存中讀取,消耗性能。

7.Expose only necessary variables

把Shader中的變量在material中暴露出來是有開銷的,因爲GPU沒辦法將這些變量認爲是constant的,編譯器沒法按常量編譯它們,這些數據必須每次都從CPU傳入到GPU中。當然把變量暴露出來,調整效果調試的時候會比較麻煩,所以這條優化可以是在項目的後期再處理。

8.Reduce mathematical complexity

複雜的數學運算會導致渲染的瓶頸,因此我們要儘量避免。一種方法是提前計算好,把結果存入一張textrue中,之後在運行時可以將這個texture傳入Shader中然後採樣獲得結果從而代替複雜的運算。

對於sin()和cos()這些函數我們duck不必這麼做,因爲它們已經被GPU高度優化過,但是其他類似 pow(), exp(), log()和我們自己寫的函數就是可以優化簡化的對象。

這種技術會有額外的內存開銷去存儲texture,也會增加Memory Bandwidth的開銷,但是Shader沒采用這種技術前本來也需要textrue的話,如果textrue的alpha通道並沒有使用的話,我們就可以把結果存入alpha通道中,這種做法就非常nice,並不會有額外的開銷,唯一需要的就是程序和美術要配合起來,程序提供所需存的結果給美術生成texture。

9.Reduce texture sampling

Memory Bandwidth開銷最大的就是texture採樣。Texture使用的越少,texture越小,性能越好。Texture越多,cache命中率就會越低,Texture越大,memory Bandwidth就會消耗的越多。

10.Avoid conditional statements

現代的CPU中,對於條件判斷語句,CPU會嘗試進行預測,判斷最有可能運行的分支,提前用空閒的核心計算。等程序真正運行到的時候,如果發現決定錯誤了,就捨棄之前的運算而選擇另外的分支。這種技術提高了CPU的運行速度,因爲提前運算或者捨棄結果都要比等待決定哪個纔是正確的分支要快,而且絕大多數的時候,CPU的預測是正確的。

但是在GPU上,因爲處理是並行的,對於條件判斷語句,必須得決定多少核心運行這個分支,多少核心運行其他分支,直到所有分支都運行完。比如一個if-else判斷,GPU需要告訴一部分核心處理true的這條分支路徑,然後再去要求其他核心處理false的路徑,除非所有核心處理同樣的path,否則必須每次都要處理兩個path。因此我們應該儘量避免在shader中使用條件判斷和多分支,當然也取決於必要程度。對於像素級別的計算,相比於多分支的開銷,可能我們倒不如接受一些不必要的計算。

11.Reduce data dependencies

編譯器會儘可能的優化我們的Shader代碼,讓GPU處理起來更舒爽。下邊這個例子是對優化非常不友好的一段代碼:

上邊的代碼數據依賴非常嚴重,每一行的計算都依賴於上一行代碼計算的結果,這種寫法導致編譯器沒辦法優化這段代碼,因爲他們沒法在指令級別並行處理。下邊的代碼則解決了這個問題:

 這一次,編譯器識別出這段代碼可以在指令級線程並行處理,這可以大大提高執行速度。

12.Surface Shaders

Unity的Surface Shaders是一種簡化的Fragment Shaders,unity引擎會自動將Surface Shaders代碼轉換,但是也就讓我們少了很多優化的機會。Surface Shaders能做的事Fragment Shaders一定可以,但是Fragment Shader能做的Surface Shaders不一定可以。個人認爲是儘量不要用Surface Shaders。

13.Use Shader-based LOD

我們可以強制Unity在渲染遠距離的物體時使用更簡單的Shaders,這樣可以有效降低Fill Rate的開銷,特別是對於我們的目標是要支持多平臺或者非常廣的硬件範圍。LOD keyword可以在Shader中使用來設置當前支持的Shader,如果當前的shader和LOD level並不匹配,則會降低至其他備用Shader一直到有匹配的Shader,我們在運行時可以通過maximumLOD屬性來動態更改shader的LOD值。

這個特性和之前介紹過的mesh的LOD技術很相似, 可以查看Shader-based LOD 的文檔獲取更多信息。https://docs.unity3d.com/Manual/SL-ShaderLOD.html

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章