动态合批、静态合批与 GPU 实例化(GPU Instancing)的本质都是通过减少 CPU 对 GPU 绘制请求(Draw Call)的次数,以达到提高性能的目的
对相于合批,GPU 实例化是相对独立的一个功能,之前有一篇 OpenGL 的文档可以参考,这篇主要记录 Unity 下如何去实现 GPU 实例化
一、再提 GPU 实例化
GPU 实例化只提交一个模型网格,然后绘制多次,每次绘制的网格属性都可以不一样:包括缩放、位置、颜色等等,即材质球虽然相同但属性可以各有各的区别
如果想要自己的 Shader 支持 GPU 实例化,需要先在对应的自定义 Shader GUI 中添加开关:
void Instancing()
{
m_MaterialEditor.EnableInstancingField();
}
之后就和 UnityStandard 一样,可以勾选材质的 Enable GPU Instancing 属性
1.1 自定义 Shader
需要添加预编译指令:
#pragma multi_compile_instancing
之后在顶点数据和片段数据中添加实例化 ID:
struct appdata_img
{
//……
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f_img
{
//……
UNITY_VERTEX_INPUT_INSTANCE_ID
};
和绘制单个对象不同的是,GPU Instancing 顶点着色器中不再只有单一的 unity_ObjectToWorld 矩阵,而是一个 unity_ObjectToWorld 矩阵数组,毕竟每个对象的位置属性必然不会一样
宏 UNITY_SETUP_INSTANCE_ID(v) 帮我们做了很多事情,其中就有拿矩阵数据去替代掉 unity_ObjectToWorld 的操作,因此在顶点着色器中要引用它,并把它放到最前面:
v2f vert(appdata v)
{
UNITY_SETUP_INSTANCE_ID(v)
v2f o;
//……
return o;
}
好了,到此为止就可以在 Game 视图中看到效果了:绘制 10000 个相同材质的简单物体只有 25 个 Batchs,这可以类比 DrawCall
然而也可以看出:5000 个物体,25个 Batches,去除天空盒之后也并不是一批渲染所有的物体:这取决于 GPU 内存缓冲区(在 Direct3D 中称为常量缓冲区)的容量限制
假设台式机 GPU 每个缓冲区的大小限制为 64KB,一个矩阵 16 x 4 = 64 个字节,算上法线转换矩阵共128字节,受于内存的2进制计量,可以得出最大批处理大小为 64000/128 = 500,渲染5000个物体需要10次批处理
- 默认情况下,UNITY_INSTANCED_ARRAY_SIZE 定义为 500,可以使用 #pragma instancing_options maxcount 编译器指令覆盖它
- 尽管台式机的最大容量为 64KB,但大多数移动设备的最大容量仅为 16KB,Unity 通过在针对 OpenGL ES 3,OpenGL Core 或 Metal 时将最大值除以四来解决此问题
上面的流程,对于每个 PASS 都是一样的,例如 Shadow Pass
1.2 混合材质与材质属性块
如果想要支持每个物体的材质属性都不同,就需要用到材质属性块:
MaterialPropertyBlock properties = new MaterialPropertyBlock();
properties.SetColor(
"_Color", new Color(Random.value, Random.value, Random.value)
);
t.GetComponent<MeshRenderer>().SetPropertyBlock(properties);
接下来是 Shader:需要了解下面几个关键宏:
- UNITY_TRANSFER_INSTANCE_ID(v, o):当需要在在片段着色器中访问每个 Instance 独有的属性时,用于在顶点着色器中将 Instance ID 从输入结构拷贝至输出结构中
- UNITY_SETUP_INSTANCE_ID(v):目的是让 Instance ID 在 Shader 函数里也能够被访问到,并且重载正确的矩阵数据(通过内部的 UnitySetupCompoundMatrices() 方法),需要在着色器的最前面调用
- UNITY_INSTANCING_CBUFFER_START(name) / UNITY_INSTANCING_CBUFFER_END(name):用于定义 Constant Buffer,每个 Instance 独有的属性必须定义在一个遵循特殊命名规则的 Constant Buffer 中
-
UNITY_DEFINE_INSTANCED_PROP(type, name):第一个参数为属性类型,第二个参数为属性名字,该宏会定义一个 Uniform 数组
-
UNITY_ACCESS_INSTANCED_PROP(bufferName, name):第一个参数为属性所在缓冲区名字,第二个参数为属性名字,该宏会使用 Instance ID 作为索引到 Uniform 数组中去取当前Instance 对应的数据
关于 UNITY_INSTANCING_CBUFFER_START,还有一个宏是 CBUFFER_START,Direct3D 11后所有着色器变量都位于 Constant Buffers(在 openGL 中是 UBO),对于 Unity 大部分的内置变量已经分组,我们自己的着色器变量也可以放在单独的 Constant Buffers 中
以颜色这个属性为例,对应的 Shader 修改如下:
1. 定义颜色属性:
UNITY_INSTANCING_BUFFER_START(TestColor)
UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
UNITY_INSTANCING_BUFFER_END(TestColor)
2. 顶点着色器:
v2f vert(appdata v)
{
UNITY_SETUP_INSTANCE_ID(v)
v2f o;
UNITY_INITIALIZE_OUTPUT(v2f, o); //将v2f变量数据初始化为零
UNITY_TRANSFER_INSTANCE_ID(v, o); //将 Instance ID 从输入结构拷贝至输出结构中
//……
}
3. 修改使用到属性的地方:
float3 GetAlbedo(v2f i)
{
float3 albedo = tex2D(_MainTex, i.uv.xy).rgb * UNITY_ACCESS_INSTANCED_PROP(TestColor, _Color).rgb;
return albedo;
}
float GetAlpha(v2f i)
{
float alpha = tex2D(_MainTex, i.uv.xy).a * UNITY_ACCESS_INSTANCED_PROP(TestColor, _Color).a;
return alpha;
}
搞定!
参考文章:
- https://zhuanlan.zhihu.com/p/356211912
- https://catlikecoding.com/unity/tutorials/rendering/part-19/
- https://zhuanlan.zhihu.com/p/34499251
- https://docs.unity3d.com/Manual/SL-BuiltinMacros.html
- https://zhuanlan.zhihu.com/p/106234207