在Unity3D中的渲染優化-批處理技術
在Unity3D中,常用的減少Draw call的優化技術就是批處理技術。批處理的原理是減少每一幀需要的Draw call數目。為了把一個物件渲染到螢幕上,CPU需要檢查哪些光源影響了該物體,繫結shader並設定它的引數,再把渲染命令傳送給GPU。當場景中包含了大量的物件時,這些操作就會非常耗時。例如,如果我們需要渲染一千個三角形,把它們按一千個單獨的網格進行渲染所花費的時間要遠遠大於渲染一個包含一千個三角形的網格。在這兩種情況下,GPU的效能消耗其實並沒有多大的區別,但CPU的draw call數目就會成為效能瓶頸。因此,批處理的思想很簡單,就是在每次呼叫draw call時儘可能多地處理多個物體。
使用同一材質的物體可以進行批處理,因為對於使用同一材質的物體,它們之間的不同僅僅在於頂點資料的差別。我們可以把這些頂點資料合併在一起,再一起傳送給GPU,這樣就可以完成一次批處理。
在Unity中支援兩種型別的批處理,一種是動態批處理,另一種是靜態批處理。對於動態批處理來說,優點是一切處理Unity自動完成的,不需要我們做任何操作,而且物體可以是移動的,缺點是限制有很多,可能一不小心就會破壞了這種機制,導致Unity無法動態批處理一些使用了相同材質的物體。而對於靜態批處理來說,它的優點是自由度很高,限制很少;但缺點是可能會佔用更多的記憶體,而經過靜態批處理後的所有物體都不可以再移動了(即便在指令碼中嘗試改變物體的位置也是無效的)。
Unity3D中的動態批處理技術
動態批處理的原理是,每一幀把可以進行批處理的模型網格進行合併,再把合併後的模型資料傳遞給GPU,然後使用同一個材質對其渲染。除了實現方便,動態批處理的另一個好處就是,經過批處理的物體仍然可以移動,這是由於在處理每幀時Unity都會重新合併一次網格。
雖然Unity的動態批處理不需要我們進行任何額外工作,但只有滿足條件的模型和材質才可以被動態批處理。(需要注意的是,隨著Unity版本的變化,這些條件也有一些改變)這些條件限制是:
1.能夠進行動態批處理的網格的頂點屬性規模要小於900.例如,如果shader中需要使用頂點位置,法線和紋理座標這三個頂點屬性,那麼要想讓模型能夠被動態批處理,它的頂點數目不能超過300.需要注意的是,這個數字在未來有可能會發生變化,因此不要依賴這個資料。
2.一般來說,所有物件都需要使用同一縮放尺度(可以是(1,1,1),(1,2,3),(1.5,1.4,1.3)等,但必須都一樣)。一個例外情況是,如果所有的物體都是用了不同的非統一縮放,那麼他們也是可以被動態批處理的。但在Unity5中,這種對模型縮放的限制已經不存在了。
3.對於使用光照貼圖紋理的物體需要小心處理。這些物體需要額外的渲染引數,例如,在光照貼圖紋理上的索引和偏移量以及縮放資訊等因此,為了讓這些物體可以被動態批處理,我們需要保證它們指向光照貼圖紋理中的同一個位置。
4.有多個Pass通道的shader會中斷批處理。在前向渲染中,我們有時需要使用額外的Pass來為模型新增更多的光照效果,但這樣一來模型就不會被動態批處理了。
Unity3D中的靜態批處理技術
靜態批處理的實現原理是,只在執行的開始階段,把需要進行靜態批處理的模型合併到一個新的網格結構中,這意味著這些模型不可以在執行時刻被移動。但由於它只需要進行一次合併操作,因此比動態批處理更加高效。但靜態批處理的缺點是需要佔用更多的記憶體來儲存合併後的幾何結構。這時因為,如果在靜態批處理前一些物體共享了相同的網格,那麼在記憶體中每一個物體都會對應一個該網格的複製品,即一個網格會變成多個網格再發送給GPU。如果這類使用相同網格的物件很多,那麼這就會成為一種效能瓶頸了。例如,如果在一個使用了1000個相同樹模型的森林中使用靜態批處理,那麼就會多使用1000倍的記憶體,這會造成嚴重的記憶體影響。這時的解決方法就是要麼忍受這種犧牲記憶體換取效能的方法,要麼不要使用靜態批處理,而使用動態批處理技術(但要小心控制模型的頂點屬性數目),或者自己編寫批處理方法。
在Unity中使用靜態批處理的方法是,在場景中選中需要靜態批處理的物體,在其Inspector面板的右上角勾選上Batching static靜態屬性。在內部實現上,Unity首先把這些靜態物體變換到世界空間下,然後為它們構建一個更大的頂點和索引快取,對於使用同一材質的物體。Unity只需要呼叫一個drawcall就可以繪製全部物體。而對於使用不同材質的物體,靜態批處理同樣可以提升渲染效能。儘管這些物體仍然需要呼叫多個draw call,但靜態批處理可以減少這些draw call之間的狀態切換,而這些切換往往是費時的操作。
我們可以在Unity的分析器中觀察到應用靜態批處理前後VBO total(Vertex Buffer Object,頂點快取物件)的變化。在一些物體共享了相同的網格的情況下,我們可以看到這些物體在使用了靜態批處理技術後,VBO total的數目變大了,這正是因為靜態批處理會佔用更多記憶體的緣故。正如上面所講,靜態批處理需要佔用更多的記憶體來儲存合併後的幾何結構,如果一些物體共享了相同的網格,那麼在記憶體中每個物體都會對應一個該網格的複製品。
如果場景中包含了除了平行光以外的其他光源,並且在Shader中定義了額外的Pass來處理它們,這些額外的Pass部分是不會被批處理的,但是處理平行光的Base Pass部分仍然會被靜態批處理。
Unity3D中使用共享材質
無論是動態批處理還是靜態批處理,都要求模型之間需要共享同一個材質。但不同的模型之間總會需要有不同的渲染屬性,例如,使用不同的紋理,顏色等。這時我們需要一些策略來儘可能的合併材質。
如果兩個材質之間只有使用的紋理不同,我們可以把這些紋理合併到一張更大的紋理中,這張更大紋理被稱為是一張圖集(atlas)。一旦使用了同一紋理,我們就可以使用同一材質,再使用不同的取樣座標對紋理取樣即可。
但有時除了紋理不同外,不同的物體在材質上還有一些微小的引數變化,例如,顏色不同,某些浮點屬性不同。但是,不管是動態批處理還是靜態批處理,它們的前提都是使用同一個材質。是同一個,而不是使用了同一shader的材質,也就是說它們指向的材質必須是同一個實體。這意味著,只要我們調整了引數,就會影響到所欲使用這個材質的物件。那麼想要微小的調整怎麼辦呢?一種常用的方法就是使用網格的頂點資料來儲存這些引數(最常見的就是頂點顏色資料)。
經過批處理後的物體會被處理成更大的VBO傳送給GPU,VBO中的資料可以作為輸入傳遞給頂點著色器,因此,我們可以巧妙地對VBO中的資料進行控制,從而達到不同效果的目的。一個例子就是,森林場景中的所有樹使用了同一材質,我們希望它們可以通過批處理來減少draw call,但不同樹的顏色可能不同。這時,我們可以利用網格頂點的顏色資料來調整。
需要注意的是,如果我們需要在指令碼中訪問共享材質,應該使用Renderer.sharedMaterial來保證修改的是和其他物體共享的材質,但這意味著修改會應用到所有使用該材質的物體上。另一個類似的API是Renderer.material,如果使用Renderer.material來修改材質,Unity會建立一個該材質的複製品,從而破壞批處理在該物體上的應用。
關於在Unity3D中使用批處理的注意事項:
1.儘可能的使用靜態批處理,但要時刻小心對記憶體的消耗,並且記住經過靜態批處理的物體不可以再被移動。
2.如果無法進行靜態批處理,而要使用動態批處理的話,那麼儘可能減少物體的數目並且讓這些物體包含少量的頂點屬性和頂點數目。
3.對於遊戲中的小道具,例如可以撿拾的金幣等,可以使用動態批處理。
4.對於動畫的這類物體,我們無法全部使用靜態批處理,但其中如果有不動的部分,可以把這部分標識成“Static”。
5.由於批處理需要把模型變換到世界空間下再合併它們,因此,如果shader中存在一些基於模型空間下座標的運算,那麼往往會得到錯誤的結果。一個解決方法是,在shader中使用DisableBatching標籤強制使該shader的材質不會被批處理。
6.使用半透明材質的物體通常需要使用嚴格的從後往前的繪製順序來保證透明混合的正確性。這意味著,當繪製順序無法滿足時,批處理無法在這些物體上被成功地應用。