Introduction
使用GPU Instancing可以一次渲染(render)相同網格的多個副本,僅使用少量DrawCalls。在渲染諸如建築、樹木、草等在場景中重復出現的事物時,GPU Instancing很有用。
每次draw call,GPU Instancing只渲染相同(identical )的網格,但是每個實例(instance)可以有不同的參數(例如,color或scale),以增加變化(variation),減少重復的出現。
GPU Instancing可以減少每個場景draw calls次數。這顯著提升了渲染性能。
Adding instancing to your Materials
為Materials啟用GPU Instancing:在Project window中選中你的Material,然后在Inspector窗口中,勾選Enable Instancing復選框。
Unity只會在Material使用的Shader支持GPU Instancing時,才會顯示這個復選框。這些支持GPU Instancing的Shaders包括:Standard, StandardSpecular 及所有的surface Shaders。更多信息,請查看standard Shaders。
下面的兩個屏幕截圖展示了具有多個GameObjects的游戲場景。上面的開啟了GPU Instancing,下面的沒有開啟。注意兩者中FPS,Batches和Saved by batching的區別。
With GPU Instancing: A simple Scene that includes multiple identical GameObjects that have GPU Instancing enabled
No GPU Instancing: A simple Scene that includes multiple identical GameObjects that do not have GPU Instancing enabled.
當你使用GPU instancing時,受到下面的限制約束:
- Unity自動為instancing調用選擇MeshRenderer組件和
Graphics.DrawMesh
。注意不支持SkinnedMeshRenderer。 - 在一次GPU instancing draw call中,Unity僅對共享相同Mesh和相同Material的GameObjects進行batching處理。為了得到更好的instancing效果,盡量使用少量Meshes和Materials。至於如何增加“變化”(variations),可以在你的shader代碼中添加per-instance數據(更多細節,請繼續往下看)。
你也可以在c#腳本中調用Graphics.DrawMeshInstanced和Graphics.DrawMeshInstancedIndirect來完成GPU instancing。
GPU Instancing在下面平台和APIs上可用:
- DirectX 11 and DirectX 12 on Windows
- OpenGL Core 4.1+/ES3.0+ on Windows, macOS, Linux, iOS and Android
- Metal on macOS and iOS
- Vulkan on Windows, Linux and Android
- PlayStation 4 and Xbox One
- WebGL (requires WebGL 2.0 API)
Adding per-instance data
默認情況下,Unity在每個instanced draw call中只對具有不同Transforms的GameObjects實例進行batching處理。為了向你的instanced GameObjects添加更多變化(variance),修改你的Shader,添加per-instance屬性(properties)例如材質顏色(material color)。
下面的例子展示了怎樣為每一個instance創建一個具有不同顏色值(color values)的instanced Shader。
Shader "Custom/InstancedColorSurfaceShader" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows
// Use Shader model 3.0 target
#pragma target 3.0
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)
void surf (Input IN, inout SurfaceOutputStandard o) {
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
當你聲明_Color
為instanced屬性(實例屬性)時,Unity將從設置在各個GameObjects上的MaterialPropertyBlock類型的對象處獲取_Color
的值,然后把它們(這些GameObjects)放到一個單獨的draw call中。對應的C#代碼如下:
MaterialPropertyBlock props = new MaterialPropertyBlock();
MeshRenderer renderer;
foreach (GameObject obj in objects)
{
float r = Random.Range(0.0f, 1.0f);
float g = Random.Range(0.0f, 1.0f);
float b = Random.Range(0.0f, 1.0f);
props.SetColor("_Color", new Color(r, g, b));
renderer = obj.GetComponent<MeshRenderer>();
renderer.SetPropertyBlock(props);
}
注意在普通情形下(沒有使用instancing sahder或_Color
不是一個per-instance屬性),draw call batches為被破壞,因為在MaterialPropertyBlock中有不同的值。
為了使這些變化生效,你必須啟用GPU Instancing。為了此,中Project window中選中你的材質,然后在Inspector窗口中勾選Enable Instancing復選框。
Adding instancing to vertex and fragment Shaders
下面的例子使用了一個簡單的unlit Shader 並使它能夠用不同的顏色instancing。
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_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)
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(Props, _Color);
}
ENDCG
}
}
}
Shader modifications
Addition | Function |
---|---|
#pragma multi_compile_instancing | 用來指導Unity生成instancing variants。在surface Shaders中不需要 |
UNITY_VERTEX_INPUT_INSTANCE_ID | 用來在vertex Shader的input/output結構體中定義一個instance ID。更多信息查看SV_InstanceID 。 |
UNITY_INSTANCING_BUFFER_START(name) / UNITY_INSTANCING_BUFFER_END(name) | 每一個per-instance屬性必須定義 在一個特定的命名的constant buffer。用這兩對宏來包裝那些你想在不同實例中值不同的屬性 |
UNITY_DEFINE_INSTANCED_PROP(float4, _Color ) |
使用給定的type和name來定義一個per-instance Shader屬性 |
UNITY_SETUP_INSTANCE_ID(v); | 用來使instance ID在Shader函數中可以被訪問。必須用在vertex Shader的最開始處。對於fragment Shaders是可選的 |
UNITY_TRANSFER_INSTANCE_ID(v, o); | 用來在vertex Shader中把instance ID從input structure拷貝到output structure。僅在你需要在fragment Shader中說per-instance數據時使用。 |
UNITY_ACCESS_INSTANCED_PROP(arrayName, color) | 用來訪問一個聲明在某個instancing constant buffer中的per-instance Shader屬性。它使用一個instance ID來索引到instance data array中。宏中的arrayName 必須與UNITY_INSTANCING_BUFFER_END(name)中的name 相匹配。 |
注意:
- 當用例多個per-instance屬性時,你不必把它們都填充到MaterialPropertyBlocks。
- 如果一個實例(instance)缺少某個per-instance屬性時,Unity會使用對應的Material中的默認值。如果沒有默認值,Unity使0值填充。不要把non-instanced屬性放在MaterialPropertyBlock中,因為這會禁用instancing。相反地,為它們創建別的Materials。
Advanced GPU instancing tips
Batching priority
在批處理(batching)時,Unity將靜態批處理(Static batching)優先於instancing。如果你把某個GameObject標記為使用static batching,並且Unity成功批處理了它,Uinty會在這個GameObject上禁用instancing,即使它的Renderer使用了instancing Shader。這種情況發生時,Inspector 窗口會顯示一條警告,建議你禁用Static Batching。為此,打開Player設置(Edit > Project Settings),然后選擇Player分類,打開Other Settings(for your platform),然后在Rendering區域內,禁用Static Batching設置。
Unity使instancing的優先級高於dynamic batching。如果Unity可以instance一個Mesh,就會禁用在這個Mesh上dynamic batching。
Graphics.DrawMeshInstanced
某些因素可能阻止GameObjects被自動地instanced。這些因素包括材質變化和深度排序。使用Graphics.DrawMeshInstanced可以強制Unity使用GPU instancing來繪制這些物體。和Graphics.DrawMesh相似,這個函數繪制一幀的網格,不需要創建不必要的GameObjects。
Graphics.DrawMeshInstancedIndirect
在腳本中使用DrawMeshInstancedIndirect
從一個compute buffer中讀取instancing draw calls的參數,包括instances的數目。對於填充GPU的所有instance data,這是有用的,而且CPU不知道 要繪制的instances的數目(例如,什么時候完成GPU culling)。更多相關解釋和例子,請看Graphics.DrawMeshInstancedIndirect的API 文檔。
Global Illumination support
自從Unity 2018.1,GPU Instancing開始使用light probes, occlusion probes(in Shadowmask mode)和lightmap STs來支持全局光照( Global Illumination, GI)渲染。Standard shaders和surface shaders自動開啟GI 支持。
受烘焙到場景中的light probes和occlusion probes影響的dynamic renderers和烘焙到相同lightmap貼圖的static renderers 可以通過Forward and Deferred 渲染循環(render loop)使用GPU Instancing,自動地被批處理(batched)。
原文:Dynamic renderers affected by light probes and occlusion probes baked in the scene, and static renderers baked to the same lightmap texture, can be automatically batched together using GPU Instancing by Forward and Deferred render loop.
對於Graphics.DrawMeshInstanced,你可以通過設置LightProbeUsage
參數為CustomProvided
來啟用light probe和occlusion probe渲染,然后提供一個拷貝了probe data的MaterialPropertyBlock。查看關於 LightProbes.CalculateInterpolatedLightAndOcclusionProbes的文檔。
Global Illumination and GPU Instancing
Unity中,GPU Instancing支持GI渲染。每個GPU instance可以支持來自不同的Light Probes,某個lightmap(可以用多個atlas regions),或者某個Light Probe Proxy Volume組件(baked for包含所有instances的空間體(space volume))。Standard shaders和surface shaders默認開啟支持。
原文: GPU Instancing supports Global Illumination (GI) rendering in Unity. Each GPU instance can support GI coming from either different Light Probes, one lightmap (but multiple atlas regions in that lightmap), or one Light Probe Proxy Volume component (baked for the space volume containing all the instances). Standard shaders and surface shaders come with this support enabled.
通過Forward和Deferred渲染循環,你可以使用GPU Instancing來自動地批處理(batch) 受到baked Light Probes(包括它們的occlusion data)影響的dynamic Mesh Renderers,或者static Mesh Renderers (baked to the same lightmap Texture)。
對於Graphics.DrawMeshInstanced,你可以通過設置LightProbeUsage
參數為CustomProvided
來啟用light probe和occlusion probe渲染,然后提供一個拷貝了probe data的MaterialPropertyBlock。查看關於 LightProbes.CalculateInterpolatedLightAndOcclusionProbes的文檔。
另外一種選擇是,你可以向Graphics.DrawMeshInstanced
傳遞一個LPPV 組件的引用和 LightProbeUsage.UseProxyVolume 。當你這樣做時,所有的instances采樣Light Probe數據的L0和L1 bands的volume。
原文: Alternatively, you can pass an LPPV component reference and LightProbeUsage.UseProxyVolume to Graphics.DrawMeshInstanced. When you do this, all instances sample the volume for the L0 and L1 bands of the Light Probe data. Use MaterialPropertyBlock if you want to supplement L2 data and occlusion data. For more information, see Light Probes: Technical Information.
Shader warming-up
從Unity 2017.3開始,如果你想在着色器第一次渲染時獲得絕對平滑的渲染效果,你需要預熱(warm up)着色器來在OpenGL上使用instancing。若你在一個不要求shader預熱的平台上為instancing預熱shaders,什么都不會發生。
更多信息查看ShaderVariantCollection.WarmUp和Shader.WarmupAllShaders。
UnityObjectToClipPos
當寫shader時,總是用UnityObjectToClipPos(v.vertex)
而不是mul(UNITY_MATRIX_MVP,v.vertex)
。
盡管你可以繼續在instanced Shaders中正常使用UNITY_MATRIX_MVP
,然而UnityObjectToClipPos
是把頂點位置從對象空間(object sapce)轉換到裁剪空間(clip space)的最高效的方式。
Further notes
- Surface Shaders默認有instancing varianats產生,除非你在
#pragma
surface directive中指定noinstancing
。Standard和StandardSpecular Shaders支持instancing,但是除了transforms外沒有別的per-instance屬性。在surface Shader中,Unity忽略#pragma multi_compile_instancing
的使用。 - 場景中沒有任何GameObject啟用GPU Instancing時,Unity剝離(strips)instancing variants。為了覆蓋剝離行為,打開Graphics設置(菜單: Edit > Project Settings,然后選擇Graphics分類),找到Shader stripping修改Instancing Variants。
(待續...)
參考:
首次發表於我的知乎專欄:https://zhuanlan.zhihu.com/p/70123645