3.1 Unity Shader 概述
3.1.1 一对好兄弟:材质和 Unity Shader
在 Unity 中我们需要配合使用材质(Material)和 Unity Shader 才能达到需要的效果
流程:
- 创建一个材质
- 创建一个 Unity Shader,并把它赋给上面的材质
- 将材质赋给要渲染的对象
- 在材质面板中调整 Unity Shader 属性,以得到满意的效果
- Unity Shader 定义了渲染所需的各种代码(各种着色器)、属性(纹理等)、和指令(渲染和标签设置等)
3.1.2 Unity 中的材质
- Unity 中的材质需要结合一个 GameObject 的 Mesh 或者 Particla Systel 组件来工作
- 与许多建模软件中提供的材质类似,它们都提供了一个面板来调整各个参数
3.1.3 Unity 中的 Shader
-
Unity Shader 与之前提到的渲染管线的 Shader 有很大不同
-
创建方法(略)。Unity 提供了 5 种模板供选择
- Standard Surface:包含标准光照模型
- Unlit:不包含光照(但包含雾效)
- Image Effect:可实现各种
屏幕后处理效果 - Compute:可利用 GPU 的并行性来进行一些与常规渲染流水线无关的
计算(用来挖矿?) - Ray Tracing:提供
光线追踪相关的功能。(2019.3 版本首次实验性加入,在Unity 2022.2 LTS 后功能全面且稳定)
-
必须与
材质结合起来,才能发生神奇的“化学反应” -
对于
表面着色器,它的属性面板中- 点击
Show generated code可打开着色器文件,方便修改和研究 - 若 Shader 是固定函数着色器,在 Fixed function 后面会有一个同样的按钮
Compile and show code下拉列表可让开发者检查针对不同图像编程接口最终编译成的 Shader 代码
- 点击
3.2 Unity Shader 的基础:ShaderLab
- 学习和编写着色器的过程的曲线一直很陡峭,Unity 为了解决这个问题,提供了一层抽象——Unity Shader
- 而我们和这层抽象打交道的方法就是使用专门为 Unity Shader 服务的语言——ShaderLab
什么是 ShaderLab?
ShaderLab 是 Unity 提供的编写 Unity Shader 的一种
说明性语言
- Unity 在背后会根据平台将 Unity Shader 代码编译成真正的代码和 Shader 文件,开发者只需要和 Unity Shader 打交道
3.3 Unity Shader 的结构
3.3.1 给 Shader 起名字(略)
3.3.2 材质和 Unity Shader 的桥梁:Properties
Properties 语义块定义通常如下:
Properties {
Name {"display name", PropertyType} = DefaultValue
Name {"display name", PropertyType} = DefaultValue
// more...
}
- 声明这些属性以便在材质属性面板中调整各种材质属性
Name:属性名字。若想在 Shader 中访问它们,就需要使用它- 通常由一个下划线开始
display name:显示的名称。材质面板上显示的名字PropertyType:属性类型
DefaultValue:默认值
例子:
- 若想显示更多类型的变量(如使用布尔值),Unity 允许我们
重载默认的材质编辑面板 - 即使不在 Properties 语义块中声明属性,也只可以直接在 CG 代码中定义变量。此时,我们可
通过脚本向 Shader 中传递属性
3.3.3 重量级成员:SubShader
- 每个 Unity Shader 文件中至少包含一个 SubShader 语义块
- 当 Unity 需要加载这个 Unity Shader 时,它会扫描所有 SubShader 语义块,然后选择首个能在目标平台运行的
- 若都不支持,就会使用
Fallback语义指定的 Unity Shader - 提供这种语义的原因在于,不同的显卡具有不同的能力。我们希望在旧的显卡上使用复杂度较低的着色器,而在高级的显卡上使用复杂度较高的
SubShader 中定义了一系列 Pass 以及可选的状态([RenderSetup])和标签([Tags])设置
- 每个 Pass 定义了一次完整的渲染流程(过多会影响性能)
状态和标签同样可在 Pass 中声明- 状态:跟 SubShader 中的语法是相同的
- 标签:部分标签跟在 SubShader 中声明的不一样
- 在 SubShader 中的设置(状态、标签)将会影响所有 Pass
状态设置
ShaderLab 提供了一系列渲染状态的设置指令,以设置显卡的各种状态(是否开启混合/深度测试等)
标签
一个键值对(都是字符串)。它们是 SubShader 和渲染引擎间的桥梁,用来告诉渲染引擎:SubShader 希望怎样以及何时渲染这个对象
标签结构:
Tags { "TagName1"="Value1" "TagName2"="Value2" }
SubShader 的标签块支持的标签类型
- 注意:上述标签仅可以在 SubShader 中声明,而不可在 Pass 块中声明
Pass 语义块
语义块结构:
Pass {
[Name]
[Tags]
[RenderSetup]
// Other code
}
- Name:该 Pass 的名称。通过这个名称,我们可以使用 ShaderLab 的 UsePass 命令来直接使用其他 Unity Shader 中的 Pass。例如:
UsePass "MyShader/MYPASSNAME" // Pass 名称必须全部大写
- 除了可在 Pass 中设置状态外,还可以使用 固定管线的着色器命令
- 同样可在 Pass 中设置标签,但跟 SubShader 的标签不同,但目的一样——告诉渲染引擎我们(Pass)希望怎样来渲染物体。标签类型
- 除了上面普通的 Pass 定义外,Unity Shader 还支持一些特殊 Pass,以便进行代码复用或实现更复杂的效果
- UsePass:复用其他 Unity Shader 中的 Pass
- GrabPass:抓取屏幕并将结果存储在一张纹理中,以用于后续的 Pass 处理
3.3.4 留一条后路:Fallback
“如果上面所有的 SubShader 在这块显卡上都不能运行,就使用这个最低级的 Shader 吧!”
语义:
Fallback "name"
// 或
Fallback Off
- 事实上,Fallback 还会影响阴影投射。在渲染阴影纹理时,Unity 会在每个 Unity Shader 中寻找一个阴影投射的 Pass。通常情况下,我们不需要自己专门实现,这是因为 Fallback 使用的内置 Shader 中包含了一个通用的 Pass。因此为每个 Unity Shader 正确设置 Fallback 非常重要
3.3.5 ShaderLab 还有其他语义吗
- 可使用 CustomEditor 语义来扩展编辑界面
- 还可以使用 Category 语义来对 Unity Shader 中的命令进行分组
3.4 Unity Shader 的形式
我们可以使用下面 3 种形式来编写 Unity Shader,真正意义上的 Shader 代码都需要包含在 ShaderLab 语义块中
Shader "MyShader" {
Properties {
// 所需的各种属性
}
SubShader {
// 真正意义上的 Shader 代码会出现在这里
// 表面着色器(Surface Shader)或者
// 顶点/片元着色器(Vertex/Fragment Shader)或者
// 固定函数着色器(Fixed Funciton Shader)
}
SubShader {
// 和上一个类似
}
}
3.4.1 Unity 的宠儿:表面着色器
Unity 自己创造的一种着色器代码类型。需要的代码量少,但渲染代价较大
- 本质上和顶点/片元着色器是一样的。Unity 在背后仍旧把它转换成对应的顶点/片元着色器
- 表面着色器是 Unity 对顶点/片元着色器的更高一层抽象(帮我们处理很多光照细节)
一个简单的表面着色器代码:
Shader "Custom/Simple Surface Shader" {
SubShader {
Tags {"RenderType"="Opaque"}
CGPROGRAM
#pragma surface surf lambert
struct Input {
float4 color : COLOR;
};
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = 1;
}
ENDCG
}
Fallback "Diffuse"
}
- 表面着色器被定义在 SubShader 语义块中的 CGPROGRAM 和 ENDCG 之间。原因是,它不需要开发者关心使用多少个 Pass
3.4.2 最聪明的孩子:顶点/片元着色器
- 更加复杂,但灵活性更高。示例代码:
- 顶点/片元着色器的代码也需要定义在 CGPROGRAM 和 ENDCG 之间。但跟表面着色器不同,顶点/片元着色器是写在 Pass 语义块内,而非 SubShader 内
3.4.3 被抛弃的角落:固定函数着色器
- 上面两种 Unity Shader 形式都使用了可编程管线,但有的旧设备并不支持,因此我们就需要使用
固定函数着色器来完成渲染。示例:
- 需要完全使用 ShaderLab 的语法来编写
3.4.4 选择哪种 Unity Shader 形式
- 除非有非常明确的需要必须使用
固定函数着色器,否则使用其余两种 - 若想和各种
光源打交道,则推荐使用表面着色器,但要小心它在移动平台的性能表现 - 若需要使用的光照数非常少,例如只有一个平行光,那么使用
顶点/片元着色器会更好 - 若有很多自定义的渲染效果,请选择
顶点/片元着色器
3.5 本书使用的 Unity Shader 形式
将着重使用顶点/片元着色器,知其然亦知其所以然
3.6 答疑解惑
3.6.1 Unity Shader != 真正的 Shader
Unity Shader 可以做的事情远多于一个传统意义上的 Shader
| 传统 Shader | Unity Shader | |
|---|---|---|
| 编码灵活性 | 仅可编写特定类型的 Shader | 在同一个文件里同时包含多种着色器代码 |
| 可配置性 | 无法设置一些渲染配置(混合、深度测试等) | 通过一行特定的指令来完成 |
| 可读性 | 需要编写冗长的代码来设置着色器的输入和输出,并小心处理位置对应关系 | 只需要在特定语句块中声明一些属性,便可通过材质来方便修改它们 |
| 对于模型数据 | 需自行编码传给着色器 | 提供了直接访问的方法 |
Unity Shader 的缺点:
- 可编写的
Shader 类型(曲面细分/几何着色器等)和语法都被限制了
3.6.2 Unity Shader 和 CG/HLSL 之间的关系
3.6.3 我可以使用 GLSL 来写吗
可以,但意味着将放弃仅支持 DirectX 的平台
- GLSL 代码需要嵌套在 GLSLPROGRAM 和 ENDGLSL 之间