1. 程式人生 > >Unity場景優化初探

Unity場景優化初探

一、什麼是批處理?

  我們知道Unity3D在螢幕上繪製一個圖形本質上呼叫OpneGL或者DirectX這樣的API,因此在這個過程中會產生一定程度上的效能消耗。DrawCall是OpenGL中描述繪製次數的一個量,例如一個基本的OpenGL繪製流程是設定顏色->繪圖方式->頂點座標->繪製->結束,在繪製的過程中每幀都會重複這個過程,這就是一次DrawCall,所以當遊戲中的繪製過程變得複雜的時候,就會帶來DrawCall的急劇增加,進而帶來遊戲的效能問題,反映到遊戲表現上就變成了優化問題。那麼在Unity3D中採取了什麼樣的措施來降低DrawCall呢?這就是我們今天要說的批處理,換句話說Unity3D使用了批處理來達到降低DrawCall的目的,批處理希望通過對物體網格的重組來獲得更高的繪製效率,試想以下如果將多個物體合併為一個物體,那麼在繪製的時候只需要繪製一次就夠了,因此從這個角度上來講這樣做肯定是可以降低DrawCall的,更深刻的一種理解是這裡體現了一種資源迴圈呼叫的思想,接觸過Android開發的朋友們一定知道ListView控制元件可以對其元素進行“快取”從而提高效率,因為我們可以發現其實ListView是對列表項進行某種程度上的“複用”從而提高了效率,在Unity3D這裡同樣遵循了這個原理。在Unity3D中進行批處理的一個前提是相同材質的物體可以被合併,如果這些物體使用不同的材質,那麼當我們把這些材質對應的紋理打成“圖集”以後可以對其進行合併,並且在合併的時候應該是用Renderer.sharedMaterial

 而非 Renderer.material以保證材質是可以共享的。關於DrawCall的相關細節大家從這裡來了解。

二、Unity3D中批處理的三種方式

  在Unity3D中有靜態批處理和動態批處理以及GPU Instancing。下面我們就來分別說說這三種不同的批處理方式!

靜態批處理

  因為我們在使用Unity3D的過程將場景中相對來說“靜態”的物體都勾選Static選項,這在Unity3D中稱為Static GameObjects,並且因為這一特性和LightmappingNavigationOff-meshLinksReflectionProbeOccluder and Occludee

等內容均有著密切的聯絡。靜態批處理允許遊戲引擎儘可能多的去降低繪製任意大小的物體所產生的DrawCall,它會佔用更多的記憶體資源和更少的CPU資源,因為它需要額外的記憶體資源來儲存合併後的幾何結構,如果在靜態批處理之前,如果有幾個物件共享相同的幾何結構,那麼將為每個物件建立一個幾何圖形。使用靜態批處理非常簡單啦,只要勾選物體的Static選項即可!

靜態批處理會在構建時將多個靜態網格物件合併為一個或多個大的網格物件,然後在執行時一次批處理渲染一個大網格中的多個物件。如果不同的物體間共享材質,則可以直接通過靜態批處理降低DrawCall 

動態批處理

  相對靜態批處理而言,動態批處理的要求更為嚴格一些,它要求批處理的動態物件具有一定的頂點,所以動態批處理只適用於包含小於900個頂點屬性的網格。如果你的著色器使用頂點位置,法線和單光,然後你可以批處理300個頂點的動態物件;而如果你的著色器使用頂點位置,法線,uv0,UV1和切線,那麼只能處理180個頂點的動態物件。接下來最為重要的一點,如果動態物件使用的是不同的材質,那麼即使進行了動態批處理從效率上來講並不會有太大的提升。

如果動態物件採用的是多維子材質,那麼批處理是無效的。如果動態物件接收實時光影,同樣批處理是無效的。動態批處理並不能降低DrawCall、面數和頂點數

動態批處理在每幀中獲取多個小型網格物件,在CPU中對其進行頂點變換,將相似的頂點組合到一起,然後一次繪製它們。

GPU Instancing

可以利用少量Draw Call繪製多個具有不同的位置、旋轉以及其他著色器屬性的相同物件。

三、導致批處理失敗的原因

有時在編輯器中可以清楚地看到,一些本應被批處理的物件出於某些原因沒有被批處理。首先,請檢查Player Settings中是否啟用批處理功能。這個步驟看似多餘,但我們遇到太多的無法處理的原因都是因為忘記開啟。 我們專門為此提供了展示專案來演示Unity在什麼情況下必須發起新的批處理請求。首先下載專案並複製到Unity專案中。請注意,你需要安裝Unity 5.6才能看到Frame Debugger中關於批處理狀態的說明。 以下是展示專案(Unity 5.6)中導致無法進行批處理的原因。每個原因對應一次單獨的批處理:

AdditionalVertex Streams — 物件使用MeshRenderer.additionalVertexStreams設定了額外的頂點資訊流。

DeferredObjects on Different Lighting Layers — 該物件位於另一不同的光照層中。

DeferredObjects Split by Shadow Distance — 兩個物體中有一個在陰影距離範圍內而另一個不是。

DifferentCombined Meshes — 該物件屬於另一個已合併的靜態網格。

DifferentCustom Properties — 該物件設定了不同的MaterialProperyBlock。

DifferentLights — 該物件受不同的前向光照(Forward Light)影響。

DifferentMaterials — 該物件使用不同的材質。

DifferentReflection Probes — 該物件受不同的反射探頭(Reflection Probe)影響。

DifferentShadow Caster Hash — 該物件使用其他的陰影投射著色器,或是設定了不同的著色器引數/關鍵詞,而這些引數/關鍵詞會影響陰影投射Pass的輸出。

DifferentShadow Receiving Settings — 該物件設定了不同的“Receive Shadows”引數,或是一些物件在陰影距離內,而另一些在距離之外。

DifferentStatic Batching Flags — 該物件使用不同的靜態批處理設定。

DynamicBatching Disabled to Avoid Z-Fighting — Player Settings中關閉了動態批處理,或在當前環境中為避免深度衝突而被臨時關閉。

InstancingDifferent Geometries — 使用GPU Instancing渲染不同的網格或子網格。

LightmappedObjects — 物件使用了不同的光照貼圖,或在相同的光照貼圖中有不同的光照貼圖UV轉換關係。

LightprobeAffected Objects — 物件受其他光照探頭(Light Probe)影響。

MixedSided Mode Shadow Casters — 物件的“Cast Shadows”設定不同。

Multipass — 物件使用了帶多個Pass的著色器。

MultipleForward Lights — 該物件受多個前向光渲染影響。

Non-instanceableProperty Set — 為instanced著色器設定來non-instanced屬性。

OddNegative Scaling — 該物件的縮放為很奇怪的負值,例如(1,-1,1)。

ShaderDisables Batching — 著色器使用“DisableBatching”標籤顯式關閉了批處理。

TooMany Indices in Dynamic Batch — 動態批處理索引過多(超過32k)。

TooMany Indices in Static Batch — 靜態批處理中的組合網格索引過多。對於OpenGL ES來說是48k,OSX是32k,其他平臺是64k。

TooMany Vertex Attributes for Dynamic Batching — 欲進行動態批處理的子網格擁有超過900個頂點屬性。

TooMany Vertices for Dynamic Batching — 欲進行動態批處理的子網格頂點數量超過300個。

四、Frame Debugger

從版本5開始,Unity包含了一個全新的視覺化幀除錯工具,Frame Debugger。該工具能幫你解決很多圖形方面的問題Z-fightingGPU狀態不正常,渲染佇列錯誤、混合操作錯誤,過多的draw call,效率低下等等。相比遊戲檢視中的狀態列表,它提供了更加詳盡的資訊,通過與渲染事件/步驟的互動和檢查,你也能學習到大量GPU管線的相關知識。真地,每個開發人員都應該瞭解這個工具。

 

1.  使用Frame debugger

 

使用Window-> Frame Debugger選單開啟其主視窗,我建議同時開啟frame debugger game view (遊戲檢視)。為此你可能需要對窗口布局做些調整,本人用來除錯圖形顯示時的介面如下圖:

 

 Recommended layout (except for the Asset Store tab)

建議的窗口布局(除了資源商店標籤頁)

 

使遊戲進入播放狀態,在frame debugger視窗中,點選enable就能得到最近渲染幀的細節資訊。

 

其中包含了兩個主要部分:事件,以及在其右側的詳細資訊。事件中基本上都是CPU傳送給GPU的命令,相當的簡單,畢竟在此無法看到真正被呼叫的API函式,只是將整個過程按照時間順序打包顯示出來。其中大部分是清除和繪製(不透明/透明)幾何體的命令,但是你也可以注意到更多的資訊,如:GUI渲染,陰影處理,圖形效果等以及相應的回撥次數。

 

在選擇一個事件後,你可以立即在details面板中得到關於該事件的詳細資訊。包括:著色器範圍和其標誌,渲染輸出事件和著色器屬性資訊。同時,遊戲檢視會顯示在該事件發生前已被渲染的物件(包括當前事件哦)。下面是一個例子

 

 Event example

 

2. Frame Debugger幹嘛用?

 

Frame debugger為我們提供了一些有價值的資訊。繪製事件的呼叫順序(也即是:渲染佇列)對渲染的正確性十分重要,如果遊戲中的元素未能正確的顯示粗來,在此你就可能發現它未被正確的放入渲染佇列中,從而導致過早或延遲渲染;特別是在進行自定義著色器開發或Z write(深度緩衝寫入)引數調整和測試時。通過這個工具為我解決了不少問題:查找出由於忘掉設定標誌位和渲染次序而導致天空盒子覆蓋了俺著色器輸出的問題。

 

其次,相對的,你也可以在此檢視draw call的呼叫次數,並通過頂點/索引數量來間接衡量場景的渲染代價。當然,著色器pass數也有幫助意義,但其複雜度在此沒有顯示,這就要求你得有些基本常識了,所有這些資訊都能幫助你提高場景的效能,比如:你會發現出於某些原因一些網格繪製未能進行靜態/動態批處理。在我的例子中,你會發現通過使用材質圖集來將兩個圖片精靈的2draw call操作合併為1個。

 

第三,通過與Frame Debugger互動,你可以快速學習GPU的體系架構知識並瞭解Unity渲染處理的流程。可以用鍵盤在不同的事件中跳轉一步步檢視場景的渲染過程。在之前的例子中,可以看到場景渲染開始於對三個快取的清理(顏色、zstencil),接著是不透明幾何體(從前面到背面),天空盒子和透明幾何體(從背面到前面)的渲染。

 

最後,你可以訪問著色器屬性以獲得關於材質和著色器的更多資訊,也可以得到被物件使用的資料的引用,如:材質。大多數人都沒用過Unity中的advanced views,你可以在inspector中通過點選右上部的paragraph圖示,並選擇“Debug”來訪問他們。

 

 

 

Shader properties

著色器屬性

其他好用的工具

4. RenderDoc

 

在進行了一些軟體的測試後,我鐵定可以為大家推薦幾款。你工具箱裡的首選必須是RenderDoc,Crytek開發的一款免費工具,用於解決底層呼叫資訊的問題,該工具使用了網路攻擊者的經典做法:它建立一個虛擬的圖形驅動作為中間人進行(MITM)捕獲應用中的DX/GL/Vulkan呼叫並提供詳細的除錯資訊。好訊息是:該工具已經支援在Unity中原生整合,你只需在windows機器上安裝RenderDoc並重啟unity的編輯器即可。

 

本節俺的目標是為大家概述該工具的用法,讓你充滿興趣的開始,使用時更加順暢,而且少花電費。

 

我們開始吧,如果安裝順利,在遊戲中你應該可以右鍵單擊並在選單中選擇Load RenderDoc

 

 

 

幾秒鐘後,進入播放模式,如果你對剛渲染的那幀感興趣,點選最大化播放按鈕左側光頭佬一樣的新圖示:

 

 

 

接著切換到新開啟的RenderDoc視窗,雙擊自動捕捉的日誌開始分析它:

 

 

 

RenderDoc captured log

 

Rendoc捕捉日誌

 

哇哦,這就是資訊的大海啊,我會淹死的。冷靜下來,一切都會順利。現在你會在幾千個視窗中得到所有幀的資料,表擔心,下面我會繼續講解真正有趣的內容。

4.1. Event browser + API calls.

4.1 事件瀏覽器+API呼叫

 

該工具的優點就是能夠獲得每個事件的底層資訊,事件瀏覽器就像UnityFrame Debugger的一個擴充套件,其中還包括耗時(按毫秒計)和其他提示資訊,能為衡量效率提供大量幫助。RenderDoc具有上下文敏感特徵,也就是說,不論何時你選擇一個事件,大部分其他視窗和區域將會顯示僅與該事件相關的資訊,其下的API 呼叫視窗則列出了為處理該事件而呼叫的函式。

 

 

 

RenderDoc: Event Browser + APIcalls

 

能得到這些耗時資料真的很棒!雖然它們可能不是絕對精確,但是具有相對準確性,也就是說,通過比較耗時你就能推斷出哪些操作的draw call需要優化。

 

4.2. Pipeline state.

管線狀態

 

我經常懷疑渲染管線的工作模式,精巧的幾何體是如何進入它的內部並轉換為50寸螢幕上畫素的?好吧,不幸的是,在這個工具中你也無法得到一個使用者友好的動畫以展示其工作過程。

 

但確實有些很好的免費工具可以做到。不過,不管怎樣,RenderDoc能讓你訪問事件中的大量管線狀態資訊:input assembler,頂點///幾何體/片段/計算著色器,柵格化器和輸出合併器。相信俺的判斷,RenderDoc不虛其名。

 

 

 

RenderDoc: Pipelinestate for a pixel shader

 

 

 

RenderDoc: Output Merger example

 

 

RenderDoc: Rasterizer state

4.3. Mesh output

網格輸出

 

RenderDoc的另一項特徵是可以通過頂點著色器的:輸入輸出位置,法線,紋理座標等資訊獲取事件中渲染的網格資料。實際上,你可以用它抓取並儲存3D程式或遊戲中正在渲染的網格,但在這邊文章中我就不結合案例講解了。

 

4.4. Texture viewer

紋理觀察器

 

這是我的菜,將事件中的輸入和輸出紋理進行視覺化展示。包括渲染到紋理或其他中間緩衝資料,在你使用中間緩衝進行影象效果處理時會非常有趣,此外,這也也能能節省紋理空間。

 

 

RenderDoc: Texture viewer

 

4.5. Pixel debugging

畫素級除錯

 

在幀緩衝中,錯誤渲染的壞畫素覆蓋正確畫素的情況並不少見,大多數開發者都碰到過這種噩夢,壞畫素在場景中留下的奇怪感受讓我們意識到有問題發生了,但卻不知原因何在,就是趕腳不對!如何確認我們的猜想並找出問題所在…

 

是的,以下是該問題的解法:選擇一個畫素,點選histroy(得到一個在幀緩衝中向那個畫素寫入資訊的事件序列)或者debug(除錯它的畫素著色器)。除錯這些問題需要真正的技術大牛,你需要彙編知識,並對畫素著色器的反彙編後的版本有一定了解。

 

 

 

RenderDoc: Pixel selection

RenderDoc:畫素選擇

 

 

 

RenderDoc: Pixel’s history

RenderDoc: 畫素歷史

 

RenderDoc: shader debugger

如何建立一個新場景地圖