1. 程式人生 > >【Unity技巧】Unity中的優化技術

【Unity技巧】Unity中的優化技術

移動設備 完整 物體 動態 多少 each blank screen text

寫在前面

這一篇是在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,影響幀率,遊戲性能就會下降。當然,這裏說得很簡單,更詳細的請自行谷歌。其他CPU的性能瓶頸還有物理、布料模擬、粒子模擬等,都是計算量很大的操作。

而對於GPU來說,它負責整個渲染流水線。它會從處理CPU傳遞過來的模型數據開始,進行Vertex Shader、Fragment Shader等一系列工作,最後輸出屏幕上的每個像素。因此它的性能瓶頸可能和需要處理的頂點數目的、屏幕分辨率、顯存等因素有關。總體包含了頂點和像素兩方面的性能瓶頸。在像素處理中,最常見的性能瓶頸之一是overdraw。Overdraw指的是,我們可能對屏幕上的像素繪制了多次。

了解了上面基本的內容後,下面涉及到的優化技術有:

  • 頂點優化
    • 優化幾何體
    • 使用LOD(Level of detail)技術
    • 使用遮擋剔除(Occlusion culling)技術

  • 像素優化
    • 控制繪制順序
    • 警惕透明物體
    • 減少實時光照

  • CPU優化
    • 減少Draw Calls

  • 帶寬優化
    • 減少紋理大小
    • 利用縮放
首先是頂點優化的部分。 頂點優化

優化幾何體

這一步主要是為了針對性能瓶頸中的”頂點處理“一項。這裏的幾何體就是指組成場景中對象的網格結構。 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有一個系列文章大家可以看看(需FQ): 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背後的技術有關系,有興趣的可以自行谷歌,比如這裏。

  • 使用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有一篇文章講了這種技術,可以去看看。

寫在最後

這篇文章是總結性質的,因此對每種技術都沒有進行非常詳細的解釋。強烈建議大家閱讀文章開頭給出的各種鏈接,寫得都很好。 原文地址:http://blog.csdn.net/candycat1992/article/details/42127811

【Unity技巧】Unity中的優化技術