3.1.3. | ShaderLab SubShader.
着色器的第二个组件是 SubShader。 每个shader都由至少一个SubShader组成,以保证程序的完美加载。 当有多个 SubShader 时,Unity 会对每个 SubShader 进行处理,并根据硬件特性从列表中的第一个到最后一个选择最合适的。 为了理解这一点,我们假设着色器将在支持 Metal graph API (iOS) 的硬件上运行。 为此,Unity 将运行第一个支持metal graph的 SubShader 并运行它。 当不支持 SubShader 时,Unity 将尝试使用与默认着色器相对应的后备组件,以便硬件可以继续执行其任务,而不会出现图形错误。
Shader "InspectorPath/shaderName"
{
Properties { … }
SubShader
{
// shader configuration here
}
}
如果我们注意 USB_simple_color 着色器,SubShader 的默认值将如下所示:
Shader "USB/USB_simple_color"
{
Properties { … }
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass { … }
}
}
3.1.4. | SubShader Tags.
Tags 是显示着色器处理方式和时间的标签。
与游戏对象标签一样,它们可用于识别着色器将如何渲染或一组着色器将如何以图形方式表现。
所有标签的语法如下:
Tags
{
"TagName1"="TagValue1"
"TagName2"="TagValue2"
}
这些可以写在两个不同的字段中,要么在 SubShader 中,要么在 Pass 中。 这一切都取决于我们想要获得的结果。 如果我们在 SubShader 中写入标签,它将影响着色器中包含的所有通道,但如果我们将其写入通道内,则只会影响选定的通道。
“Queue”是我们更频繁使用的标签,因为它允许我们定义对象表面的外观。 默认情况下,所有表面都被定义为“不透明”,即它们不具有透明度。
如果我们查看 USB_simple_color 着色器,我们会在 SubShader 中找到以下代码行,它为着色器定义了一个不透明表面。
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass { … }
}
3.1.5. | Queue Tags.
默认情况下,此标签不会以图形方式显示为一行代码。 这是因为它是在 GPU 中默认编译的,因为它与每种材质的对象处理顺序直接相关。
Tags { "Queue"="Geometry" }
这个Tag与相机和GPU有着密切的关系。
每次我们在场景中定位一个对象时,我们都会将其信息传递给 GPU(例如顶点位置、法线、颜色等)。 在游戏视图的情况下,情况是一样的,不同之处在于我们发送到 GPU 的信息对应于相机视锥体内的对象。 一旦信息进入 GPU,我们就会将此数据发送到 VRAM,并要求它在屏幕上绘制对象。
绘制对象的过程称为 “draw call”。 着色器的pass越多,渲染中的绘制调用就越多。 一个pass相当于一次绘制调用,因此,如果我们的着色器内部有两个pass,那么该材质只会在 GPU 上生成两次绘制调用。
现在,GPU 是如何在屏幕上绘制这些元素的呢? 简而言之,GPU 将首先绘制距离相机最远的对象,最后绘制距离相机最近的元素。 该计算将根据物体与相机之间的距离(沿着其“Z”轴)进行。
(图3.1.5a。从图中我们可以看到,由于距离相机最远,所以首先在屏幕上绘制三角形。最后绘制正方形,并共同生成2个绘制调用)
Unity有一个称为“渲染队列”的处理队列,它允许我们修改GPU上对象的处理顺序。 修改渲染队列有两种方法:
- 通过检查器中材料的属性。
- 或使用Tag“Queue”。
如果我们修改shader中的Queue值,材质中Render Queue的默认值也会被修改。
该属性的顺序值范围为 0 到 5000,其中“0”对应最远的元素,“5000”对应距离相机最近的元素。 这些顺序值具有预定义的组,它们是:
• Background.
• Geometry.
• AlphaTest.
• Transparent.
• Overlay.
Tags { “Queue”=”Background” } 从 0 到 1499,默认值 1000。
Tags { “Queue”=”Geometry” } 从 1500 到 2399,默认值 2000。
Tags { “Queue”=”AlphaTest” } 从 2400 到 2699,默认值 2450。
Tags { “Queue”=”Transparent” } 从 2700 到 3599,默认值 3000。
Tags { “Queue”=”Overlay” } 从 3600 到 5000,默认值 4000。
Background 主要用于距离相机很远的元素(例如天空盒)。
Geometry 是队列中的默认值,用于场景中的不透明对象(例如,一般的图元和对象)。
AlphaTest 用于半透明物体,该物体必须位于不透明物体前面,但位于透明物体后面(例如玻璃、草或植被)。
Transparent 用于必须位于其他元素前面的透明元素。
Overlay 对应于场景中最重要的那些元素(例如 UI 图像)。
Shader "InspectorPath/shaderName"
{
Properties { … }
SubShader
{
Tags { "Queue"="Geometry" }
}
}
High Definition RP 使用渲染队列的方式与Built-in RP 不同,因为材质不会直接在检查器中显示此属性,而是引入了两种控制方法:
• Material order.
• render order.
HDRP 一起使用这两种顺序方法来控制对象处理
3.1.6. | Render Type Tags.
根据Unity官方文档,
使用 RenderType 标签覆盖着色器的行为
上述说法是什么意思? 基本上,使用此标签,我们可以在 SubShader 中从一种状态更改为另一种状态,从而在与给定类型匹配的任何材质上添加效果。
为了实现其功能,我们至少需要两个着色器:
- 替代品(我们想要在运行时添加的颜色或效果)
- 另一个要替换(分配给材质的着色器)
其语法如下:
Tags { "RenderType"="type" }
与队列标签一样,RenderType 具有不同的可配置值,这些值根据我们要执行的任务而变化。 其中,我们可以发现。
• Opaque. (Default.)
• Transparent.
• TransparentCutout.
• Background.
• Overlay.
• TreeOpaque.
• TreeTransparentCutout.
• TreeBillboard.
• Grass.
• GrassBillboard.
默认情况下,每次创建新着色器时都会设置“不透明”类型。 同样,Unity 中的大多数内置着色器都分配有此值,因为它们没有透明度配置。 然而,我们可以自由地改变这个类别; 一切都取决于我们应用于匹配的效果。
为了彻底理解这个概念,我们将执行以下操作。 在我们的项目中,
- 我们将确保在场景中创建一些 3D 对象。
- 我们将生成一个 C# 脚本,称为 USBReplacementController。
- 然后我们将创建一个着色器,并将其命名为 USB_replacement_shader。
- 最后,我们将添加一个材质,称为 USB_replaced_mat。
我们将使用 Camera.SetReplacementShader 在材质 USB_replaced_mat 上动态分配一个着色器。 材质着色器必须具有与替换着色器相同的标签渲染类型才能执行该功能。
举例来说,我们将把 Mobile/Unlit 着色器分配给 USB_replaced_mat。 该内置着色器的“RenderType”类型标签等于“Opaque”。 因此,着色器USB_replacement_shader必须匹配相同的RenderType才能执行操作。
(图 3.1.6a. Unlit shader(支持光照贴图)已分配到材质 USB_replaced_mat 上)
USBReplacementController 脚本必须作为组件直接分配给相机。 该控制器将负责用替换着色器替换着色器,只要它们在 RenderType 中具有相同的配置。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
public class USBReplacementController : MonoBehaviour
{
// replacement shader
public Shader m_replacementShader;
private void OnEnable()
{
if (m_replacementShader != null)
{
// the camera will replace all the shaders in the scene with
// the replacement one the “RenderType” configuration must match in both shader
GetComponent<Camera>().SetReplacementShader(m_replacementShader, "RenderType");
}
}
private void OnDisable()
{
// let's reset the default shader
GetComponent<Camera>().ResetReplacementShader();
}
}
值得一提的是,我们在类上定义了 [ExecuteInEditMode] 函数。 该属性允许我们在编辑模式下预览更改。
我们将使用 USB_replacement_shader 作为替换着色器。
我们已经知道,每次我们创建一个新的着色器时,它都会将其 RenderType 配置为“Opaque”。 因此,USB_replacement_shader 可能会替换我们之前分配给材质的 Unlit 着色器。
为了清楚地预览变化,我们将进入 USB_ replacement_shader 的片段着色器阶段并添加红色,我们将其乘以输出颜色。
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
// add a red color
fixed4 red = fixed4(1, 0, 0, 1);
return col * red;
}
我们必须确保在 USBReplacementController 脚本中找到的 Shader 类型替换变量中包含 USB_replacement_shader。
(图3.1.6b.USBReplacementController已分配给相机)
此外,我们之前在场景中添加的那些对象必须具有 USB_replaced_mat 材质。
(图 3.1.6c。材质 USB_replaced_mat 已分配给 3D 对象;分配给四边形、立方体和球体)
由于 USBReplacementController 类已包含 OnEnable 和 OnDisable 函数,因此如果我们激活或停用脚本,我们可以看到内置着色器 Unlit 在编辑模式下如何被 USB_replacement_shader 替换,在渲染中应用红色。
(图3.1.6d.USB_replacement_shader已经替换了内置着色器,最终渲染中的Unlit)
3.1.7. | SubShader Blending.
混合是将两个像素混合为一个像素的过程。 其命令与Built-in RP 和Scriptable RP 兼容。
混合发生在称为“合并”的阶段,该阶段将像素的最终颜色(已在片段着色器阶段处理过的像素)与其深度结合起来。 该阶段发生在渲染管道的末尾; 在片段着色器阶段之后,是执行模板缓冲区、z 缓冲区和颜色混合的地方。
默认情况下,此属性不会写入我们的着色器中,因为它是一个可选函数,主要在我们处理透明对象时使用,例如,当我们必须在另一个像素前面绘制具有低不透明度级别的像素时。
它的默认值是“Blend Off”,但我们可以激活它来生成不同类型的混合,就像 Photoshop 中出现的那样。
其语法如下:
Blend [SourceFactor] [DestinationFactor]
“混合”是一个函数,需要两个称为“因子”的值来进行操作,并且基于一个方程式,它将成为我们在屏幕上获得的最终颜色。 根据Unity中的官方文档,定义Blending值的方程式如下:
B = SrcFactor * SrcValue [OP] DstFactor * DstValue.
要理解这个操作,我们必须考虑以下几点: 片段着色器阶段首先发生,然后作为可选过程; 合并阶段。
“SrcValue”(源值)已在片段着色器阶段处理过,对应像素的 RGB 颜色输出。
“DstValue”(目标值)对应于已写入“目标缓冲区”中的 RGB 颜色,即“渲染目标”(SV_Target)。 当着色器中的混合选项未激活时,SrcValue 会覆盖 DstValue。 但是,如果我们激活此操作,两种颜色都会混合以获得新颜色,从而覆盖之前的 DstValue。
“SrcFactor”(源因子)和 “DstFactor”(目标因子)是根据其配置而变化的三个维度的向量。 它们的主要功能是修改SrcValue和DstValue值以达到有趣的效果。
我们可以在 Unity 文档中找到一些因子:
• Off,禁用混合选项。
• One,(1,1,1)。
• Zero,(0,0,0)。
• SrcColor 等于SrcValue 的RGB 值。
• SrcAlpha 等于SrcValue 的Alpha 值。
• OneMinusSrcColor,1 减去SrcValue 的RGB 值(1 - R、1 - G、1 - B)。
• OneMinusSrcAlpha,1 减去SrcValue 的Alpha (1 - A, 1 - A, 1- A)。
• DstColor 等于DstValue 的RGB 值。
• DstAlpha 等于DstValue 的Alpha 值。
• OneMinusDstColor,1 减去DstValue 的RGB 值(1 - R、1 - G、1 - B)。
• OneMinusDstAlpha,1 减去DstValue 的Alpha(1 - A、1 - A、1- A)。
值得一提的是,Alpha通道的混合与我们处理像素的RGB颜色的方式相同,但它是在一个独立的进程中完成的,因为它不经常使用。 同样,通过不执行此过程,可以优化渲染目标上的写入。
我们将上面的解释举例如下。
假设我们有一个 RGB 颜色像素,其值为 [0.5R、0.45G、0.35B]。 该颜色已由片段着色器阶段处理,因此它对应于 DstValue。 现在,我们将该值乘以等于 [1, 1, 1] 的“SrcFactor One”。 每个数字乘以“1”都会得到相同的值,因此,SrcFactor 和 DstValue 之间的结果与其初始值相同。
B = [0.5R, 0.45G, 0.35B] [OP] DstFactor * DstValue.
“OP”指的是我们要执行的操作。 默认情况下,它设置为“Add”。
B = [0.5R, 0.45G, 0.35B] + DstFactor * DstValue.
一旦我们获得了第一个操作的值,它就会被DstValue覆盖,因此,被设置为相同的颜色[0.5R,0.45G,0.35B]。 因此,我们将该颜色乘以“DstFactor DstColor”,它等于 DstFactor 中的当前值。
DstFactor [0.5R, 0.45G, 0.35B] * DstValue [0.5R, 0.45G, 0.35B] = [0.25R, 0.20G, 0.12B].
最后,像素的输出颜色为。
B = [0.5R, 0.45G, 0.35B] + [0.25R, 0.20G, 0.12B].
B = [0.75R, 0.65G, 0.47B]
如果我们想在着色器中激活混合,我们必须使用混合命令,然后使用 SrcFactor,然后使用 DstFactor。
Shader "InspectorPath/shaderName"
{
Properties { … }
SubShader
{
Tags { "Queue"="Transparent" "RenderType"="Transparent" }
Blend SrcAlpha OneMinusSrcAlpha
}
}
如果我们想在着色器中使用混合,则需要添加和修改“渲染队列”。 我们已经知道,“Queue”标签的默认值为“Geometry”,这意味着我们的对象将显得不透明。 如果我们希望我们的对象看起来透明,那么我们必须首先将“队列”更改为“透明”,然后添加某种混合。
最常见的混合类型如下:
• Blend SrcAlpha OneMinusSrcAlpha 普通透明混合
• Blend One One 添加剂混合颜色
• Blend OneMinusDstColor 一种温和的添加剂混合颜色
• Blend DstColor 零乘法混合颜色
• Blend DstColor SrcColor 乘法混合 x2
• Blend SrcColor 一种混合叠加
• Blend OneMinusSrcColor One 柔光混合
• Blend Zero OneMinusSrcColor 负色混合
配置混合的另一种方法是通过依赖项“UnityEngine.Rendering.BlendMode”。 这行代码允许我们从检查器中更改材质中对象的混合。 要进行设置,首先我们必须将“Toggle Enum”添加到我们的属性中,然后声明 SrcFactor 和 DstFactor。
其语法如下:
[Enum(UnityEngine.Rendering.BlendMode)]
_SrcBlend ("Source Factor", Float) = 1
[Enum(UnityEngine.Rendering.BlendMode)]
_DstBlend ("Destination Factor", Float) = 1
Shader "InspectorPath/shaderName"
{
Properties
{
[Enum(UnityEngine.Rendering.BlendMode)]
_SrcBlend ("SrcFactor", Float) = 1
[Enum(UnityEngine.Rendering.BlendMode)]
_DstBlend ("DstFactor", Float) = 1
}
SubShader
{
Tags { "Queue"="Transparent" "RenderType"="Transparent" }
Blend [_SrcBlend] [_DstBlend]
}
}
混合选项可以写在不同的字段中:在 SubShader 字段或 Pass 字段中,位置将取决于通道数和我们需要的最终结果。
3.1.8. | SubShader AlphaToMask.
有一些类型的混合非常容易控制,例如“SrcAlpha OneMinusSrcAlpha Blend”,它添加了包含 Alpha 通道的透明效果,但在其他情况下,混合无法为我们的着色器生成透明度。 在本例中,使用“AlphaToMask”属性,该属性在 Alpha 通道上应用遮罩,并且是一种与Built-in RP 和Scriptable RP 兼容的技术。
与混合不同,蒙版只能将值“一或零”分配给 Alpha 通道,这是什么意思? 虽然混合可以产生不同级别的透明度; 从“0.0f”到“1.0f”的级别,AlphaToMask 只能生成整数。 这转化为更严格的透明度类型,适用于特定情况,例如,它对于一般植被和创建空间门户效果非常有用。
• AlphaToMask On
• AlphaToMask Off 默认值
要激活此命令,我们可以在 SubShader 字段和 pass 字段中声明它。 它只有两个值:“On”和“Off”,其声明方式如下:
Shader "InspectorPath/shaderName"
{
Properties { … }
SubShader
{
Tags { "RenderType"="Opaque" }
AlphaToMask On
}
}
需要注意的是,与混合不同,在这种情况下,不需要添加透明度标签或其他命令。 我们只需添加 AlphaToMask,第四个颜色通道“A”就会自动获取程序中蒙版的质量。
3.1.9. | SubShader ColorMask.
此命令允许我们的 GPU 限制自身写入选定的颜色通道,并且与Built-in RP 和Scriptable RP 兼容。
当我们创建着色器时,默认情况下 GPU 会写入与颜色 (RGBA) 相对应的所有通道,但是,在某些情况下,我们可能只想显示某些颜色通道(例如红色通道或效果的“R”通道)。
• ColorMask R 我们的对象将看起来是红色的
• ColorMask G 我们的对象将看起来是绿色
• ColorMask B 我们的对象将看起来是蓝色的
• ColorMask A 我们的对象将受到透明度的影响。
• ColorMask RG 我们可以使用两个通道混合。
我们已经知道,缩写 RGBA 代表红色、绿色、蓝色和 Alpha,因此,如果我们将遮罩配置为值“G”,我们将仅将绿色通道显示为颜色输出。 这个ShaderLab命令使用起来非常简单,我们可以在SubShader和Pass中使用它。
其语法如下:
Shader "InspectorPath/shaderName"
{
Properties { … }
SubShader
{
Tags { "Queue"="Geometry" }
ColorMask RGB
}
}
3.2.0. | SubShader 剔除和深度测试。
要理解这两个概念,我们首先必须了解 Z-Buffer(也称为深度缓冲区)和 Depth Testing 是如何工作的。
在开始之前,我们必须考虑像素具有深度值。
这些值存储在深度缓冲区中,它确定一个对象是否位于屏幕上另一个对象的前面或后面。
另一方面,深度测试是确定深度缓冲区中的像素是否会被更新的条件。
我们已经知道,像素有一个分配的值,该值以 RGB 颜色测量并存储在颜色缓冲区中。 Z 缓冲区添加了一个额外的值,用于根据到相机的距离来测量像素的深度,但仅适用于其截锥体内的那些表面,这允许两个像素颜色相同但深度不同。
物体距离相机越近,Z 缓冲区值越低,缓冲区值较低的像素会覆盖缓冲区值较高的像素。
为了理解这个概念,我们假设场景中有一个相机和一些基元,它们都位于“Z”空间轴上。 现在,为什么在 z 轴上? Z-Buffer中的“Z”来自这样的事实:Z值测量相机和空间“Z”轴上的物体之间的距离,而“X和Y”值测量屏幕上的水平和垂直位移 。
“Buffer”这个词指的是临时存储数据的“内存空间”,因此,Z-Buffer 指的是我们场景中的物体和相机之间的深度值,这些值被分配给每个像素。
例如,我们要绘制一个总共 36 个像素的屏幕。
每次我们在场景中放置一个对象时,该对象都会占据屏幕上的某个像素区域。 因此,假设我们想要在场景中放置一个绿色方块。 鉴于其性质,它将占据从像素 8 到 29,因此,该区域内的所有像素都被激活并涂成绿色,同样,该信息将被发送到 Z-Buffer 和 Color Buffer.。
(图3.2.0a。Z-Buffer存储场景中物体的深度,Color Buffer存储RGBA颜色信息)。
我们在场景中放置一个新的正方形,这次是红色的并且更靠近相机。 为了与前一个区分开来,我们将这个正方形变小,占据像素15到29。正如我们所看到的,这个区域已经被初始正方形的信息占据了,那么这里会发生什么呢? 由于红色方块距相机的距离较短,因此这会覆盖 Z 缓冲区和颜色缓冲区的值,激活该区域中的像素,替换之前的颜色
(图3.2.0b)
如果向场景中添加更靠近相机的新元素,将以相同的方式重复此过程。 总之,Z 缓冲区和颜色缓冲区的值将被最靠近相机的对象覆盖。
生成有吸引力的视觉效果的一种方法是修改 Z 缓冲区值。 为此,我们将讨论 Unity 中包含的三个选项:Cull、ZWrite 和 ZTest。
与Tags一样,culling 和 depth testing 选项可以写入不同的字段中:SubShader 字段或 Pass 字段内。 该位置将取决于我们想要实现的结果以及我们想要处理的传递次数。
为了理解这个概念,我们假设我们想要创建一个着色器来表示钻石的表面。 为此,我们需要两个passes:第一个pass用于钻石的背景颜色,第二个pass用于其表面的光泽。 在这种假设的情况下,由于我们需要两个 passes 来实现不同的功能,因此有必要在每个 pass 中独立配置剔除选项。
3.2.1. | ShaderLab Cull.
此属性在 Built-in RP 和Scriptable RP 中兼容,控制在像素深度处理中将删除多边形的哪个面。 这是什么意思? 回想一下,多边形对象具有内表面和外表面。 默认情况下,外表面是可见的(剔除); 但是,我们可以按照如下所示的方案激活内部面。
• Cull Off 对象的两个面均被渲染。
• Cull Back 默认情况下,渲染对象的背面。
• Cull Front 渲染对象的正面。
该命令具有三个值:Back、Front 和 Off。 默认情况下,“Back”处于活动状态,但是,通常出于优化目的,与剔除相关的代码行在着色器中不可见。 如果我们想修改剔除选项,我们必须添加“Cull”一词,后跟我们要使用的模式。
Shader "InspectorPath/shaderName"
{
Properties { … }
SubShader
{
// Cull Off
// Cull Front
Cull Back
}
}
我们还可以通过依赖项“UnityEngine.Rendering.CullMode”动态配置 Unity Inspector 中的剔除选项,该依赖项从枚举抽屉中声明并作为函数中的参数传递。
Shader "InspectorPath/shaderName"
{
Properties
{
[Enum(UnityEngine.Rendering.CullMode)]
_Cull ("Cull", Float) = 0
}
SubShader
{
Cull [_Cull]
}
}
另一个有用的选项是通过语义 SV_IsFrontFace 实现的,它允许我们在两个网格面上投影不同的颜色和纹理。 为此,我们只需声明一个布尔变量并将此类语义分配为片段着色器阶段的参数。
fixed4 frag (v2f i, bool face : SV_IsFrontFace) : SV_Target
{
fixed4 colFront = tex2D(_FrontTexture, i.uv);
fixed4 colBack = tex2D(_BackTexture, i.uv);
return face ? colFront : colBack;
}
值得一提的是,此选项仅在着色器中的“Cull”命令先前设置为“Off”时才有效。
3.2.2. | ShaderLab ZWrite.
该命令控制将对象的表面像素写入Z-Buffer,也就是说,它允许我们忽略或尊重相机和对象之间的深度距离。 ZWrite有两个值,分别是:On和Off,其中“On”对应其默认值。 我们通常在处理透明度时使用此命令,例如,当我们激活混合选项时。
• ZWrite Off 为了透明度。
• ZWrite On 默认值。
使用透明时为什么要禁用 Z 缓冲区? 主要是因为半透明像素叠加(Z-fighting)。 当我们处理半透明物体时,GPU 通常不知道哪个物体位于另一个物体的前面,当我们在场景中移动相机时,会在像素之间产生重叠效果。 要解决此问题,我们必须通过将 ZWrite 命令设置为“关闭”来停用 Z 缓冲区,如下例所示:
Shader "InspectorPath/shaderName"
{
Properties { … }
SubShader
{
Tags { "Queue"="Transparent" "RenderType"="Transparent" }
Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off
}
}
当我们有两个或多个物体与相机的距离相同时,就会发生 Z 冲突,从而导致 Z 缓冲区中的值相同。
(图3.2.2a)
当尝试在渲染管道末端渲染像素时会出现这种效果。 由于 Z 缓冲区无法确定哪个元素在另一个元素后面,因此它会产生闪烁的线条,这些线条会根据相机的位置而改变形状。
要解决此问题,我们只需使用“ZWrite off”命令禁用 Z 缓冲区即可。
3.2.3. | ShaderLab ZTest.
ZTest 控制深度测试的执行方式,通常用于 multi - passes 着色器中以生成颜色和深度的差异。 该属性有七个不同的值,它们是:
• Less.
• Greater.
• LEqual.
• GEqual.
• Equal.
• NotEqual.
• Always.
其中它们对应的是比较操作。
ZTest Less: (<) 在前面绘制对象。 它忽略与着色器对象相同距离或后面的对象。
ZTest Greater: (>) 绘制后面的对象。 它不会绘制与着色器对象处于相同距离或前面的对象。
ZTest LEqual:(≤) 默认值。 绘制前方或等距离的物体。 它不会在着色器对象后面绘制对象。
ZTest GEqual: (≥) 将对象绘制在后面或相同距离。 不在着色器对象前面绘制对象。
ZTest Equal:(==) 绘制距离相同的对象。 不在着色器对象的前面或后面绘制对象。
Z 测试不等于:(!=) 绘制距离不同的对象。 不绘制与着色器对象距离相同的对象。
ZTest 始终:绘制所有像素,无论对象相对于相机的距离如何。
为了理解这个命令,我们将做以下练习:假设场景中有两个对象; 一个立方体和一个球体。 球体相对于相机位于立方体前面,像素深度符合预期。
(图3.2.3a)
如果我们再次将球体放置在立方体后面,深度值将如预期,为什么? 因为 Z 缓冲区存储屏幕上每个像素的深度值。 深度值是根据物体与相机的接近度来计算的。
(图3.2.3b)
现在,如果我们激活 ZTest Always 会发生什么? 在这种情况下,将不会进行深度测试,因此,所有像素将在屏幕上显示相同的深度。
(图3.2.3c)
其语法如下:
Shader "InspectorPath/shaderName"
{
Properties { … }
SubShader
{
Tags { "Queue"="Transparent" "RenderType"="Transparent" }
ZTest LEqual
}
}
3.2.4. | ShaderLab Stencil.
根据Unity官方文档:
模板缓冲区为帧缓冲区中的每个像素存储八位整数值(0 到 255)。 在为给定像素运行片段着色器阶段之前,GPU 可以将模板缓冲区中的当前值与确定的参考值进行比较。 这个过程称为模板测试。 如果模板测试通过,GPU 将执行深度测试。 如果模板测试失败,GPU 会跳过该像素的其余处理。 这意味着您可以使用模板缓冲区作为掩码来告诉 GPU 绘制哪些像素以及丢弃哪些像素。
为了理解上面的描述,我们必须考虑到模板缓冲区本身就是一个必须创建的“纹理”。 为此,它为帧缓冲区中的每个像素存储一个从 0 到 255 的整数值。
(图3.2.4a)
我们已经知道,当我们在场景中定位对象时,它们的信息会被发送到顶点着色器阶段(例如顶点位置)。 在此阶段中,对象的属性从对象空间转换为世界空间,然后是视图空间,最后是剪辑空间。 此过程仅适用于相机视锥体内的那些对象。
当这些信息被正确处理后,它会被发送到光栅化器,这使我们能够以像素为单位投影场景中对象的坐标,但是,在到达这一点之前,它会经历称为剔除和深度测试的前一处理阶段。 在此阶段,我们可以在着色器中操作各种过程,其中包括 Cull、ZWrite、ZTest 和 Stencil。
基本上,模板缓冲区的作用是激活模板测试,它允许丢弃“片段”(像素),以便它们不在片段着色器阶段进行处理,从而在我们的着色器中生成遮罩效果。 模板测试执行的函数具有以下语法:
if ( StencilRef & StencilReadMask [Comp] StencilBufferValue & StencilReadMask)
{
Accept Pixel.
}
else
{
Discard Pixel.
}
“StencilRef”是我们要传递给Stencil Buffer作为参考的值,这是什么意思? 请记住,模板缓冲区是覆盖对象像素区域的“纹理”。 StencilRef 作为一个 id 来映射 Stencil Buffer 中找到的所有像素。
例如,我们将 StencilRef 值设置为 2。
(图. 3.2.4b)
在上面的示例中,覆盖胶囊区域的所有像素都已标记有 StencilRef 的值,因此现在 Stencil Buffer 等于 2。
随后,为所有具有参考值(默认值为 255)的像素创建掩码 (StencilReadMask)。
(图. 3.2.4c)
因此,上述操作如下。
if ( 2 & 255 [Comp] 2 & 255)
{
Accept Pixel.
}
else
{
Discard Pixel.
}
“Comp”是指给出真值或假值的比较函数。 如果该值为 true,则程序将该像素写入帧缓冲区,否则,该像素将被丢弃。
在比较函数中,我们可以找到以下预定义运算符:
• Comp Never: 该操作将始终传递false。
• Comp Less: <.
• Comp Equal: ==.
• Comp LEqual: ≤.
• Comp Greater: >.
• Comp NotEqual: !=.
• Comp GEqual: ≥.
• Comp Always: 该操作将始终传递true。
我们至少需要两个着色器来使用模板缓冲区:一个用于遮罩,一个用于遮罩对象。
假设场景中有三个对象:一个立方体、一个球体和一个正方形。 球体位于立方体内部,我们想使用正方形作为“遮罩”来隐藏立方体,以便可以看到内部的球体。 为了创建这个示例蒙版,我们将创建一个名为 USB_stencil_ref 的着色器,其语法如下
SubShader
{
Tags { "Queue"="Geometry-1" }
ZWrite Off
ColorMask 0
Stencil
{
Ref 2 // StencilRef
Comp Always
Pass Replace
}
}
我们来分析一下上面的内容。 我们做的第一件事是配置“Queue”等于“Geometry minus one”。 几何默认为 2000,因此,几何减去 1 等于 1999,这将处理我们的正方形(mask),我们将首先在 Z-Buffer 中应用此着色器。 然而,正如我们所知,Unity默认情况下根据对象在场景中与相机相关的位置来处理对象,因此如果我们想禁用此功能,我们必须将属性“ZWrite”设置为“Off”。
然后我们将“ColorMask 设置为0”,以便帧缓冲区中的遮罩像素被丢弃并显示为透明。
此时,我们的方块仍然不能用作遮罩,因此我们接下来要做的是添加命令“Stencil”,使其发挥遮罩的作用。
“Ref 2” (StencilRef),我们的参考值,使用“比较操作”中定义的操作在 GPU 上与模板缓冲区的当前内容进行比较。
“Comp Always” 确保在模板缓冲区中设置“2”,同时考虑到屏幕上正方形覆盖的区域。 最后,“Pass Replace”指定 StencilRef 值“替换”Stencil Buffer 的当前值。
现在我们要分析我们想要屏蔽的对象。 我们将创建另一个名为 USB_stencil_value 的着色器来说明这一点。 其语法如下:
SubShader
{
Tags { "Queue"="Geometry" }
Stencil
{
Ref 2
Comp NotEqual
Pass Keep
}
…
}
(图3.2.4e)
与之前的着色器不同,我们将保持 Z-Buffer 处于活动状态,以便根据其相对于相机的位置在 GPU 上进行渲染。 然后我们添加 Stencil 命令,以便可以遮盖我们的对象。 我们再次添加“Ref 2”,将此着色器与遮罩的 USB_stencil_ref 链接起来。
然后,在比较操作中,我们分配“Comp NotEqual”。 这表明将渲染正方形周围暴露的立方体区域,因为模板测试通过了(无比较或为 true)。
另一方面,对于正方形区域,如果相等(Equal),则 Stencil Test 将不会通过,并且像素将被丢弃。 “Pass Keep”表示Cube维护Stencil Buffer当前的内容。
如果我们想要使用多个mask,我们可以给出一个不同于“2”的 Ref 属性编号。
3.2.5. | ShaderLab Pass.
我们着色器中的第三个组件对应于通道(Pass)。
一个着色器内可以有多个passes,但是,默认情况下 Unity 仅在 SubShader 字段内添加一个通道。
如果我们查看 USB_simple_color 着色器,我们会发现包含以下通道。
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata { … };
struct v2f { … };
sampler2D _MainTex;
float4 _MainTex;
v2f vert (appdata v) { … }
fixed4 frag (v2f i) : SV_Target { … }
}
Pass 字面意思是渲染通道。 对于那些使用 3D 软件(例如 Maya 或 Blender)进行渲染的人来说,这个概念会更容易理解,因为在处理图像时,它可以单独生成不同的图层或通道(例如,颜色通道、光通道、 occlusion Pass等),从而获得不同层中的单独构图。
每个pass一次渲染一个对象,也就是说,如果我们的着色器中有两个passes,则该对象将在 GPU 上渲染两次,相当于两次绘制调用。
一次传递等于一次绘制调用,这就是为什么我们必须使用尽可能少的传递次数,否则,我们可能会生成大量的图形负载。
Shader "InspectorPath/shaderName"
{
Properties { … }
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
// first default pass
}
Pass
{
// second additional pass
}
}
}