前面的文章讲过,shader其实就是渲染流水线某些特定阶段,如顶点着色器阶段、片元着色器阶段等。
untiy中药配合使用材质和shader才能达到需要的效果,shader定义了渲染所需的各种代码(如顶点着色器和片元着色器)、属性(使用哪些纹理)和指令(渲染和标签设置等),而材质允许我们调节这些属性,最终赋值给相应模型。
unity中的材质需要结合一个gameObject 的Mesh或者Particle Systems组件来工作,他决定了游戏对象的模样。默认情况下新建材质将使用standard shader,这是一种基于物理渲染的着色器。
untiy默认提供了4种shader模板,通常使用Unlit Shader来生成一个基本的顶点/片元着色器模板。
在unity中,所有的unity shader都是使用shaderLab来编写的。shaderLab是unity提供的编写unity shader的一种说明性语言,它使用了一些嵌套在花括号内部的语义来描述一个unity shader的文件结构。这些结构包含了许多渲染所需的数据,例如Properies语句块中定义了着色器所需的各种属性,这些属性将出现在材质面板中。
unity在背后会根据使用的平台来把这些结果编译成真正的代码和shader文件,而开发者只需要和unity shader打交道即可。
Properties语句块包含了一系列的属性,这些属性会出现在材质面板中。
声明这些属性是为了在材质面板中能够方便地调整各种材质属性。如果需要在shader中访问它们,就需要使用每个属性的名字。这些属性的名字通常由一个下划线开始。显示的名称则是出现在材质面板上的名字。我们要为每个属性指定它的类型,常见的类型如下表:
除此之外还需要为每个属性指定一个默认值,在第一次把shader赋值给材质时,材质面板上显示的就是这些默认值。
对应Int、Float、Range这些数字类型的属性,其默认值就是一个单独的数字,对于Color和Vector这类属性,默认值是圆括号包围的一个四维向量;对于2D、Cube、3D这三种纹理类型,默认值是通过一个字符串跟上一个花括号指定的。字符串要么是空的要么是内置的纹理名称,如"white"等。
有时我们想要在材质面板上显示更多类型的变量,例如使用布尔变量来控制shader中使用哪种计算。unity允许我们重载默认的材质面板,以提供更多自定义的数据类型。比如官方的一个例子CustomShaderGUI。
为了让shader中可以访问到这些属性,需要在Cg代码片中定义和这些属性类型相匹配的变量。即便我们不在Properties语义块中声明这些属性,也可以直接在Cg代码片中定义变量,此时可以通过脚本向shader中传递这些属性。因此Properties语义块的作用仅仅是为了让这些属性可以出现在材质面板中。
一个shader文件可以包含多个subshader语义块,最少要有一个。unity扫描所有subshader语义块,选择第一个能够在目标平台上运行的subshader,如果都不支持则使用fallback语义指定的unity shader。
unity提供这种语义的原因在于,不同的显卡具有不同的能力。例如一些旧的显卡仅能支持一定数目的操作指令,而一些更高级的显卡可以支持更多的指令数,我们希望在旧的显卡上计算复杂度较低的着色器,而在高级的显卡上使用计算复杂度较高的着色器,提供更好的画面。
Subshader中定义了一系列pass以及可选的状态和标签设置,每个pass定义了一次完整的渲染流程,但如果pass数目过多,往往会造成渲染性能的下降。因此我们应尽量使用最小数目的pass,状态和标签同样可以在pass声明,不同的是,subshader中一些标签设置是特定的。也就是说这些标签设置和pass中使用的标签是不一样的。而对于状态设置来说,其使用的语法是相同的,但是如果我们在subshader进行了这些设置,那么会用于所有pass。
(1)状态设置
shaderLab提供了一系列渲染状态的设置指令,这些指令可以设置显卡各种状态,例如是否开启混合/深度测试等。下表给出了常见的渲染状态设置选项。
当在subshader块中设置了上述渲染状态时,将会应用到所有pass,若不想这样,可以在pass语义块中单独进行上面的设置。
(2)subshader标签
subshader标签是一个键值对,它的键和值都是字符串类型,这些键值对是对subshader和渲染引擎之间的沟通桥梁,它们用来告诉unity渲染引擎希望怎样以及何时渲染这个对象。标签结构如下:
Tags["name1"="value1"]
subshader标签块支持的标签类型如下表所示:
需要注意,上述标签仅可以在subshader中声明,而不可以在pass块声明,pass块虽然可以定义标签,但这些标签不同于subshader的标签类型。
(3)pass语义块
首先可以在pass中定义该pass的名称,可以使用UsePass命令来直接使用其他unity shader中的Pass。
这样可以提高代码复用性,由于unity内部会把所有pass的名称转换成大写字母表示,因此可以使用UsePass命令时必须使用大写字母的名字。
其次可以对Pass设置渲染状态,subshader的状态设置同样适用于pass。在pass中还可以呀使用固定管线的着色器命令。
Pass同样可以设置标签,也是告诉渲染引擎希望怎么来渲染该物体,下表给出了pass中使用的标签类型。
除了上面普通的Pass定义外,shader还支持一些特殊pass,以便进行代码复用或实现更复杂的效果。
(1)UsePass:可以使用该命令来复用其他unity shader中的pass。
(2)GrabPass:该Pass负责抓取屏幕并将结果存储在一张纹理中,以用于后续的Pass处理。
紧跟在各个Subshader语义块后面的,可以是一个Fallback指令。他告诉unity,如果上面所有subshader在这块显卡上都不能运行,那么就使用这个最低级的shader吧。
Fallback "name"
或者
Fallback Off
我们可以通过一个字符串来告诉unity这个最低级的shader是谁,也可以任性地关闭Fallback功能,如果这么做了,意思就是如果一块显卡跑不了上面所有subsh,就不用管它了。
事实上Fallback还会影响阴影的投射,在渲染阴影纹理时,unity会在shader中寻找一个阴影投射的pass,通常情况下不需要自己专门实现一个Pass,这是因为Fallback使用的内置shader包含了这样一个通用Pass,因此为每个shader正确设置fallback是非常重要的。
除了上述的语义,还有一些不常用到的语义,例如想要自定义材质面板编辑界面可以使用CustomEditor语义来扩展编辑界面,还可以使用Category语义来对shader中的命令进行分组,不过很少用到。
shader除了设置渲染状态,最重要的任务还是指定各种着色器所需的代码,这些代码可以写在subshader语义块中(表面着色器的做法),也可以在Pass语义块中(顶点/片元着色器和固定函数着色器的做法)。
这是unity为自己创造的一种着色器代码类型,它需要的代码量很少,unity在背后做了很多工作,但渲染的代价比较大。它的本质上和顶点片元着色器是一样的,会背后转换成对应的顶点片元着色器,可以理解成表面着色器是unity对顶点/片元更高一层的抽象,它的存在价值在于,untiy为我们处理了很多光照细节,使得我们不需要操心这些东西。
在untiy中可以使用Cg/HLSL语言编写顶点/片元着色器,它们更加复杂,但灵活性也更高。
上面两种unity shader形式都使用了可编程管线。而对于一些较旧的设备,它们不支持可编程的渲染管线着色器,因此就需要使用固定函数着色器来完成渲染,这些着色器往往只可以完成一些非常简单的效果。现在绝大部分GPU都支持可编程的渲染管线,这种固定管线的编程方式已经逐渐被抛弃,在unity5.2之后所有固定函数着色器都会在背后也被编译成对应的顶点/片元着色器。