由浅到浅入门批量渲染(三)
上回簡單總結了一下動態合批,這次我們繼續說說實例化渲染。
| 實例化渲染
當我們想要呈現這樣的場景:一片茂密的森林、廣闊的草原或崎嶇的山路時,會發現在這些場景中存在大量重復性元素:樹木、草和巖石。
仙境怕是也不過如此吧
它們都使用了相同的模型,或者模型的種類很少,比如:樹可能只有幾種;但為了做出差異化,它們的顏色略有不同,高低參差不齊,當然位置也各不相同。
使用靜態合批來處理它們(假設它們都沒有動畫),是不合適的。因為數量太多(林子大了,多少樹都有),所以合并后的網格體積可能非常大,這會引起內存的增加;而且,這個合并后的網格還是由大量重復網格組成的,不劃算。
使用動態合批來處理他們,雖然不會“合并”網格,但是仍然需要在渲染前遍歷所有頂點,進行空間變換的操作;雖然單顆樹、石頭的頂點數量可能不多,但由于數量很多,所以也會在一定程度上增加CPU性能的開銷,沒必要。
那么,對于場景中這些模型重復、數量多的渲染需求,有沒有適合的批處理策略呢?有吧,實例化渲染就是為了解決這樣的問題。
| 簡述工作原理
實例化渲染,是通過調用“特殊”的渲染接口,由GPU完成的“批處理”。
它與傳統的渲染方式相比,最大的差別在于:調用渲染命令時需要告知GPU這次渲染的次數(繪制N個)。當GPU接到這個命令時,就會連續繪制N個物體到我們的屏幕上,其效率遠高于連續調用N次傳統渲染命令的和(一次繪制一個)。
舉個例子,假設希望在屏幕上繪制出兩個顏色、位置均不同的箱子。如果使用傳統的渲染,則需要調用兩次渲染命令(DrawCall = 2),分別為:畫一個紅箱子 和 畫一個綠箱子。
兩個顏色、位置各異的箱子
如果使用實例化渲染,則只需要調用一次渲染命令(DrawCall = 1),并且附帶一個參數2(表示繪制兩個)即可。
當然,如果只是這樣,那GPU就會把兩個箱子畫在相同的位置上。所以我們還需要告訴GPU兩個箱子各自的位置(其實是轉換矩陣)以及顏色。
這個位置和顏色我們會按照數組的方式傳遞給GPU,大概這個樣子吧:
分別傳遞保存位置和顏色的數組
那接下來GPU在進行渲染時,就會在渲染每一個箱子的時候,根據當前箱子的索引(第幾個),拿到正確的屬性(位置、顏色)來進行繪制了。
一個簡單的實例化渲染流程
 ?
| Unity是如何處理實例化的
我們通過一個簡單的場景,來看一下Unity為實例化渲染做了什么。
實例化渲染兩個彩色箱子
顏色屬性通過MaterialPropertyBlock傳入
通過GPA觀察Unity做了什么。
GPA中的VertexBuffer和IndexBuffer中的信息
注:Unity默認Cube網格,包含24個頂點和36個索引。
- 頂點緩沖區Size = (Position(float3) + Normal(float3) + Tangent(float4) + TexCoord(float2) + TexCoord1(float2)) x 24 = 1344Byte
- 索引緩沖區Size = Index(ushort) x 36 = 72Byte
可見,頂點、索引緩沖區內,確實只有一個網格的數據。
那么GPU如何判斷每個Cube的繪制位置,及其顏色呢?
結合引擎為Dx平臺生成的shader(我的測試環境使用的是Pc),可以很容易找到對應的數據。
轉換矩陣及顏色被分別填入Constant Buffer中
Constant Buffer中的矩陣(Dx為行向量)
Constant Buffer中的屬性(顏色)
可見,渲染時GPU可以通過當前實例化單位的索引,二手手游賬號轉讓平臺從Buffer中獲取到對應的屬性,完成正確的繪制。
| Unity中啟用實例化渲染
當然,相比于上述無用的知識點,如何在Unity中使用實例化渲染可能更為重要。
在Unity中可以通過自動或手動的方式,啟用實例化渲染。
自動啟用實例化渲染
使用支持實例化渲染的Shader,并勾選材質球上的啟用開關,Unity便會對滿足條件的物體,自動開啟實例化渲染。
有這個選項即表示該Shader支持實例化渲染
自定義Shader
如果你希望自己的Shader也支持實例化渲染,應重點注意以下內容:
 ?
#pragma multi_compile_instancing
啟用實例化渲染(材質球上將出現啟用實例化的勾選框);
UNITY_VERTEX_INPUT_INSTANCE_ID
在a2v及v2f的結構中定義實例化索引下標(SV_InstanceID?),也就是當前渲染單位的索引,用于從Constant?Buffer中提取正確的屬性(做顯示差異化用);
UNITY_INSTANCING_BUFFER_START ~ END
在這個起止區域內定義屬性,才能在著色器中正確的根據索引提取出當前渲染單位所對應的屬性;
UNITY_SETUP_INSTANCE_ID
定義在著色器的起始位置,使頂點著色器(或片段著色器)可以正確的訪問到實例化單位的索引;
UNITY_ACCESS_INSTANCED_PROP
根據索引訪問到這個單位對應的屬性,如上面例子中每個箱子的顏色屬性。
這里只是簡述一些相對重要的內容(湊些字數),官方文檔中有更詳細內容,建議優先了解。
手動實例化渲染
使用??Graphics.DrawMeshInstanced?和?Graphics.DrawMeshInstancedIndirect?來手動執行 GPU 實例化,詳見官方文檔中的解釋,這里就不再贅述了。
| 實例化渲染的使用要求
并非所有設備都可以使用實例化渲染。
在Unity官方文檔中,列舉了各平臺支持實例化渲染的最低要求。
官方文檔中對實例化渲染的最低API支持要求
當然,我們也可以通過引擎中SystemInfo.supportsInstancing屬性來判斷環境是否支持實例化渲染。
那支持實例化渲染的機器占比大概是多少呢?由于國內大多數游戲公司都是以手游項目糊口。所以開發者可能會更多關注其在安卓平臺上的情況。
根據Android開發者的官方數據顯示,截至2020年8月30日,約88%的活躍安卓設備,都已經支持實例化渲染,所以基本上可以放心使用。
android開發者官網發布的活躍設備OpenGL ES版本占比信息
| 與靜、動態合批的差異
靜、動態合批實質上是將可以合批的對象真正的合并成一個大物體后,再通知GPU進行渲染,也就是其頂點索引緩沖區中必須包含全部參與合批對象的頂點信息;因此,可以認為是CPU完成的批處理。
 ?
實例化渲染是對網格信息的重復利用,無論最終要渲染出幾個單位,其頂點和索引緩沖區內都只有一份數據,可以認為是GPU完成的批處理。
 其實這么總結也有點問題,本質上講:動、靜態合批解決的是合批問題,也就是先有大量存在的單位,再通過一些手段合并成為批次;而實例化渲染其實是個復制的事兒,是從少量復制為大量,只是利用了它“可以通過傳入屬性實現差異化”的特點,在某些條件下達到了與合批相同的效果。
| 簡單總結靜、動態合批及實例化渲染
無論是靜態合批、動態合批或實例化渲染,本質上并無孰優孰劣,它們都只是提高渲染效率的解決方案,也都有自己適合的場景或擅長解決的問題。
個人以為:
如果你的場景中存在多數靜止的、使用了不同網格、相同材質的物體,特別是當你的相機通常只能照到一部分物體時(如第一視角),可以優先嘗試下靜態合批,通過犧牲一些內存來提升渲染效率;
針對那些運動的、網格頂點數很少、材質相同的物體,比如飛行的各種箭矢、炮彈等,使用動態合批,通過增加一些CPU處理頂點的性能開銷,來提升渲染效率,也許是不錯的選擇;
如果有大量模型相同、材質相同、或盡管表現上有一些不同,但仍然可以通過屬性來實現這些差異化的物體時,啟用實例化渲染通常可以在很大程度上提升渲染效率。
| 寫在最后
按計劃下次更新的內容應該是“優化骨骼蒙皮動畫,以及兩種常用的批量渲染方式”,但覺得內容有點多,所以將其分為兩個部分;因此,下次更新的內容變為“優化骨骼蒙皮動畫”,而“兩種常用的骨骼蒙皮動畫單位的批量渲染方式”,將作為本系列的最后一次更新內容。
總結
以上是生活随笔為你收集整理的由浅到浅入门批量渲染(三)的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 由浅到浅入门批量渲染(二)
- 下一篇: 使用LitJson进行序列化和反序列化
