Unity SRP 二:Draw Calls



前言

提示:学习内容来自以下文献
catlikecoding.com


提示:以下是本篇文章正文内容,下面案例可供参考

1.0、Shaders

为了绘制一些东西,CPU必须告诉GPU绘制什么以及如何绘制。所绘制的通常是一个网格。如何绘制是由Shader定义的,这是一组指令的GPU。除了网格,shader还需要额外的信息来完成它的工作,包括对象的变换矩阵和材质属性。

Unity的LW/Universal和HD RPs允许你使用Shader Graph包来设计着色器,它可以为你生成着色器代码。但我们的自定义RP不支持,所以我们必须自己编写着色器代码。这让我们完全控制和理解着色器的作用。

1.1、Unlit Shader

我们的第一个着色器将简单地绘制一个纯色网格,没有任何照明。一个着色器资产可以通过 Assets / Create / Shader 菜单中的一个选项创建。Unlit着色器是最合适的,但我们将从创建的着色器文件中删除所有默认代码。命名资产Unlit,并将其放入自定义RP下的新着色器文件夹中。
在这里插入图片描述
大多数情况下,着色器代码看起来像c#代码,但它包含了各种不同的方法,包括一些过去有意义但现在不再有意义的古老的部分。

shader被定义为一个类,但是只有shader关键字后面跟着一个字符串,用于在材质的shader下拉菜单中为它创建一个条目。我们使用自定义RP/Unlit。它后面是一个代码块,其中包含更多前面有关键字的块。有一个属性块来定义材质属性,然后是一个SubShader块,它需要有一个Pass块在里面,它定义了一种渲染东西的方式。用空块创建该结构:

Shader "Custom RP/Unlit" {
	
	Properties {}
	
	SubShader {
		
		Pass {}
	}
}

这定义了一个最小的着色器,编译并允许我们创建一个使用它的材料:
在这里插入图片描述
默认的着色器实现渲染网格纯白色。材质显示了渲染队列的默认属性,它从着色器中自动获取并设置为2000,这是不透明几何的默认值。它也有一个切换来启用双面全局照明,但这与我们无关。

1.2、HLSL Programs

我们用来编写着色器代码的语言是高级着色语言,简称为HLSL。我们必须把它放在Pass块中,在HLSLPROGRAM和ENDHLSL关键字之间。我们必须这样做,因为它也可以在Pass块中放入其他非hlsl代码:

		Pass {
			HLSLPROGRAM
			ENDHLSL
		}

为了绘制一个网格,GPU必须栅格化所有的三角形,将其转换为像素数据。它通过将顶点坐标从3D空间转换为2D可视化空间,然后填充所有被结果三角形覆盖的像素来实现这一点。这两个步骤是由独立的着色程序控制的,这两个我们都必须定义。第一个被称为 顶点内核/程序/着色器 ,第二个被称为 片段内核/程序/着色器 。一个片段对应于一个显示像素或纹理纹理,尽管它可能不代表最终结果,因为它可能会被覆盖,当稍后在它上面绘制的东西。

我们必须用一个名称来标识两个程序,这是通过pragma指令完成的。这些是以#pragma开头的单行语句,后面跟着顶点或片段以及相关的名称。我们将使用UnlitPassVertex和UnlitPassFragment:

			HLSLPROGRAM
			#pragma vertex UnlitPassVertex
			#pragma fragment UnlitPassFragment
			ENDHLSL

pragma 是什么意思?
pragma 一词来自希腊语,指的是一种行动,或一些需要做的事情。在许多编程语言中都使用它来发出特殊的编译器指令

着色器编译器现在会抱怨它找不到声明的着色器内核。我们必须编写具有相同名称的HLSL函数来定义它们的实现。我们可以直接在pragma指令下面执行这个操作,但我们将把所有HLSL代码放在一个单独的文件中。具体来说,我们将使用UnlitPass。HLSL文件在同一个资产文件夹中。我们可以通过添加一个带有文件相对路径的#include指令来指示着色器编译器插入文件的内容:

			HLSLPROGRAM
			#pragma vertex UnlitPassVertex
			#pragma fragment UnlitPassFragment
			#include "UnlitPass.hlsl"
			ENDHLSL

Unity没有一个方便的菜单选项来创建HLSL文件,所以你必须复制着色器文件,将其重命名为UnlitPass,将其文件扩展名从外部更改为HLSL,并清除其内容:
在这里插入图片描述

1.3、Include Guard(引用防卫)

HLSL文件被用来对代码进行分组,就像c#类一样,尽管HLSL没有类的概念。除了代码块的局部作用域之外,只有一个全局作用域。所以任何东西都可以在任何地方访问。包含文件也不同于使用名称空间。它会将文件的全部内容插入到include指令的位置,因此,如果多次包含同一个文件,就会得到重复的代码,这很可能会导致编译器错误。为了防止这种情况发生,我们将在UnlitPass.hlsl中添加一个include守卫

可以使用#define指令来定义任何标识符,通常是大写的。我们将使用它在文件的顶部定义CUSTOM_UNLIT_PASS_INCLUDED:

#define CUSTOM_UNLIT_PASS_INCLUDED

这是一个简单宏的例子,它定义了一个标识符。如果它存在,这意味着我们的文件已经包含在内。所以我们不想再包含它的内容。如果使用不同的措词,我们只希望在尚未定义代码时插入代码。我们可以用#ifndef指令来检查。在定义宏之前执行此操作:

#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDED

在#ifndef后面的所有代码都将被跳过,因此如果宏已经被定义,则不会被编译。我们必须通过在文件末尾添加#endif指令来结束它的作用域:

#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDED
#endif

现在我们可以确保文件的所有相关代码不会被插入多次,即使我们不止一次地包含它。

1.4、Shader Functions(Shader 函数)

我们在include守卫的范围内定义着色器函数。它们的编写方式就像c#方法一样,没有任何访问修饰符。从什么都不做的简单void函数开始:

#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDED

void UnlitPassVertex () {}

void UnlitPassFragment () {}

#endif

这足以让我们的着色器编译。结果可能是一个默认的青色着色器,如果有任何显示:
在这里插入图片描述
为了产生有效的输出,我们必须让fragment函数返回一个颜色。颜色是用一个包含红色、绿色、蓝色和alpha元素的浮点4向量定义的。可以通过float4(0.0, 0.0, 0.0, 0.0)定义纯黑色,但也可以写入一个0,因为单个值会自动扩展为一个完整的vector。alpha值无关紧要,因为我们创建的是一个不透明的着色器,所以0很好:

float4 UnlitPassFragment () {
	return 0.0;
}

为什么使用0.0而不是0?
0.0代表浮点数,0代表整数。虽然数值一样,但是对编译器来说不一样。
用float还是用half精度?
大多数移动GPU支持两种精度类型,其中half效率更高。因此,如果你需要针对移动设备进行优化,则应尽可能多地使用half。经验上来说,只要结果足够好,就只将float用于位置和纹理坐标,half用于其他所有内容。fixed通常可以等价于half

此时,着色器编译器将会失败,因为我们的函数缺少语义。我们必须用返回的值来表示我们的意思,因为我们可能会产生许多具有不同含义的数据。在本例中,我们为呈现目标提供了默认的系统值,通过在UnlitPassFragment的参数列表后面写一个冒号和SV_TARGET来表示:

float4 UnlitPassFragment () : SV_TARGET {
	return 0.0;
}

1.5、Space Transformation(空间变换)

当所有顶点都被设置为0时,网格折叠成一个点,没有任何东西被渲染。顶点函数的主要工作是将原始顶点位置转换为正确的空间。当函数被调用时,如果我们请求可用的顶点数据,函数就会被提供。我们通过给UnlitPassVertex添加参数来做到这一点。我们需要顶点位置,这是在对象空间中定义的,所以我们将其命名为positionOS,使用与Unity的新rp相同的约定。位置的类型是float3,因为它是一个3D点。让我们首先返回它,通过float4添加1作为第四个必需的组件(positionOS, 1.0):

float4 UnlitPassVertex (float3 positionOS) : SV_POSITION {
	return float4(positionOS, 1.0);
}

顶点的Position不是float4吗?
通常,在3D空间中的点是使用4D矢量定义的,其第四分量设置为1,而方向矢量则将其设置为零。这使得可以使用相同的变换矩阵正确地变换位置和方向。但是,仅在位置和方向混合时才需要此技术,通常情况并非如此。相反,它可以将不同的代码用于简化旋转转换的计算

我们还必须为输入添加语义,因为顶点数据可以包含不止一个位置。在本例中,我们需要在参数名称后面直接添加一个颜色的POSITION:

float4 UnlitPassVertex (float3 positionOS : POSITION) : SV_POSITION {
	return float4(positionOS, 1.0);
}

在这里插入图片描述
网格再次显示,但不正确,因为我们输出的位置在错误的空间。空间转换需要矩阵,当图形绘制时,矩阵会被发送到GPU。我们必须将这些矩阵添加到我们的着色器中,但因为它们总是相同的,我们将把Unity提供的标准输入放在一个单独的HLSL文件中,这既可以保持代码的结构,也可以在其他着色器中包含代码。添加一个UnityInput.hlsl文件,并把它放在自定义RP下的ShaderLibrary文件夹中,以镜像Unity的RP的文件夹结构。
在这里插入图片描述
以CUSTOM_UNITY_INPUT_INCLUDED 引用防护开始文件,然后在全局作用域中定义名为unity_ObjectToWorld的浮点4x4矩阵。在c#类中,这将定义一个字段,但在这里,它被称为统一值。它由GPU在每次绘制时设置一次,在绘制过程中对所有顶点和片段函数的调用保持恒定一致:

#ifndef CUSTOM_UNITY_INPUT_INCLUDED
#define CUSTOM_UNITY_INPUT_INCLUDED

float4x4 unity_ObjectToWorld;

#endif

我们可以使用矩阵将对象空间转换为世界空间。因为这是常见的功能,让我们为它创建一个函数,并将其放入另一个文件中,这次是Common。hlsl在同一个ShaderLibrary文件夹中。我们在这里包含UnityInput,然后声明一个TransformObjectToWorld函数,输入和输出都使用float3:

#ifndef CUSTOM_COMMON_INCLUDED
#define CUSTOM_COMMON_INCLUDED

#include "UnityInput.hlsl"

float3 TransformObjectToWorld (float3 positionOS) {
	return 0.0;
}
	
#endif

空间转换是通过调用带有矩阵和向量的mul函数来完成的。在本例中,我们确实需要一个4维向量,但由于它的第四个分量总是1,我们可以使用float4(positionOS, 1.0)自己添加它。我们可以通过访问向量的xyz属性从它提取前三个组件:

float3 TransformObjectToWorld (float3 positionOS) {
	return mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz;
}

我们现在可以在UnlitPassVertex切换到世界空间。因为Common.hlsl存在于不同的文件夹中,我们可以通过相对路径…/ShaderLibrary/Common.hlsl找到它。然后使用TransformObjectToWorld来计算一个positionWS变量,并返回世界空间的位置:

#include "../ShaderLibrary/Common.hlsl"

float4 UnlitPassVertex (float3 positionOS : POSITION) : SV_POSITION {
	float3 positionWS = TransformObjectToWorld(positionOS.xyz);
	return float4(positionWS, 1.0);
}

结果仍然是错误的,因为我们需要在均匀的裁剪空间中找到一个位置。这个空间定义了一个立方体,其中包含摄像机视图中的所有内容,在透视摄像机的情况下扭曲成梯形。从世界空间到这个空间的转换可以通过与视图投影矩阵相乘来完成,该视图投影矩阵考虑了相机的位置,方向,投影,视野和远近裁剪平面。把它添加到UnityInput.hlsl:

float4x4 unity_ObjectToWorld;

float4x4 unity_MatrixVP;

添加一个TransformWorldToHClip到Common.hlsl,它的工作原理与TransformObjectToWorld相同,只不过它的输入是在世界空间中的位置,它使用另一个矩阵,并产生一个float4:

float3 TransformObjectToWorld (float3 positionOS) {
	return mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz;
}

float4 TransformWorldToHClip (float3 positionWS) {
	return mul(unity_MatrixVP, float4(positionWS, 1.0));
}

让UnlitPassVertex使用函数在正确的空间中返回位置:

float4 UnlitPassVertex (float3 positionOS : POSITION) : SV_POSITION {
	float3 positionWS = TransformObjectToWorld(positionOS.xyz);
	return TransformWorldToHClip(positionWS);
}

在这里插入图片描述

1.6、Core Library(核心库)

我们刚才定义的两个函数非常常见,它们也包含在Core RP Pipeline包中。核心库定义了许多更有用和重要的东西,所以让我们安装这个包,删除我们自己的定义,然后包含相关文件,在本例中为Packages / com.unity.render-pipelines.core / ShaderLibrary / SpaceTransforms.hlsl:
在这里插入图片描述

//float3 TransformObjectToWorld (float3 positionOS) {
//	return mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz;
//}

//float4 TransformWorldToHClip (float3 positionWS) {
//	return mul(unity_MatrixVP, float4(positionWS, 1.0));
//}

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"

这会导致编译失败,因为SpaceTransforms.hlsl中的代码不存在unity_ObjectToWorld。相反,它期望通过宏将相关矩阵定义为UNITY_MATRIX_M,因此在通过在单独的行上编写#define UNITY_MATRIX_M unity_ObjectToWorld来包含文件之前,先执行此操作。之后,所有UNITY_MATRIX_M出现都将被unity_ObjectToWorld取代。这是有原因的,我们稍后会看到

#define UNITY_MATRIX_M unity_ObjectToWorld

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"

逆矩阵unity_WorldToObject也是如此,应通过UNITY_MATRIX_I_M定义,通过UNITY_MATRIX_V定义的unity_MatrixV矩阵,以及通过UNITY_MATRIX_VP定义的unity_MatrixVP。最后,还有通过UNITY_MATRIX_P定义的投影矩阵,可以作为glstate_matrix_projection使用。我们不需要这些额外的矩阵,但是如果不包含它们,代码将不会编译:

#define UNITY_MATRIX_M unity_ObjectToWorld
#define UNITY_MATRIX_I_M unity_WorldToObject
#define UNITY_MATRIX_V unity_MatrixV
#define UNITY_MATRIX_VP unity_MatrixVP
#define UNITY_MATRIX_P glstate_matrix_projection

将额外的矩阵添加到UnityInput中:

float4x4 unity_ObjectToWorld;
float4x4 unity_WorldToObject;

float4x4 unity_MatrixVP;
float4x4 unity_MatrixV;
float4x4 glstate_matrix_projection;

最后缺少的东西不是矩阵。它是unity_WorldTransformParams,它包含了一些我们在这里不需要的转换信息。它是一个定义为real4的向量,real4本身不是一个有效的类型,而是float4或half4的别名,这取决于目标平台:

float4x4 unity_ObjectToWorld;
float4x4 unity_WorldToObject;
real4 unity_WorldTransformParams;

该别名和许多其他基本宏是为每个图形API定义的,我们可以通过包含Packages / com.unity.render-pipelines.core / ShaderLibrary / Common.hlsl来获得全部信息。在包含UnityInput.hlsl之前,请在我们的Common.hlsl文件中执行此操作。如果你对它们的内容感到好奇的话,也可以查看这些文件:

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "UnityInput.hlsl"

1.7、Color

渲染对象的颜色可以通过调整UnlitPassFragment来改变。例如,我们可以通过返回float4(1.0, 1.0, 0.0, 1.0)而不是0来将其设置为黄色:

float4 UnlitPassFragment () : SV_TARGET {
	return float4(1.0, 1.0, 0.0, 1.0);
}

在这里插入图片描述
为了使配置每个材质的颜色成为可能,我们必须将其定义为一个统一的值。在include指令下面,在UnlitPassVertex函数之前执行。我们需要一个float4,并将其命名为_BaseColor。前面的下划线是表示它表示材质属性的标准方法。在UnlitPassFragment中返回这个值而不是硬编码的颜色:

#include "../ShaderLibrary/Common.hlsl"

float4 _BaseColor;

float4 UnlitPassVertex (float3 positionOS : POSITION) : SV_POSITION {
	float3 positionWS = TransformObjectToWorld(positionOS);
	return TransformWorldToHClip(positionWS);
}

float4 UnlitPassFragment () : SV_TARGET {
	return _BaseColor;
}

我们回到黑色,因为默认值是0。为了将其链接到材质,我们必须在Unlit着色器文件的属性块中添加_BaseColor:

	Properties {
		_BaseColor
	}

属性名后面必须跟一个用于检查器的字符串和一个Color类型标识符,就像给一个方法提供参数一样:

		_BaseColor("Color", Color)

最后,我们必须提供一个默认值,在本例中是通过给它分配一个由4个数字组成的列表。我们用白色的:

		_BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)

在这里插入图片描述
现在可以用我们的着色器创建多个材质,每个材质都有不同的颜色。

2.0、Batching(批次)

每次绘制调用都需要CPU和GPU之间的通信。如果有大量数据需要发送给GPU,那么它可能会浪费时间等待。当CPU忙于发送数据时,它不能做其他事情。这两个问题都会降低帧速率。现在我们的方法很简单:每个对象都有自己的绘制调用。这是最糟糕的方式,尽管我们最终发送的数据很少,所以现在没问题。

举个例子,我用76个球体制作了一个场景,每个球体都使用四种材料:红、绿、黄、蓝。渲染需要78次绘制调用,球体76次,天空盒1次,清除渲染目标1次。
76 spheres, 78 draw calls
如果你打开游戏窗口的Stats面板,你就可以看到渲染帧所需要的概览。这里有趣的事实是,它显示了77个批处理—忽略clear—其中0被批处理保存:
在这里插入图片描述

2.1、SRP Batcher(SRP合批)

批处理是结合绘制调用的过程,减少了CPU和GPU之间的通信时间。最简单的方法是启用SRP批处理程序。然而,这只适用于兼容的着色器,而我们的Unlit着色器不是。您可以通过在检查器中选择它来验证这一点。有一条SRP批处理线表示不兼容性,在这条线下面给出了一个原因:
在这里插入图片描述
SRP批次并没有减少抽取调用的数量,而是使它们更精简。它将材质属性缓存到GPU上,这样就不必每次绘制调用时都发送材质属性。这既减少了需要通信的数据量,也减少了CPU在每次绘制调用时必须做的工作。但这只有在着色器遵循统一数据的严格结构时才有效.

所有的材质属性都必须定义在一个具体的内存缓冲区中,而不是在全局层面上。这是通过包装_BaseColor声明在cbuffer块与UnityPerMaterial名称。这类似于struct声明,但必须以分号结束。它通过将_BaseColor放入特定的常量内存缓冲区来隔离它,尽管它仍然在全局级别上可访问:

cbuffer UnityPerMaterial {
	float _BaseColor;
};

常量缓冲区并不是所有平台上都支持的——比如OpenGL ES 2.0——所以我们可以使用CBUFFER_START和CBUFFER_END宏,而不是直接使用cbuffer,这两个宏是我们从Core RP库中包含的。第一个以缓冲区名称作为参数,就像它是一个函数一样。在本例中,除了cbuffer代码将不存在于不支持它的平台之外,我们最终得到了与之前完全相同的结果:

CBUFFER_START(UnityPerMaterial)
	float4 _BaseColor;
CBUFFER_END

我们还需要对unity_ObjectToWorld,unity_WorldToObject和unity_WorldTransformParams执行此操作,它们必须分组在UnityPerDraw缓冲区中:

CBUFFER_START(UnityPerDraw)
	float4x4 unity_ObjectToWorld;
	float4x4 unity_WorldToObject;
	real4 unity_WorldTransformParams;
CBUFFER_END

在这个示例中,如果我们使用特定的一组值,则需要全部定义它们。对于转换组,即使我们不使用它,我们也需要包括float4 unity_LODFade。顺序无关紧要,但是Unity会将其直接放在unity_WorldToObject之后,因此我们也要这样做:

CBUFFER_START(UnityPerDraw)
	float4x4 unity_ObjectToWorld;
	float4x4 unity_WorldToObject;
	float4 unity_LODFade;
	real4 unity_WorldTransformParams;
CBUFFER_END

在这里插入图片描述
与我们的着色器兼容,下一步是启用SRP批处理程序,这是通过设置GraphicsSettings完成的。useScriptableRenderPipelineBatching为true。我们只需要这样做一次,所以让我们在管道实例创建时这样做,通过向CustomRenderPipeline添加一个构造函数方法:

	public CustomRenderPipeline () {
		GraphicsSettings.useScriptableRenderPipelineBatching = true;
	}

在这里插入图片描述
“统计”面板显示已节省了76个批次,但是为负数。帧调试器现在也在RenderLoopNewBatcher.Draw下显示一个SRP Batch条目,但是请记住,它不是单个绘制调用,而是它们的优化队列

2.2、Many Colors(多种颜色)

即使我们使用四种材质,也可以得到一个批次。之所以可行,是因为它们的所有数据都缓存在GPU上,并且每个绘制调用仅需包含一个指向正确内存位置的偏移量。唯一的限制是每种材质的内存布局需要相同,这是因为我们对所有材质都使用相同的着色器,每个着色器仅包含一个颜色属性。Unity不会比较材质的确切内存布局,它只是仅批处理使用完全相同的着色器变体的绘制调用。

如果只需要几种不同的颜色,它可以很好地工作,但是如果要为每个球体赋予自己的颜色,那么就需要创建更多的材质。那么假如可以为动态每个对象设置颜色,是不是会更加方便?默认情况下这是不可能的,但是可以通过创建自定义组件类型来支持它。将其命名为PerObjectMaterialProperties。作为一个示例,将其放在“ Custom RP”下的“ Examples”文件夹中。

这个想法是,一个游戏对象可以附加一个PerObjectMaterialProperties组件,该组件具有“Base Color”配置选项,该选项将用于为其设置_BaseColor材质属性。它需要知道shader属性的标识符,可以通过Shader.PropertyToID检索该标识符并将其存储在静态变量中,就像在CameraRenderer中为shader pass标识符所做的那样,但这个例子里它是整数。

using UnityEngine;

[DisallowMultipleComponent]
public class PerObjectMaterialProperties : MonoBehaviour {
	
	static int baseColorId = Shader.PropertyToID("_BaseColor");
	
	[SerializeField]
	Color baseColor = Color.white;
}

在这里插入图片描述
设置每个对象的材质属性是通过MaterialPropertyBlock对象完成的。我们只需要一个PerObjectMaterialProperties实例,并且它可以重用,所以为它声明一个 static 字段:

	static MaterialPropertyBlock block;

如果还没有一个block ,就创建一个新block ,然后使用属性标识符和颜色在其上调用SetColor,然后通过SetPropertyBlock将block 应用于游戏对象的Renderer组件,该组件会复制其设置。在OnValidate中执行此操作,以便结果立即显示在编辑器中:

	void OnValidate () {
		if (block == null) {
			block = new MaterialPropertyBlock();
		}
		block.SetColor(baseColorId, baseColor);
		GetComponent<Renderer>().SetPropertyBlock(block);
	}

OnValidate什么时候调用?
加载或更改组件后,将在Unity编辑器中调用OnValidate。因此,每次加载场景时以及编辑组件时。因此,各个颜色会立即显示并响应编辑。
把组件添加给24个球,并给它们不同的颜色

我把这个组件加入到24个任意的球体中,并给它们不同的颜色:
在这里插入图片描述
(使用MaterialPropertyBlock会打断SRP合批)不幸的是,SRP批处理程序不能处理每个对象的材质属性。因此,这24个球体回落到一个常规的绘制调用,由于排序,可能也会将其他球体分成多个批次
在这里插入图片描述
此外,OnValidate不会在构建中被调用。要让单独的颜色出现在那里,我们还必须在Awake中应用它们,我们可以通过调用那里的OnValidate来实现:

	void Awake () {
		OnValidate();
	}

2.3、GPU Instancing(GPU实例化)

还有一种合并DrawCall的方法,该方法适用于逐对象的材质属性。这就是所谓的GPU实例化(GPUInstancing),其工作原理是一次对具有相同网格物体的多个对象发出一次绘图调用。CPU收集所有每个对象的变换和材质属性,并将它们放入数组中,然后发送给GPU。然后,GPU遍历所有条目,并按提供顺序对其进行渲染。

因为GPU实例需要通过数组提供数据,所以我们的着色器当前不支持该数据。进行此工作的第一步是在着色器的Pass块的顶点和片段编译片段上方添加#pragma multi_compile_instancing指令:

			#pragma multi_compile_instancing
			#pragma vertex UnlitPassVertex
			#pragma fragment UnlitPassFragment

这将使Unity生成着色器的两个变体,一个支持GPU实例化,一个不支持GPU实例化。材质检查器中也出现了一个切换选项,允许我们选择每个材质使用哪个版本:
在这里插入图片描述
支持GPU实例化需要更改方法,为此,需要包括来自核心着色器库的UnityInstancing.hlsl文件。在定义UNITY_MATRIX_M和其他宏之后并在包含SpaceTransforms.hlsl之前完成此操作:

#define UNITY_MATRIX_P glstate_matrix_projection

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"

UnityInstancing.hlsl的作用是重新定义这些宏来访问实例数据数组。但是要进行这项工作,需要知道当前正在渲染的对象的索引。索引是通过顶点数据提供的,因此需要使其可用。UnityInstancing.hlsl定义了宏来简化此过程,但是它假定顶点函数具有struct参数。

可以声明一个结构(就像cbuffer一样)并将其用作函数的输入参数。我们还可以在结构内部定义语义。这种方法的优点是,它比长参数列表更清晰易读。因此,将UnlitPassVertex的positionOS参数包装在Attributes结构中,以表示顶点输入数据:

struct Attributes {
	float3 positionOS : POSITION;
};

float4 UnlitPassVertex (Attributes input) : SV_POSITION {
	float3 positionWS = TransformObjectToWorld(input.positionOS);
	return TransformWorldToHClip(positionWS);
}

当使用GPU实例化时,对象索引也可以用作顶点属性。我们可以在适当的时候添加它,只需将UNITY_VERTEX_INPUT_INSTANCE_ID放在Attributes中:

struct Attributes {
	float3 positionOS : POSITION;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};

接下来,在 UnlitPassVertex 的开头添加 UNITY_SETUP_INSTANCE_ID(input)。这将从输入中提取索引,并将其存储在其他实例宏所依赖的全局静态变量中:

float4 UnlitPassVertex (Attributes input) : SV_POSITION {
	UNITY_SETUP_INSTANCE_ID(input);
	float3 positionWS = TransformObjectToWorld(input.positionOS);
	return TransformWorldToHClip(positionWS);
}

这足以使GPU实例化进行工作了,因为SRP批处理程序拥有优先权,所以我们现在还没有得到想要的结果。现在尚不支持逐实例的材质数据。如果要添加的话,需要在需要时用数组引用替换_BaseColor。通过用UNITY_INSTANCING_BUFFER_START替换CBUFFER_START以及用UNITY_INSTANCING_BUFFER_END替换CBUFFER_END来完成,它需要一个参数。这不必与开始时一样,但也没有什么别的理由把它设置为不同

//CBUFFER_START(UnityPerMaterial)
//	float4 _BaseColor;
//CBUFFER_END

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
	float4 _BaseColor;
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

然后,将_BaseColor的定义替换为UNITY_DEFINE_INSTANCED_PROP(Float 4,_BaseColor):

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
	//	float4 _BaseColor;
	UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

使用实例化时,我们现在还需要在UnlitPassFragment中提供实例索引。为了简单起见,我们将使用一个结构,通过UNITY_TRANSFER_INSTANCE_ID(input,output)使UnlitPassVertex输出位置和索引。复制索引(如果存在)。我们像Unity一样命名此结构Varying,因为它包含的数据在同一三角形的片段之间可能会有所不同:

struct Varyings {
	float4 positionCS : SV_POSITION;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};

Varyings UnlitPassVertex (Attributes input) { //: SV_POSITION {
	Varyings output;
	UNITY_SETUP_INSTANCE_ID(input);
	UNITY_TRANSFER_INSTANCE_ID(input, output);
	float3 positionWS = TransformObjectToWorld(input.positionOS);
	output.positionCS = TransformWorldToHClip(positionWS);
	return output;
}

将此结构作为参数添加到UnlitPassFragment。然后像以前一样使用UNITY_SETUP_INSTANCE_ID来使索引可用。现在需要通过UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial,_BaseColor)访问material属性:

float4 UnlitPassFragment (Varyings input) : SV_TARGET {
	UNITY_SETUP_INSTANCE_ID(input);
	return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
}

在这里插入图片描述
现在,Unity可以将24个球体与每个对象的颜色组合在一起,从而减少了绘制调用的次数。最后进行了四个实例化的绘制调用,因为这些球体仍使用其中的四种材质。GPU实例化仅适用于共享相同材质的对象。当它们需要重新覆盖材质颜色时,都可以使用相同的材质,然后允许将它们分批绘制:
在这里插入图片描述
注意,根据目标平台和每个实例必须提供多少数据,批处理大小有限制。如果你超过了这个限制,你就会得到多于一批的产品。此外,如果有多种材料在使用,分类仍然可以分割批次。

2.4、Drawing Many Instanced Meshes(绘制多个实例网格)

当成百上千个对象可以在一次DC中合并时,GPU instancing 就成为了一个重要的优势。但是手动编辑场景中这么多的物体是不切合实际的。所以让我们随机产生他们吧。创建一个MeshBall的示例组件,当它Awake时会产生许多对象。让它缓存_BaseColor着色器属性,并为材质和Mesh添加支持instancing所需要的一些配置项:

using UnityEngine;

public class MeshBall : MonoBehaviour {

	static int baseColorId = Shader.PropertyToID("_BaseColor");

	[SerializeField]
	Mesh mesh = default;

	[SerializeField]
	Material material = default;
}

用这个组件创建一个游戏对象。我给它一个默认的球体网格来绘制:
在这里插入图片描述
我们可以生成很多新的游戏对象,但没必要这么做。相应的,只要填充变换矩阵和颜色的数组,并告诉GPU用它们渲染网格就好。这是GPU instancing 最有用的地方。最多可以一次提供1023个实例,因此让我们添加具有该长度的数组的字段,以及需要传递颜色数据的MaterialPropertyBlock。这时,颜色数组的元素类型必须为Vector4:

	Matrix4x4[] matrices = new Matrix4x4[1023];
	Vector4[] baseColors = new Vector4[1023];

	MaterialPropertyBlock block;

创建一个Awake方法,用半径为10的球体内的随机位置和随机的RGB颜色数据填充数组:

	void Awake () {
		for (int i = 0; i < matrices.Length; i++) {
			matrices[i] = Matrix4x4.TRS(
				Random.insideUnitSphere * 10f, Quaternion.identity, Vector3.one
			);
			baseColors[i] =
				new Vector4(Random.value, Random.value, Random.value, 1f);
		}
	}

在Update中,如果不存在block,则创建一个block,并在其上调用SetVectorArray来配置颜色。之后,以Mesh,sub-mesh的索引为零,材质,矩阵数组,元素数量和属性块作为参数调用Graphics.DrawMeshInstanced。我们在此处设置block,以便mesh ball能够支持热重载:

	void Update () {
		if (block == null) {
			block = new MaterialPropertyBlock();
			block.SetVectorArray(baseColorId, baseColors);
		}
		Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 1023, block);
	}

在这里插入图片描述
进入游戏模式将产生一个密集的球体。它需要多少绘制调用取决于平台,因为每个绘制调用的最大缓冲区大小不同。在我的例子中,渲染需要3次绘制调用。

请注意,各个网格的绘制顺序与我们提供数据的顺序相同。除此之外,没有任何排序或剔除的方法,所以一旦某个批处理在视锥范围内消失,整个批处理都将消失。

2.5、Dynamic Batching(动态合批)

减少DC的第三种方法称为动态批处理。这是一种古老的技术,它将共享相同材质的多个小网格合并为一个较大的网格,而该网格被绘制。但如果使用逐对象材质属性(per-object material properties)时,会失效。

较大的网格一般按需生成,所以动态合批仅适用于较小的网格。球体还是太大了,但立方体可以使用。要跟踪查看它的过程,需要禁用GPU实例化,然后在CameraRenderer.DrawVisibleGeometry中将enableDynamicBatching设置为true。

		var drawingSettings = new DrawingSettings(
			unlitShaderTagId, sortingSettings
		) {
			enableDynamicBatching = true,
			enableInstancing = false
		};

还要禁用 SRP 批处理器,因为它具有优先级

		GraphicsSettings.useScriptableRenderPipelineBatching = false;

在这里插入图片描述
一般来说,GPU实例化优于动态批处理。该方法也有一些注意事项,例如,当涉及不同的比例时,不能保证较大网格的法线向量为单位长度。此外,绘制顺序也将更改,因为它现在是单个网格而不是多个。

还有静态批处理,它的工作原理类似,但是会提前标记为静态批处理的对象。除了需要更多的内存和存储空间之外,它没有任何注意事项。RP不关心这个,因此使用起来不用过多担心。

2.6、Configuring Batching(配置合批)

那种方式更好可能取决于很多因素,所以把它们处理成可配置的选项会更好。首先,添加布尔参数以控制是否将动态批处理和GUI实例化用于DrawVisibleGeometry,而不是对其进行硬编码:

	void DrawVisibleGeometry (bool useDynamicBatching, bool useGPUInstancing) {
		var sortingSettings = new SortingSettings(camera) {
			criteria = SortingCriteria.CommonOpaque
		};
		var drawingSettings = new DrawingSettings(
			unlitShaderTagId, sortingSettings
		) {
			enableDynamicBatching = useDynamicBatching,
			enableInstancing = useGPUInstancing
		};}

渲染现在必须提供这个配置,然后依赖RP来提供它:

	public void Render (
		ScriptableRenderContext context, Camera camera,
		bool useDynamicBatching, bool useGPUInstancing
	) {DrawVisibleGeometry(useDynamicBatching, useGPUInstancing);}

CustomRenderPipeline将通过字段跟踪选项,在其构造函数方法中进行设置,然后在Render中传递它们。还将SRP批处理程序的bool参数添加到构造函数中,而不是始终启用它:

	bool useDynamicBatching, useGPUInstancing;

	public CustomRenderPipeline (
		bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher
	) {
		this.useDynamicBatching = useDynamicBatching;
		this.useGPUInstancing = useGPUInstancing;
		GraphicsSettings.useScriptableRenderPipelineBatching = useSRPBatcher;
	}

	protected override void Render (
		ScriptableRenderContext context, Camera[] cameras
	) {
		foreach (Camera camera in cameras) {
			renderer.Render(
				context, camera, useDynamicBatching, useGPUInstancing
			);
		}
	}

最后,将所有三个选项作为配置字段添加到CustomRenderPipelineAsset,并将它们传递给createpieline中的构造函数调用:

	[SerializeField]
	bool useDynamicBatching = true, useGPUInstancing = true, useSRPBatcher = true;

	protected override RenderPipeline CreatePipeline () {
		return new CustomRenderPipeline(
			useDynamicBatching, useGPUInstancing, useSRPBatcher
		);
	}

在这里插入图片描述
现在可以改变我们的RP所使用的方法。切换一个选项会立即生效,因为Unity编辑器会在检测到资产被更改时创建一个新的RP实例。

3.0、Transparency(透明)

着色器现在可以用来创建Unlit的不透明材质。颜色的alpha通道,通常表示透明度,但目前修改它不会有任何效果。可以将渲染队列设置为Transparent,但这只是在对象被绘制时,提供应该按什么顺序,而不是如何去绘制时。
在这里插入图片描述
我们不需要编写一个单独的着色器来支持透明材质。通过一些工作,我们的Unlit着色器可以支持不透明和透明渲染。

3.1、Blend Modes(混合模式)

不透明渲染和透明渲染之间的主要区别是,我们是替换之前绘制的任何内容还是与之前的结果结合以产生透视效果。可以通过设置源和目标混合模式来控制。这里的源是指现在绘制的内容,目标是先前绘制的内容,以及最终产生的结果。为此添加两个着色器属性:_SrcBlend和_DstBlend。它们是blend modes的枚举,我们可以使用的最佳类型是Float,默认情况下将源设置为1,将目标设置为零:

	Properties {
		_BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)
		_SrcBlend ("Src Blend", Float) = 1
		_DstBlend ("Dst Blend", Float) = 0
	}

为了使编辑更容易,我们可以将Enum属性添加到属性中,使用完全限定的UnityEngine.Rendering.BlendMode枚举类型作为参数:

		[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Src Blend", Float) = 1
		[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 0

在这里插入图片描述
默认值表示我们已经使用的不透明混合配置。源设置为1,表示完全添加,而目标设置为零,表示忽略。

标准透明度的源混合模式是SrcAlpha,这意味着渲染颜色的RGB分量乘以其alpha分量。因此,alpha值越低越弱。然后将目标混合模式设置为相反:OneMinusSrcAlpha,以达到总权重1
在这里插入图片描述
可以在Pass块中使用Blend语句和两个模式来定义混合模式。想使用着色器属性,可以通过将其放在方括号内来访问它们:

		Pass {
			Blend [_SrcBlend] [_DstBlend]

			HLSLPROGRAM
			…
			ENDHLSL
		}

在这里插入图片描述

3.2、Not Writing Depth(不写深度)

透明呈现通常不会写入深度缓冲区,因为它不会从中受益,甚至可能产生不希望的结果。我们可以通过ZWrite语句控制是否写入深度。我们可以再次使用着色器属性,这次使用_ZWrite:

			Blend [_SrcBlend] [_DstBlend]
			ZWrite [_ZWrite]

使用自定义Enum(Off, 0, On, 1)属性来定义着色器属性,以创建一个默认开启值为0和1的开关:

		[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Src Blend", Float) = 1
		[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 0
		[Enum(Off, 0, On, 1)] _ZWrite ("Z Write", Float) = 1

在这里插入图片描述

3.3、Texturing(纹理)

之前,我们使用Alpha贴图来创建非均匀的半透明材质。通过向着色器添加_BaseMap纹理属性,现在也可以支持。在本例中,它的类型为2D,使用Unity的标准白色纹理作为默认设置,并以white 字符串表示。同样,也必须以空代码块结束texture属性定义。在很早很早以前,它就用来控制纹理设置,知道今天仍然能够使用,主要目的还是为了兼容,防止出现奇怪的错误:

		_BaseMap("Texture", 2D) = "white" {}
		_BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)

在这里插入图片描述
纹理需要上传到GPU的内存里,这一步Unity会为我们做。着色器需要一个相关纹理的句柄,我们可以像定义一个uniform 值那样定义它,只是我们使用名为TEXTURE2D的宏参数。我们还需要为纹理定义一个采样器状态,考虑到wrap 和filter的模式,该状态控制着色器应如何采样。通过SAMPLER宏实现,例如TEXTURE2D,但在名称前添加了sampler。用来匹配Unity自动提供的采样器状态。

纹理和采样器状态都是着色器资源。不能按实例提供,必须在全局范围内声明。在UnlitPass.hlsl中的着色器属性之前执行此操作。

TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
	UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

除此之外,Unity还通过与纹理属性同名的float4使纹理的平铺和偏移可用,但附加了_ST,这表示缩放和平移或类似的东西。这个属性是UnityPerMaterial缓冲区的一部分,因此可以根据实例进行设置:

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
	UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
	UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

要采样纹理,就需要纹理坐标,它是顶点属性的一部分。具体来说,我们需要第一对坐标,或者更多。这是通过将具有TEXCOORD0含义的float2字段添加到属性来完成的。由于它是用于base map的,纹理空间尺寸通常被命名为U和V,因此我们将其命名为baseUV:

struct Attributes {
	float3 positionOS : POSITION;
	float2 baseUV : TEXCOORD0;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};

要将坐标传递给片段函数,因为在会那里对纹理进行采样。因此也将float2 baseUV添加到Varyings中。这次我们不需要添加特殊含义,只是传递的数据并不需要让GPU关注。但是,基于语法,我们仍然必须赋予它一些含义。所以可以给它添加任何 unused 的标识符,这里就简单地使用VAR_BASE_UV:

struct Varyings {
	float4 positionCS : SV_POSITION;
	float2 baseUV : VAR_BASE_UV;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};

当我们在UnlitPassVertex中复制坐标时,还可以应用存储在_BaseMap_ST中的scale 和 offset 。这样,我们就可以按每个顶点而不是每个片段进行操作了。scale 存储在XY中,offset 存储在ZW中,我们可以通过swizzle属性访问:

Varyings UnlitPassVertex (Attributes input) {float4 baseST = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseMap_ST);
	output.baseUV = input.baseUV * baseST.xy + baseST.zw;
	return output;
}

现在,UV坐标可用于UnlitPassFragment,并在整个三角形内插值。在这里,通过使用SAMPLE_TEXTURE2D宏对纹理,采样器状态和坐标作为参数,对纹理进行采样。最终颜色是通过乘法相结合的纹理和单一颜色。将两个相同大小的向量相乘会导致所有匹配分量相乘,因此在这种情况下,红色乘以红色,绿色乘以绿色,依此类推:

float4 UnlitPassFragment (Varyings input) : SV_TARGET {
	UNITY_SETUP_INSTANCE_ID(input);
	float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);
	float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
	return baseMap * baseColor;
}

在这里插入图片描述
因为我们的纹理的RGB数据是均匀白色的,所以颜色不受影响。但阿尔法通道各不相同,因此透明度不再一致。

3.4、Alpha Clipping(透明裁剪)

透视表面的另一种用法是在表面上挖孔。着色器也可以通过丢弃通常会渲染的某些片段来做到这一点。但这会产生硬边,而不是我们当前看到的平滑过渡。这种技术称为alpha clip。完成此操作的通常方法是定义一个截止阈值。alpha值低于此阈值的片段将被丢弃,而所有其他片段将保留。

添加一个_Cutoff属性,默认情况下将其设置为0.5。由于alpha始终位于零和1之间,因此我们可以使用Range(0.0,1.0)作为其类型:

		_BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)
		_Cutoff ("Alpha Cutoff", Range(0.0, 1.0)) = 0.5

同样将其添加到UnlitPass.hlsl的材质属性中:

	UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
	UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)

过调用UnlitPassFragment中的clip函数来丢弃片段。如果我们传递的值为零或更小,它将中止并丢弃该片段。因此,将最终的alpha值(可通过a或w属性访问)减去截止阈值传递给它:

	float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);
	float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
	float4 base = baseMap * baseColor;
	clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff));
	return base;

在这里插入图片描述
在这里插入图片描述
材质通常在透明混合或Alpha裁剪中二选一,而不是同时使用。除了丢弃的片段外,典型的clip 材质是完全不透明的,并且确实会写入深度缓冲区。它使用AlphaTest渲染队列,这意味着它将在所有完全不透明的对象之后渲染。这样做是因为丢弃片段使某些GPU优化无法实现,因为不会再假定三角形完全覆盖了它们后面的内容。首先,通过绘制完全不透明的对象,它们可能最终覆盖了部分alpha剪裁对象,可以节省处理一些隐藏片元:
在这里插入图片描述
在这里插入图片描述
但是,要使此优化工作有效,必须确保仅在需要时才使用剪裁。通过添加功能切换着色器属性来实现。这是一个Float属性,默认情况下设置为零,具有一个控制着色器关键字的Toggle属性,我们将使用_CLIPPING。属性本身的名称无关紧要,因此只需使用_Clipping:

		_Cutoff ("Alpha Cutoff", Range(0.0, 1.0)) = 0.5
		[Toggle(_CLIPPING)] _Clipping ("Alpha Clipping", Float) = 0

在这里插入图片描述

3.5、Shader Features(Shader功能)

启用切换功能会将_CLIPPING关键字添加到材质的激活的关键字列表中,而禁用则将其删除。但这并不会单独改变什么。必须告诉Unity根据关键字是否已定义来编译着色器的不同版本。为此,我们将#pragma shader_feature _CLIPPING添加到其Pass中的指令中:

			#pragma shader_feature _CLIPPING
			#pragma multi_compile_instancing

现在,无论是否定义了_CLIPPING,Unity都将编译着色器代码。它将生成一个或两个变体,具体取决于我们如何配置材质。因此,我们可以使代码以定义为条件,就像包含保护一样,但是这个示例中,想在定义_CLIPPING时包括裁切。我们可以使用#ifdef _CLIPPING,但是我更喜欢#if defined(_CLIPPING)。

	#if defined(_CLIPPING)
		clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff));
	#endif

3.6、Cutoff Per Object(逐对象裁切)

由于cutoff 是UnityPerMaterial缓冲区的一部分,因此可以按实例进行配置。那就把该功能添加到PerObjectMaterialProperties中吧。除了需要在属性块上调用SetFloat而不是SetColor之外,它的作用与颜色相同

	static int baseColorId = Shader.PropertyToID("_BaseColor");
	static int cutoffId = Shader.PropertyToID("_Cutoff");

	static MaterialPropertyBlock block;

	[SerializeField]
	Color baseColor = Color.white;

	[SerializeField, Range(0f, 1f)]
	float cutoff = 0.5f;void OnValidate () {
		…
		block.SetColor(baseColorId, baseColor);
		block.SetFloat(cutoffId, cutoff);
		GetComponent<Renderer>().SetPropertyBlock(block);
	}

在这里插入图片描述
在这里插入图片描述

3.7、Ball of Alpha-Clipped Spheres(球形 alpha裁剪)

同样的道理也适用于MeshBall。现在我们可以使用剪辑材料,但所有的实例最终都有完全相同的洞:
在这里插入图片描述
通过给每个实例一个随机的旋转,加上一个在0.5-1.5范围内的随机均匀比例,来增加一些变化。但是,与其设置每个实例的cut off ,不如将它们的颜色的Alpha通道更改为0.5–1范围。虽然这会带来不太精确的控制,但它可以表现出来随机

			matrices[i] = Matrix4x4.TRS(
				Random.insideUnitSphere * 10f,
				Quaternion.Euler(
					Random.value * 360f, Random.value * 360f, Random.value * 360f
				),
				Vector3.one * Random.Range(0.5f, 1.5f)
			);
			baseColors[i] =
				new Vector4(
					Random.value, Random.value, Random.value,
					Random.Range(0.5f, 1f)
				);

在这里插入图片描述
注意,Unity最终仍然会向GPU发送一个截止值数组,每个实例一个截止值,即使它们都是相同的。这个值是材料的一个拷贝,所以通过改变,它可以改变所有球体的洞一次,即使他们仍然不同

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值