关于UnityShader实例化那些事

概述

        本篇文章会介绍内置渲染管线和URP渲染管线下,Unity Shader如何实现实例化和实例化时的一些细节。注意本文不对单通道渲染的实例化、粒子的实例化和Dots实例化做过多的说明,只会简单提及。

环境

    Windows10
    Unity2022.3.34f1c1
    Universal RP 14.0.11

内置渲染管线

简单shader实现实例化

        Unity官方文档中是有具体的例子的。文档地址:实例化简单实现。这里我直接将源码贴进来,方便我自己解释。虽然官方文档中已经对实例化的参数进行了详细的说明,但是我觉得既然自己去写文章了,那就自己再记录一下。

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
Shader "Custom/SimplestInstancedShader"
{
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
}

SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct v2f
{
float4 vertex : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID // use this to access instanced properties in the fragment shader.
};

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

v2f vert(appdata v)
{
v2f o;

UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}

fixed4 frag(v2f i) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(i);
return UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
}
ENDCG
}
}
}
### 辨别是否启用实例化         为了显示是否实例化成功,这里我本应该给出没有实例化的Shader代码,来做一个比较。但是实际上我们并不用再去写一个没有实例化的Shader。如果你使用的是Unity自带的材质面板(如果你在Shader中没有使用CustomEditor关键字,那么你就是用Unity自带的材质面板。URP也是同理。)如果在Shader中,你声明了#pragma multi_compile_instancing,那么在材质面板中就会显示一个Enable GPU Instancing选项(如果你有使用中文翻译,则这里是启用GPU实例化)。如果你勾选了这个选项,那么在材质面板中就会显示实例化相关的参数。反之其就不会走实例化。

        在开始比较前,我们先做一些准备。打开Unity并在场景中放置两个立方体(请确保立方体都可以被摄像机完全看见)。然后我们创建材质并赋予上文的Shader。一开始,我们先不勾选Enable GPU Instancing选项。将我们创建的材质赋予两个立方体。接下来,我们依照路径点击Window->Analysis->Frame Debugger,打开帧调试器。我们启动帧调试器后可以看到,两个立方体分别都被绘制了一次。这就是没有实例化的情况。图片如下:

图片中的CubeCube(1)是我场景中立方体的名称。这时候我们再将Enable GPU Instancing选项勾起,帧调试器中的内容就会改变成下图的形式:

从帧调试器中我们可以看到,两个立方体实际上只进行了一次绘制。我们可以从帧调试器的结果看出,我们的实例化代码成功运作了(废话,官方的代码当然是可以的)。那如果我们使用两个材质,但是两个材质的参数全部一致的情况下,会发生什么情况呢?实际上,这种情况下两个立方体会分别都被绘制了一次。

UNITY_VERTEX_INPUT_INSTANCE_ID

        UNITY_VERTEX_INPUT_INSTANCE_ID在官方的文档中有如下的描述:

1
2
UNITY_VERTEX_INPUT_INSTANCE_ID:在顶点着色器输入/输出结构中定义实例ID。要使用此宏,请启用INSTANCING_ON着色器关键字。否则,Unity不会设置实例ID。
要访问实例ID,请在#ifdef INSTANCING_ON块中使用vertexInput.instanceID。如果您不使用此块,则变体将无法编译。
UNITY_VERTEX_INPUT_INSTANCE_ID的作用就是在运行过程中让我们的设定的数据结构中设定实例ID。而在官方给出的Shader的样例中还有一段注释:
1
2
3
4
UNITY_VERTEX_INPUT_INSTANCE_ID// use this to access instanced properties in the fragment shader.

翻译:
使用UNITY_VERTEX_INPUT_INSTANCE_ID以在片元着色器中访问实例化属性

如果没有在需要实例化的着色器结构中声明UNITY_VERTEX_INPUT_INSTANCE_ID,那么在这个着色器结构使用到UNITY_SETUP_INSTANCE_ID访问实例化ID时会报错。而这个报错是只有在打包时才会出现。

UNITY_INSTANCING_BUFFER_START 和 UNITY_INSTANCING_BUFFER_END

        在上面的Shader中变量_ColorUNITY_INSTANCING_BUFFER_STARTUNITY_INSTANCING_BUFFER_END包裹。在文档中,对于他们的说明如下:

1
2
3
UNITY_INSTANCING_BUFFER_START(bufferName):声明名为bufferName的每个实例常量缓冲区的开始。使用此宏与UNITY_INSTANCING_BUFFER_END一起封装想要每个实例唯一的属性的声明。使用UNITY_DEFINE_INSTANCED_PROP在缓冲区内声明属性。

UNITY_INSTANCING_BUFFER_END(bufferName):声明名为bufferName的每个实例常量缓冲区的结束。使用此宏与UNITY_INSTANCING_BUFFER_START一起封装想要每个实例唯一的属性的声明。使用UNITY_DEFINE_INSTANCED_PROP在缓冲区内声明属性。
而关于UNITY_DEFINE_INSTANCED_PROP,官方文档中有如下的描述:
1
UNITY_DEFINE_INSTANCED_PROP(type, propertyName):使用指定的类型和名称定义每个实例的着色器属性。
为了更加能说明使用它们的意义,我们来实验一些东西。首先我们把之前的代码改为如下的形式:

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
Shader "Test/SimplestInstancedTest"
{
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
_ExtraColor ("Extra Color", Color) = (0.5, 0.5, 0.5, 0.5)
}

SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct v2f
{
float4 vertex : SV_POSITION;
float4 color : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID // use this to access instanced properties in the fragment shader.
};

float4 _ExtraColor;

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

v2f vert(appdata v)
{
v2f o;

UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
o.vertex = UnityObjectToClipPos(v.vertex);
o.color = normalize(v.vertex);
return o;
}

fixed4 frag(v2f i) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(i);
return UNITY_ACCESS_INSTANCED_PROP(Props, _Color) * _ExtraColor * i.color;
}
ENDCG
}
}
}

这里我们多加了一个_ExtraColor的变量,且这个变量并没有被UNITY_INSTANCING_BUFFER_STARTUNITY_INSTANCING_BUFFER_END包裹。这时候我们仍然勾选Enable GPU Instancing选项,然后将材质赋予两个立方体。这时候我们再打开帧调试器,你会发现两个立方体仍然只进行了一次绘制。接下来,我们创建一个脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Test
{
public class ShaderInstanceTest : MonoBehaviour
{
public Color color;
public MeshRenderer meshRenderer;
public bool changeColor;

private void Update()
{
if (changeColor)
{
meshRenderer.material.SetColor("_ExtraColor", color);
changeColor = false;
}
}
}
}

然后我们将脚本放置在其中一个立方体上,并将此立方体的MeshRenderer赋给脚本中的数值。接下来我们运行并更改颜色。最后我们打开帧调试器看看情况,你会发现两个立方体都被分别绘制了一次。那如果是被UNITY_INSTANCING_BUFFER_STARTUNITY_INSTANCING_BUFFER_END包裹的_Color会有什么不同吗?其实没什么不同,两个立方体还被分别绘制了一次。

        这样实践下来好像实例化根本就没必要使用UNITY_INSTANCING_BUFFER_STARTUNITY_INSTANCING_BUFFER_END。我们好像就只需要加上#pragma multi_compile_instancing就可以了。当然UNITY_INSTANCING_BUFFER_STARTUNITY_INSTANCING_BUFFER_END必然是有它们的用处的,不然Unity也不会加上这个。实际上我们修改材质参数还有其他的方法,而这个方法就可以体现UNITY_INSTANCING_BUFFER_STARTUNITY_INSTANCING_BUFFER_END的作用了。我们将上面的脚本代码改为如下形式:

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Test
{
public class ShaderInstanceTest : MonoBehaviour
{
public Color color;
public MeshRenderer meshRenderer;
public bool changeColor;

private MaterialPropertyBlock propertyBlock;

private void Awake()
{
propertyBlock = new MaterialPropertyBlock();
}

private void Update()
{
if (changeColor)
{
propertyBlock.SetColor("_Color", color);
meshRenderer.SetPropertyBlock(propertyBlock);
changeColor = false;
}
}
}
}
这时候你会在帧调试器中发现,两个立方体又被只绘制了一次。而你改为_ExtraColor时,两个立方体是分别被绘制了一次。这个现象是我唯一发现的参数用UNITY_INSTANCING_BUFFER_STARTUNITY_INSTANCING_BUFFER_END包裹的意义。关于MaterialPropertyBlock官方文档上有一段这样的描述
1
2
3
4
MaterialPropertyBlock is used by Graphics.RenderMesh and Renderer.SetPropertyBlock. Use it in situations where you want to draw multiple objects with the same material, but slightly different properties. For example, if you want to slightly change the color of each mesh drawn. Changing the render state is not supported.

有道翻译+自己理解:
MaterialPropertyBlock由Graphics.RenderMesh和Renderer.SetPropertyBlock使用。当你想用同一个材质渲染多个物体,但有些属性不同时,可以使用它。例如,如果你想对每个渲染的物体稍微改变颜色。而改变渲染状态则不被支持。
其实在实例化文档中也有部分关于MaterialPropertyBlock的说明。
1
当你使用多个每个实例属性时,你不需要在MaterialPropertyBlock对象中填写所有这些属性。此外,如果一个实例缺少属性,Unity 会从引用的材料中获取默认值。如果材料没有为该属性设置默认值,Unity 将值设置为0。不要在MaterialPropertyBlock中放置非实例化属性,因为这会禁用实例化。相反,为它们创建不同的材料。

额外的小细节

        帧调试器其实会显示出参数的信息。

但是当然你勾选Enable GPU Instancing选项后,被UNITY_INSTANCING_BUFFER_STARTUNITY_INSTANCING_BUFFER_END包裹的属性将不会显示在帧调试器中。

而这时候,你会发现调试器中多了一些东西。其中在KeyWords部分,我们多一个名为INSTANCING_ON的参数,表示实例化开启。这里我们可以用这个修改一下声明变量的形式。

1
2
3
4
5
6
7
8
9
#ifdef INSTANCING_ON
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)

#define _Color UNITY_ACCESS_INSTANCED_PROP(Props, _Color)
#else
float4 _Color;
#endif

这样我们的片元着色器就可以改为如下形式:

1
2
3
4
5
fixed4 frag(v2f i) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(i);
return _Color * _ExtraColor * i.color;
}
如果这个变量多次引用,我们就不用每次都要用UNITY_ACCESS_INSTANCED_PROP(Props, _Color)而是直接用_Color来引用。

UNITY_SETUP_INSTANCE_ID

        UNITY_SETUP_INSTANCE_ID官方文档上的描述为:

1
UNITY_SETUP_INSTANCE_ID(v):在顶点着色器输入/输出结构中定义实例ID。要使用此宏,请启用INSTANCING_ON着色器关键字。否则,Unity不会设置实例ID。要访问实例ID,请在#ifdef INSTANCING_ON块中使用vertexInput.instanceID。如果您不使用此块,则变体将无法编译。
这里明确说明了片段着色器是可以不加的。但是这加于不加的区别又在何处呢?我用实例底代码进行测试的时候,我并没有发现有什么太大的区别。我也没有找到更多的资料来说明加和不加的区别。但是既然差不多,我认为还是都加上吧。不过这里再次强调一下,使用UNITY_SETUP_INSTANCE_ID的结构体内部必须要有UNITY_VERTEX_INPUT_INSTANCE_ID的声明。否则在打包编译的时候会报错。

UNITY_TRANSFER_INSTANCE_ID

        UNITY_TRANSFER_INSTANCE_ID官方文档上的描述为:

1
UNITY_TRANSFER_INSTANCE_ID(v, o):在顶点着色器中将实例ID从输入结构复制到输出结构。如果您需要在片段着色器中访问每个实例的数据,请使用此宏。
关于其,我也没找到更多的资料。在我实验中,我发现即使去除了UNITY_TRANSFER_INSTANCE_ID,实例化也能正常工作。且性能没有太大的差别。但是如果我们没有声明UNITY_VERTEX_INPUT_INSTANCE_ID,那么在片段着色器中就不能使用UNITY_SETUP_INSTANCE_ID,否则会报错。但是我想既然官方的示例代码也加上了,这加上去应该还是有用的吧。

UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO

        UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO在官方文档上没有找到详细描述。但是在单通道实例化渲染中提到了这个。

1
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO 根据 unity_StereoEyeIndex 的值告诉 GPU 应该渲染到纹理数组中的哪只眼睛。此宏还从顶点着色器传输 unity_StereoEyeIndex 的值,确保仅当在片元着色器 frag 方法中调用 UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX 时才能在片元着色器中访问该值。
这个宏大概率是给VR等设备用的,我既没有这个设备也没有这个需求,所以我只是在此提及一下。

粒子实例化

        基础的实例化,我们使用上面的操作就可以完成。但是粒子的实例化,我们需要额外引用一个头文件

1
#include "UnityStandardParticleInstancing.cginc"
而在官方文档中也存在一个案例。但是我几乎没怎么用过粒子实例化,所以我就不多说了。

有关贴图

        在设定贴图的时候,你会发现贴图的声明并不能放到UNITY_INSTANCING_BUFFER_STARTUNITY_INSTANCING_BUFFER_END中。所以我们进行贴图变化的时候就无法使用实例化了。

小总结

        基本上内置渲染管线会用到的实例化宏,我这边都介绍了。而URP渲染管线其实仍然复用了一些宏,但是它还是有些区别的。

URP渲染管线

        在URP中,其自带SRP Batcher可编程渲染管线 SRP Batcher)。而当你的Shader满足SRP Batcher的条件时,URP会自动使用SRP Batcher,且这个优先级高于我们的实例化。这里我们将上面的代码进行改变使其符合URP的写法,Shader如下:

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
Shader "Test/InstanceShader"
{
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
_ExtraColor ("Extra Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

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

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

struct appdata
{
float4 vertex : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct v2f
{
float4 vertex : SV_POSITION;
float4 color : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

#ifdef INSTANCING_ON
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
UNITY_DEFINE_INSTANCED_PROP(float4, _ExtraColor)
UNITY_INSTANCING_BUFFER_END(Props)

#define _Color UNITY_ACCESS_INSTANCED_PROP(Props, _Color)
#define _ExtraColor UNITY_ACCESS_INSTANCED_PROP(Props, _ExtraColor)
#else
CBUFFER_START(UnityPerMaterial)
float4 _ExtraColor;
float4 _Color;
CBUFFER_END
#endif

v2f vert (appdata v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
o.vertex = TransformObjectToHClip(v.vertex.xyz);
o.color = normalize(v.vertex);
return o;
}

half4 frag (v2f i) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(i);
return _Color * _ExtraColor * i.color;
}
ENDHLSL
}
}
}
然后我们打开一个URP的项目,依照之前的做法在场景中放置两个立方体,并将带有上面Shader的材质赋给这两个立方体。在材质中,我们将Enable GPU Instancing勾选上。这时候打开帧调试器,然后你就会看到在SRP Batcher中这个两个立方体被只绘制了一次。如图所示:

但这并不是实例化,而是Unity优先使用了SRP Batcher。而且我们可以从帧调试器中看到,在执行的时候KeyWords中并不存在INSTANCING_ON。这似乎表明在URP中并不需要进行实例化,反正Unity会自己调用SRP Batcher进行合批绘制。

        当然事实肯定不会是这样的。Unity要使用SRP Batcher,也是要满足一定的条件。具体的条件大家可以去查阅文档:可编程渲染管线 SRP Batcher。简单点来说就是SRP Batcher需要不同的物体使用同一种材质。而且这个条件比实例化的还要苛刻,因为我们即使使用MaterialPropertyBlock也是会打破这个条件的。当然这也告诉我们,如果我们想看到URP下的实例化,那么我们就可以使用MaterialPropertyBlock来阻止SRP Batcher。我们仍然用之前的脚本,然后将其赋给场景中的两个立方体。运行并修改颜色后,我们就可以看到实例化的结果了。

Dots实例化

        如果你有看过URP下Lit Shader的实现,你会发现它在变量声明的时候使用了另外的宏:UNITY_DOTS_INSTANCING_ENABLED。这个是表示Unity是否开启Dots实例化的宏。

这里是Lit Shader中变量声明的部分,此部分在LitInput.hlsl下:

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
#ifdef UNITY_DOTS_INSTANCING_ENABLED

UNITY_DOTS_INSTANCING_START(MaterialPropertyMetadata)
UNITY_DOTS_INSTANCED_PROP(float4, _BaseColor)
UNITY_DOTS_INSTANCED_PROP(float4, _SpecColor)
UNITY_DOTS_INSTANCED_PROP(float4, _EmissionColor)
UNITY_DOTS_INSTANCED_PROP(float , _Cutoff)
UNITY_DOTS_INSTANCED_PROP(float , _Smoothness)
UNITY_DOTS_INSTANCED_PROP(float , _Metallic)
UNITY_DOTS_INSTANCED_PROP(float , _BumpScale)
UNITY_DOTS_INSTANCED_PROP(float , _Parallax)
UNITY_DOTS_INSTANCED_PROP(float , _OcclusionStrength)
UNITY_DOTS_INSTANCED_PROP(float , _ClearCoatMask)
UNITY_DOTS_INSTANCED_PROP(float , _ClearCoatSmoothness)
UNITY_DOTS_INSTANCED_PROP(float , _DetailAlbedoMapScale)
UNITY_DOTS_INSTANCED_PROP(float , _DetailNormalMapScale)
UNITY_DOTS_INSTANCED_PROP(float , _Surface)
UNITY_DOTS_INSTANCING_END(MaterialPropertyMetadata)

static float4 unity_DOTS_Sampled_BaseColor;
static float4 unity_DOTS_Sampled_SpecColor;
static float4 unity_DOTS_Sampled_EmissionColor;
static float unity_DOTS_Sampled_Cutoff;
static float unity_DOTS_Sampled_Smoothness;
static float unity_DOTS_Sampled_Metallic;
static float unity_DOTS_Sampled_BumpScale;
static float unity_DOTS_Sampled_Parallax;
static float unity_DOTS_Sampled_OcclusionStrength;
static float unity_DOTS_Sampled_ClearCoatMask;
static float unity_DOTS_Sampled_ClearCoatSmoothness;
static float unity_DOTS_Sampled_DetailAlbedoMapScale;
static float unity_DOTS_Sampled_DetailNormalMapScale;
static float unity_DOTS_Sampled_Surface;

void SetupDOTSLitMaterialPropertyCaches()
{
unity_DOTS_Sampled_BaseColor = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, _BaseColor);
unity_DOTS_Sampled_SpecColor = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, _SpecColor);
unity_DOTS_Sampled_EmissionColor = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, _EmissionColor);
unity_DOTS_Sampled_Cutoff = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float , _Cutoff);
unity_DOTS_Sampled_Smoothness = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float , _Smoothness);
unity_DOTS_Sampled_Metallic = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float , _Metallic);
unity_DOTS_Sampled_BumpScale = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float , _BumpScale);
unity_DOTS_Sampled_Parallax = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float , _Parallax);
unity_DOTS_Sampled_OcclusionStrength = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float , _OcclusionStrength);
unity_DOTS_Sampled_ClearCoatMask = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float , _ClearCoatMask);
unity_DOTS_Sampled_ClearCoatSmoothness = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float , _ClearCoatSmoothness);
unity_DOTS_Sampled_DetailAlbedoMapScale = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float , _DetailAlbedoMapScale);
unity_DOTS_Sampled_DetailNormalMapScale = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float , _DetailNormalMapScale);
unity_DOTS_Sampled_Surface = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float , _Surface);
}

#undef UNITY_SETUP_DOTS_MATERIAL_PROPERTY_CACHES
#define UNITY_SETUP_DOTS_MATERIAL_PROPERTY_CACHES() SetupDOTSLitMaterialPropertyCaches()

#define _BaseColor unity_DOTS_Sampled_BaseColor
#define _SpecColor unity_DOTS_Sampled_SpecColor
#define _EmissionColor unity_DOTS_Sampled_EmissionColor
#define _Cutoff unity_DOTS_Sampled_Cutoff
#define _Smoothness unity_DOTS_Sampled_Smoothness
#define _Metallic unity_DOTS_Sampled_Metallic
#define _BumpScale unity_DOTS_Sampled_BumpScale
#define _Parallax unity_DOTS_Sampled_Parallax
#define _OcclusionStrength unity_DOTS_Sampled_OcclusionStrength
#define _ClearCoatMask unity_DOTS_Sampled_ClearCoatMask
#define _ClearCoatSmoothness unity_DOTS_Sampled_ClearCoatSmoothness
#define _DetailAlbedoMapScale unity_DOTS_Sampled_DetailAlbedoMapScale
#define _DetailNormalMapScale unity_DOTS_Sampled_DetailNormalMapScale
#define _Surface unity_DOTS_Sampled_Surface

#endif

关于其,官方网站上也有关于它的文档。以下是文档中的部分节选:

1
2
3
4
5
6
7
8
9
10
11
12
在传统的实例化着色器中,着色器会接收一个数组,该数组包含每个实例属性在常数或统一缓冲区中的值,这样每个数组中的每个元素都包含在绘制中单个实例的属性值。在 DOTS 实例化着色器中,Unity 将一个 32 位整数传递给每个 DOTS 实例化属性。这个 32 位整数被称为元数据值。这个整数可以代表您想要代表的内容,但通常它代表着色器装入实例数据的缓冲区中的偏移量。

与传统的实例化相比,DOTS 实例化具有许多优点,包括以下

1. 实例数据存储在 GraphicsBuffer 中,并保留在 GPU 上,这意味着 Unity 不需要在渲染每个实例时再次设置它。只在实例实际发生变化时才设置数据可以显著提高性能,在这种情况实例数据变化很少或根本不变化。这比传统的实例化更有效率,因为传统的实例化需要将每个帧的数据都设置好。

2. 设置实例数据的过程与设置绘制调用的过程是分开的。这使得绘制调用设置轻量级且高效。BRG 通过 SRP Batcher 的特殊快速路径实现这一点,该快速路径只对每个绘图调用执行最小的工作。这项工作的责任转移到您身上,并让您在每次绘制调用中控制要渲染的内容。

3. 绘制调用的尺寸不再受常数或统一缓冲区中可容纳多少实例数据的限制。这使得 BRG 能够使用单次绘制调用渲染更多实例的数量。
注意:实例索引的数量仍然限制了绘制调用的尺寸,因为每个索引仍然需要一些数据。然而,索引消耗的内存远少于完整的实例化属性集,这意味着可以在常数或统一缓冲区中容纳更多实例。例如,每个索引需要 16 字节,如果特定平台上的缓冲区内存限制为 64kb,则可以在缓冲区中容纳 4096 个索引。

3. 如果每个实例对给定属性使用相同的值,则所有实例可以从内存中的同一位置加载该值。这可以节省内存并减少为每个实例复制值所消耗的 GPU 周期。

我也想试着说明更多关于Dots实例化的事情,但是我发现实际上Unity自带的Lit Shader中在非Dots项目下并不能满足#ifdef UNITY_DOTS_INSTANCING_ENABLED这个条件。也就是说里面中所有对于变量的操作都是没有意义的。你必须要将DOTS_INSTANCING_ON这个KeyWord强制开启才有作用。但是开启后模型的渲染就会出现问题。我也花了很多时间在网上找过资料发现根本就没有人有做这个的分享。所以在URP下,我现在能想到的实例化还是只能用之前的办法。当然既然在Dots项目下,那么在Dots下做项目的大家就只要简单仿造上面的流程就好了(特别说明,这时的物体必须是Dots下的实例,否则也是走普通的实例化)。

参考资料