渲染大量球体-优化DrawCall
支持GPU-Instance
使用材质属性块
LOD-Groups支持GPU-Instance
Unity 2017.1.0f3html
指示GPU绘制须要花时间;向其传递mesh和material属性也要花时间。如今已知两种节省Draw Call的方式:static和dynamic batching数组
Unity能够将多个静态物体的网格合并为一个更大的静态网格,从而减小draw call。 注意:只有使用相同材质的对象才能以这种方式组合。 这是以必须存储更多网格数据为代价的。 启用动态批处理后,Unity在运行时会对视图中的动态对象执行相同的操做。 这仅适用于小型网格物体,不然开销将变得太大。dom
还有另外一种组合draw call的方法:GPU instance或Geometry instance。与动态批处理同样,此操做在运行时针对可见对象。 它的目标是让GPU一次性渲染同一网格的多个副本。 所以,它不能组合不一样的网格或材质,但不只限于小网格。编辑器
using UnityEngine; public class GPUInstancingTest : MonoBehaviour { public Transform prefab; public int instances = 5000; public float radius = 50f; //单位圆内随机一点并放大坐标50倍,生成5000个球体 //而后查看statistics统计的draw Call信息 void Start () { for (int i = 0; i < instances; i++) { Transform t = Instantiate(prefab); t.localPosition = Random.insideUnitSphere * radius; t.SetParent(transform); } } }
使用forward render path统计到的draw call,去掉背景和camera Effect两个draw call:ide
5000 draw call函数
可是当使用cube代替球体优化
6 draw callui
默认状况下,GPU Instance不会开启,必须设计shader以支持它。 即便这样,也必须为每种材料显式启用实例化。 Unity的standard着色器有一个开关。像标准着色器的GUI同样,咱们将为shader扩展面板建立“高级选项”部分。 能够经过调用MaterialEditor.EnableInstancingField方法来添加切换。spa
void DoAdvanced () { GUILayout.Label("Advanced Options", EditorStyles.boldLabel); editor.EnableInstancingField(); }
仅当shader实际支持instance时,才会显示该切换。 咱们能够经过将#pragma multi_compile_instancing指令添加到着色器base-pass启用此支持。 这将为一些关键字启用着色器变体,自定义关键字INSTANCING_ON,其余关键字也能够。设计
#pragma multi_compile_fwdbase #pragma multi_compile_fog #pragma multi_compile_instancing
instance开关
合并了,可是显示有错误
批处理数量已减小到42,这意味着如今仅用40个批处理便可渲染全部5000个球体。帧速率也高达80 fps,可是只有几个球体可见。错误缘由:虽然5000个球体仍在渲染,可是在合批中同一批次的全部球体的顶点转换时都使用了同一个位置:它们都使用同一批次中第一个球的转换矩阵。 发生这种状况是由于如今同一批中全部球体的矩阵都做为数组发送到GPU。 在不告知着色器要使用哪一个数组索引的状况下,它始终使用第一个索引。
上述错误解决办法:每一个Instance相对应的数组索引称为其Instance ID,GPU经过顶点数据将其传递到着色器的vertex程序。在大多数平台上,它是一个无符号整数,名为instanceID,具备SV_InstanceID语义。 咱们能够简单地使用UNITY_VERTEX_INPUT_INSTANCE_ID宏将其包含在咱们的VertexData结构中。 它在UnityCG中包含的UnityInstancing.cginc文件中定义。 它为咱们提供了实例ID的正肯定义,或者在未启用实例化时不提供任何内容。将其添加到VertexData结构。
struct VertexData { UNITY_VERTEX_INPUT_INSTANCE_ID float4 vertex : POSITION; … };
启用instance后,咱们如今能够在顶点程序中访问instanceID。 有了它,咱们能够在变换顶点位置时使用正确的矩阵。 可是,UnityObjectToClipPos函数没有矩阵参数,它函数内部始终使用unity_ObjectToWorld矩阵。要解决此问题,UnityInstancing包含文件会使用矩阵数组的宏覆盖unity_ObjectToWorld。 这能够被认为是肮脏的宏技巧,但无需更改现有着色器代码便可工做,从而确保了向后兼容性。
要使它工做,instance的数组索引必须对全部着色器代码全局可用。必须经过UNITY_SETUP_INSTANCE_ID宏进行手动设置,该宏必须在vertex程序最早计算,而后再执行其余的代码。
InterpolatorsVertex MyVertexProgram (VertexData v) { InterpolatorsVertex i; UNITY_INITIALIZE_OUTPUT(Interpolators, i); UNITY_SETUP_INSTANCE_ID(v); i.pos = UnityObjectToClipPos(v.vertex); … }
正确显示
矩阵替换内部实现?
//UnityInstancing中的实际代码要复杂得多。 它要处理平台差别,其余使用实例化的方法以及用于立 //体声渲染的特殊代码,从而致使间接定义的多个步骤。 它还必须从新定义UnityObjectToClipPos,因 //为UnityCG首先包含UnityShaderUtilities。 //缓冲区宏将在后面说明。 static uint unity_InstanceID; CBUFFER_START(UnityDrawCallInfo) // Where the current batch starts within the instanced arrays. int unity_BaseInstanceID; CBUFFER_END #define UNITY_VERTEX_INPUT_INSTANCE_ID uint instanceID : SV_InstanceID; #define UNITY_SETUP_INSTANCE_ID(input) \ unity_InstanceID = input.instanceID + unity_BaseInstanceID; // Redefine some of the built-in variables / // macros to make them work with instancing. UNITY_INSTANCING_CBUFFER_START(PerDraw0) float4x4 unity_ObjectToWorldArray[UNITY_INSTANCED_ARRAY_SIZE]; float4x4 unity_WorldToObjectArray[UNITY_INSTANCED_ARRAY_SIZE]; UNITY_INSTANCING_CBUFFER_END #define unity_ObjectToWorld unity_ObjectToWorldArray[unity_InstanceID] #define unity_WorldToObject unity_WorldToObjectArray[unity_InstanceID]
每台设备不同,最终获得的批次数量可能与当前实验获得的数量不一样。如今这状况下,以40批渲染5000个球体实例,这意味着每批125个球体。
每一个批次都须要本身的矩阵数组。 此数据发送到GPU并存储在内存缓冲区中,在Direct3D中称为常量缓冲区,在OpenGL中称为统一缓冲区。 这些缓冲区具备最大大小,这限制了一批中能够容纳多少个实例。 假设台式机GPU每一个缓冲区的限制为64KB。
一个矩阵由16个浮点数组成,每一个浮点数均为4个字节。 所以,每一个矩阵64个字节。 每一个实例都须要一个对象到世界的转换矩阵。 可是,咱们还须要一个世界到对象的矩阵来转换法线向量。 所以,最终每一个实例有128个字节。 这致使最大批处理大小为“ 64000/128 = 500”,这只能在10个批处理中渲染5000个球体。
内存单位是2进制,因此1KB表示1024字节,而不是1000。所以,'(64 * 1024)/ 128 = 512 '。UNITY_INSTANCED_ARRAY_SIZE默认定义为500,但您可使用编译器指令覆盖它。例如,#pragma instancing_options maxcount:512将最大值设置为512。可是,这将致使断言失败错误,所以实际限制为511。到目前为止,500和512之间没有太大的差异。
即便假设台式机的最大容量为64KB成立,可是大多数移动设备的最大容量远远达不到64,可能仅为16KB。 Unity经过在针对OpenGL ES 3,OpenGL Core或Metal时将最大值除以四来解决此问题。 由于我在编辑器中使用的是OpenGL Core,因此最终的最大批处理大小为“ 500/4 = 125”。
能够经过添加编译器指令#pragma instancing_options force_same_maxcount_for_gl来禁用此自动减小功能。 多个instance选项组合在同一指令中。 可是,这可能会致使在部署到移动设备上时发生故障,所以请当心使用。
那假设均等缩放选项呢? 能够使用#pragma instancing_options指示全部instance对象具备统一的缩放比例。 这消除了将世界到对象矩阵用于法线转换的须要(少存储一个矩阵)。 设置此选项后,虽然UnityObjectToWorldNormal函数确实会更改其行为,但它不会消除第二个矩阵数组。 所以,至少在Unity 2017.1.0中,此选项实际上没有任何做用。
到目前为止,一直没有阴影。 从新打开主阴影的Soft shadow,并确保阴影距离足以包含全部球体
批处理爆炸
为大量物体渲染阴影会增长GPU耗能。可是咱们也能够在渲染球体阴影时使用GPU instance。在shadow caster-pass中添加instance指令;同时也增长UNITY_VERTEX_INPUT_INSTANCE_ID
and UNITY_SETUP_INSTANCE_ID
#pragma multi_compile_shadowcaster #pragma multi_compile_instancing
struct VertexData { UNITY_VERTEX_INPUT_INSTANCE_ID … }; … InterpolatorsVertex MyShadowVertexProgram (VertexData v) { InterpolatorsVertex i; UNITY_SETUP_INSTANCE_ID(v); … }
instanced 阴影
咱们仅在base-pass和shadow caster-pass中添加了instance支持。 所以,批处理不适用于其余光源。 要验证这一点,停用主光源并添加一些会影响多个球体的聚光灯或点光源。 不要为它们打开阴影,由于那样会下降帧速率。
批处理爆炸
上图,彻底不支持多光源批处理。 要将instance与多个光源结合使用,只能切换到延迟渲染路径。 为此,请将所需的编译器指令添加到着色器的延迟传递中。
#pragma multi_compile_prepassfinal #pragma multi_compile_instancing
多光源instance
全部批处理都有一个限制:它们仅限于具备相同材料的对象。 当咱们但愿渲染的对象具备多样性时,此限制就会成为问题。
随机改变球体的颜色
void Start () { for (int i = 0; i < instances; i++) { Transform t = Instantiate(prefab); t.localPosition = Random.insideUnitSphere * radius; t.SetParent(transform); t.GetComponent<MeshRenderer>().material.color = new Color(Random.value, Random.value, Random.value); } }
球体与随机的颜色,没有批量和阴影
即便咱们为物料启用了批处理,它也再也不起做用。因为每一个球体如今都有本身的材质,所以每一个球体的着色器状态也必被更改。 这显示在统计面板中为SetPass call 数量。它曾经是全部领域的一体机,可是如今是5000。
除了为每一个球体建立新的材质实例外,咱们还可使用材质属性块。 这些是小的修改,设置属性块的颜色并将其传递给球体的渲染器,而不是直接分配材质的颜色。
void Start () { MaterialPropertyBlock properties = new MaterialPropertyBlock(); for (int i = 0; i < instances; i++)
{ Transform t = Instantiate(prefab); t.localPosition = Random.insideUnitSphere * radius; t.SetParent(transform);// MaterialPropertyBlock properties = new MaterialPropertyBlock();properties.SetColor
( "_Color", new Color(Random.value, Random.value, Random.value) ); t.GetComponent<MeshRenderer>().SetPropertyBlock(properties); } }
渲染instance对象时,Unity经过将数组传递到GPU内存来使转换矩阵可用于GPU。 Unity对存储在材料属性块中的属性执行相同的操做。 但这要起做用,咱们必须在shader中定义一个适当的缓冲区。
声明instance缓冲区的工做相似于建立诸如插值器之类的结构,可是确切的语法因平台而异。 咱们可使用UNITY_INSTANCING_CBUFFER_START和UNITY_INSTANCING_CBUFFER_END宏来解决差别。 启用实例化后,它们将不执行任何操做。
将_Color变量的定义放在instance缓冲区中。 UNITY_INSTANCING_CBUFFER_START宏须要一个名称参数。 实际名称可有可无。 宏以UnityInstancing_为其前缀,以防止名称冲突。
UNITY_INSTANCING_CBUFFER_START(InstanceProperties)
float4 _Color;
UNITY_INSTANCING_CBUFFER_END
像变换矩阵同样,启用instance后,颜色数据做为数组上传到GPU。UNITY_DEFINE_INSTANCED_PROP宏会为咱们处理正确的声明语法。
UNITY_INSTANCING_CBUFFER_START(InstanceProperties) //float4 _Color; UNITY_DEFINE_INSTANCED_PROP(float4, _Color) UNITY_INSTANCING_CBUFFER_END
最后要访问fragment程序中的数组,咱们还须要在其中知道instanceID。 所以,将其添加到插值器结构中。
struct InterpolatorsVertex { UNITY_VERTEX_INPUT_INSTANCE_ID … }; struct Interpolators { UNITY_VERTEX_INPUT_INSTANCE_ID … };
在vertex顶点程序中,将ID从顶点数据复制到插值器。 启用实例化时,UNITY_TRANSFER_INSTANCE_ID宏定义此简单操做,不然不执行任何操做。
InterpolatorsVertex MyVertexProgram (VertexData v) { InterpolatorsVertex i; UNITY_INITIALIZE_OUTPUT(Interpolators, i); UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, i); … }
在片断程序的开头,使ID全局可用,就像在顶点程序中同样。
FragmentOutput MyFragmentProgram (Interpolators i) { UNITY_SETUP_INSTANCE_ID(i); … }
如今,咱们必须在不使用instance时以_Color的形式访问颜色,而在启用实例化时以_Color [unity_InstanceID]的形式访问颜色。 使用UNITY_ACCESS_INSTANCED_PROP宏可同时支持上述两种访问。
float3 GetAlbedo (Interpolators i) { float3 albedo = tex2D(_MainTex, i.uv.xy).rgb * UNITY_ACCESS_INSTANCED_PROP(_Color).rgb; … } float GetAlpha (Interpolators i) { float alpha = UNITY_ACCESS_INSTANCED_PROP(_Color).a; … }
新版本若是编译有错误: 从2017.3及以上版本, UNITY_ACCESS_INSTANCED_PROP macro改了.它须要两个参数:buffer名,颜色名使用UNITY_ACCESS_INSTANCED_PROP(InstanceProperties, _Color).
如今,咱们的颜色随机的球再次被批处理。 咱们能够用相同的方式使其余属性可变。 对于颜色,浮点数,矩阵和四份量浮点向量,这是可能的。 若是要改变纹理,可使用单独的纹理数组,并将索引添加到实例化缓冲区。其余属性修改相似。
能够在同一个缓冲区中组合多个属性,但要牢记大小限制。 还应注意,缓冲区被划分为32位块,所以单个浮点数须要与向量相同的空间。 您也可使用多个缓冲区,可是也有一个限制,它们不是免费提供的。 启用instance后,每一个要缓冲的属性都将成为一个数组,所以仅对须要根据instance变化的属性执行此操做。
咱们的阴影也取决于颜色。 调整shader阴影以便每一个实例也能够支持惟一的颜色。
//float4 _Color; UNITY_INSTANCING_CBUFFER_START(InstanceProperties) UNITY_DEFINE_INSTANCED_PROP(float4, _Color) UNITY_INSTANCING_CBUFFER_END … struct InterpolatorsVertex { UNITY_VERTEX_INPUT_INSTANCE_ID … }; struct Interpolators { UNITY_VERTEX_INPUT_INSTANCE_ID … }; float GetAlpha (Interpolators i) { float alpha = UNITY_ACCESS_INSTANCED_PROP(_Color).a; … } InterpolatorsVertex MyShadowVertexProgram (VertexData v) { InterpolatorsVertex i; UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, i); … } float4 MyShadowFragmentProgram (Interpolators i) : SV_TARGET { UNITY_SETUP_INSTANCE_ID(i); … }
void Start () { MaterialPropertyBlock properties = new MaterialPropertyBlock(); for (int i = 0; i < instances; i++) { Transform t = Instantiate(prefab); t.localPosition = Random.insideUnitSphere * radius; t.SetParent(transform); //MaterialPropertyBlock properties = new MaterialPropertyBlock(); properties.SetColor ( "_Color", new Color(Random.value, Random.value, Random.value) ); //t.GetComponent<MeshRenderer>().SetPropertyBlock(properties); MeshRenderer r = t.GetComponent<MeshRenderer>(); if (r) { r.SetPropertyBlock(properties); } else {
//对LOD子对象设置颜色 for (int ci = 0; ci < t.childCount; ci++) { r = t.GetChild(ci).GetComponent<MeshRenderer>(); if (r) { r.SetPropertyBlock(properties); } } } } }
不幸的是没有有效的批处理。Unity可以对以相同的LOD颜色球体进行批处理,可是若是能够像往常同样进行批处理会更好。 咱们能够经过用缓冲数组替换unity_LODFade来实现。能够经过为支持实例化的每一个过程添加lodfade实例化选项来指示Unity的着色器代码执行此操做。
#pragma multi_compile_instancing #pragma instancing_options lodfade
instance LOD fading