一、為什麼要使用GPU Instancing?

以往我們優化cpu的時候,為了降低Drawcall的消耗,我們通常採用靜態批處理,動態批處理等技術,然而這也是有弊端的。通常一個大的場景中,存在大量相同的植被等物件,靜態批處理後,對內存的增加是非常大的,動則就是幾十兆的內存。而動態批處理,對於合批要求挺多的,同時可能存在,動態合批消耗過大,得不償失。如果我們自己在邏輯代碼裡面進行動態合批,對於mesh的readwrite屬性是要求開啟的,這無疑也增大了內存的佔用,複雜的合批處理可能會消耗更多的cpu時間。

Unity在5.4版本及之後,新增了一項功能,那就是GPU InstancingGPU Instancing的出現,給我們提供了新的思路,對於大場景而言將所有的場景物件一次性都載入,對內存來說是很有壓力的,我們可以將這些靜態的物件如植被等全部從場景中剔除,而保存其位置、縮放、uv偏移、lightmapindex等相關信息,在需要渲染的時候,根據其保存的信息,通過Instance來渲染,這能夠減少那些因為內存原因而不能合批的大批量相同物件的渲染時間。下面這兩張圖都是同個場景下渲染多個gameobject,圖1開啟了GPU Instancing,而圖2沒有。

圖 1

圖 2

在Unite2017大會上Unity的開發工程師為我們演示了關於GPU Instancing的一些實現,但目前它只支持標準的表面instance,同時不支持lightmap、燈光探測器、陰影、裁剪等功能。這些都需要我們自己來實現。(這裡只指Unity5.6及前面的版本)

二、如何使用GPU Instancing?

首先我們來看看Unity自帶的支持標準表面著色器,通過

Create->Shader->StandardSurfaceShader(Instanced)

可以創建一個標準表面著色器(instance),下面是此著色器中的一段代碼 (PS: 我所實驗的是Unity 5.5的版本,而Unity5.6中已經沒有這個選項,同時Unity5.6在材質屬性面板中有一個Enable Instance Variants 勾選項,勾選表示支持Instance)

SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM // Physically based Standard lighting model, and enable shadows on all light types // And generate the shadow pass with instancing support #pragma surface surf Standard fullforwardshadows addshadow // Use shader model 3.0 target, to get nicer looking lighting #pragma target 3.0 // Enable instancing for this shader #pragma multi_compile_instancing // Config maxcount. See manual page. // #pragma instancing_options sampler2D _MainTex; struct Input { float2 uv_MainTex; }; half _Glossiness; half _Metallic; // Declare instanced properties inside a cbuffer. // Each instanced property is an array of by default 500(D3D)/128(GL) elements. Since D3D and GL imposes a certain limitation // of 64KB and 16KB respectively on the size of a cubffer, the default array size thus allows two matrix arrays in one cbuffer. // Use maxcount option on #pragma instancing_options directive to specify array size other than default (divided by 4 when used // for GL). UNITY_INSTANCING_CBUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color) // Make _Color an instanced property (i.e. an array) UNITY_INSTANCING_CBUFFER_END void surf (Input IN, inout SurfaceOutputStandard o) { // Albedo comes from a texture tinted by color fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * UNITY_ACCESS_INSTANCED_PROP(_Color); o.Albedo = c.rgb; // Metallic and smoothness come from slider variables o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = c.a; } ENDCG }

然後再來來看看官網文檔Vertex/Fragment著色器的例子,shader代碼如下

Shader "SimplestInstancedShader"{ Properties { _Color ("Color", Color) = (1, 1, 1, 1) } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID // necessary only if you want to access instanced properties in fragment Shader. }; UNITY_INSTANCING_CBUFFER_START(MyProperties) UNITY_DEFINE_INSTANCED_PROP(float4, _Color) UNITY_INSTANCING_CBUFFER_END v2f vert(appdata v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, o); // necessary only if you want to access instanced properties in the fragment Shader. o.vertex = UnityObjectToClipPos(v.vertex); return o; } fixed4 frag(v2f i) : SV_Target { UNITY_SETUP_INSTANCE_ID(i); // necessary only if any instanced properties are going to be accessed in the fragment Shader. return UNITY_ACCESS_INSTANCED_PROP(_Color); } ENDCG } }}

最後針對上面的Shader來解釋下其中的幾條關鍵宏。

UNITY_VERTEX_INPUT_INSTANCE_ID

用於在Vertex Shader輸入 / 輸出結構中定義一個語義為SV_InstanceID的元素。

UNITY_INSTANCING_CBUFFER_START(name) / UNITY_INSTANCING_CBUFFER_END

每個Instance獨有的屬性必須定義在一個遵循特殊命名規則的Constant Buffer中。使用這對宏來定義這些Constant Buffer。「name」參數可以是任意字元串。

UNITY_DEFINE_INSTANCED_PROP(float4, _Color)

定義一個具有特定類型和名字的每個Instance獨有的Shader屬性。這個宏實際會定義一個Uniform數組。

UNITY_SETUP_INSTANCE_ID(v)

這個宏必須在Vertex Shader的最開始調用,如果你需要在Fragment Shader裏訪問Instanced屬性,則需要在Fragment Shader的開始也用一下。這個宏的目的在於讓Instance IDShader函數裏也能夠被訪問到。

UNITY_TRANSFER_INSTANCE_ID(v, o)

在Vertex Shader中把Instance ID從輸入結構拷貝至輸出結構中。只有當你需要在Fragment Shader中訪問每個Instance獨有的屬性時才需要寫這個宏。

UNITY_ACCESS_INSTANCED_PROP(_Color)

訪問每個Instance獨有的屬性。這個宏會使用Instance ID作為索引到Uniform數組中去取當前Instance對應的數據。(這個宏在上面的shader中沒有出現,在下面我自定義的shader中有引用到)。

三、如何使用lightmap、陰影、裁剪功能?

當然首先我們還是得在我們的通道中包含指令,不然都是白搭。

#pragma multi_compile_instancing

- lightmap的支持 -

對Unity內置lightmap的獲取。我們定義兩個編譯開關,然後在自定義頂點輸入輸出結構包含lightmap的uv。

#pragma multi_compile LIGHTMAP_OFF LIGHTMAP_ON //開關編譯選項 struct v2f{ float4 pos : SV_POSITION; float3 lightDir : TEXCOORD0; float3 normal : TEXCOORD1; float2 uv : TEXCOORD2; LIGHTING_COORDS(3, 4)#ifdef LIGHTMAP_ON flost2 uv_LightMap : TEXCOORD5;#endif UNITY_VERTEX_INPUT_INSTANCE_ID}

然後在頂點函數中進行如下處理

#ifdef LIGHTMAP_ON o.uv_LightMap = v.texcoord1.xy * _LightMap_ST.xy + _LightMap_ST.zw;#endif

最後在像素函數中進行解碼處理。

DecodeLightmap函數可以針對不同的平臺對光照貼圖進行解碼。

#ifdef LIGHTMAP_ON fixed3 lm = DecodeLightmap(UNITY_SAMPLE_TEX2D(_LightMap, i.uv_LightMap.xy)); finalColor.rgb *= lm;#endif

當然我們也可以通過屬性來將lightmap傳遞給shader,這裡就不寫了。

- 陰影 -

當使用標準表面著色器時,Unity可以輕易的為我們提供陰影支持,但Vertex/fragment著色器中我們需要增加一些指令,同時還需要自己添加陰影投射通道。首先增加標籤,表示接收正向基礎光照為主光源。

Tags{ "LightMode" = "ForwardBase" }

然後增加如下指令,確保shder為所需要的通道執行正確的編譯,同時因為我們需要裡面的光照處理。

#ifdef LIGHTMAP_ON o.uv_LightMap = v.texcoord1.xy * _LightMap_ST.xy + _LightMap_ST.zw;#endif

同時在我們的輸入輸出結構中添加

LIGHTING_COORDS宏,這個宏指令定義了對陰影貼圖和光照貼圖採樣所需的參數。

LIGHTING_COORDS(3, 4)

完整的代碼如下:

pass{ Tags{ "LightMode" = "ForwardBase" } CGPROGRAM #pragma target 3.0 #pragma fragmentoption ARB_precision_hint_fastest #pragma vertex vertShadow #pragma fragment fragShadow #pragma multi_compile_fwdbase #pragma multi_compile_instancing #include "UnityCG.cginc" #include "AutoLight.cginc" #pragma multi_compile LIGHTMAP_OFF LIGHTMAP_ON //開關編譯選項 sampler2D _DiffuseTexture; float4 _DiffuseTint; float4 _LightColor0; sampler2D _LightMap;//傳進來的lightmap float4 _LightMap_ST;// struct v2f { float4 pos : SV_POSITION; float3 lightDir : TEXCOORD0; float3 normal : TEXCOORD1; float2 uv : TEXCOORD2; LIGHTING_COORDS(3, 4) #ifdef LIGHTMAP_ON flost2 uv_LightMap : TEXCOORD5; #endif UNITY_VERTEX_INPUT_INSTANCE_ID }; UNITY_INSTANCING_CBUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color) // Make _Color an instanced property (i.e. an array) UNITY_INSTANCING_CBUFFER_END v2f vertShadow(appdata_base v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, o); o.pos = mul(UNITY_MATRIX_MVP, v.vertex); o.uv = v.texcoord; o.lightDir = normalize(ObjSpaceLightDir(v.vertex)); o.normal = normalize(v.normal).xyz; #ifdef LIGHTMAP_ON //o.uv_LightMap = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw; o.uv_LightMap = v.texcoord1.xy * _LightMap_ST.xy + _LightMap_ST.zw; #endif TRANSFER_VERTEX_TO_FRAGMENT(o); return o; } float4 fragShadow(v2f i) : SV_Target { UNITY_SETUP_INSTANCE_ID(i); float3 L = normalize(i.lightDir); float3 N = normalize(i.normal); float attenuation = LIGHT_ATTENUATION(i) * 2; float4 ambient = UNITY_LIGHTMODEL_AMBIENT * 2; float NdotL = saturate(dot(N, L)); float4 diffuseTerm = NdotL * _LightColor0 * _DiffuseTint * attenuation; float4 diffuse = tex2D(_DiffuseTexture, i.uv)*UNITY_ACCESS_INSTANCED_PROP(_Color);//這裡用宏訪問Instance的顏色屬性 float4 finalColor = (ambient + diffuseTerm) * diffuse; #ifdef LIGHTMAP_ON //fixed3 lm = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, i.uv_LightMap.xy)); fixed3 lm = DecodeLightmap(UNITY_SAMPLE_TEX2D(_LightMap, i.uv_LightMap.xy)); finalColor.rgb *= lm; #endif return finalColor; } ENDCG }

有了上面的通道還不夠,那只是告訴著色器,我們能夠捕獲到其陰影所需的一切了;最後我們需要陰影投射通道

/*陰影投射需要自定義,否則不支持GPU Instance同時需要包括指令multi_compile_instancing以及在vert及frag函數中取instance id否則多個對象將得不到陰影投射*/Pass{ Tags{ "LightMode" = "ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_shadowcaster #pragma multi_compile_instancing #include "UnityCG.cginc" sampler2D _Shadow; struct v2f { V2F_SHADOW_CASTER; float2 uv:TEXCOORD2; UNITY_VERTEX_INPUT_INSTANCE_ID }; v2f vert(appdata_base v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, o);// o.uv = v.texcoord.xy; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o); return o; } float4 frag(v2f i) : SV_Target { UNITY_SETUP_INSTANCE_ID(i); fixed alpha = tex2D(_Shadow, i.uv).a; clip(alpha - 0.5); SHADOW_CASTER_FRAGMENT(i) } ENDCG}

- 裁剪 -

裁剪,我們可以通過邏輯控制來進行處理,一是場景載入策略,如四叉樹場景管理,根據當前所在區塊來決定渲染目標,二是通過當前攝像機空間來裁剪目標,這裡簡單的說下通過攝像機視錐體空間裁剪的方法(四叉樹動態場景管理網上搜索是有demo的)

bool IsCanCulling(Transform tran){ //必要時候,攝像機的視域體的計算 放置在裁剪判斷之外,避免多次坐標變換開銷,保證每幀只有一次 Vector3 viewVec = Camera.main.WorldToViewportPoint(tran.position); var far = Camera.main.farClipPlane ; var near = Camera.main.nearClipPlane; if (viewVec.x > 0 && viewVec.x < 1 && viewVec.y > 0 && viewVec.y < 1 && viewVec.z > near && viewVec.z < far) return false; else return true;}

四、C#端調用

在C#端,我們可以通過Graphics.DrawMeshInstanced 介面直接向GPU輸送繪製調用,這裡在初始化階段隨機的生成了一些位置信息,然後在每幀更新階段調用

Graphics.DrawMeshInstanced 介面進行繪製

public class testInstance : MonoBehaviour{ //草材質用到的mesh Mesh mesh; Material mat; public GameObject m_prefab; Matrix4x4[] matrix; ShadowCastingMode castShadows;//陰影選項 public int InstanceCount = 10; //樹的預製體由樹榦和樹葉兩個mesh組成 MeshFilter[] meshFs; Renderer[] renders; //這個變數類似於unity5.6材質屬性的Enable Instance Variants勾選項 public bool turnOnInstance = true; void Start() { if (m_prefab == null) return; Shader.EnableKeyword("LIGHTMAP_ON");//開啟lightmap //Shader.DisableKeyword("LIGHTMAP_OFF"); var mf = m_prefab.GetComponent<MeshFilter>(); if (mf) { mesh = m_prefab.GetComponent<MeshFilter>().sharedMesh; mat = m_prefab.GetComponent<Renderer>().sharedMaterial; } //如果一個預製體 由多個mesh組成,則需要繪製多少次 if(mesh == null) { meshFs = m_prefab.GetComponentsInChildren<MeshFilter>(); } if(mat == null) { renders = m_prefab.GetComponentsInChildren<Renderer>(); } matrix = new Matrix4x4[InstanceCount]; castShadows = ShadowCastingMode.On;//隨機生成位置與縮放 for (int i = 0; i < InstanceCount; i++) { /// random position float x = Random.Range(-50, 50); float y = Random.Range(-3, 3); float z = Random.Range(-50, 50); matrix[i] = Matrix4x4.identity; /// set default identity //設置位置 matrix[i].SetColumn(3, new Vector4(x, 0.5f, z, 1)); /// 4th colummn: set position //設置縮放 //matrix[i].m00 = Mathf.Max(1, x); //matrix[i].m11 = Mathf.Max(1, y); //matrix[i].m22 = Mathf.Max(1, z); } } void Update() { if (turnOnInstance) { castShadows = ShadowCastingMode.On; if(mesh) Graphics.DrawMeshInstanced(mesh, 0, mat, matrix, matrix.Length, props, castShadows, true, 0, null); else { for(int i = 0; i < meshFs.Length; ++i) { Graphics.DrawMeshInstanced(meshFs[i].sharedMesh, 0, renders[i].sharedMaterial, matrix, matrix.Length, props, castShadows, true, 0, null); } } } }}

五、效果展示

下面場景中使用了1023棵樹,8*1023棵草。用1023這個數是因為DrawMeshInstanced傳遞的矩陣長度為1023,而1023個mesh其實是分成3個drawcall完成的。

UnityInstance.cginc中是這麼定義的:

#define UNITY_MAX_INSTANCE_COUNT 500

所以一個drawcall只能允許最大500個實例。另外,這裡草和樹的shader是我用了js的資源,所以陰影和lightmap的我就沒增加,我用cube這個模型做的demo裏是有這方面處理的。

六、結論

在OpenGL ES3.0及以上設備中,我們完全可以使用GpuInsttance技術來更好的提升我們的遊戲性能,將更多的Cpu時間留給複雜的邏輯,比如說戰鬥等遊戲體驗要求較高的模塊;而在較舊的ES2.0的設備,我們完全可以採用現有的做法來兼容,而這時候我們可能需要做的更多的就是精簡模型,通過Lod等其他策略來進行優化。


推薦閱讀:
相關文章