Unity 渲染原理(五)Shader Lab

上一篇博客了解了一些Unity Shader的概念,继续看一下如何编写一个Shader。

概述

为 Unity 编写着色器时,使用以下语言:

  • 一种称为 HLSL 的编程语言。使用它可编写着色器程序本身。有关 HLSL 的更多信息,请参阅 Unity 中的 HLSL。
  • 一种称为 ShaderLab 的 Unity 特定语言。使用它可定义 Shader 对象,它充当着色器程序的容器。有关 ShaderLab 的更多信息,请参阅 ShaderLab。
    不需要为不同的平台使用不同的语言;Unity 针对不同的图形 API 将 HLSL 和 ShaderLab 代码编译为不同语言

如果需要,还可以直接用 GLSL 和 Metal 编写着色器程序。在常规工作流程中,不建议或不需要这样做。

有不同的着色器编写方法:

  • 最常见的方法是使用 HLSL 编写顶点和片元着色器。有关更多信息,请参阅编写顶点和片元着色器。
  • 在内置渲染管线中,还可以编写表面着色器。这是编写与光照交互的着色器的一种简化方法。有关更多信息,请参阅表面着色器。
  • 出于向后兼容的原因,Unity 还支持“固定函数风格”ShaderLab 命令。因此,您可以使用 ShaderLab 编写着色器,而无需使用 HLSL。这不再是建议方法

Shader Lab

ShaderLab 是一种在着色器源文件中使用的声明性语言。它使用嵌套大括号语法来描述 Shader 对象。

在 ShaderLab 中可以定义很多内容,但最常见的是:

  • 定义 Shader 对象的整体结构。参阅 ShaderLab:创建着色器、ShaderLab:创建子着色器和 ShaderLab:创建通道。
  • 使用代码块添加用 HLSL 编写的着色器程序。
  • 在执行着色器程序或执行涉及另一个通道的操作之前,使用命令设置 GPU 的渲染状态。
  • 从着色器代码中公开属性,以便在 Material Inspector 中编辑它们并将其保存为材质资源的一部分。
  • Specifying package requirements for SubShaders and Passes. This enables Unity to run certain SubShaders and Passes only when particular packages are installed in the Unity project. See ShaderLab: specifying package requirements.
  • 定义当 Unity 无法在当前硬件上使用 Shader 对象运行任何 SubShader 时的回退行为。

定义Shader对象

Shader 对象是 Unity 特定的概念;它是着色器程序和其他信息的封装器。它允许您在同一个文件中定义多个着色器程序,并告诉 Unity 如何使用它们。

Shader 对象具有嵌套结构;它将信息组织成结构(称为 SubShader 和 Pass)。

在 Shader 代码块中,可以:

  • 使用 Properties 代码块定义材质属性。
  • 使用 SubShader 代码块定义一个或多个子着色器。
  • 分配一个自定义编辑器,它确定着色器资源在 Unity 编辑器中的显示方式。或者,可以为不同的渲染管线分配不同的自定义编辑器。
  • 使用 Fallback 代码块分配一个回退 Shader 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Shader "Examples/ShaderSyntax"
{
CustomEditor = "ExampleCustomEditor"

Properties
{
// 此处是材质属性声明
}
SubShader
{
// 此处是定义子着色器的其余代码

Pass
{
// 此处是定义通道的代码
}
}

Fallback "ExampleFallbackShader"
}

在 ShaderLab 代码中,可以定义材质属性。材质属性是 Unity 作为材质资源一部分存储的属性。这使美术师可以创建、编辑和共享具有不同配置的材质。

如果使用材质属性:

  • 可以通过**对材质调用函数(**例如 Material.SetFloat)来获取或设置 Shader 对象中的变量值。
  • 可以使用材质 Inspector 查看和编辑值。
  • Unity 会将进行的更改保存为材质资源的一部分,因此它们可在会话之间持续存在。

所有材质属性声明都遵循以下基本格式:

1
[optional: attribute] name("display text in Inspector", type name) = default value

前面的attribute是属性特性,可以没有,也可以同时有多个。
name则一般是用下划线开头

我们可以从两个角度将属性分类:

  • 按数据类型划分:有float,Texture2D等。
  • 按材质属性的特性划分,如[MainTexture], [MainColor]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Shader "Examples/MaterialPropertyShaderLab"
{
Properties
{
// 在材质 Inspector 中更改此值以影响 Offset 命令的值
_OffsetUnitScale ("Offset unit scale", Integer) = 1
}
SubShader
{
// 此处是定义 SubShader 其余部分的代码

Pass
{
Offset 0, [_OffsetUnitScale]

// 此处是定义 Pass 其余部分的代码
}
}
}

除了定义材质属性,Shader对象中我们也可以分配回退和制定自定义编辑器

定义子着色器(SubShader)

在 ShaderLab 中,通过将 SubShader 代码块置于 Shader 代码块中,可以定义子着色器。

在 SubShader 代码块中,可以:

  • 使用 LOD 代码块为 SubShader 分配 LOD(细节级别)值。
  • 使用 Tags 代码块将数据的键值对分配给子着色器。
  • 使用 ShaderLab 命令将 GPU 指令或着色器代码添加到 SubShader。
  • 使用 Pass 代码块定义一个或多个通道。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Shader "Examples/SinglePass"
{
SubShader
{
Tags { "ExampleSubShaderTagKey" = "ExampleSubShaderTagValue" }
LOD 100

// 此处是应用于整个子着色器的 ShaderLab 命令。

Pass
{
Name "ExamplePassName"
Tags { "ExamplePassTagKey" = "ExamplePassTagValue" }

// 此处是应用于此通道的 ShaderLab 命令。

// 此处是 HLSL 代码。
}
}
}

子着色器标签

在 ShaderLab 中,可以通过将 Tags 代码块置于 SubShader 代码块中来向子着色器分配标签。

请注意,子着色器和通道都使用 Tags 代码块,但其工作方式不同。向通道分配子着色器标签没有效果,反之亦然。区别在于放置 Tags 代码块的位置:

  • 要定义通道标签,请将 Tags 代码块置于 Pass 代码块内部。
  • 要定义子着色器标签,请将 Tags 代码块置于 SubShader 代码块内部,但是在 Pass 代码块外部。

可以使用 Material.GetTag API 从 C# 脚本中读取子着色器标签,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using UnityEngine;

public class Example : MonoBehaviour
{
// 将此附加到具有渲染器组件的游戏对象
string tagName = "ExampleTagName";

void Start()
{
Renderer myRenderer = GetComponent<Renderer>();
string tagValue = myRenderer.material.GetTag(ExampleTagName, true, "Tag not found");
Debug.Log(tagValue);
}
}

子着色器标签有很多:

  • RenderPipeline 标签向 Unity 告知子着色器是否与通用渲染管线 (URP) 或高清渲染管线 (HDRP) 兼容。
  • Queue 标签向 Unity 告知要用于它渲染的几何体的渲染队列。渲染队列是确定 Unity 渲染几何体的顺序的因素之一。
  • 使用 RenderType 标签可覆盖 Shader 对象的行为。
  • ForceNoShadowCasting 标签阻止子着色器中的几何体投射(有时是接收)阴影。确切行为取决于渲染管线和渲染路径。
  • DisableBatching 子着色器标签阻止 Unity 将动态批处理应用于使用此子着色器的几何体。
  • 在内置渲染管线中,IgnoreProjector 子着色器标签向 Unity 告知几何体是否受投影器影响。这对于排除投影器不兼容的半透明几何体多半很有用。
  • PreviewType 子着色器 Tag 告知 Unity 编辑器如何在材质 Inspector 中显示使用此子着色器的材质。
  • 在使用 Legacy Sprite Packer 的项目中使用此子着色器标签可警告用户着色器依赖于原始纹理坐标,因此不应将其纹理打包到图集中。

子着色器LOD值

可以将 LOD 值指定给子着色器。此值指示其计算方面的需求。

在运行时,您可以为单个 Shader 对象或所有 Shader 对象设置着色器 LOD 的值。然后 Unity 优先考虑具有较低 LOD 值的子着色器。有关 Unity 如何选择何时使用子着色器的信息

子着色器通道

通道是 Shader 对象的基本元素。它包含设置 GPU 状态的指令,以及在 GPU 上运行的着色器程序。

简单的 Shader 对象可能只包含一个通道,但更复杂的着色器可以包含多个通道。您可以为 Shader 对象不同部分定义单独的通道实现不同的工作方式;例如,需要更改渲染状态、不同的着色器程序或不同的 LightMode 标签的部分。

要在 ShaderLab 中定义一个常规的通道,您需要在一个 SubShader 代码块中放置一个 Pass 代码块。

在 Pass 代码块中,您可以:

  • 使用 Name 代码块为通道指定一个名称。
  • 使用 Tags 代码块将数据的键值对分配给通道。
  • 使用 ShaderLab 命令执行操作。
  • 使用着色器代码块将着色器代码添加到通道。

通道标签

每个通道都可以有自己的标签,我们列举几个内置的标签:如LightMode,PassFlags,RequireOptions等,具体可以看:https://docs.unity3d.com/cn/current/Manual/shader-predefined-pass-tags-built-in.html

为Shader Lab添加着色器程序

在 Unity 中,您通常使用 HLSL 编写着色器程序。要将 HLSL 代码添加到您的着色器资源,应将该代码放在一个着色器代码块 中。

本页面包含有关使用着色器代码块的信息。有关编写 HLSL 本身的信息。

注意:Unity 还支持使用其他语言编写着色器程序,不过通常不需要或不推荐这样做。

着色器代码块的类型

要添加 HLSL 代码,您可以使用以下类型的着色器代码块:

HLSLPROGRAM
CGPROGRAM
HLSLINCLUDE
CGINCLUDE
要了解何时使用哪个,您必须了解它们的前缀(HLSL 或 CG)及后缀(PROGRAM 或 INCLUDE)。

HLSL 和 CG 前缀

以 HLSL 或 CG 为前缀的块之间的区别是:

以 CG 为前缀的着色器代码块较旧。默认情况下,它们包含几个 Unity 的内置着色器 include 文件,如果您需要此功能,这会很方便。内置 include 文件仅与内置渲染管线兼容。
以 HLSL 为前缀的着色器代码块较新。默认情况下,它们不包含 Unity 的内置着色器 include 文件,因此您必须手动包含要使用的任何库代码。它们适用于任何渲染管线。
有关 Unity 内置着色器 include 文件的信息,请参阅内置着色器 include 文件。

PROGRAM 和 INCLUDE 后缀

以 PROGRAM 或 INCLUDE 为后缀的块之间的区别是:

以 PROGRAM 为后缀的着色器代码块被称为着色器程序块。您可以使用它们来编写着色器程序。您在这些块中编写 HLSL 着色器代码,然后将它们放在 ShaderLab 代码中的 Pass 块中。
以 INCLUDE 为后缀的着色器代码块被称为着色器 include 块。您可以使用它们在同一源文件中的着色器程序块之间共享公共代码。您编写要在这些块中共享的 HLSL 着色器代码,然后将它们放置在您的 ShaderLab 代码中的 Pass、SubShader 或 Shader 块中。它的工作方式与在 HLSL 代码中使用 include 的方式类似。

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
Shader "Examples/ExampleShader"
{
SubShader
{

HLSLINCLUDE
// 在此编写要共享的 HLSL 代码
ENDHLSL

Pass
{
Name "ExampleFirstPassName"
Tags { "LightMode" = "ExampleLightModeTagValue" }

// 在此编写设置渲染状态的 ShaderLab 命令

HLSLPROGRAM
// 此 HLSL 着色器程序自动包含上面的 HLSLINCLUDE 块的内容
// 在此编写 HLSL 着色器代码
ENDHLSL
}

Pass
{
Name "ExampleSecondPassName"
Tags { "LightMode" = "ExampleLightModeTagValue" }

// 在此编写设置渲染状态的 ShaderLab 命令

HLSLPROGRAM
// 此 HLSL 着色器程序自动包含上面的 HLSLINCLUDE 块的内容
// 在此编写 HLSL 着色器代码
ENDHLSL
}

}
}

Shader Lab 命令

ShaderLab 命令分为以下类别:

  • 用于在 GPU 上设置渲染状态的命令。
  • 用于创建具有特定用途的通道。
  • 如果使用旧版 “fixed function style” 命令,无需编写 HLSL 也可创建着色器程序。

用于设置渲染状态的命令
在 Pass 代码块中使用这些命令可为该 Pass 设置渲染状态,或者在 SubShader 代码块中使用这些命令可为该 SubShader 以及其中的所有 Pass 设置渲染状态。

  • AlphaToMask:设置 alpha-to-coverage 模式。
  • Blend:启用和配置 alpha 混合。
  • BlendOp:设置 Blend 命令使用的操作。
  • ColorMask:设置颜色通道写入掩码。
  • Conservative:启用和禁用保守光栅化。
  • Cull:设置多边形剔除模式。
  • Offset:设置多边形深度偏移。
  • Stencil:配置模板测试,以及向模板缓冲区写入的内容。
  • ZClip:设置深度剪辑模式。
  • ZTest:设置深度测试模式。
  • ZWrite:设置深度缓冲区写入模式。

通道命令
在 SubShader 中使用这些命令可定义具有特定用途的通道。

UsePass 定义一个通道,它从另一个 Shader 对象导入指定的通道的内容。
GrabPass 创建一个通道,将屏幕内容抓取到纹理中,以便在之后的通道中使用。-