Unity3D引擎的多例化技术
多例化技术概述
假如需要绘制有很多模型的场景,而大部分模型都是使用的同一个模型,即他们使用同一组顶点数据,在渲染时指定不同的世界坐标,绘制在不同的位置上。例如成千上万的小草,每颗小草都需要调用一次drawcall,此时性能瓶颈在CPU往GPU发送数据次数过多,因为这些操作都是在相对缓慢的CPU到GPU总线上进行的。
如果能够一次性将数据发送到GPU,然后使用一个绘制函数让渲染流水线利用这些数据绘制多个相同的物体将大大提升性能,这种技术就是GPU多例化(GPU instancing)技术。DirectX和OpenGL等渲染流水线已经实现了这个功能。Unity3D引擎在此基础上进行包装,使得在每个平台上都能使用同一套代码使用GPU多例化技术。以Direct3D 11平台为例,当准备好顶点数据、设置好顶点缓冲区后,接下来进入输入组装阶段(input assemble stage)。输入组装阶段时使用硬件实现的。此阶段根据用户输入的顶点缓冲区信息、图元拓扑(primitive topology)结构信息和描述顶点布局(vertice layout)格式信息,把顶点组装成图元,然后发送给顶点缓冲区。
GPU多例化的思想,就是把每个实例的不同信息存储在缓冲区(可能是顶点缓冲区,也可能是存储着着色器uniform变量的常量缓冲区)中,然后直接操作缓冲区的数据来设置。而不用多例化技术,每次调用都是很相似的,除了在设置世界矩阵时,无非就是更新着色器常量缓冲区的某个uniform变量,这里又是一次CPU到GPU的数据传递操作。既然可以在定义顶点信息结构体时指定每个顶点的法线、切线和纹理映射坐标信息,那完全也可以为每个顶点增加一个描述世界矩阵的属性,无非就是在顶点信息结构体中多加一个float4x4类型的属性变量而已。
假设需要渲染100个相同的模型,每个模型有256个三角形,那么需要两个缓冲区。一个用来描述顶点的模型信息,因为带渲染的模型是相同的,所以这个缓冲区只存储了256个三角形(如果不用任何的优化组织方式,则有768个顶点);另一个就是用来描述模型在世界坐标系下的位置信息,如果不考虑旋转和缩放,100个模型即占用100个float3类型的存储空间。
如何在材质中启用多例化技术
要在Unity3D中启用多例化技术,首先应在材质文件的Inspector面板中选中Enable Instancing复选框。
必须要注意的是,只有材质文件使用的着色器文件中声明了支持GPU多例化技术,材质文件的Inspector面板中才会出现Enable Instacing复选框。引擎提供的Standard着色器、StandardSpecular着色器及所有的外观着色器都支持GPU多例化技术。下图展示了使用与未使用GPU多例化技术的效果和性能比较,可见使用了GPU多例化技术后性能大幅提升。
添加逐实例数据
每一次多例化绘制调用(instanced draw call)时,默认地,Unity3D仅对使用了同一网格材质,但是有着不同的位置变换信息的游戏对象进行批次化。为了让GPU多例化技术不仅应用在只有不同位置变换信息的游戏对象,如材质颜色不同,也可以使用GPU多例化技术。可以在自定义着色器代码中添加逐多例(per-instance)属性。如下代码所示。在外观着色器中给材质颜色变量增加GPU多例化支持的代码。
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, to get nicer looking lighting
#pragma target 3.0
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
// Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
// See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
// #pragma instancing_options assumeuniformscaling
UNITY_INSTANCING_CBUFFER_START(Props)
// put more per-instance properties here
UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color);
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) * _Color;
o.Albedo = c.rgb;
// Metallic and smoothness come from slider variables
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
上面代码使用UNITY_INSTANCING_BUFFER_START宏开始宣告要使用GPU多例化技术的变量,使用UNITY_DEFINE_INSTANCED_PROP宏声明_Color是逐实例变量,使用UNITY_INSTANCING_BUFFER_END宏结束使用技术的宣告。
在C#层设置game object中的多例化材质颜色属性(逐实例属性)
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);
}
在顶点片元着色器中使用多例化技术
前文提到默认地外观着色器是开启了多例化技术支持的,对于普通的顶点着色器和片元着色器也可以使用此技术,但是要在代码中添加关键语句。
Shader "Unlit/SimpleInstancedShader"
{
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//Step1. 必须使用这个编译指示符宣告使用GPU多例化技术(这样材质面板就有Enable GPU Instancing复选框)
#pragma multi_compile_instancing
#include "UnityCG.cginc"//用多例化技术宏需要用到该头文件
struct appdata
{
float4 vertex : POSITION;
//Step2. 定义变量
//uint instanceID : SV_InstanceID;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 vertex : SV_POSITION;
//Step3. 同Step2.
UNITY_VERTEX_INPUT_INSTANCE_ID
};
//Step4. 宣告多例化属性变量
//cbuffer UnityInstancingProps {
// float4 _Color[500];
//}
UNITY_INSTANCING_CBUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color)
UNITY_INSTANCING_CBUFFER_END
v2f vert (appdata v)
{
v2f o;
//Step5. 计算数组中的实例ID=实例id+数组起始位置id
//unity_InstanceID = v.instanceID + unity_BaseInstanceID;
UNITY_SETUP_INSTANCE_ID(v);
//Step6. 将appdata里的instanceID传递给v2f里的instanceID
//o.instanceID = v.instanceID;
UNITY_TRANSFER_INSTANCE_ID(v, o);
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//Step7. 同Step5.
UNITY_SETUP_INSTANCE_ID(i);
//Step8. 通过unity_InstanceID作为真实的索引,去数组取该实例的属性值_Color[unity_InstanceID].
return UNITY_ACCESS_INSTANCED_PROP(_Color);
}
ENDCG
}
}
}