概述
总体来说,在Unity中我们需要配合使用材质和Unity Shader才能达到需要的效果。一个最常见的流程是。
1)创建一个材质
2)创建一个Unity Shader,并把它赋给上一步创建的材质
3)把材质赋给要渲染的对象
4)在材质面板中调整Unity Shader的属性,以得到满意的效果
下图显示了Unity Shader和材质是如何一起工作来控制物体的渲染的。
Unity中的材质需要结合一个GameObject的Mesh 或者Particle Systems 组件来工作。它决定了我们的游戏对象看起来是什么样子的(这当然也需要Unity Shader的配合)。
Unity Shader 的基础:ShaderLab
Unity Shader 为控制渲染过程提供了一层抽象。如果没有使用Unity Shader(上左),开发者需要很多文件设置打交道,才能让画面呈现想要的效果,而在Unity Shader的帮助下(上右),开发者只需要使用ShaderLab来编写Unity Shader文件就可以完成所有的工作。
在Unity中,所有的Unity Shader 都是使用ShaderLab来编写的。ShaderLab是Unity提供的编写Unity Shader 的一种说明性语言。它使用了一些嵌套在花括号内部的语义来描述一个Unity Shader文件的结构。这些结构包含了许多渲染所需的数据,例如Properties 语句块中定义了着色器所需的各种属性,这些属性将会出现在材质面板中。从设计上来说,ShaderLab类似于CgFx的Direct3D Effects (.Fx)语言,它们都定义了要显示一个材质所需要的所有东西,而不仅仅是着色器代码。
一个Unity Shader 的基础结构如下所示
Unity 会在背后根据使用的平台来把这些结构编译成真正的代码和Shader 文件,而开发者只需要和Unity Shader 打交道即可。
Unity Shader 的结构
每个Unity Shader文件的第一行都需要通过Shader语义来指定该Unity Shader的名字。这个名字由一个字符串来定义,例如"MyShader"。当为材质选择使用的Unity Shader 时,这些名称就会出现在材质面板的下拉面板里。通过在字符串中添加斜杠(“/”),可以控制Unity Shader在材质面板中出现的位置。
Properties语义块中包含了一系列属性,这些属性将会出现在材质面板中。
Properties语义块通常定义如下:
- Properties
- {
- Name ("display name",PropertyType) = DefaultValue
- }
开发者们声明这些属性是为了在材质面板中能够方便地调整各种材质属性。如果我们需要再Shader 中访问它们,就需要使用每个属性的名字。在Unity中,这些属性的名字通常由一个下划线开始。显示的名称则是出现在材质面板上的名字。我们需要为每个属性指定它的类型,常见的属性类型如下表。除此之外,我们还需要为每个属性指定一个默认值,在我们第一次把该Unity Shader赋给某个材质,材质面板上显示的就是这些默认值。
每个Unity Shader 文件可以包含多个SubShader语义块,但最少要有一个,当Unity需要加载这个Unity Shader时,Unity 会扫描所有的SubShader 语义块,然后选择第一个能够在目标平台上运行的SubShader。如果都不支持的话,Unity就会使用Fallback语义指定的Unity Shader。
Unity提供这种语义的原因在于,不同的显卡具有不同的能力。
SubShader语义块中包含的定义通常如下。
SubShader中定义了一系列Pass以及可选的状态和标签设置。每个Pass定义了一次完整的渲染流程,但如果Pass的数目过多,往往会造成渲染性能的下降。因此,我们应尽量使用最小数目的Pass。状态和标签同样可以在Pass声明。不同的是,SubShader中的一些标签设置是特定的。也就是说,这些标签设置和Pass中使用的标签是不一样的。而对于状态设置来说,其使用的语法是相同的。但是,如果我们在SubShader进行了这些而设置,那么将会用于所有的Pass。
ShaderLab提供了一系列渲染状态的设置指令,这些指令可以设置显卡的各种状态,下表给出了ShaderLab中常见的渲染状态设置选项。
当在SubShader块中设置了上述渲染状态时,将会应用到所有的Pass。如果我们不想这样(例如在双面渲染中,我们希望在第一个Pass中剔除正面来对背面进行渲染,在第二个Pass中剔除背面来对正面进行渲染),可以在Pass语义块中单独进行上面的设置。
SubShader的标签是一个键值对,它的键和值都是字符串类型。这些键值对是SubShader和渲染引擎之间的沟通桥梁。它们用来告诉Unity的引擎:SubShader我希望怎样以及何时渲染这个对象。支持的标签类型如下:
需要注意的是,上述标签仅可以在SubShader中声明,而不可再Pass块中声明。Pass块虽然也可以定义标签,但这些标签是不同于SubShader的标签类型。
Pass语义块的定义如下:
首先,我们可以在Pass中定义该Pass的名称,例如:
- Name "MyPassName"
- UsePass "MyShader/MYPASSNAME"
其次,我们可以对Pass设置渲染状态。SubShader的状态设置通用适用于Pass。除了上面提到的状态设置外,在Pass中我们还可以使用固定渲染管线的着色器命令。
Pass同样可是设置标签,但它的标签不同于SubShader的标签。这些标签页是用于告诉渲染引擎我们希望怎样来渲染该物体。下表给出了Pass中使用的标签类型。
除了上面普通的Pass定义外,Unity Shader 还支持一些特殊的Pass,以便进行代码复用或实现更复杂的效果。
紧跟着在各个SubShader语义块后面的,可以是一个Fallback指令。它用于告诉Unity,如果上面所有的SubShader在这块显卡上都不能允许,那就使用这个最低级的吧。
它的语义如下:
- Fallback "name"
- //或者
- Fallback off
事实上,Fallback 还会影响阴影的投射。在渲染阴影纹理时,Unity会在每个Unity Shader中寻找一个阴影投射的Pass。通常情况下,我们不需要自己专门实现一个Pass,这是因为Fallback使用的内置Shader包含了这样一个通用的Pass。因此,为每个Unity Shader正确设置Fallback是非常重要的。
Unity Shader 的形式
尽管Unity Shader可以做的事情非常多,但其最重要的任务还是指定各种着色器所需的代码。这些着色器代码可以写在SubShader语义块中(表面着色器的做法),也可以写在Pass语义块中(顶点/片元着色器和固定函数着色器的做法)。
在Unity中,我们可以使用下面3种形式来编写Unity Shader。而不管使用哪种形式,真正意义上的Shader代码都需要包含在ShaderLab 语义块中,如下所示:
- Shader "MyShader"
- {
- Properties
- {
- //所需的各种属性
- }
- SubShader
- {
- //真正意义上的Shader代码会出现在这里
- //表面着色器或者顶点/片元着色器或者固定函数着色器
- }
表面着色器是Unity自己创造的一种着色器代码类型。它需要的代码量很少,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、每个Pass如何渲染等问题,Unity会在背后为我们做好这些事情。我们要做的只是告诉它:“使用这些纹理去填充颜色,使用这个法线纹理去填充法线,使用Lambert光照模型。”
CGPROGRAM 和 ENDCG之间的代码是使用CG/HLSL 编写的,也就是说,我们需要把CG/HLSL语言嵌套在ShaderLab语言中。值得注意的是,这里的CG/HLSL是Unity经封装后提供的,它的语法和标准的CG/HLSL语法几乎一样,但还是由细微不同的,例如有些原声的函数和用法Unity并没有提供支持。
在Unity中我们可以使用CG/HLSL 语言来编写顶点/片元着色器。它们更加复杂,但灵活性也更高。
一个非常简单的顶点/片元着色器示例代码如下:
- Shader "Custom/Simple VertextFrament Shader"
- {
- SubShader
- {
- Pass
- {
- CGPROGRAM
- #pragma vertext vert
- #pragma frament frag
- float4 vert(float4 v : POSITION) : SV_POSITION
- {
- return mul (UNITY_MAX_MVP,v);
- }
- float4 frag() : SV_Target
- {
- return fixed(1.0,1.0,1.0,1.0);
- }
- ENDCG
- }
- }
- }
和表面着色器类似,顶点/片元着色器的代码也需要定义在CGPROGRAM 和 ENDCG之间,但不同的是,顶点/片元着色器是写在Pass语义块内,而非SubShader内的。原因是,我们需要自己定义每个Pass需要使用的Shader代码。虽然我们可能需要编写更多的代码,但带来的好处是灵活性很高。更重要的是,我们可以控制渲染的实现细节。同样,这里的CGPROGRAM和ENDCG之间的代码也是使用CG/HLSL编写的。
上述两种Unity Shader 形式都使用了可编程管线。而对于一些较旧的设备,它们不支持可编程管线着色器,因此,这时候我们就需要使用固定函数着色器来完成渲染。这些着色器往往只可以完成一些非常简单的效果。
一个非常简单的固定函数着色器示例代码如下:
- Shader "Tutorial/Basic"
- {
- Properties{
- _Color ( "Main Color" , Color) = (1,0.5,0.5,1)
- }
- SubShader{
- Pass{
- Material{
- Diffuse[_Color]
- }
- Lightinh On
- }
- }
- }
对于固定函数着色器来说,我们需要完全使用ShaderLab的语法来编写,而非使用CG/HLSL。
由于现在绝大多数GPU都支持可编程的渲染管线,这种固定管线 编程方式已经逐渐被抛弃。实际上,在Unity5.2中,所有固定函数着色器都会在背后Unity被编译成对应的顶点/片元着色器,因此真正意义上的固定函数着色器已经不存在了。
如何选择哪种Unity Shader:
除非有非常明确的需求必须要使用固定函数着色器,例如需要在非常旧的设备上运行游戏,否则不要使用固定函数着色器。
如果想和各种光源打交道,你可能更喜欢使用表面着色器,但需要小心它在移动平台的性能表现。
如果光照数量少,例如只有一个平行光,那么使用顶点/片元着色器是一个更好的选择。
更重要的是,如果有很多自定义的渲染效果,那么顶点/片元着色器更好。