1. 程式人生 > >Unity Shader入門精要學習筆記 - 第16章 Unity中的渲染優化技術

Unity Shader入門精要學習筆記 - 第16章 Unity中的渲染優化技術

也會 檢測 特點 著色器 版本 切換 代碼優化 學習 順序

轉自馮樂樂的 《Unity Shader 入門精要》

移動平臺的特點

為了盡可能一處那些隱藏的表面,減少overdraw(即一個像素被繪制多次),PowerVR芯片(通常用於ios設備和某些Android設備)使用了基於瓦片的延遲渲染(TBDR)架構,把所有的渲染圖像裝入一個個瓦片中,再由硬件找到可見的片元,而只有這些可見片元才會執行片元著色器。另一些基於瓦片的GPU架構,如Adreno(高通的芯片)和Mali(ARM的芯片)則會適應early-Z 或相似的技術進行一個低精度的深度檢測,來剔除那些不需要渲染的片元。還有一些GPU,如Tegra(英偉達的芯片),則使用了傳統的架構設計,因此在這些設備上,overdraw更可能造成性能的瓶頸。

影響性能的因素

我們可以把造成遊戲性能瓶頸的主要原因分成以下幾個方面

1)CPU

過多的draw call

復雜的腳本或者物理模擬

2)GPU

頂點處理(過多的頂點、過多的逐頂點計算)

片元處理(過多的片元、過多的逐片元計算)

3)帶寬

使用了尺寸很大且未壓縮的紋理

分辨率過高的幀緩存

對於CPU來說,限制它的主要是每一幀的draw call 的數目。draw call ,簡單來說,就是CPU在每次通知GPU進行渲染之前,都需要提前準備好頂點數據,然後調用一系列API把它們放到GPU可以訪問到的指定位置,最後,調用一個繪制命令。而調用繪制命令時,就會產生一個draw call 。過多的draw call 會造成CPU的性能瓶頸,這是因為每次調用draw call 時,CPU往往都是需要改變很多渲染狀態的設置,而這些操作是非常耗時的。當然,其他原因也可能造成CPU瓶頸,例如物理、不了模擬、蒙皮、粒子模擬等,這些都是計算了很大的操作。

對於GPU來說,它負責整個渲染流水線。它從處理CPU傳遞過來的模型數據開始,進行頂點著色器、片元著色器等一系列工作,最後輸出屏幕上的每個像素。因此,GPU的性能瓶頸和需要處理的頂點數目、屏幕分辨率、顯存等因素有關。

我們後面涉及的優化技術有

1)CPU優化

使用批處理技術減少draw call 數目。

2)GPU優化

減少需要處理的頂點數目(優化幾何體、使用模型的LOD技術、使用模型的LOD技術、使用遮擋剔除技術)

減少需要處理的片元數目(控制繪制順序、警惕透明物體、減少實時光照)

減少計算復雜度(使用Shader的LOD技術、代碼方面優化)

3)節省內存帶寬

減少紋理大小

利用分辨率縮放

使用模型的LOD技術 Unity中的渲染分析工具
Unity內置了一些工具,來幫助我們方便地查看和渲染相關的各個統計數據。這些數據可以幫助我們分析遊戲渲染性能,從而更有針對性地進行優化。 Unity5 提供了一個全新的窗口,即渲染統計窗口來顯示當前遊戲的各個渲染統計變量,可以通過在Game視圖右上方的菜單中單擊Stats按鈕來打開它,如下圖所示。 技術分享 從上圖可以看出,渲染統計窗口包含了音頻、圖像和網格3個方面信息。 渲染統計窗口顯示了很多重要的渲染數據。下表給出了渲染統計窗口中顯示的各個信息。 技術分享 技術分享 Unity 5 的渲染統計窗口相較於之前版本中的有了一些變化,最明顯的區別之一就是去掉了draw call 數目的顯示,而添加了批處理數目的顯示。Bathes 和 Saved by batching 更容易讓開發者理解批處理的優化結果。當然,我們想要查看draw call 的數目等其他更加詳細的數據,可以通過Unity編輯器的性能分析器來查看。 我們可以通過單擊Window -> Profiler 來打開Unity 的性能分析器。性能分析器中的渲染區域提供了更多關於渲染的統計信息,下圖給出了渲染分析結果。 技術分享 性能分析器顯示了絕大部分在渲染統計窗口中提供的信息,例如,綠線顯示了批處理數目、藍線顯示了Pass數目等,同時還給出了許多其他非常有用的信息,例如,draw call 數目、動態批處理/靜態批處理的數目、渲染紋理的數目和內存占用等。 我們可以通過Window -> Frame Debugger 打開幀調試器,如下圖顯示。 技術分享 幀調試器的調試面板上顯示了渲染這一幀所需要的所有的渲染事件,在本例中,事件數目為14,其中包含了10個 draw call 事件(其他渲染事件多為清空緩存等)。單擊面板上的每個事件,我們可以在Game視圖查看該事件的繪制結果,同時渲染統計面板上的數據也會顯示成截止到當前事件為止的各個渲染統計數據。 減少drawcall 數目 我們最常看到的優化技術大概就是批處理了。批處理的實現原理就是為了減少每一幀需要的draw call數目。為了把一個對象渲染到屏幕上,CPU需要檢查哪些光源影響了該物體,綁定Shader 被設置它的參數,再把渲染名利發送給GPU。當場景中包含了大量對象時,這些操作就會非常耗時。一個極端的例子是,如果我們需要渲染一千個三角形,把它們按一千個單獨的網格進行渲染所花費的時間要遠遠大於渲染一個包含了一千個三角形的網格。在這兩種情況下,GPU的性能消耗其實並沒有多大的區別,但CPU的draw call數目就會成為性能瓶頸。因此,批處理的思想很簡單,就是在每次面對draw call 時盡可能多地處理多個物體。 使用同一個材質的物體可以一起處理。對於使用同一個材質的物體,它們之間的不同僅僅在於頂點數據的差別。我們可以把這些頂點數據合並在一起,再一起發送給GPU,就可以完成一次批處理。 Unity中支持兩種批處理方式:一種是動態批處理,一種是靜態批處理。對於動態批處理來說,有點是一切處理都是Unity自動完成的,不需要我們自己做任何操作,而且物體是可以移動的,但缺點是,限制很多,可能一不小心就會破壞了這種機制,導致Unity無法動態批處理一些使用了相同材質的物體。而對於靜態批處理來說,它的優點是自由度很高,限制很少;但缺點是可能會占用更多的內存,而且經過靜態批出裏的所有物體都不可以再移動了。 動態批處理的原理是,每一幀把可以進行批處理的模型網格進行合並,再把合並後模型數據傳遞給GPU,然後使用同一個材質對其渲染。處理實現方便,動態批處理的另一個好處是,經過批處理的物體仍然可以移動,這是由於在處理每幀時Unity都會重新合並一次網格。 雖然Unity 的動態批處理不需要我們進行任何額外工作,但只有滿足條件的模型和才會可以被動態批處理。需要註意的限制有: 1)能夠進行動態批處理的網格頂點屬性規模要小於900.例如,如果Shader中需要使用頂點位置、法線和紋理坐標這3個頂點屬性,那麽想要讓模型能夠被動態批處理,它的頂點數目不能超過300。需要註意的是,這個數字未來有可能會發生變化,因此不要依賴這個數據。 2)一般來說,所有對象都需要使用同一個縮放尺度。一個例外的情況是,如果所有的物體都使用了不同的非統一縮放,那麽它們也是可以被動態批處理的。但在Unity 5 中,這種對模型縮放的限制已經不存在了。 3)使用光照紋理的物體需要格外小心處理。這些物體需要額外的渲染參數,例如,在光照紋理上的索引、偏移量和縮放信息等。因此,為了讓這些物體可以被動態批處理,我們需要保證它們指向光照紋理中的同一個位置。 4)多Pass的Shader會中斷批處理。在前向渲染中,我們有時需要使用額外的Pass來為模型添加更多的光照效果,但這樣一來模型就會被動態批處理了。 我們有個這樣一個場景,包含了3個立方體,它們使用一個材質,同時還包含了一個使用其他材質的球體。場景中還包含了一個平行光,但我們關閉了它的陰影效果,以避免陰影計算對批處理數目的影響。這樣一個場景的渲染統計數據如下圖所示。 技術分享 從上圖可以看出,要渲染這樣一個包含了4個物體的場景需要兩個批處理。其中,一個批處理用於繪制經過動態批處理合並後的3個立方體網格,另一個批處理用於繪制球體。我們可以從Save by batching 看出批處理幫我們節省了兩個draw call。 現在,我們再向場景中添加一個點光源,並調整它的位置使它可以照亮場景中的4個物體。由於場景中的物體都使用了多個Pass的Shader,因此,點光源會對它們產生光照影響。下圖給出了添加點光源後的渲染統計數據。 技術分享 從上圖可以看出,渲染一幀所需的批處理數目增大到了8,而Save by batching 的數目也變成了。需要註意的是,只有物體在點光源影響範圍內,Unity才會調用額外的Pass來處理它。因此,如果場景中點光源距離物體很遠,那麽它們仍然會被動態批處理的。 動態批處理的限制條件比較多,例如很多時候,我們的模型數據往往會超過900的頂點限制。這時我們可以使用靜態批處理。 相對於動態批處理來說,靜態批處理適用於任何大小的幾何模型。它的實現原理是,只在運行開始階段,把需要進行靜態批處理的模型合並到一個新的網格結構中,這意味著這些模型不可以再運行時刻被移動。但由於它只需要進行一次合並操作,因此,比動態批處理更加高效。缺點在於,它往往需要占用更多的內存來存儲合並後的幾何結構。這是因為,如果在靜態批處理前一些物體共享了相同的網格,那麽在內存中每一個物體都會對應一個該網格的復制品,即一個網格會變成多個網格再發送給GPU。如果這類使用同一網格的對象很多,那麽這就會成為一個性能瓶頸了。例如,在一個使用了1000個相同樹模型的森林中使用靜態批處理,那麽,就會多使用1000倍的內存,這會造成嚴重的內存影響。這時候,解決方法是要麽忍受,要麽使用動態批處理,要麽自己編寫批處理方法。 靜態批處理的實現非常簡單,只需要把物體面板上的static 復選框勾選上即可(實際上我們只需要勾選Batching Static),如下圖所示。 技術分享 在內部實現上,Unity首先把這些靜態物體變換到世界空間下,然後為它們構建一個更大頂點和所有緩存。對於使用了同一個材質的物體,Unity只需要調用一個draw call 就可以繪制全部物體。而對於使用了不同材質的物體,靜態批處理同樣可以提升渲染性能。經過這些物體仍然需要調用多個draw call。但靜態批處理可以減少這些draw call之間的狀態切換,而這些切換往往是費時的操作。 從之前的內容可以看出,無論是動態批處理還是靜態批處理,都要求模型之間需要共享同一個材質。但不同的模型之間總會需要有不同的渲染屬性,我們需要一些策略來盡可能地合並材質。 如果兩個材質之間只有使用的紋理不同,我們可以把這些紋理合並到一張更大的紋理中,這張更大的紋理被稱為是一張圖集。一旦使用了同一張紋理,我們就可以使用同一個材質,再使用不同的采樣坐標對紋理采樣即可。 但有時,出了紋理不同外,不同的物體在材質上還有一些微小的參數變化,例如,顏色不同、某些浮點屬性不同。但是不管是動態批處理還是靜態批處理,它們的前提都是要使用一個材質。是同一個,而不是使用了同一種Shader 的材質,也就是說它們指向的材質必須是同一個實體。這意味著,只要我們調整了參數,就會影響到所有使用這個材質的對象。那麽想要微小的調整怎麽辦?一種常用的方法就是使用網格的頂點數據來存儲這些參數。 前面說過,經過批處理後的我退會被處理成更大的VBO發送給GPU,VBO中的數據可以作為輸入傳遞給頂點著色器,因此,我們可以巧妙地對VBO中的數據進行控制,從而達到不同效果的目的。一個例子是,森林場景中所有的樹使用了同一種材質,我們希望它們可以通過批處理來減少draw call,但不同樹的顏色可能不同。這時,我們可以利用網格的頂點的顏色數據來調整。 需要註意的是,如果我們需要在腳本中訪問共享材質,應該使用Renderer.shaderdMaterial 來保證修改的是和其他物體共享的材質,但這意味著修改會應用到所有使用該材質的物體上。另一個類似的API 是Renderer.material ,如果使用 Renderer.material 來修改材質,Unity會創建一個該材質的復制品,從而破壞批處理在該物體上的應用。 在選擇使用動態批處理還是靜態批處理時,建議是: 1)盡可能使用靜態批處理,但得時間小心對內存的消耗,並且記住經過靜態批處理的物體不可以再被移動。 2)如果無法進行靜態批處理,而要使用動態批處理的話,小心上面提到的各種限制。 3)對於遊戲中的小道具,例如可以撿拾的金幣,可以使用動態批處理。 4)對於包含動畫的這類物體,我們無法全部使用靜態批處理,但其中如果有不動的部分,可以把這部分標識成"Static" 除了上述提示外,在使用批處理時還有一些需要註意的地方。由於批處理需要把多個模型變換到世界空間下再合並它們,因此,如果Shader 中存在一些基於模型空間下的坐標的運算,那麽往往會得到錯誤的結果。一個解決方法是,在Shader中使用DisableBatching標簽來強制該Shader的材質不會被批處理。另一個註意事項是,使用半透明材質的物體通常需要使用嚴格的從後往前的繪制順序來保證透明度混合的正確性。對於這些物體,Unity會首先保證它們的繪制順序,再嘗試對它們進行批處理。這意味著,當繪制順序無法滿足時,批處理無法在這些物體上被成功應用。 減少需要處理的頂點數目 3個常用的頂點優化策略。 1)優化幾何體 在很多建模軟件中宏,都有相應的優化選項,可以自動優化網格結構。 2)模型的LOD技術 原理是,當一個物體離攝像機很遠時,模型的很多細節我無法察覺到的。因此,LOD允許當對象逐漸原理攝像機時,減少模型上的面片數量,從而提高性能。 在Unity 中,我們可以使用LOD Group組件來為一個物體構建一個LOD。我們需要為同一個對象準備多個包含不同細節程序的模型,然後把它們賦給LOD Group 組件中的不同等級,Unity 就會自動判斷當前位置上需要使用哪個等級的模型。 3)遮擋剔除技術 遮擋剔除可以用來消除那些在其他物體後面看不到的物體,這意味著資源不會浪費在計算那些看不到的頂點上,進行提升性能。 我們需要把遮擋剔除和攝像機的視錐體剔除區分開來。視錐體剔除只會剔除掉那些不在攝像機的視野範圍內的對象,但不會判斷視野中是否有物體被其他物體擋住。而遮擋剔除會使用一個虛擬的射線機來遍歷場景,從而構建一個潛在可見的對象幾何的層級結構。在運行時刻,每個攝像機將會使用這個數據來識別哪些物體是可見的,而那些物體被其他物體擋住不可見。使用遮擋剔除,不僅可以減少處理的頂點數目,還可以減少overdraw。 要在Unity中使用遮擋剔除技術,我們需要進行一系列額外的處理工作。 減少需要處理的片元數目 另一個造成GPU瓶頸的是需要處理過多的片元。這部分優化重點在於減少overdraw。簡單來說,overdraw 值得是同一個像素被繪制了多次。 1)控制繪制順序 由於深度測試的存在,如果我們可以保證物體都是從前往後繪制的,那麽就可以很大程度上減少overdraw。這是因為,在後面繪制的物體由於無法通過深度測試,因此,就不會再進行後面的渲染處理。 在Unity中,那些渲染隊列數目小於2500(如"Background" "Geometry" 和 "AlphaTest")的對象都被認為是不透明物體,這些物體總體上是從前往後繪制的,而使用其他的隊列(如"Transparent" "Overlay")的物體,則是從後往前繪制的。這意味著,我們可以盡可能地把物體的隊列設置為不透明物體的渲染隊列,而盡量避免使用半透明隊列。 而且,我們還可以充分利用Unity的渲染隊列來控制繪制順序。 例如將隊列設置"Geometry+1"這樣。 2)時刻警惕透明物體 對於半透明物體對象來說,由於它們沒有開啟深度寫入,因此,如果要得到正確的渲染效果,就必須從後往前渲染。這意味著,半透明物體幾乎一定會造成overdraw。例如GUI對象來說,它們大多數被設置為半透明物體,如果屏幕中GUI占據的比例太多。而主攝像機又沒有進行調整而是投影整個屏幕,那麽GUI就會造成大量的overdraw。 因此,我們盡量減少窗口中GUI所占的面積。也可以把GUI的繪制和三維場景的繪制交給不同的攝像機,而其中負責三維場景的攝像機的視角範圍盡量不要和GUI的相互重疊。 在移動平臺上,透明度測試也會影響遊戲性能。雖然透明度測試沒有關閉深度測試,但由於它的實現使用了discard或clip操作,而這些操作會導致一些硬件的優化策略失效。 3)減少實時光照和陰影 實時光照對於移動平臺是一種非常昂貴的操作。如果場景中包含了過多的點光源,並且使用了多個Pass的Shader,那麽狠可能就會造成性能下降。我們可以使用烘焙技術,把光照提前烘焙到一張光照紋理中,然後在運行時刻只需要根據紋理采樣得到光照結果即可。 節省帶寬 之前提到過,使用紋理圖集可以幫助我們減少draw call 的數目,而這些紋理的大小同樣是一個需要考慮的問題。需要註意的是,所有紋理的長寬比最好是正方形,而且長寬值最好是2的整數冪。這是因為有很多優化策略只有在這種時候才可以發揮最大效果。在Unity5 中,即便我們導入的紋理長寬值並不是2的整數冪,Unity也會自動把長寬裝換到離它最近的2的整數冪值。但我們仍然應該在制作美術資源時就把這條規則謹記在心,防止由於縮放而造成不好的影響。 除此之外,我們還應該盡可能使用多級漸遠紋理技術和紋理壓縮。在Unity中,我們可以通過紋理導入面板來查看紋理的各個導入屬性。通過把紋理類型設置為Advanced,就可以自定義許多選項,例如,是否生成多級漸遠紋理(mipmaps)。如下圖所示,當勾選了Generate Mip Maps 選項後,Unity就會為同一張紋理創建出很多不同大小的小紋理,構成一個紋理金字塔。 技術分享 紋理壓縮同樣可以節省帶寬。但對於像Android 這樣的平臺,有很多不同架構的GPU,紋理壓縮就變得有點復雜,因為不同的GPU架構有它自己的紋理壓縮格式。所幸的是,Unity可以根據不同的設備選擇不同的壓縮格式,而我們只需要把紋理壓縮格式設置為自動壓縮即可。但是,GUI類型的紋理同樣是個例外,一些時候由於對畫質的要求,我們不希望對這些紋理進行壓縮。 過高的屏幕分辨率也是造成性能下降的原因之一,尤其是對於很多低端手機,除了分辨率高其他硬件條件並不盡如人意,這恰恰是遊戲性能的兩個瓶頸:過大的屏幕分辨率和糟糕的GPU。因此,我們可能需要對於特定及其進行分辨率的縮放。當然,這樣可能會造成遊戲效果的下降,但性能和畫面需要平衡。 減少計算復雜度 Shader 的LOD技術可以控制使用的Shader等級。它的原理是,只有Shader 的LOD 值小於某個設定的值,這個Shader才會被使用,而使用了那些超過設定值的Shader 的物體將不會被渲染。 我們通常會在SubShader 中使用類似下面的語句來指明該Shader 的LOD值。
  1. SubShader{
  2. Tags{"RenderType"="Opaque"}
  3. LOD 200
  4. }
我們也可以在Unity Shader的導入面板上看到該Shader 的LOD值。在默認情況下,允許的LOD得你是無限大的。這意味著,任何被當前顯卡支持的Shader都可以被使用。但是,在某些情況下,我們可能需要去掉一些使用了復雜計算的Shader渲染。這時,我們可以使用Shader.maximumLOD 或 Shader.globalMaximumLOD 來設置允許的LOD值。
Unity 內置的Shader 使用了不同的LOD值,例如,Diffuse 的LOD為 200,而Bumped Specular 的LOD 為400 代碼優化方面:首先盡可能使用低精度的浮點值進行運算。還需要註意的是,我們應當盡量避免在不同精度之間的轉換。 還有就是盡可能不要使用全屏的屏幕後處理效果。 盡可能不要使用分支語句和循環語句。 盡可能避免使用類似sin、tan、pow、log等較為復雜的數學運算。使用查找表來替代它。 盡可能不使用discard

Unity Shader入門精要學習筆記 - 第16章 Unity中的渲染優化技術