Unity渲染管线,初探SRP
渲染管線流程圖
應用階段
主要任務:
- 手動準備好場景數據,攝像機位置,視錐體,包含哪些模型,使用哪些光源等等。
- 把不可見的物體,剔除出去
- 設置模型的渲染狀態,包括材質(漫反射顏色、高光反射顏色),使用的紋理,Shader等,還有其他
作用:輸出幾何信息,即渲染圖元,點,線,三角面等
階段:
- 把數據加載到顯存中
- 設置渲染狀態
- 調用DrawCall(渲染命令)
幾何階段
主要任務:
- 主要處理要繪制的幾何相關的事情,例如,需要繪制的圖元是什么,怎么,在哪繪制它們,幾何階段負責和每個渲染圖元打交道
- 輸出屏幕的二維頂點坐標,每個頂點對應的深度值,著色等相關信息,并傳遞給下一個階段
階段:
- 頂點著色器(可編程):頂點的空間變換和頂點著色
- 曲面細分著色器(可選):細分圖元(對于一些有大量曲面的模型,進行曲面細分可以讓曲面更加圓潤;如果為這些細分的頂點再準備一些位置信息,那么這些細分的頂點將有助于我們展現一個細節更加豐富的模型。這也是貼圖置換(Displacement Mapping)的基本思路。)
- 幾何著色器(可選):執行逐圖元的著色操作,或者用于產生更多的圖元。在這個階段,開發者可以控制GPU對頂點進行增刪改操作。幾何著色器與頂點著色器都可以對頂點的坐標進行修改,但幾何體著色器并行調用硬件困難,并行程度低,效率和頂點著色器有很大的差距;如果不是要做頂點增、刪這些僅僅能用幾何著色器實現的效果,那么還是用頂點著色器來完成吧。
- 投影:GPU將頂點從攝像機觀察空間轉換到裁剪空間(又被稱為齊次裁剪空間),為之后的剔除過程以及投射到二維平面做準備
- 裁剪(可配置):可自定義裁剪區域,比如裁剪正面還是背面,不在攝像機視野內的頂點裁剪掉,并剔除某些三角形圖元的面片
- 屏幕映射:負責每個圖元的坐標轉換到屏幕坐標系中
光柵化階段
主要任務:
- 使用上個階段傳遞的數據來產生屏幕上的像素,并渲染出最終的圖像
- 決定每個渲染圖元中的哪些像素應該被繪制在屏幕上
- 對上一個階段得到的逐頂點數據(紋理坐標、頂點顏色等)進行插值,然后在進行逐像素處理
階段:
- 三角形設置:把頂點數據收集并組裝為簡單的基本體(線、點或三角形)
- 三角形遍歷:這個過程將檢驗屏幕上的某個像素是否被一個三角形網格所覆蓋,被覆蓋的區域將生成一個片元(Fragment)
- 片元著色器(可編程):實現逐片元的著色操作
- 逐片元操作(可配置):對每個片元進行操作,將它們的顏色以某種形式合并,得到最終在屏幕上像素顯示的顏色。主要的工作有兩個:對片元進行測試(Test)并進行合并。測試步驟決定了片元最終會不會被顯示出來。在OpenGL中,主要的測試有:裁剪測試(Scissor Test)、透明度測試(Alpha Test)、模板測試(Stencil Test)以及深度測試(Depth Test)。這個階段是高度可配置的。如果一個片元通過了上面所有的測試,那它終于可以來到合并環節了。合并有兩種主要的方式,一種是直接進行顏色的替換,另一種是根據不透明度進行混合(Blend),而混合操作同樣是可配置的,程序員可以設定是把這兩種顏色進行相加、相減還是相乘等等。在經過上面的層層測試后,片元顏色就會被送到顏色緩沖區。GPU會使用雙重緩沖(Double Buffering)的策略,即屏幕上顯示前置緩沖(Front Buffer),而渲染好的顏色先被送入后置緩沖(Back Buffer),再替換前置緩沖,以此避免在屏幕上顯示正在光柵化的圖元。
- 最終屏幕圖像
SRP/URP/HDRP之間的關系
下圖是各個管線的關系圖
根據上圖所示,URP是Unity可編程渲染管線(SRP)的一種,所以了解URP之前需要先了解SRP是什么。
SRP是什么
SRP全稱為Scriptable Render Pipeline(可編程渲染管線/腳本化渲染管線),是Unity提供的新渲染系統,可以在Unity通過C#腳本調用一系列API配置和執行渲染命令的方式來實現渲染流程,SRP將這些命令傳遞給Unity底層圖形體系結構,然后再將指令發送給圖形API。
說白了就是我們可以用SRP的API來創建自定義的渲染管線,可用來調整渲染流程或修改或增加功能。
它主要把渲染管線拆分成二層:
- 一層是比較底層的渲染API層,像OpenGL,D3D等相關的都封裝起來。
- 另一層是渲染管線上層,上層代碼使用C#來編寫。在C#這層不需要關注底層在不同平臺上渲染API的差別,也不需要關注具體如何做一個Draw Call
URP是什么
它的全稱為Universal Render Pipeline(通用渲染管線), 它是Unity官方基于SRP提供的模板,它的前身是LWRP(Lightweight RP即輕量級渲染管線), 在2019.3開始改名為URP,它涵蓋了范圍廣泛的不同平臺,是針對跨平臺開發而構建的,性能比內置管線要好,另外可以進行自定義,實現不同風格的渲染,通用渲染管線未來將成為在Unity中進行渲染的基礎 。
平臺范圍:可以在Unity當前支持的任何平臺上使用
HDRP是什么
它的全稱為High Definition Render Pipeline(高清晰度渲染管線),它也是Unity官方基于SRP提供的模板,它更多是針對高端設備,如游戲主機和高端臺式機,它更關注于真實感圖形和渲染,該管線僅于以下平臺兼容:
- Windows和Windows Store,帶有DirectX 11或DirectX 12和Shader Model 5.0
- 現代游戲機(Sony PS4和Microsoft Xbox One)
- 使用金屬圖形的MacOS(最低版本10.13)
- 具有Vulkan的Linux和Windows平臺
在此文章對HDRP不過多描述。
為什么誕生SRP
內置渲染管線的缺陷
- 定制性差:過去,Unity有一套內置渲染管線,渲染管線全部寫在引擎的源碼里。大家基本上不能改動,除非是買了Unity源碼客戶,當然大部分開發者是不會去改源碼,所以過去的管線對開發者來說,很難進行定制。
- 代碼膿腫,效果效率無法做到最佳:內置渲染管線在一個渲染管線里面支持所有的二十多個平臺,包括非常高端的PC平臺,也包括非常低端的平臺,很老的手機也要支持,所以代碼越來越濃腫,很難做到使效率和效果做到最佳。
目的:
- 為了解決僅有一個默認渲染管線,造成的可配置型、可發現性、靈活性等問題。決定在C++端保留一個非常小的渲染內核,讓C#端可以通過API暴露出更多的選擇性,也就是說,Unity會提供一系列的C# API以及內置渲染管線的C#實現;這樣一來,一方面可以保證C++端的代碼都能嚴格通過各種白盒測試,另一方面C#端代碼就可以在實際項目中調整。
DrawCall Batch SetPassCalls
?在看URP 和 內置渲染管線 性能對比之前最好先了解DrawCall,Batches,SetPassCalls分別是什么值。
Set Pass Call代表渲染狀態切換,主要出現在材質不一致的時候,進行渲染狀態切換。我們知道一個batch包括,提交vbo(頂點緩沖區對象),提交ibo(索引緩沖區對象),提交shader,設置好硬件渲染狀態,設置好光源屬性等(注意提交紋理嚴格意義上并不包括在一個batch內,紋理可以被緩存并多幀復用)。如果一個batch和另一個batch使用的不是同種材質或者同一個材質的不同pass,那么就要觸發一次set pass call來重新設定渲染狀態。例如,Unity要渲染20個物體,這20個物體使用同種材質(但不一定mesh等價),假設兩次dynamic batch各自合批了10個物體,則對于這次渲染,set pass call為1(只需要渲染一個材質),batch為2(向GPU提交了兩次VBO,IBO等數據)。
Draw call嚴格意義上,CPU每次調用圖形API的渲染函數(使用OpenGL舉例,是glDrawElements或者DrawIndexedPrimitive)都算作一次Draw Call,但是對于Unity而言,它可以多個Draw Call合并成一個Batch去渲染。
真正造成開銷較大的地方,第一個在于在于切換渲染狀態,第二在于整理和提交數據。在真正的實踐過程當中,可以不用過于介意Draw call這個數字(因為沒有提交數據或者切換渲染狀態的話,其實多來幾個draw call沒什么所謂),但是Set Pass Call和Batch兩個數字都要想辦法降低。由于二者存在強相關性,那么通常降低一個,就一并可以降低第二個。
Unity提供了三種批次合并的方法,分別是Static Batching,GPU Instancing和Dynamic Batching。它們的原理分別如下:
Static Batching,將靜態物體集合成一個大號vbo提交,但是只對要渲染的物體提交其IBO。這么做不是沒有代價。比如說,四個物體要靜態批次合并前三個物體每個頂點只需要位置,第一套uv坐標信息,法線信息,而第四個物體除了以上信息,還多出來切線信息,則這個VBO會在每個頂點都包括所有的四套信息,毫無疑問組合這個VBO是要對CPU和顯存有額外開銷的。要求每一次Static Batching使用同樣的material,但是對mesh不要求相同。
Dynamic Batching將物體動態組裝成一個個稍大的vbo+ibo提交。這個過程不要求使用同樣的mesh,但是也一樣要求同樣的材質。但是,由于每一幀CPU都要將每個物體的頂點從模型坐標空間變換到組裝后的模型的坐標空間,這樣做會帶來一定的計算壓力。所以對于Unity引擎,一個批次的動態物體頂點數是有限制的。
GPU Instancing是只提交一個物體的mesh,但是將多個使用同種mesh和material的物體的差異化信息(包括位置,縮放,旋轉,shader上面的參數等。shader參數不包括紋理)組合成一個PIA(per instanced attribute實例屬性)提交。在GPU側,通過讀取每個物體的PIA數據,對同一個mesh進行各種變換后繪制。這種方式相比static和dynamic節約顯存,又相比dynamic節約CPU開銷。但是相比這兩種批次合并方案,會略微給GPU帶來一定的計算壓力。但這種壓力通常可以忽略不計。限制是必須相同材質相同物體,但是不同物體的材質上的參數可以不同。
所以Unity默認策略是優先static,其次gpu instancing,最后dynamic。當然如果頂點數過于巨大(比如渲染它幾千顆使用同種mesh的樹),那么gpu instancing或許比static batching是一個更加合適的方案。
URP 和 內置渲染管線 性能對比
主要提速的有兩個方面
1. 光照處理(包括陰影)
2. SRP Bacher (SRP 批處理)(重點)
首先來說說光照處理部分
?如上圖所示,老的渲染管線使用Multi-Pass的Forward Rendering,就是多Pass的前向渲染。最大的問題是如果要在場景里要加很多動態光的話,每一個動態光都有可能會增加一個Pass,這個動態光所影響的物體要多畫一遍。
這就導致如果游戲里想要有多個動態光的話,可能這個場景會被畫很多遍,性能會很差。它帶來的問題是所有的游戲幾乎都不會用多個動態光,因為實在太費性能了。
在過去制作移動的游戲的過程中,大家的標準做法都是烘焙Lightmap。
現在URP就解決了這個問題。實現了一個單PASS的正向渲染。可以支持多盞動態光,但是全部動態燈光都會放在一個Pass里渲染,這樣帶來的問題是要限制燈光的數量,因為每次Draw Call去畫的時候,傳給GPU的參數是有限的。
如果燈光數量特別多,參數太多,那就會無法在一次Draw Call里完成很多個燈光。所以我們有一些限制,在輕量級渲染管線LWRP里,目前是支持1盞平行光,每個對象可能只能接受4個動態光。每個攝像機也有一些限制,這是為了我們可以把所有的計算放在一個Pass里面。
接下來看看內置渲染管線和URP各種情況下的光照處理對比
以下是分別在四種情況下對比所得出的結論
結論:內置渲染管線跟只有一個平行光時比起來Batches將近增加了一倍,而URP的Batches和SetPass calls跟一個平行光時一樣,一點都沒有增加。
結論:在陰影的處理方面URP性能比內置渲染管線好很多。
URP光照處理最終結論:
1. 性能上陰影處理方面比內置渲染管線好很多。
2. URP平行光基礎上添加動態光沒有帶來額外的Batches和SetPass calls性能開銷。
接下來看看SRP Batcher(重點)
SRP Batcher?是什么
SRP Batcher 是一個底層渲染循環,可通過許多使用同一著色器變體的材質來加快場景中的 CPU 渲染速度。
就算是不同的材質球,只要是用一個著色器變體的物體都可以批處理到一塊。(在2019.3.4版本 渲染順序也會打斷批處理,這點上官網沒有說明,也許后續版本已經處理了),它的主要目的是減少渲染狀態設置的開銷,還有就是把物體屬性用專用代碼快速更新。
上面解釋都提到了變體,那么變體怎么理解呢?
multi_compile和shader_feature這兩個關鍵字就是實現著色器變體的指令。
下面是自定義的multi_compile 關鍵字。
上圖中定義了兩個multi_complie,white和black兩個關鍵字,此時Unity會生成兩個變體,一個是走white邏輯的變體,另外一個是走black邏輯的變體。然后在腳本中通過Material.EnableKeyWord和Shader.EnableKeyword來開啟某功能,通過Material.DisableKeyword和Shader.DisableKeyword來關閉某功能。為什么這么做呢?主要是為了避免分支語句(if )導致的性能下降。
。
除了關鍵字還有這些
?參數不一致的話也無法批處理到一塊。
shader_feature關鍵字跟multi_compile不一樣的是未被選擇的變體會在打包的時候被舍棄(multi_complie不會),shader_feature主要是在材質球選項上控制開關,比如
?這個選項。
URP的Lit Shader里有很多實現變體指令,所以這些Shader生成的變體也有很多。如圖:
?過去,Unity 中,可以在一幀內的任何時間修改任何材質的屬性。但是,這種做法有一些缺點。當Draw Calls使用新材質時,需要進行很多處理。場景內的材質越多,設置GPU數據所需的CPU資源就越多。解決此問題的傳統方法是減少 DrawCall 的數量以優化 CPU 渲染成本,因為 Unity 在發出 DrawCall 之前必須進行很多設置。實際的 CPU 成本來自該設置,而不是來自 GPU DrawCall 本身(DrawCall 只是 Unity 需要推送到 GPU 命令緩沖區的少量字節)。
這是官網說的提速效果:
Unity 2018引入了可編程渲染管線SRP,其中包含新的底層渲染循環SRP Batcher批處理器,它可以大幅提高CPU在渲染時的處理速度,根據場景內容的不同,提升效果為原來的1.2~4倍不等。
SRP Batcher是怎么優化的?
SRP Batcher使材質數據持久保留在 GPU 內存中。如果材質內容不變,SRP Batcher 不需要設置緩沖區并將緩沖區上傳到 GPU。還有 SRP Batcher 會使用專用的代碼路徑來快速更新大型 GPU 緩沖區中的 Unity 引擎屬性,如下圖。
?
上面的功能能解決什么問題呢?也就是CPU不需要再設置渲染狀態和一大堆渲染數據設置,只需要物體跟緩沖區的數據綁定就可以了。
SRP Batcher 正是通過批處理一系列?Bind?和?Draw?GPU 命令來減少 DrawCall 之間的 GPU 設置。
內置渲染管線和URP的CPU原理圖對比
內置渲染管線:(紅框部分就是SRP Batcher優化的性能部分)?
URP:
在把材質數據和物體數據上傳好后的流程圖:(GPU沒有詳細畫,主要看CPU)
上面流程圖中綁定的意思是大家都知道Shader里有很多變量,如紋理貼圖,Property定義的變量以及內置變量等,個人理解是把緩沖區里存的渲染數據設置給了Shader變量。
根據上面內容我們可以知道SRP Batcher并沒有減少DrawCall,而是優化了DrawCall之前的設置開銷。
SRP Batch值我們可以在Frame Debug窗口可以看得到。
SRP Batcher 兼容性
為了使 SRP Batcher 代碼路徑能夠渲染對象:
- 渲染的對象必須是mesh或者skinned mesh。該對象不能是粒子。
- 著色器必須與 SRP Batcher 兼容。HDRP 和 URP 中的所有光照和無光照著色器均符合此要求(這些著色器的“粒子”版本除外)。
為了使著色器與 SRP Batcher 兼容:
- 必須在一個名為“UnityPerDraw”的 CBUFFER 中聲明所有內置引擎屬性。例如:unity_ObjectToWorld。
- 必須在一個名為?UnityPerMaterial?的 CBUFFER 中聲明所有材質屬性。
?
Property定義的屬性也是屬于PerMaterial.
?可以在 Inspector 面板中查看著色器的兼容性狀態。
總結
以上是生活随笔為你收集整理的Unity渲染管线,初探SRP的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 高斯克吕格投影中有关带号与经度的关系
- 下一篇: 做算法,为什么建议你一定要学懂C++?