渲染大量球體-優化DrawCall
支持GPU-Instance
使用材質屬性塊
LOD-Groups支持GPU-Instance
Unity 2017.1.0f3html
指示GPU繪製須要花時間;向其傳遞mesh和material屬性也要花時間。如今已知兩種節省Draw Call的方式:static和dynamic batching數組
Unity能夠將多個靜態物體的網格合併爲一個更大的靜態網格,從而減小draw call。 注意:只有使用相同材質的對象才能以這種方式組合。 這是以必須存儲更多網格數據爲代價的。 啓用動態批處理後,Unity在運行時會對視圖中的動態對象執行相同的操做。 這僅適用於小型網格物體,不然開銷將變得太大。dom
還有另外一種組合draw call的方法:GPU instance或Geometry instance。與動態批處理同樣,此操做在運行時針對可見對象。 它的目標是讓GPU一次性渲染同一網格的多個副本。 所以,它不能組合不一樣的網格或材質,但不只限於小網格。編輯器
using UnityEngine; public class GPUInstancingTest : MonoBehaviour { public Transform prefab; public int instances = 5000; public float radius = 50f; //單位圓內隨機一點並放大座標50倍,生成5000個球體 //而後查看statistics統計的draw Call信息 void Start () { for (int i = 0; i < instances; i++) { Transform t = Instantiate(prefab); t.localPosition = Random.insideUnitSphere * radius; t.SetParent(transform); } } }
使用forward render path統計到的draw call,去掉背景和camera Effect兩個draw call:ide
5000 draw call函數
可是當使用cube代替球體優化
6 draw callui
默認狀況下,GPU Instance不會開啓,必須設計shader以支持它。 即便這樣,也必須爲每種材料顯式啓用實例化。 Unity的standard着色器有一個開關。像標準着色器的GUI同樣,咱們將爲shader擴展面板建立「高級選項」部分。 能夠經過調用MaterialEditor.EnableInstancingField方法來添加切換。spa
void DoAdvanced () { GUILayout.Label("Advanced Options", EditorStyles.boldLabel); editor.EnableInstancingField(); }
僅當shader實際支持instance時,纔會顯示該切換。 咱們能夠經過將#pragma multi_compile_instancing指令添加到着色器base-pass啓用此支持。 這將爲一些關鍵字啓用着色器變體,自定義關鍵字INSTANCING_ON,其餘關鍵字也能夠。設計
#pragma multi_compile_fwdbase #pragma multi_compile_fog #pragma multi_compile_instancing
instance開關
合併了,可是顯示有錯誤
批處理數量已減小到42,這意味着如今僅用40個批處理便可渲染全部5000個球體。幀速率也高達80 fps,可是隻有幾個球體可見。錯誤緣由:雖然5000個球體仍在渲染,可是在合批中同一批次的全部球體的頂點轉換時都使用了同一個位置:它們都使用同一批次中第一個球的轉換矩陣。 發生這種狀況是由於如今同一批中全部球體的矩陣都做爲數組發送到GPU。 在不告知着色器要使用哪一個數組索引的狀況下,它始終使用第一個索引。
上述錯誤解決辦法:每一個Instance相對應的數組索引稱爲其Instance ID,GPU經過頂點數據將其傳遞到着色器的vertex程序。在大多數平臺上,它是一個無符號整數,名爲instanceID,具備SV_InstanceID語義。 咱們能夠簡單地使用UNITY_VERTEX_INPUT_INSTANCE_ID宏將其包含在咱們的VertexData結構中。 它在UnityCG中包含的UnityInstancing.cginc文件中定義。 它爲咱們提供了實例ID的正肯定義,或者在未啓用實例化時不提供任何內容。將其添加到VertexData結構。
struct VertexData { UNITY_VERTEX_INPUT_INSTANCE_ID float4 vertex : POSITION; … };
啓用instance後,咱們如今能夠在頂點程序中訪問instanceID。 有了它,咱們能夠在變換頂點位置時使用正確的矩陣。 可是,UnityObjectToClipPos函數沒有矩陣參數,它函數內部始終使用unity_ObjectToWorld矩陣。要解決此問題,UnityInstancing包含文件會使用矩陣數組的宏覆蓋unity_ObjectToWorld。 這能夠被認爲是骯髒的宏技巧,但無需更改現有着色器代碼便可工做,從而確保了向後兼容性。
要使它工做,instance的數組索引必須對全部着色器代碼全局可用。必須經過UNITY_SETUP_INSTANCE_ID宏進行手動設置,該宏必須在vertex程序最早計算,而後再執行其餘的代碼。
InterpolatorsVertex MyVertexProgram (VertexData v) { InterpolatorsVertex i; UNITY_INITIALIZE_OUTPUT(Interpolators, i); UNITY_SETUP_INSTANCE_ID(v); i.pos = UnityObjectToClipPos(v.vertex); … }
正確顯示
矩陣替換內部實現?
//UnityInstancing中的實際代碼要複雜得多。 它要處理平臺差別,其餘使用實例化的方法以及用於立 //體聲渲染的特殊代碼,從而致使間接定義的多個步驟。 它還必須從新定義UnityObjectToClipPos,因 //爲UnityCG首先包含UnityShaderUtilities。 //緩衝區宏將在後面說明。 static uint unity_InstanceID; CBUFFER_START(UnityDrawCallInfo) // Where the current batch starts within the instanced arrays. int unity_BaseInstanceID; CBUFFER_END #define UNITY_VERTEX_INPUT_INSTANCE_ID uint instanceID : SV_InstanceID; #define UNITY_SETUP_INSTANCE_ID(input) \ unity_InstanceID = input.instanceID + unity_BaseInstanceID; // Redefine some of the built-in variables / // macros to make them work with instancing. UNITY_INSTANCING_CBUFFER_START(PerDraw0) float4x4 unity_ObjectToWorldArray[UNITY_INSTANCED_ARRAY_SIZE]; float4x4 unity_WorldToObjectArray[UNITY_INSTANCED_ARRAY_SIZE]; UNITY_INSTANCING_CBUFFER_END #define unity_ObjectToWorld unity_ObjectToWorldArray[unity_InstanceID] #define unity_WorldToObject unity_WorldToObjectArray[unity_InstanceID]
每臺設備不同,最終獲得的批次數量可能與當前實驗獲得的數量不一樣。如今這狀況下,以40批渲染5000個球體實例,這意味着每批125個球體。
每一個批次都須要本身的矩陣數組。 此數據發送到GPU並存儲在內存緩衝區中,在Direct3D中稱爲常量緩衝區,在OpenGL中稱爲統一緩衝區。 這些緩衝區具備最大大小,這限制了一批中能夠容納多少個實例。 假設臺式機GPU每一個緩衝區的限制爲64KB。
一個矩陣由16個浮點數組成,每一個浮點數均爲4個字節。 所以,每一個矩陣64個字節。 每一個實例都須要一個對象到世界的轉換矩陣。 可是,咱們還須要一個世界到對象的矩陣來轉換法線向量。 所以,最終每一個實例有128個字節。 這致使最大批處理大小爲「 64000/128 = 500」,這隻能在10個批處理中渲染5000個球體。
內存單位是2進制,因此1KB表示1024字節,而不是1000。所以,'(64 * 1024)/ 128 = 512 '。UNITY_INSTANCED_ARRAY_SIZE默認定義爲500,但您可使用編譯器指令覆蓋它。例如,#pragma instancing_options maxcount:512將最大值設置爲512。可是,這將致使斷言失敗錯誤,所以實際限制爲511。到目前爲止,500和512之間沒有太大的差異。
即便假設臺式機的最大容量爲64KB成立,可是大多數移動設備的最大容量遠遠達不到64,可能僅爲16KB。 Unity經過在針對OpenGL ES 3,OpenGL Core或Metal時將最大值除以四來解決此問題。 由於我在編輯器中使用的是OpenGL Core,因此最終的最大批處理大小爲「 500/4 = 125」。
能夠經過添加編譯器指令#pragma instancing_options force_same_maxcount_for_gl來禁用此自動減小功能。 多個instance選項組合在同一指令中。 可是,這可能會致使在部署到移動設備上時發生故障,所以請當心使用。
那假設均等縮放選項呢? 能夠使用#pragma instancing_options指示全部instance對象具備統一的縮放比例。 這消除了將世界到對象矩陣用於法線轉換的須要(少存儲一個矩陣)。 設置此選項後,雖然UnityObjectToWorldNormal函數確實會更改其行爲,但它不會消除第二個矩陣數組。 所以,至少在Unity 2017.1.0中,此選項實際上沒有任何做用。
到目前爲止,一直沒有陰影。 從新打開主陰影的Soft shadow,並確保陰影距離足以包含全部球體
批處理爆炸
爲大量物體渲染陰影會增長GPU耗能。可是咱們也能夠在渲染球體陰影時使用GPU instance。在shadow caster-pass中添加instance指令;同時也增長UNITY_VERTEX_INPUT_INSTANCE_ID
and UNITY_SETUP_INSTANCE_ID
#pragma multi_compile_shadowcaster #pragma multi_compile_instancing
struct VertexData { UNITY_VERTEX_INPUT_INSTANCE_ID … }; … InterpolatorsVertex MyShadowVertexProgram (VertexData v) { InterpolatorsVertex i; UNITY_SETUP_INSTANCE_ID(v); … }
instanced 陰影
咱們僅在base-pass和shadow caster-pass中添加了instance支持。 所以,批處理不適用於其餘光源。 要驗證這一點,停用主光源並添加一些會影響多個球體的聚光燈或點光源。 不要爲它們打開陰影,由於那樣會下降幀速率。
批處理爆炸
上圖,徹底不支持多光源批處理。 要將instance與多個光源結合使用,只能切換到延遲渲染路徑。 爲此,請將所需的編譯器指令添加到着色器的延遲傳遞中。
#pragma multi_compile_prepassfinal #pragma multi_compile_instancing
多光源instance
全部批處理都有一個限制:它們僅限於具備相同材料的對象。 當咱們但願渲染的對象具備多樣性時,此限制就會成爲問題。
隨機改變球體的顏色
void Start () { for (int i = 0; i < instances; i++) { Transform t = Instantiate(prefab); t.localPosition = Random.insideUnitSphere * radius; t.SetParent(transform); t.GetComponent<MeshRenderer>().material.color = new Color(Random.value, Random.value, Random.value); } }
球體與隨機的顏色,沒有批量和陰影
即便咱們爲物料啓用了批處理,它也再也不起做用。因爲每一個球體如今都有本身的材質,所以每一個球體的着色器狀態也必被更改。 這顯示在統計面板中爲SetPass call 數量。它曾經是全部領域的一體機,可是如今是5000。
除了爲每一個球體建立新的材質實例外,咱們還可使用材質屬性塊。 這些是小的修改,設置屬性塊的顏色並將其傳遞給球體的渲染器,而不是直接分配材質的顏色。
void Start () { MaterialPropertyBlock properties = new MaterialPropertyBlock(); for (int i = 0; i < instances; i++)
{ Transform t = Instantiate(prefab); t.localPosition = Random.insideUnitSphere * radius; t.SetParent(transform);// MaterialPropertyBlock properties = new MaterialPropertyBlock();properties.SetColor
( "_Color", new Color(Random.value, Random.value, Random.value) ); t.GetComponent<MeshRenderer>().SetPropertyBlock(properties); } }
渲染instance對象時,Unity經過將數組傳遞到GPU內存來使轉換矩陣可用於GPU。 Unity對存儲在材料屬性塊中的屬性執行相同的操做。 但這要起做用,咱們必須在shader中定義一個適當的緩衝區。
聲明instance緩衝區的工做相似於建立諸如插值器之類的結構,可是確切的語法因平臺而異。 咱們可使用UNITY_INSTANCING_CBUFFER_START和UNITY_INSTANCING_CBUFFER_END宏來解決差別。 啓用實例化後,它們將不執行任何操做。
將_Color變量的定義放在instance緩衝區中。 UNITY_INSTANCING_CBUFFER_START宏須要一個名稱參數。 實際名稱可有可無。 宏以UnityInstancing_爲其前綴,以防止名稱衝突。
UNITY_INSTANCING_CBUFFER_START(InstanceProperties)
float4 _Color;
UNITY_INSTANCING_CBUFFER_END
像變換矩陣同樣,啓用instance後,顏色數據做爲數組上傳到GPU。UNITY_DEFINE_INSTANCED_PROP宏會爲咱們處理正確的聲明語法。
UNITY_INSTANCING_CBUFFER_START(InstanceProperties) //float4 _Color; UNITY_DEFINE_INSTANCED_PROP(float4, _Color) UNITY_INSTANCING_CBUFFER_END
最後要訪問fragment程序中的數組,咱們還須要在其中知道instanceID。 所以,將其添加到插值器結構中。
struct InterpolatorsVertex { UNITY_VERTEX_INPUT_INSTANCE_ID … }; struct Interpolators { UNITY_VERTEX_INPUT_INSTANCE_ID … };
在vertex頂點程序中,將ID從頂點數據複製到插值器。 啓用實例化時,UNITY_TRANSFER_INSTANCE_ID宏定義此簡單操做,不然不執行任何操做。
InterpolatorsVertex MyVertexProgram (VertexData v) { InterpolatorsVertex i; UNITY_INITIALIZE_OUTPUT(Interpolators, i); UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, i); … }
在片斷程序的開頭,使ID全局可用,就像在頂點程序中同樣。
FragmentOutput MyFragmentProgram (Interpolators i) { UNITY_SETUP_INSTANCE_ID(i); … }
如今,咱們必須在不使用instance時以_Color的形式訪問顏色,而在啓用實例化時以_Color [unity_InstanceID]的形式訪問顏色。 使用UNITY_ACCESS_INSTANCED_PROP宏可同時支持上述兩種訪問。
float3 GetAlbedo (Interpolators i) { float3 albedo = tex2D(_MainTex, i.uv.xy).rgb * UNITY_ACCESS_INSTANCED_PROP(_Color).rgb; … } float GetAlpha (Interpolators i) { float alpha = UNITY_ACCESS_INSTANCED_PROP(_Color).a; … }
新版本若是編譯有錯誤: 從2017.3及以上版本, UNITY_ACCESS_INSTANCED_PROP macro改了.它須要兩個參數:buffer名,顏色名使用UNITY_ACCESS_INSTANCED_PROP(InstanceProperties, _Color).
如今,咱們的顏色隨機的球再次被批處理。 咱們能夠用相同的方式使其餘屬性可變。 對於顏色,浮點數,矩陣和四份量浮點向量,這是可能的。 若是要改變紋理,可使用單獨的紋理數組,並將索引添加到實例化緩衝區。其餘屬性修改相似。
能夠在同一個緩衝區中組合多個屬性,但要牢記大小限制。 還應注意,緩衝區被劃分爲32位塊,所以單個浮點數須要與向量相同的空間。 您也可使用多個緩衝區,可是也有一個限制,它們不是免費提供的。 啓用instance後,每一個要緩衝的屬性都將成爲一個數組,所以僅對須要根據instance變化的屬性執行此操做。
咱們的陰影也取決於顏色。 調整shader陰影以便每一個實例也能夠支持惟一的顏色。
//float4 _Color; UNITY_INSTANCING_CBUFFER_START(InstanceProperties) UNITY_DEFINE_INSTANCED_PROP(float4, _Color) UNITY_INSTANCING_CBUFFER_END … struct InterpolatorsVertex { UNITY_VERTEX_INPUT_INSTANCE_ID … }; struct Interpolators { UNITY_VERTEX_INPUT_INSTANCE_ID … }; float GetAlpha (Interpolators i) { float alpha = UNITY_ACCESS_INSTANCED_PROP(_Color).a; … } InterpolatorsVertex MyShadowVertexProgram (VertexData v) { InterpolatorsVertex i; UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, i); … } float4 MyShadowFragmentProgram (Interpolators i) : SV_TARGET { UNITY_SETUP_INSTANCE_ID(i); … }
void Start () { MaterialPropertyBlock properties = new MaterialPropertyBlock(); for (int i = 0; i < instances; i++) { Transform t = Instantiate(prefab); t.localPosition = Random.insideUnitSphere * radius; t.SetParent(transform); //MaterialPropertyBlock properties = new MaterialPropertyBlock(); properties.SetColor ( "_Color", new Color(Random.value, Random.value, Random.value) ); //t.GetComponent<MeshRenderer>().SetPropertyBlock(properties); MeshRenderer r = t.GetComponent<MeshRenderer>(); if (r) { r.SetPropertyBlock(properties); } else {
//對LOD子對象設置顏色 for (int ci = 0; ci < t.childCount; ci++) { r = t.GetChild(ci).GetComponent<MeshRenderer>(); if (r) { r.SetPropertyBlock(properties); } } } } }
不幸的是沒有有效的批處理。Unity可以對以相同的LOD顏色球體進行批處理,可是若是能夠像往常同樣進行批處理會更好。 咱們能夠經過用緩衝數組替換unity_LODFade來實現。能夠經過爲支持實例化的每一個過程添加lodfade實例化選項來指示Unity的着色器代碼執行此操做。
#pragma multi_compile_instancing #pragma instancing_options lodfade
instance LOD fading