Batching

  • Static Batching
    • Static Batching会在Build阶段提取多个使用相同材质的,且不会移动,旋转和缩放模型的Vertex buffer和Index buffer。
    • 然后将顶点数据变换到世界空间下,存储到最终Static Batching所使用的Vertex buffer中,并记录每个子模型的Index buffer在Static Batching所使用的Index buffer中的位置。
    • 在绘制阶段,会一次性提交合并后的模型顶点数据,最终引擎会根据子模型的可见性确定需要绘制的子模型,设置一次渲染状态,再调用多次Draw Call分别绘制子模型。
    • 所以Static Batching并没有减少Draw Call的数量,但在绘制阶段避免了多次数据提交和渲染状态的切换。
  • Dynamic Batching
    Dynamic Batching会在运行时将使用同一材质的模型进行合并渲染,也就是将符合条件的GameObject放在一个Draw Call中绘制。使用Dynamic Batching有一些限制:
    • 模型最高900个顶点属性,300个顶点。假如我们的Shader中每个顶点使用了Position, Normal, UV,那么模型只能有300个顶点。如果在Shader中使用了Position, Normal, UV0, UV1, Tangent,那么顶点数就要减少到180个(5*180=900)。
    • GameObject之间有镜像变换的不能进行合批
    • 使用Multi-pass Shader的GameObject禁止合批
    • 拥有lightmap的对象,无法合批
    • GameObject接收实时阴影无法合批
    • Dynamic Batching在降低Draw Call的同时会导致额外的CPU性能消耗,所以仅在合批操作的性能消耗小于不合批,Dynamic Batching才有意义。

GPUInstance

介绍

GPU Instancing是指由GPU和图形API支持的,用一个DrawCall同时绘制多个具有相同网格物体的技术。假如现在有一个包含大量模型的场景,而这些模型的网格数据都一样,不同的仅仅是世界空间下坐标不同。如果按照正常的渲染流程,DrawCall次数是和物件数量相同的,随着物件数量的上升CPU往GPU上传的数据就会越来越多,很快就会遇到性能的瓶颈。
使用GPU Instancing技术时,数据的上传是一次性打包上传至GPU的,紧接着调用GPU和图形的API利用这些数据绘制多个物件。Unity中的具体实现步骤如下:

  • 将Per-Instance Data(世界矩阵,颜色等自定义变量)打包成Uniform Array,存储在Instance Constant Buffers中
  • 对于可以使用Instancing的Batch,调用各平台图形API的Instancing DrawCall,为每个Instance生成一个不同的SV_InstanceID
  • 在Shader中使用SV_InstanceID作为Uniform Array的索引来获取当前Instance的Per-Instance Data
    GPU Instancing技术并不是总能提高性能的,如果场景中有大量使用相同材质和相同网格的物体并性能问题是由DrawCall次数过多导致的,这时使用GPU Instancing可以得到不错的性能提升。在实际的游戏项目中植被和树木是最适合使用的。这里要注意的是GPU Instancing是通过减少DrawCall来降低CPU开销的,但这同事也会为GPU带来额外的开销。适合的才是最好的,切勿沉迷性能优化无法自拔。

流程

Shader支持GPUInstance 并且启用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
 Shader "Goat/LightMap/CutoutInstanceLightmap"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
[NoScaleOffset]_MainTex ("MainTex", 2D) = "white" {}
_LightmapST("LightmapST",Vector)=(0,0,0,0)
_AlphaCutOff ("AlphaCutOff", Range(0, 1)) = 0.5
}
SubShader
{
Tags {
"RenderType"="TransparentCutout"
"Queue"="AlphaTest"
"IgnoreProjector"="True"
}

Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#pragma multi_compile_instancing


#pragma multi_compile __ DIRE
#pragma multi_compile __ BAKE_SHADOW


#include "UnityCG.cginc"

struct appdata
{
UNITY_VERTEX_INPUT_INSTANCE_ID
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 uv1 : TEXCOORD1;
#if DIRE
float4 normal : NORMAL;
#endif
};

struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
float2 lightUV : TEXCOORD2;
#if DIRE
float4 normal : TEXCOORD3;
#endif
};

UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(fixed4, _LightmapST)
UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)

sampler2D _MainTex;
float _AlphaCutOff;

#if DIRE
sampler2D LightmapInd;
#endif

v2f vert (appdata v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v)
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;

#if DIRE
o.normal = v.normal;
#endif

fixed4 lightmapST = UNITY_ACCESS_INSTANCED_PROP(Props, _LightmapST);
o.lightUV = v.uv1 * lightmapST.xy + lightmapST.zw;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 prop_color = UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
fixed4 col = tex2D(_MainTex, i.uv) * prop_color;
clip(col.a-_AlphaCutOff);

half4 bakedColorTex = UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightUV);
float3 lightMap = DecodeLightmap(bakedColorTex);

#if DIRE
half3 world_normal = UnityObjectToWorldNormal(i.normal);
// 如果使用了定向的光照贴图技术,则从贴图获取定向的入射辐射度方向
fixed4 bakedDirTex = tex2D(LightmapInd, i.lightUV.xy);
//return bakedDirTex;

// 从定向光照贴图中获取对应的纹素颜色作为间接照明部分的漫反射颜色
lightMap = DecodeDirectionalLightmap (lightMap, bakedDirTex, world_normal);
#endif

col.rgb *= lightMap;

UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDHLSL
}
}
}



限制

  • 使用Lightmap的物体无法使用Instancing
  • 受不同Light Probe / Reflection Probe影响的物体无法使用Instancing
  • 使用包含多个Pass的Shader物体,只有第一个Pass可以Instancing
  • 前向渲染时,受多个光源影响的物体只有Base Pass可以Instancing, Add Passes不行
  • Instancing 适用于MeshRenderer组件和Graphics.DrawMesh()
  • 需要物件使用相同的Material和Mesh
  • 需要把Shader改成Instanced的版本
  • 当所有条件均满足的情况下,Instancing是自动进行的,并且优先级高于 Static/Dynamic Batching

细节

GPU Instancing是指由GPU和图形API支持的,用一个DrawCall同时绘制多个具有相同网格物体的技术。假如现在有一个包含大量模型的场景,而这些模型的网格数据都一样,不同的仅仅是世界空间下坐标不同。如果按照正常的渲染流程,DrawCall次数是和物件数量相同的,随着物件数量的上升CPU往GPU上传的数据就会越来越多,很快就会遇到性能的瓶颈。
使用GPU Instancing技术时,数据的上传是一次性打包上传至GPU的,紧接着调用GPU和图形的API利用这些数据绘制多个物件。Unity中的具体实现步骤如下:

  • 将Per-Instance Data(世界矩阵,颜色等自定义变量)打包成Uniform Array,存储在Instance Constant Buffers中
  • 对于可以使用Instancing的Batch,调用各平台图形API的Instancing DrawCall,为每个Instance生成一个不同的SV_InstanceID
  • 在Shader中使用SV_InstanceID作为Uniform Array的索引来获取当前Instance的Per-Instance Data
  • GPU Instancing技术并不是总能提高性能的,如果场景中有大量使用相同材质和相同网格的物体并性能问题是由DrawCall次数过多导致的,这时使用GPU Instancing可以得到不错的性能提升。在实际植被和树木是最适合使用的。这里要注意的是GPU Instancing是通过减少DrawCall来降低CPU开销的,但这同事也会为GPU带来额外的开销。
1
2
3
4
5
6
7
UNITY_VERTEX_INPUT_INSTANCE_ID和UNITY_SETUP_INSTANCE_ID。
Unity中Instancing相关的指令都定义在Unity安装目录\Editor\Data\CGIncludes\UnityInstancing.cgine文件中。
////////////////////////////////////////////////////////
// basic instancing setups
- UNITY_VERTEX_INPUT_INSTANCE_ID //Declare instance ID field in vertex shader input / output struct.

- UNITY_SETUP_INSTANCE_ID //Should be used at the very beginning of the vertex shader / fragment shader, so that succeeding code can have access to the global unity_InstanceID.Also procedural function is called to setup instance data.
  • UNITY_VERTEX_INPUT_INSTANCE_ID:在Shader输入/输出结构体中 声明一个Instance ID。

  • UNITY_SETUP_INSTANCE_ID:这个宏必须在Vertex Shader或Fragment Shader的一开始就调用,只有调用了这个宏以后,才可以在Shader中通过全局的InstanceID来访问到结构体数据。