本系列为作者学习UnityShader入门精要而作的笔记,内容将包括:
总之适用于同样开始学习Shader的同学们进行有取舍的参考。
从本章节开始我们要学习Shader相关的知识了,诸位看客可能有的人很高兴了,“太好了终于可以写Shader了,我已经迫不及待了”。其实还没有,第三章我们只是介绍Shader的基础知识。“那下章就可以写Shader了”,其实也不是,下章我们要学习数学基础。起码要下下章才能开始学习编写Shader代码,还得从最基本的语法开始学习,路漫漫其修远兮,心急吃不了热豆腐。
根据第二章中我们学习的知识,我们知道了渲染管线中有各种可编程的着色器阶段和许多可以配置的渲染设置。其实UnityShader的作用就是对各类着色器代码以及渲染设置进行管理。
经常接触3D开发的开发者们都知道,在Unity中,想要为一个模型网格体渲染出强大的视觉效果,往往离不开两个东西:
一个常见的网格体渲染设置的流程是:
创建一个材质
创建一个UnityShader,并把它赋给上一步中创建的材质(或是选择Unity预设的Shader)
把材质赋给要渲染的对象
在材质面板中调整UnityShader的属性,以得到满意的效果
下图显示了Shader和材质是如何一起工作来控制物体的渲染的:
UnityShader中定义了渲染所需的各种代码(如顶点着色器和片元着色器)、属性(如使用哪些纹理等)和指令(渲染和标签设置等)。而材质则允许我们调节这些属性,并最终赋值给相应的模型。
我们知道了如果需要为网格渲染需要赋予材质,而材质需要赋予Shader。空白的网格体其实相当于一张空白画布,把材质赋予网格体相当于在画布上绘画,而Shader就像是材质的调色板,可以通过改变Shader中的属性来修改网格上画面的属性和渲染状态。
在此处,我认为有必要理清材质(Material)、着色器(Shader)、贴图(Map)、纹理(Texture)的关系 。首先从集合上的包含关系来看,材质Material(包含着色器)>贴图Map>纹理Texture 。
Material本质上是一个数据集,它包含了Shader的渲染设置,以及供Shader读取的那些数据(包括了纹理texture和各类贴图map),而实际上纹理应当被贴图包含,纹理的全称应该被称为Texture Map,也就是纹理贴图,以上图为例,unity中的Albedo其实是反射率贴图,用于表示模型的纹理和颜色,因此我们常常把纹理贴图赋值给这个属性。
而贴图不仅仅包含了基本的纹理,实际上还包含了UV坐标,各种输入输出控制等其他信息,所以会有很多贴图,Height Map(光照贴图),Normal Map(法线贴图)。
本质上贴图的英文Map代表的不是位图bitmap,这个Map是一个动词,翻译过来是“映射”,其功能就是把纹理通过 UV 坐标映射到3D 物体表面。
(上述答案可能有误,关于贴图和纹理的关系网上众说纷纭,其实没必要咬文嚼字)
为了和前面通用的Shader语义进行区分,我们把Unity中的Shader文件统称为Unity Shader。因为Unity Shader和我们之前提到的渲染管线的Shader有很大不同(这将在后文中解释)。
我们可以在资源栏右键创建一个Unity Shader,Unity为我们提供了四种模板(上图是2022版本,提供了5种)。
Standard Surface Shader 会产生一个包含标准光照模型的表面着色器模板。UnlitShader会产生一个不包含光照(但包含雾效)的基本的顶点/片元着色器。Image Effect Shader 为我们实现各种屏幕的后处理效果提供一个基本的模板。Compute Shader 会产生一种特殊的Shader 文件,这类Shader旨在利用GPU的并行性来进行一些与常规渲染流水线无关的计算。Ray Tracing Shader是利用RTX显卡实现光线追踪的Shader。
Standard Surface Shader是一种典型的表面着色器的实现方法。但是本书的重点在于如何在Unity中编写顶点/片元着色器,因此后续的学习中我们通常会使用Unlit Shader来编写Shader。
在Shader的面板上,我们可以看到该Shader的面板属性(这些属性在材质中是可以直接通过编辑器来赋值和调整的),也对应了最下方的Properties: 显示的属性(Properties显示了属性名和其类型,方便我们在C#中通过代码进行调用)。
在Shader的Imported Object面板上,我们可以看到一些和该Unity Shader相关的信息,例如它是否是一个表面着色器(Surface shader)、是否是固定函数着色器(Fixed Function)等来判断其着色器类型,还有一些信息是我们在UnityShader中的标签设置相关,例如是否计算阴影(Cast shadows),使用的渲染队列(Render queue)、LOD层级等
对于表面着色器,我们可以通过点击Show generated code来生产代码文件,该文件将显示Unity为该Shader的表面着色器生产的顶点/片元着色器。(同样的如果该Shader是一个固定函数着色器,也可以点击生成对应的顶点/片元着色器)
点击Compile and show code下拉列表可以让开发者检查Unity Shader针对不同图像编程接口最终编译的Shader代码。直接单机该按钮以生成可查看的底层汇编指令,我们可以利用这些代码来分析和优化着色器。
除此之外,Unity Shader的导入面板还可以查看使用的渲染队列(Render queue)、是否关闭批处理(Disable batching)、属性列表(Properties)等信息,非常方便。
在学习和编写着色器的过程中,为了自定义渲染效果往往需要和很多文件和设置打交道,这些过程很容易消磨掉初学者的耐心。而且,一些细节问题也往往需要开发者花费较多的时间去解决。
Unity为了解决上述问题,为我们提供了一层抽象——Unity Shader,我们与这层抽象打交道的途径就是使用Unity提供的一种专门为Unity Shader服务的语言——ShaderLab。
Unity Shader是Unity为开发者提供的高层级的渲染抽象层。
由于渲染过程中涉及的步骤和设置太多。开发者需要和很多的设置文件打交道,而使用ShaderLab来编写Unity Shader就可以完成左图中的所有工作。在Unity中我们可以使用这种方式来更轻松地控制渲染。
所有的Unity Shader都是使用ShaderLab来编写的。它使用了一些嵌套在花括号内部的语义来描述了一个Unity Shader文件的结构。这些结构包含了许多渲染所需的数据,例如Properites语句块中定义了着色器所需的各种属性,这些属性我们可以Import面板中检视,或者在材质面板中进行调整。ShaderLab定义了一个材质所需的所有东西,而不仅仅是着色器代码。
Shader "ShaderName"
{
Properties
{
//属性
}
SubShader
{
//显卡A使用的子着色器
}
SubShader
{
//显卡B使用的子着色器
}
Fallback "VertexLit"
}
上述代码是一个Unity Shader的基础结构,Unity会将其编译为真正的代码和Shader文件。
每个Unity Shader文件的第一行都需要通过Shader语义来指定该Unity Shader的名字。例如:
Shader "Custom/MyShader"
其中,最后一级反斜杠后的字符串代表了该Unity Shader的名称,而前面的字符串代表了路径,以每个反斜杠后的字符为一级,当添加路径之后这个Shader就可以出现在材质球的Shader选择的下拉列表里。
如上图所示,我们可以根据名字在材质面板的相应路径下找到该Unity Shader。
Properties语句块中包含了一系列属性,这些属性将会出现在材质面板中。
Properties语义块的定义如下:
Properties {
Name ("display name", PropertyType) = DefaultValue
Name ("display name", PropertyType) = DefaultValue
//更多属性
}
每个属性字段应当包含三个主要定义:
通常如果我们需要在C#脚本中访问Shader属性,则需要根据Name的值来进行访问。而在材质面板中则需要修改对应的Display Name 的属性值。
属性类型 | 默认值的定义语法 | 例子 |
---|---|---|
Int | number | _Int(“Int”,Int) = 2 |
Float | number | _Float(“Float”,Float) = 1.5 |
Range(min,max) | number | _Range(“Range”,Range(0.0,5.0)) = 3.0 |
Color | (number,number,number,number) | _Color(“Color”,Color) = (1,1,1,1) |
Vector | (number,number,number,number) | _Vector(“Vector”,Vector) = (2,3,6,1) |
2D | “defaulttexture”{} | _2D(“2D”,2D) = “” {} |
Cube | “defaulttexture”{} | _Cube(“Cube”,Cube) = “white” {} |
3D | “defaulttexture”{} | _3D(“3D”,3D) = “black” {} |
上表展示了一些Unity Shader中的基本属性,主要可以分为三大类。
如果我们想要在Unity的材质面板中显示更多类型的遍历,例如使用bool来控制Shader面板中的计算方法,Unity也允许我们重载材质编辑面板,在后文中也能学习到。
每个Unity Shader文件可以包含多个SubShader(子着色器)语义块,但最少需要有一个,当Unity需要加载这个Unity Shader时,Unity会扫描所有的SubShader语义快,然后选择第一个能够在目标平台上运行的SubShader。
Unity之所以要提供这样的语义,是由于不同显卡支持的指令数不同,我们希望在旧显卡上使用计算复杂度较低的着色器,在高级显卡上使用计算复杂度较高的着色器。
SubShader语句块的定义如下:
SubShader {
//可选的
[Tags]
//可选的
[RenderSetup]
Pass {
}
// Other Passes
}
SubShader定义了一系列Pass以及可选的状态([RenderSetup]) 和标签([Tags]) 设置,每个Pass都定义了一次完整的渲染流程,如果我们定义了多个Pass,那么渲染时每帧的效果也需要经过多次Pass的渲染,也就是要经历多次渲染流程。
因此,如果Pass的数量过多,往往会造成渲染性能的下降。因此,我们应尽量使用数目少的Pass(除非真的有必要定义多个Pass)。
标签和状态同样可以在Pass中声明。不同的是,SubShader中的一些标签设置是特定的,这些特定的标签设置与在Pass中使用的标签是不一样的,虽然语法相同,但是如果我们在SubShader中进行了这些标签设置,那么它们将会作用与所有Pass。(类似全局变量与局部变量?)
状态名称 | 设置指令 | 解释 |
---|---|---|
Cull | Cull Back | Front | Off | 设置剔除模式:剔除背面/正面/关闭剔除 |
ZTest | ZTest Less Greater |LEqual |GEqual | Equal | NotEqual | Always | 设置深度测试时使用的函数 |
ZWrite | ZWrite On | Off | 开启/关闭深度写入 |
Blend | Blend SrcFactor DstFactor | 开启并设置混合模式 |
若在SubShader中设置了上述渲染状态,则会应用到所有的Pass,如果我们不想影响全局,例如在双面渲染中,我们希望第一个Pass中剔除正面来对背面进行渲染,第二个Pass中剔除背面来对正面进行渲染,我们就应当在不同的Pass块中单独进行上面的设置。
(我很喜欢这些高级语言的直白性,一条英文指令+中文解释就能很形象地描述这条指令的作用,并且十分好记)
Tag是一个键值对(Key/Value Pair),其键和值都是字符串类型的。这些键值对是SubShader和渲染引擎之间的沟通桥梁,它们告诉Unity的渲染引擎我们希望怎样以及何时渲染这个对象:
标签的结构如下:
Tags { " TagNamel " = "Valuel " "TagName2" = "Value2 " }
标签类型 | 说明 | 例子 |
---|---|---|
Queue | 控制渲染顺序,指定该物体属于哪一个渲染队列,通过这种方式可以保证所有的透明物体可以在所有不透明物体后面被渲染。我们也可以自定义使用的渲染队列来控制物体的渲染顺序 | Tags {“Queue” = “Transparent“} |
RenderType | 对着色器进行分类,例如这是一个不透明的着色器,或是一个透明的着色器。这可以被用于着色器替换(Shader Replacement)功能。 | Tags {“RenderType” = “Opaque“} |
DisableBatching | 一些SubShader在使用Unity的批处理功能会出现问题,例如使用了模型空间下的坐标进行顶点动画。这时可以通过该标签来直接指明是否对该SubShader使用批处理 | Tags {“DisableBatching" = “True”} |
ForceNoShadowCasting | 控制使用该SubShader的物体是否会投射阴影 | Tags {“ForceNoShadowCasting" = “True”} |
IgnoreProjector | 如果该标签值为“True”,那么使用该SubShader的物体将不会收到Projector(projector指Unity中的投影仪组件)的影响。通常用于半透明物体 | Tags {“IgnoreProjector" = “True”} |
CanUseSpriteAtlas | 当该SubShader是用于Sprite时,将该标签设为“False” | Tags {“CanUseSpriteAtlas" = “False”} |
PreViewType | 指明材质面板将如何预览该材质。默认情况下,材质将显示为一个球,我们可以通过把该标签的值设为"Plane""SkyBox"来改变预览类型 | Tags {“PreViewType" = “Plane”} |
具体的标签设置我们会在后文中学习。
需要注意,上述标签仅可以在SubShader中声明,而不可以在Pass块中声明。Pass块虽然也可以定义标签,但这些标签是不同于SubShader的标签类型。
Pass 语义块包含的语义如下:
Pass{
[Name]
[Tags]
[RenderSetup]
//Other code
}
我们可以在语句块中定义该Pass的名称,例如:
Name "MyPassName"
通过Pass名,我们可以使用ShaderLab的UsePass 命令来直接使用其他Unity Shader的Pass块:
Use Pass "MyShader/MYPASSNAME"
这样可以提高代码的复用性。需要注意的是,由于Unity 内部会把所有 Pass 的名称转换成大写字母,因此在使用 UsePass命令时必须使用大写全拼的名字。
(哈哈,这不是我们面向对象编程的类函数方法调用吗,下次使用记得标明出处)
其次,我们也可以对Pass设置渲染状态,SubShader的状态设置同样适用于Pass。除了上述提到的状态设置外,在Pass块中我们还可以使用固定管线的着色器命令(谁用啊)。
Pass 同样可以设置标签 但它的标签不同于 SubShader 的标签。这些标签也是用于告诉渲染引擎我们希望怎样来渲染该物体。
标签类型 | 说明 | 例子 |
---|---|---|
LightMode | 定义该Pass在Unity的渲染流水线中的角色 | Tags {“LightMode” = “ForwardBase”} |
RequireOptions | 用于指定当满足某些条件时才渲染该Pass,它的值是一个由空格分隔的字符串,目前Unity支持的选项有:SoftVegetation。在后续版本可能增加更多的选项(喜报,根据Unity官方文档,直至2023.2版本依然只有这一个选项) | Tags {“RequireOptions” = “SoftVegetation”} |
除了上述普通的Pass定义外,Unity Shader还支持一些特殊的Pass,以进行代码复用或实现更复杂的效果:
最后,当我们写完SubShader块定义后,我们可以追加一个Fallback(失败回滚)指令,它用于告诉Unity:“如果上面定义的所有SubShader块都无法在显卡上运行,那就使用这个最低级的Shader吧!”
(就像是ShaderLab中的Try Catch语法一样,Catch到错误后抛出异常并处理)
语义如下:
Fallback "Name"
// 或者
Fallback Off
我们可以根据Name字符串来指定这个最低级的Unity Shader是谁(注意这里的Name指的并不是Pass,也不是SubShader,而是一个Unity Shader的Name!)。当然我们也可以用Fallback Off
关闭,关闭了之后即使失败引擎也不管它了。
// 最常见的Fallback Shader
Fallback "VertexLit"
事实上,Fallback还会影响到阴影的投射。在渲染阴影纹理的时候,Unity会在每个Unity Shader中寻找一个阴影投射的Pass。通常情况下我们不需要自己专门实现一个Pass,这是由于Fallback使用的内置Shader中包含了这样一个通用的Pass。如果我们Fallback使用的Shader没有阴影渲染的Pass的话那就使得阴影渲染出问题了。
因此,为每个Unity Shader正确设置Fallback也是非常重要的。
除了上述语义,ShaderLab还有一些不常用的语义。如果我们不满足于Unity内置的属性类型,想要自定义材质面板的编辑界面,就可以使用CustomEditor语义来扩展编辑界面。我们还可以用Category来对Unity Shader中的命令进行分组。书中对这二者并未详细讲解,如果后续学习中涉及到了再展开来讲吧。