圖形渲染及優化—Unity合批技術實踐
在前面的文章中我介紹了Batch對於渲染效率的影響,這次來說說在Unity開發過程中常用的幾種合批技術。
Resource Merging
我們可以在美術資源生產的過程中做很多渲染批次方面的優化。通常我們可以將一些使用相同材質的物體模型合併成一個模型,在遊戲渲染的時候一次提交給渲染API進行繪製,降低了Draw call的數量。但是這樣帶來了一個問題,所有合併的模型必須一次全部繪製。哪怕這個模型只有一個邊邊角角在攝像機的視野裡面,引擎的場景管理系統也要將整個模型提交給渲染API進行繪製。雖然管線中後續的計算會剔除不可見三角形,但是這樣還是造成了一定的計算資源浪費。
對於場景中擺放位置非常相近的一些物體,這些物體通常可以被攝像機同時看到,我們可以通過合併模型的方法來達到降低Draw call數量的目的而不會帶來不必要的計算代價。
如果一些模型引用的材質除了Texture其他引數都相同,我們可以將這些模型引用的Texture進行合併,將它們合併成一張更大的Texture。這樣所有的模型都可以引用相同的材質,然後通過只設置一次渲染狀態進行繪製了。雖然Draw call數量沒有減少,但是避免了渲染狀態的切換,同樣達到了合批渲染的目的(原理請參看上一篇文章《圖形渲染及優化—Batch》)。這項技術稱為Texture atlasing,這是美術工作中最常用的方法,具體做法有大量內容可查。
Unity引擎內建了兩種合批渲染技術:Static batching(靜態合批)和Dynamic batching(動態合批)。
Static Batching
如果我們的遊戲場景中有一些共享同一材質的模型存在,並且這些模型一直都不會移動、旋轉和縮放。我們可以將這些模型設定為Static:
在Build的時候Unity會自動地提取這些共享材質的靜態模型的Vertex buffer和Index buffer。根據其擺放在場景中的位置等最終狀態資訊,將這些模型的頂點資料變換到世界空間下,儲存在新構建的大Vertex buffer和Index buffer中。並且記錄每一個子模型的Index buffer資料在構建的大Index buffer中的起始及結束位置。
在後續的繪製過程中,一次性提交整個合併模型的頂點資料,根據引擎的場景管理系統判斷各個子模型的可見性。然後設定一次渲染狀態,呼叫多次Draw call分別繪製每一個子模型。
Static batching減少Draw call的數量(因為是通過index buffer控制的),但是由於我們預先把所有的子模型的頂點變換到了世界空間下,並且這些子模型共享材質,所以在多次Draw call呼叫之間並沒有渲染狀態的切換,渲染API會快取繪製命令,起到了渲染優化的目的(原理請參看上一篇文章《圖形渲染及優化—Batch》) 。另外,在執行時所有的頂點位置處理不再需要進行計算,節約了計算資源。一般的遊戲場景中存在著大量的靜態物體,所以我們可以儘量將不會移動、旋轉和縮放的模型設定為“Static”來提高渲染的效率。
Static batching相比於我們前面說的,在美術資源生產階段合併模型的辦法更加靈活。我們可以在最終遊戲場景設計階段隨意修改場景,並且在Unity中構建完各個場景之後,由Unity將模型打包合併。另外如果在美術資源生產階段合併模型,後續在引擎裡渲染的時候無法剔除不可見的子模型,造成了計算資源的浪費,這點Static batching根據可見性進行渲染,相對節省了計算資源。
Static batching也會帶來一些效能的負面影響。Static batching會導致應用打包之後體積增大,應用執行時所佔用的記憶體體積也會增大。見下圖:
在很多不同的GameObject引用同一模型的情況下,如果不開啟Static batching,GameObject共享的模型會在應用程式包內或者記憶體中只存在一份,繪製的時候提交模型頂點資訊,然後設定每一個GameObjec的材質資訊,分別呼叫渲染API繪製。
開啟Static batching,在Unity執行Build的時候,場景中所有引用相同模型的GameObject都必須將模型頂點資訊複製,並經過計算變化到最終在世界空間中,儲存在最終生成的Vertex buffer中。這就導致了打包的體積及執行時記憶體的佔用增大。
所以有時我們不得不對某些GameObject避免使用Static batching來節省一些執行時記憶體空間。
Dynamic Batching
Dynamic batching是專門為優化場景中共享同一材質的動態GameObject的渲染設計的。為了合併渲染批次,Dynamic batching每一幀都會有一些CPU效能消耗。如果我們開啟了Dynamic batching,Unity會自動地將所有共享同一材質的動態GameObject在一個Draw call內繪製。
Dynamic batching的原理也很簡單,在進行場景繪製之前將所有的共享同一材質的模型的頂點資訊變換到世界空間中,然後通過一次Draw call繪製多個模型,達到合批的目的。模型頂點變換的操作是由CPU完成的,所以這會帶來一些CPU的效能消耗。並且計算的模型頂點數量不宜太多,否則CPU序列計算耗費的時間太長會造成場景渲染卡頓,所以Dynamic batching只能處理一些小模型。
目前Unity限制能進行Dynamic batching的模型最高能有900個頂點屬性。這裡注意不是900個頂點,而是900個定點屬性。如果我們在Shader中使用了Vertex Position,Normal and single UV,那麼能夠進行Dynamic batching的模型最多隻能夠有300個頂點。如果我們在Shader中使用了Vertex Position、Normal、UV0、UV1 and Tangent那麼頂點的數量就減少到180個。這個頂點屬性數量的要求是Unity自己設計的,未來隨著硬體效能的提升,這個值也會調整。
Dynamic batching是否能夠執行還有一些需要注意的地方:
1.Unity的文件中說,GameObject之間如果有映象變換不能進行合批,例如,"GameObject A with +1 scale and GameObject B with –1 scale cannot be batched together"。
2.使用Multi-pass Shader的物體會禁用Dynamic batching,因為Multi-pass Shader通常會導致一個物體要連續繪製多次,並切換渲染狀態。這會打破其跟其他物體進行Dynamic batching的機會。
3.Unity的Forward Rendering Path中如果一個GameObject接受多個光照那麼光照的繪製會按照下面的流程進行:
如果一個GameObject接受一個以上的per-pixel light那麼就會為每一個per-pixel light產生多餘的模型提交和繪製。我們可以在Quality Settings中將的per-pixel light數量限制為1.
這樣設定之後Unity通過各種條件判斷只會選擇出一個Light執行per-pixel lighting。GameObject的繪製在一個Pass內就可以完成,避免了附加的Pass,提高了可合批的概率。
4.在Unity的渲染管線中無論採用Forward Rendering或者是Deferred Rendering,半透明物體的渲染都要在最後採用Forward Rendering的方式渲染(Deferred Rendering無法支援半透明渲染)。所有的半透明物體在繪製之前要進行嚴格的排序,繪製操作按序執行。因為物體繪製的順序被嚴格限定,所以物體間能夠進行動態合批的概率很小。
5.我們知道能夠進行合批的前提是多個GameObject共享同一材質,但是對於Shadow casters的渲染是個例外。僅管Shadow casters使用不同的材質,但是隻要它們的材質中給Shadow Caster Pass使用的引數是相同的,他們也能夠進行Dynamic batching。例如,場景中我們擺放了許多的箱子,他們的材質中引用的Texture不同, 但是在做Shadow Caster Pass渲染的時候Texture會被忽略,這些箱子是可以合批渲染的。這是因為Shadow渲染如果使用ShadowMap或其他的衍生演算法,場景中的物體要繪製兩遍。第一遍繪製Shadow pass要做一次深度渲染,只提取場景中Shadow casters物體的深度值。下面是Unity文件中的一段話:
Shadow Caster Pass在做Depth buffer渲染的時候我們不需要Fragment Shader關於顏色相關的結果,僅僅一個簡單的能夠輸出深度值的Fragment Shader足以。而且對於幾乎所有物體而言使用的Shadow Caster Pass又是相同的,所以這些Shadow caster物體就可以進行Dynamic batching了。
在目前比較主流的遊戲引擎中,進行其他繪製之前都會首先執行一次Depth buffer渲染,尤其是在採用Deferred Rendering的時候。這一步不僅僅是為Shadow的繪製,更是為了後續執行其他畫素計算相關Shader的時候能夠在Early-Z階段剔除不必要執行的Fragment計算,降低填充率,提高效率。
Dynamic batching在降低Draw call的同時會導致額外的CPU效能消耗,所以僅僅在合批操作的效能消耗小於不合批,Dynamic batching才會有意義。而新一代圖形API( Metal、Vulkan)在批次間的消耗降低了很多,所以在這種情況下使用Dynamic batching很可能不能獲得性能提升。Dynamic batching相對於Static batching不需要預先複製模型頂點,所以在記憶體佔用和釋出的程式體積方面要優於Static batching。但是Dynamic batching會帶來一些執行時CPU效能消耗,Static batching在這一點要比Dynamic batching更加高效。所以我們在實踐中可以根據具體的場景靈活地平衡兩種合批技術的使用。
Static Batching In Runtime
Unity還提供了一種靈活度很高的執行時靜態合批方法。我們可以在執行時呼叫StaticBatchingUtility.Combine實現將一些模型合併成一個完整模型。
這個函式的實現有兩個版本:
// StaticBatchingUtility.Combine prepares all children of the staticBatchRoot for static batching.
// Once combined, children cannot change their Transform properties; however, staticBatchRoot can be moved.
public static void Combine(GameObject staticBatchRoot);
// StaticBatchingUtility.Combine prepares all gos for static batching.staticBatchRoot is treated as their parent.
// Once combined, gos cannot change their Transform properties; however, staticBatchRoot can be moved.
// The GameObject in gos must have MeshFilter components attached for this to work.
public static void Combine(GameObject[] gos, GameObject staticBatchRoot);
具體的使用方法上面的註釋已經說的很清楚了,這裡就不再討論了。使用這種方法我們可以避免最終打包的應用體積增大,但是由於在執行時通過CPU做模型的合併,會到來一次性的執行時記憶體和CPU開銷。
Bone Matrix Palette Batching
對於Skinned Mesh Unity通常是無法合批渲染的。但是我們可以借用Skinned Mesh的一些原理來實現對於動態GameObject的另外一種合批渲染技術。對於動態物體可能每一幀每個物體的Transform都不一樣。我們可以模仿“Hardware Skinning”的渲染方式,但是讓每個物體僅受一根骨骼影響,建立一個Transform matrix palette,其中存放的是我們打包的變換矩陣。我們將矩陣資料打包到兩個float4中:
1.Axis / Angle
2.Translate / Uniform scale
在渲染時我們充分利用Shader的暫存器空間,一次將所有同批次物體的Transform傳給Shader。渲染時頂點自動查詢其引用的Transform進行頂點變換。
通過查詢gl_MaxVertexUniformVectors可知OpenGL ES 3.0的各個實現至少應提供256個vertex uniform vector。我們假設實現只提供256個vertex uniform vector,每個物體的方位資訊需要2個float4,那麼我們最多可以一個Draw call合批渲染接近128個物體。
12年的時候我為公司的自研引擎開發物理引擎部分,當時有一個功能是Ragdoll。這個大家一定很熟悉,在很多追求真實物理效果的遊戲中都會用到。在做Ragdoll的同時嘗試了將身體各個部分肢解。是不是聽起來有點殘酷。。。
當我把身體各個部分肢解開,完全由身體繫結的剛體自由控制的時候,我突然想到每一個身體部分如果是一個原本就獨立的物體,那麼可以利用骨骼動畫技術來做一些物體合批渲染。
這項技術比Static batching節約記憶體空間,又比Dynamic batching節約運算資源,在某些合適的場景裡感興趣的朋友可以試試。其實這種合批方式的本質跟我們下面介紹的Instancing渲染很像的。
GPU Instancing
這個特性是從OpenGL ES 3.0開始支援的,Unity5.4開始加入。具體的原理這裡就不介紹了,如果介紹這個恐怕又得寫一篇。。。 不瞭解的人可以去Google一下或者看一下Unity的文件。這裡主要強調一下這個特性與Unity中的Static batching和Dynamic batching的關係:
1.Static batching的優先順序要比Instancing的優先順序高,如果一個GameObject被標記為static物體並且在Build階段成功地執行了靜態合批,那麼如果這個物體還要使用Instancing Shader渲染的話,Instancing會失效。
2.Dynamic batching的優先順序要低於Instancing。如果一個GameObject使用Instancing渲染的話,那麼對於它的Dynamic batching會失效。
Unity Batch Debugging
很多時候我們需要Profile資料來檢視我們渲染的場景的Draw call數量和Batch數量。Unity提供了Stats Pane、Profiler和Frame Debugger三個很方便的工具來幫助我們獲得Batch相關資料。
Stats Pane
切換到Game檢視,點選Stats會彈出Stats pane:
通過Stats pane我們可以獲得兩個資料:
1.Batches – 目前繪製場景可視區域的Batch數量。
2.Saved by batching – 通過合批渲染我們減少的Batch(Draw call)數量。
Profiler
通過Window -> Profiler或者Ctrl+7我們可以開啟Unity Profiler工具。點選Rendering下面會出現我們需要的資料顯示區域:
這裡我們可以獲得比Stats pane更加詳細的資料。我們能夠看到Static Batching和Dynamic Batching的具體執行情況,每種型別的合批技術處理的三角形及頂點的數量。
Frame Debugger
通過Window -> Frame Debugger我們可以開啟Unity的Frame Debugger除錯工具:
點選“Enable”按鈕就可以開始除錯場景的Frame渲染了。
通過Frame Debugger我們可以獲得比Unity Profiler更加詳細的批次渲染資料。我們可以檢視每一個批次的渲染順序,批次內合併了哪些內容,並且可以看出是什麼因素導致了合批的失敗而開啟了一個新的批次。
我釋出在這裡的文章都是轉至我的微信訂閱號,如果你想及時獲得最新發布的文章,可以關注我的微信訂閱號。