UnityShader33:GPU 实例化

动态合批、静态合批与 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次批处理

  1. 默认情况下,UNITY_INSTANCED_ARRAY_SIZE 定义为 500,可以使用 #pragma instancing_options maxcount 编译器指令覆盖它
  2. 尽管台式机的最大容量为 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;
}

搞定!

参考文章:

猜你喜欢

转载自blog.csdn.net/Jaihk662/article/details/118805517