【Unity技巧】Unity中的最佳化技術,unity技巧最佳化技術
寫在前面
這一篇是在Digital Tutors的一個系列教程的基礎上總結擴充而得的~Digital Tutors是一個非常棒的教程網站,包含了多媒體領域很多方面的資料,非常酷!除此之外,還參考了Unity Cookie中的一個教程。還有很多其他參考在下面的連結中。
這篇文章旨在簡要地說明一下常見的各種最佳化策略。不過對每個基礎有非常深入地講解,需要的童鞋可以自行去相關資料。
還有一些我認為非常好的參考文章:
Performance Optimization for Mobile Devices
4 Ways To Increase Performance of your Unity Game
Unite 2013 Optimizing Unity Games for Mobile Platforms
Unity optimization Tips
影響效能的因素
首先,我們得瞭解,影響遊戲效能的因素哪些,才能對症下藥。對於一個遊戲來說,有兩種主要的計算資源:CPU和GPU。它們會互相合作,來讓我們的遊戲可以在預期的幀率和解析度下工作。CPU負責其中的幀率,GPU主要負責解析度相關的一些東西。
總結起來,主要的效能瓶頸在於:
- CPU
- 過多的Draw Calls
- 複雜的指令碼或者物理類比
- 頂點處理
- 像素(Fragment)處理
- 過多的fragment,overdraws
- 過多的逐像素計算
- 頻寬
- 尺寸很大且未壓縮的紋理
- 解析度過高的framebuffer
對於CPU來說,限制它的主要是遊戲中的Draw Calls。那麼什麼是Draw Call呢?如果你學過OpenGL,那麼你一定還記得在每次繪圖前,我們都需要先準備好頂點資料(位置、法線、顏色、紋理座標等),然後調用一系列API把它們放到GPU可以訪問到的指定位置,最後,我們需要調用_glDraw*命令,來告訴GPU,“嘿,我把東西都準備好了,你個懶傢伙趕緊出來幹活(渲染)吧!”。而調用_glDraw*命令的時候,就是一次Draw Call。那麼為什麼Draw Call會成為效能瓶頸呢(而且是CPU的瓶頸)?上面說到過,我們想要繪製映像時,就一定需要調用Draw Call。例如,一個情境裡有水有樹,我們渲染水的時候使用的是一個material以及一個shader,但渲染樹的時候就需要一個完全不同的material和shader,那麼就需要CPU重新準備頂點資料、重新設定shader,而這種工作實際是非常耗時的。如果情境中,每一個物體都使用不同的material、不同的紋理,那麼就會產生太多Draw Call,影響幀率,遊戲效能就會下降。當然,這裡說得很簡單,更詳細的請自行Google。其他CPU的效能瓶頸還有物理、布料類比、粒子類比等,都是計算量很大的操作。
而對於GPU來說,它負責整個渲染流水線。它會從處理CPU傳遞過來的模型資料開始,進行Vertex Shader、Fragment Shader等一系列工作,最後輸出螢幕上的每個像素。因此它的效能瓶頸可能和需要處理的頂點數目的、螢幕解析度、顯存等因素有關。總體包含了頂點和像素兩方面的效能瓶頸。在像素處理中,最常見的效能瓶頸之一是overdraw。Overdraw指的是,我們可能對螢幕上的像素繪製了多次。
瞭解了上面基本的內容後,下面涉及到的最佳化技術有:
- 頂點最佳化
- 使用LOD(Level of detail)技術
- 使用遮擋剔除(Occlusion culling)技術
- 像素最佳化
- CPU最佳化
- 頻寬最佳化
首先是頂點最佳化的部分。
頂點最佳化
最佳化幾何體
這一步主要是為了針對效能瓶頸中的”頂點處理“一項。這裡的幾何體就是指組成情境中對象的網格結構。
3D遊戲製作都由模型製作開始。而在建模時,有一條我們需要記住:
儘可能減少模型中三角形的數目,一些對於模型沒有影響、或是肉眼非常難察覺到區別的頂點都要儘可能去掉。例如在下面左圖中,正方體內部很多頂點都是不需要的,而把這個模型匯入到Unity裡就會是右面的情景:
在Game視圖下,我們可以查看情境中的三角形數目和頂點數目:
可以看到一個簡單的正方形就產生了這麼多頂點,這是我們不希望看到的。
同時,
儘可能重用頂點。在很多三維建模軟體中,都有相應的最佳化選項,可以自動最佳化網格結構。最後最佳化後,一個正方體可能只剩下8個頂點:
它對應的頂點數和三角形數目如下:
等等!這裡,你可能要問了,為什麼頂點數是24,而不是8呢?美術朋友們經常會遇到這樣的問題,就是建模軟體裡顯示的模型頂點數和Unity中的不一樣,通常Unity會多很多。誰才是對的呢?其實,它們是站在不同的角度上計算的,都有各自的道理,但我們真正應該關心的是Unity裡的數目。
我們這裡簡單解釋一下。三維軟體裡更多地是站在我們人類的角度理解頂點的,即我們看見的一個點就是一個。而Unity是站在GPU的角度上,去計算頂點數目的。而在GPU看來,看起來是一個的很有可能它要分開處理,從而就產生了額外的頂點。這種將頂點一分為多的原因,主要有兩個:一個是UV splits,一個是Smoothing splits。而它們的本質其實都是因為對於GPU來說,頂點的每一個屬性和頂點之間必須是一對一的關係。UV splits的產生,是因為建模時,一個頂點的UV座標有多個。例如之前的立方體的例子,由於每個面都有共同的頂點,因此在不同面上,同一個頂點的UV座標可能發生改變。這對於GPU來說,這是不可理解的,因此它必須把這個頂點拆分成兩個具有不同UV座標的定頂點,它才甘心。而Smoothing splits的產生也是類似的,不同的時,這次一個頂點可能會對應多個法線資訊或切線資訊。這通常是因為我們要決定一個邊是一條Hard Edge還是Smooth Edge。Hard Edge通常是下面這樣的效果(注意中間的摺痕部分):
而如果觀察它的頂點法線,就會發現,摺痕處每個頂點其實包含了兩個不同的法線。因此,對於GPU來說,它同樣無法理解這樣的事情,因此會把頂點一分為二。而相反,Smooth Edge則是下面的情況:
對於GPU來說,它本質上只關心有多少個頂點。因此,儘可能減少頂點的數目其實才是我們真正對需要關心的事情。因此,最後一條最佳化建議就是:
移除不必要的
Hard Edge以及紋理銜接,即避免
Smoothing splits和UV splits。
使用LOD(Level of detail)技術
LOD技術有點類似於Mipmap技術,不同的是,LOD是對模型建立了一個模型金字塔,根據攝像機距離對象的遠近,選擇使用不同精度的模型。它的好處是可以在適當的時候大量減少需要繪製的頂點數目。它的缺點同樣是需要佔用更多的記憶體,而且如果沒有調整好距離的話,可能會造成類比的突變。
在Unity中,可以通過LOD Group來實現LOD技術:
通過上面的LOD Group面板,我們可以選擇需要控制的模型以及距離設定。下面展示了油桶從一個完整網格到簡化網格,最後完全被剔除的例子:
使用遮擋剔除(Occlusion culling)技術
遮擋剔除是用來消除躲在其他物件後面看不到的物件,這代表資源不會浪費在計算那些看不到的頂點上,進而提升效能。關於遮擋剔除,Unity Taiwan有一個系列文章大家可以看看(需翻牆):
Unity 4.3 關於Occlusion Culling : 基本篇
Unity 4.3 關於Occlusion Culling : 最佳做法
Unity 4.3 關於Occlusion Culling : 錯誤診斷
具體的內容大家可以自行尋找。
現在我們來談像素最佳化。
像素最佳化
像素最佳化的重點在於減少overdraw。之前提過,overdraw指的就是一個像素被繪製了多次。關鍵在於控制繪製順序。
Unity還提供了查看overdraw的視圖,在Scene視圖的Render Mode->Overdraw。當然這裡的視圖只是提供了查看物體遮擋的層數關係,並不是真正的最終螢幕繪製的overdraw。也就是說,可以理解為它顯示的是如果沒有使用任何深度檢驗時的overdraw。這種視圖是通過把所有對象都渲染成一個透明的輪廓,通過查看透明顏色的累計程度,來判斷物體的遮擋。
圖,紅色越是濃重的地方表示overdraw越嚴重,而且這裡涉及的都是透明物體,這意味著效能將會受到很大影響。
控制繪製順序
需要控制繪製順序,主要原因是為了最大限度的避免overdraws,也就是同一個位置的像素可以需要被繪製多變。在PC上,資源無限,為了得到最準確的渲染結果,繪製順序可能是從後往前繪製不透明物體,然後再繪製透明物體進行混合。但在移動平台上,這種會造成大量overdraw的方式顯然是不適合的,我們應該盡量從前往後繪製。從前往後繪製之所以可以減少overdraw,都是因為深度檢驗的功勞。
在Unity中,那些Shader中被設定為“Geometry” 隊列的對象總是從前往後繪製的,而其他固定隊列(如“Transparent”“Overla”等)的物體,則都是從後往前繪製的。這意味這,我們可以盡量把物體的隊列設定為“Geometry” 。
而且,我們還可以充分利用Unity的隊列來控制繪製順序。例如,對於天空盒子來說,它幾乎覆蓋了所有的像素,而且我們知道它永遠會在所有物體的後面,因此它的隊列可以設定為“Geometry+1”。這樣,就可以保證不會因為它而造成overdraws。
時刻警惕透明物體
而對於透明對象,由於它本身的特性(可以看之前關於Alpha Test和Alpha Blending的一篇文章)決定如果要得到正確的渲染效果,就必須從後往前渲染(這裡不討論使用深度的方法),而且拋棄了深度檢驗。這意味著,透明物體幾乎一定會造成overdraws。如果我們不注意這一點,在一些機器上可能會造成嚴重的效能下面。例如,對於GUI對象來說,它們大多被設定成了半透明,如果螢幕中GUI佔據的比例太多,而主攝像機又沒有進行調整而是投影整個螢幕,那麼GUI就會造成螢幕的大量overdraws。
因此,如果情境中大面積的透明對象,或者有很多層覆蓋的多層透明對象(即便它們每個的面積可以都不大),或者是透明的粒子效果,在行動裝置上也會造成大量的overdraws。這是應該盡量避免的。
對於上述GUI的這種情況,我們可以盡量減少視窗中GUI所佔的面積。如果實在無能為力,我們可以把GUI繪製和三維情境的繪製交給不同的攝像機,而其中負責三維情境的攝像機的視角範圍盡量不要和GUI重疊。對於其他情況,只能說,儘可能少用。當然這樣會對遊戲的美觀度產生一定影響,因此我們可以在代碼中對機器的效能進行判斷,例如首先關閉所有的耗費效能的功能,如果發現這個機器表現非常良好,再嘗試開啟一些特效功能。
減少即時光照
即時光照對於移動平台是個非常昂貴的操作。如果只有一個平行光還好,但如果情境中包含了太多光源並且使用了很多多Passes的shader,那麼很有可能會造成效能下降。而且在有些機器上,還要面臨shader失效的風險。例如,一個情境裡如果包含了三個逐像素的點光源,而且使用了逐像素的shader,那麼很有可能將Draw Calls提高了三倍,同時也會增加overdraws。這是因為,對於逐像素的光源來說,被這些光源照亮的物體要被再渲染一次。更糟糕的是,無論是動態批處理還是動態批處理(其實文檔中只提到了對動態批處理的影響,但不知道為什麼實驗結果對靜態批處理也沒有用),對於這種逐像素的pass都無法進行批處理,也就是說,它們會中斷批處理。
例如,下面的情境中,四個物體都被標識成了“Static”,它們使用的shader都是內建的Bumped Diffuse。而所有的點光源都被標識成了“Important”,即是逐像素光。可以看到,運行後的Draw Calls是23,而非3。這是因為,只有“Forward Base”的Pass時發生了靜態批處理(這裡的動態批處理由於多Pass已經完全失效了),節省了一個Draw Calls,而後面的“Forward Add” Pass,每一次渲染都是一個單獨的Draw Call(而且可以看到Tris和Verts數目也增加了):
這點正如文檔中說的:The draw calls for “additional per-pixel lights” will not be batched。原因我不是很清楚,這裡有一個討論,但裡面的意思說是對靜態批處理沒有影響,和我這裡的結果不一樣,知道原因的麻煩給我留言,非常感謝。我也在Unity論壇裡提問裡。
我們看到很多成功的移動遊戲,它們的畫面效果看起來好像包含了很多光源,但其實這都是騙人的。
使用Lightmaps
Lightmaps的很常見的一種最佳化策略。它主要用於情境中整體的光照效果。這種技術主要是提前把情境中的光照資訊儲存在一張光照紋理中,然後在運行時刻只需要根據紋理採樣得到光照資訊即可。
當然與之配合的還有Light Probes技術。風宇沖有一個系列文章講過,但是時間比較久遠,但教程我相信網上有很多。
使用God Rays
情境中很多小型光源效果都是靠這種方法類比的。它們一般並不是真的光源產生的,很多情況是通過透明紋理進行類比。具體可以參見之前的文章。
CPU最佳化
減少Draw Calls
批處理(Batching)
這方面的最佳化教程想必是最多的了。最常見的就是通過批處理(Batching)了。從名字上來理解,就是一塊處理多個物體的意思。那麼什麼樣的物體可以一起處理呢?答案就是
使用同一個材質的物體。這是因此,對於使用同一個材質的物體,它們之間的不同僅僅在於頂點資料的差別,即使用的網格不同而已。我們可以把這些頂點資料合并在一起,再一起發送給GPU,就可以完成一次批處理。
Unity中有兩種批處理方式:一種是動態批處理,一種是靜態批處理。對於動態批處理來說,好訊息是一切處理都是自動的,不需要我們自己做任何操作,而且物體是可以移動的,但壞訊息是,限制很多,可能一不小心我們就會破壞了這種機制,導致Unity無法批處理一些使用了相同材質的物體。對於靜態批處理來說,好訊息是自由度很高,限制很少,壞訊息是可能會佔用更多的記憶體,而且經過靜態批處理後的所有物體都不可以再移動了。
首先來說動態批處理。
Unity進行動態批處理的條件是,物體使用同一個材質並且滿足一些特定條件。Unity總是在不知不覺中就為我們做了動態批處理。例如下面的情境:
這個情境共包含了4個物體,其中兩個箱子使用了同一個材質。可以看到,它的Draw Calls現在是3,並且顯示Save by batching是1,也就是說,Unity靠Batching為我們節省了1個Draw Call。下面,我們來把其中一個箱子的大小隨便改動一下,看看會發生什麼:
可以發現,Draw Calls變成了4,Save by batching的數目也變成了0。這是為什麼呢?它們明明還是只使用了一個材質啊。原因就是前面提到的那些需要滿足的其他條件。動態批處理雖然自動得令人感動,但它對模型的要求很多:
- 頂點屬性的最大限制為900,而且未來有可能會變。不要依賴這個資料。
- 一般來說,那麼所有對象都必須需要使用同一個縮放尺度(可以是(1, 1, 1)、(1, 2, 3)、(1.5, 1.4, 1.3)等等,但必須都一樣)。但如果是非統一縮放(即每個維度縮放尺度不一樣,例如(1, 2, 1)),那麼如果所有的物體都使用不同的非統一縮放也是可以批處理的。這個要求很怪異,為什麼批處理會和縮放有關呢?這和Unity背後的技術有關係,有興趣的可以自行Google,比如這裡。
- 使用lightmap的物體不會批處理。多passes的shader會中斷批處理。接受即時陰影的物體也不會批處理。
上述除了最常見的由於縮放導致破壞批處理的情況,還有就是頂點屬性的限制。例如,在上面的情境中我們添加之前未最佳化後的箱子模型:
可以看到Draw Calls一下子變成了5。這是因為新添加的箱子模型中,包含了474個頂點,而它使用的頂點屬性有位置、UV座標、法線等資訊,使用的總和超過了900。
動態批處理的條件這麼多,一不小心它就不幹了,因此Unity提供了另一個方法,靜態批處理。接著上面的例子,我們保持修改後的縮放,但把四個物體的“Static Flag”勾選上:
點擊Static後面的三角下拉框,我們會看到其實這一步設定了很多東西,這裡我們想要的只是“Batching static”一項。這時我們再看Draw Calls,恩,還是沒有變化。但是不要急,我們點擊運行,變化出現了:
Draw Calls又回到了3,並且顯示Save by batching是1。這就是得利於靜態批處理。而且,如果我們在運行時刻查看模型的網格,會發現它們都變成了一個名為Combined Mesh (roo: scene)的東西。這個網格是Unity合并了所有標識為“Static”的物體的結果,在我們的例子裡,就是四個物體:
你可以要問了,這四個對象明明不是都使用了一個材質,為什麼可以合并成一個呢?如果你仔細觀察的話,會發現裡面標明了“4 submeshes”,也就是說,這個合并後的網格其實包含了4個子網格,也就是我們的四個對象。對於合并後後的網格,Unity會判斷其中使用同一個材質的子網格,然後對它們進行批處理。
但是,我們再細心點可以發現,我們的箱子使用的其實是同一個網格,但合并後卻變成了兩個。而且,我們觀察運行前後Stats視窗中的“VBO total”,它的大小由241.6KB變成了286.2KB,變大了!還記得靜態批處理的缺點嗎?就是可能會佔用更多的記憶體。文檔中是這樣寫的:
“Using static batching will require additional memory for storing the combined geometry. If several objects shared the same geometry before static batching, then a copy of geometry will be created for each object, either in the Editor or at runtime. This might not always be a good idea - sometimes you will have to sacrifice rendering performance by avoiding static batching for some objects to keep a smaller memory footprint. For example, marking trees as static in a dense forest level can have serious memory impact.”
也就是說,如果在靜態批處理前有一些物體共用了相同的網格(例如這裡的兩個箱子),那麼每一個物體都會有一個該網格的複製品,即一個網格會變成多個網格被發送給GPU。在上面的例子看來,就是VBO的大小明顯增大了。如果這類使用同一網格的對象很多,那麼這就是一個問題了,這種時候我們可能需要避免使用靜態批處理,這意味著犧牲一定的渲染效能。例如,如果在一個使用了1000個重複樹模型的森林中使用靜態批處理,那麼結果就會產生1000倍的記憶體,這會造成嚴重的記憶體影響。這種時候,解決方案要麼我們可以忍受這種犧牲記憶體換取效能的方法,要麼不要使用靜態批處理,而使用動態批處理(前提是大家使用相同的縮放大小,或者大家都使用不同的非統一縮放大小),或者自己編寫批處理的方法。當然,我認為最好的還是使用動態批處理來解決。
有一些小提示可以使用:
- 儘可能選擇靜態批處理,但得時刻小心對記憶體的消耗。
- 如果無法進行靜態批處理,而要使用動態批處理的話,那麼請小心上面提到的各種注意事項。例如:
- 儘可能讓這樣的物體少並且儘可能讓這些物體包含少量的頂點屬性。
- 不要使用統一縮放,或者都使用不同的非統一縮放。
- 對於遊戲中的小道具,例如可以撿拾的金幣等,可以使用動態批處理。
- 對於包含動畫的這類物體,我們無法全部使用靜態批處理,但其中如果有不動的部分,可以把這部分標識成“Static”。
一些討論:
How static batching works
Static batching use a ton of memory?
Unity3D draw call optimization
合并紋理(Atlas)
雖然批處理是個很好的方式,但很容易就打破它的規定。例如,情境中的物體都使用Diffuse材質,但它們可能會使用不同的紋理。因此,儘可能把多張小紋理合并到一張大紋理(Atlas)中是一個好主意。
利用網格的頂點資料
但有時,除了紋理不同外,還有對於不同的物體,它們在材質上還有一些微小的參數變化,例如顏色不同、某些浮點參數不同。但鐵定律是,
不管是動態批處理還是靜態批處理,它們的前提都是要使用同一個材質。是同一個,而不是同一種,也就是說它們指向的材質必須是同一個實體。這意味著,只要我們調整了參數,就會影響到所有使用這個材質的對象。那麼想要微小的調整怎麼辦呢?由於Unity中的規定非常死,那麼我們只好想些“歪門邪道”,其中一種就是使用網格的頂點資料(最常見的就是頂點顏色資料)。
前面說過,經過批處理後的物體會被處理成一個VBO發送給GPU,VBO中的資料可以作為輸入傳遞給Vertex Shader,因此我們可以巧妙地對VBO中的資料進行控制,從而達到不同效果的目的。一個例子是,還是之前的森林,所有的樹使用了同一種材質,我們希望它們可以通過動態批處理來實現,但不同樹的顏色可能不同。這時我麼可以利用網格的頂點資料來調整。具體方法,可以參見後面會寫的一篇文章。
但這種方法的缺點就是會需要更多的記憶體來儲存這些用於調整參數用的頂點資料。沒辦法,永遠沒有絕對完美的方法。
頻寬最佳化
減少紋理大小
之前提到過,使用Texture Atlas可以協助減少Draw Calls,而這些紋理的大小同樣是一個需要考慮的問題。在這之前要提到一個問題就是,所有紋理的長寬比最好是正方形,而且長度值最好是2的整數冪。這是因為有很多最佳化策略只有在這種時候才可以發揮最大效用。
Unity中查看紋理參數可以通過紋理的面板:
而調整參數可以通過紋理的Advance面板:
上面各種參數的說明可以參見文檔。其中和最佳化相關的主要有“Generate Mip Maps”、“Max Size”和“Format”幾個選項。
“Generate Mip Maps”會為同一張紋理建立出很多不同大小的小紋理,構成一個紋理金字塔。而在遊戲中可以根據距離物體的遠近,來動態選擇使用哪一個紋理。這是因為,在距離物體很遠的時候,就算我們使用了非常精細的紋理,但肉眼也是分辨不出來的,這種時候完全可以使用更小、更模糊的紋理來代替,而這大量可以節省訪問的像素的數目。但它的缺點是,由於需要為每一個紋理建立一個影像金字塔,因此它會需要佔用更多的記憶體。例如上面的例子,在勾選“Generate Mip Maps”前,記憶體佔用是0.5M,而勾選了“Generate Mip Maps”後,就變成了0.7M。除了記憶體的佔用以外,一些時候我們也不希望使用Mipmaps,例如GUI紋理等。我們還可以在面板中查看產生的Mip Maps:
Unity中還提供了查看情境中物體的Mip Maps的使用方式。更確切的說是,展示了物體理想的紋理大小。其中紅色表示這個物體可以使用更小的紋理,藍色表示應該使用更大的紋理。
“Max Size”決定了紋理的長寬值,如果我們使用的紋理本身超過了這個最大值,Unity會對其進行縮小來滿足這個條件。這裡再重複一點,所有紋理的長寬比最好是正方形,而且長度值最好是2的整數冪。這是因為有很多最佳化策略只有在這種時候才可以發揮最大效用。
“Format”負責紋理使用的壓縮模式。通常選擇這種自動模式就可以了,Unity會負責根據不同的平台來選擇合適的壓縮模式。而對於GUI類型的紋理,我們可以根據對畫質的要求來選擇是否進行壓縮,具體可以參見之前關於畫質的文章。
我們還可以根據不同的機器來選擇使用不同解析度的紋理,以便讓遊戲在某些老機器上也可以運行。
利用縮放
很多時候解析度也是造成效能下降的原因,尤其是現在很多國內山寨機,除了解析度高其他硬體簡直一塌糊塗,而這恰恰中了遊戲效能的兩個瓶頸:過大的螢幕解析度+糟糕的GPU。因此,我們可能需要對於特定機器進行解析度的放縮。當然,這樣會造成遊戲效果的下降,但效能和畫面之間永遠是個需要權衡的話題。
在Unity中設定螢幕解析度可以直接調用Screen.SetResolution。實際使用中可能會遇到一些情況,雨松MOMO有一篇文章講了這種技術,可以去看看。
寫在最後
這篇文章是總結性質的,因此對每種技術都沒有進行非常詳細的解釋。強烈建議大家閱讀文章開頭給出的各種連結,寫得都很好。