目录
一对好兄弟:材质和Unity Shader
物体想要达到需要的效果一般流程为:
- 创建一个材质;
- 创建一个 Unity Shader,并把它赋给上一步创建的材质;
- 把材质赋给想要渲染的对象;
- 把材质面板中调整Unity Shader 的属性,以得到满意的效果。
Unity Shader和材质。首先创建需要的Unity Shader和材质,然后把Unity Shader赋给材质,并在材质面板上调整属性(如使用的纹理、漫反射系数等)。最后,将材质赋给相应的模型来查看最终的渲染效果 。
Unity中shader
为了与前面通用的Shader语义进行区分,我们把Unity中的shader文件统称为 Unity Shader。(这是因为 Unity Shader和我们之前提及的渲染管线的Shader有很大的不同)
创建新的Unity Shader,我们可以在 Unity 菜单栏选择 Assets -> Creat -> Shader 来选择,或者直接 Project 视图右击 -> Creat->Shader来创建。
在Unity2017.4.2(书中为5.2)一共提供了4种 Unity Shader 模板来选择 —— Standard Surface Shader , Unlit Shader , Image Effect Shader , Compute Shader.
其中 Standard Surface Shader 会产生一个包含了标准光照模型的表面着色器模板,Unlit Shader则会产生一个不包含光照(但包含雾效)的基本的顶点/片元着色器,Image Effect Shader则为我们实现各种屏幕后处理效果提供一个模板。最后,Compute Shader 会产生一个特殊的 Shader 文件,这类Shader旨在利用GPU的并行性来进行一些与常规渲染流水线无关的计算。
关于Compute Shader具体可以在 Unity 手册的 Compute Shader 一文(https://docs.unity3d.com/Manual/class-ComputeShader.html)中找到更多介绍。
由于在本人看的书中重点在于如何在Unity 中编写顶点/片元着色器,因此以后的学习中,通常会使用Unlit Shader来生成一个基本的顶点/片元着色器模板。
一个unity shader Standard Surface Shader面板如下
在 Default Maps 可以指定该 Unity Shader 使用的默认纹理。
在接下去的面板中,Unity会显示出和该Unity Shader相关的信息,例如它是否是一个 表面着色器(Surface Shader)、是否是一个固定函数着色器(Fixed Function Shader)等,还有一些信息是和我们在 Unity Shader 中的标签设置有关,例如是否投射阴影、使用的渲染队列、LOD值等。
对于表面着色器,可以单击 show generated code 打开一个新的文件,在该文件里将显示 Unity 在背后为该表面着色器生成的顶点/片元着色器。
同样如果Unity Shader是一个固定函数着色器, 在Fixed Function 的后面也会出现一个 Show generated code 按钮。
Compile and show code 下拉列表可以让开发者检查该Unity Shader针对不同图像编程接口(例如OpenGL、D3D9等),如果直接点击也可以查看底层命令。
ShaerLab
Unity提供了一种专门为 Unity Shader 服务的语言——ShaerLab。
Unity Shader 为控制渲染过程提供了一层抽象。如果没有使用Unity Shader(左图),开发者需要和很多文件和设置打交道,才能让画面呈现出想要的效果;而在Unity Shader的帮助下(右图),开发者只需要使用ShaderLab来编写Unity Shader文件就可以完成所有的工作。
在Unity中,所有的 Unity Shader 都是使用 ShaderLab 来编写的。 ShaderLab是Unity提供的编写 Unity Shader 的一种说明性语言。它使用了一些嵌套在花括号内部的语义来描述一个 Unity Shader 文件的结构。
一个 Unity Shader 的基础结构如下:
Shader "ShaderName" {
Properties {
//属性
}
SubShader {
//显卡A使用的子着色器
}
SubShader {
//显卡B使用的子着色器
}
Fallback "VertecLit"
}
Unity Shader 的结构
1.创建
给创建的 Unity Shader 起一个名字的时候,如下,起了一个”MyShader“
我们可以在材质里面找到的 Shader里面找到它,
打开这个 MyShader 的程序,我们可以看见第一行就有它的位置信息。
Shader "Custom/MyShader" {
这里用"/"可以控制 Unity Shader 在材质面板出现的位置。
2.Properties
Properties语义块中包含了一系列属性,这些属性将会出现在材质面板中。
Properties语义块的定义通常如下:
Properties {
Name ("display name", PropertyType) = DefaultValue
Name ("display name", PropertyType) = DefaultValue
//更多属性
}
这些属性的名字(Name)通常由下划线开始,是显示在 Unity Shader 的面板。
显示的名字(display name)则是出现在材质面板上的名字,我们需要为每个属性定义它的类型(PropertyType)
常见的属性如下(除此之外我们还需要为每个属性指定一个默认值,当我们第一次把该Unity Shader赋给某个材质时,材质就显示这些默认值):
属性类型 | 默认值的定义语法 | 例子 |
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" {} |
对于 Int、Float、Range 这些数字类型的属性,其默认值就是一个单独的数字;
对于Color和Vector这类属性,默认值是用圆括号包围的一个四维向量;
对于2D、Cube、3D这3种纹理类型,默认值的定义稍微复杂,它们的默认值是通过一个字符串后跟一个花括号来指定,其中,字符串要么是空的,要么是内置的纹理名称,如"white"、"black"、"gray"或者"bump"。花括号的用处原本是用于指定一些纹理属性的,但是Unity已删除了本身自带的,如果我们需要类似的功能,就需要自己在顶点着色器中编写计算相应的纹理坐标代码。
PS:需要说明的是,Properties语义块的作用仅仅是为了让这些属性能够出现在材质面板中,即使不声明,也能在程序里定义修改。
3.SubShader
每一个 Unity Shader 文件可以包含多个 SubShader 语义块,但最少要有一个。
当Unity 需要加载这个 Unity Shader 时,Unity 会扫描所有的 SubShader 语义块,然后选择第一个能够在该目标平台上运行的SubShader。如果都不支持的话, Unity就会使用 Fallback 语义指定的 Unity Shader。
Unity 提供这种语义的原因在于,不同的显卡具有不同的能力。
SubShader 语义块中包含的定义通常如下:
SubShader {
//可选的
[Tags]
//可选的
[RenderSetup]
Pass {
}
//Other Passes
}
SubShader中定义了一系列 Pass 以及可选的状态([RenderSetup])和 标签([Tags])设置。
每个Pass定义了一次完整的渲染流程,但如果Pass的数目过多的动,往往会造成渲染性能下降。因此,我们应尽量使用最小数目的Pass. 状态和标签同样可以在Pass声明。不同的是,在SubShader中的一些标签设置是特定的。也就是说,这些标签设置和Pass中使用的标签是不一样的。而对于状态设置来说,其使用的语法是相同的。但是,如果我们在SubShader 进行了这些设置,那么将会用于所以的Pass.
-
状态设置([RenderSetup])
SubShader 提供了一系列渲染状态的设置命令,这些指令可以设置显卡的各种状态,例如是否开启混合/深度测试等。
下表给出了 SubShader 中常见的渲染状态设置选项:
状态名称 | 设置指令 | 解释 |
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语义块中单独进行上面的设置。
-
SubShader的标签([Tags])
SubShader的标签是一个键值对(Key / Value Pair),它的键和值都是字符串类型。这些键值对是SubShader和渲染引擎之间的沟通桥梁。它们用来告诉Unity的渲染引擎:我们希望怎么样以及何时渲染这个对象。
结构如下:
Tags {"TagName1" = "Value1" "TagName2" = "Value2"}
SubShader的标签块支持的标签类型如下:
标签类型 | 说明 | 例子 |
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的影响.通常用于半透明物体 | Tags {"IgnoreProjector" = "True"} |
CanUseSpriteAtlas | 当该SubShader是用于精灵(sprites)时,将该标签设为"False" | Tags {"CanUseSpriteAtlas" = "False"} |
PreviewType | 指明材质面板将如何预览该材质.默认情况下,材质将显示为一个球形,我们可以通过把该标签的值设为"Plane""SkyBox"来改变预览类型 | Tags {"PreviewType" = "Plane"} |
需要注意,上述标签仅可以在 SubShader的标签块中声明,而不可以在Pass块中声明。Pass块虽然也可以定义标签,但这些标签不同于SubShader的标签类型。
-
Pass语义块
Pass语义块的含义如下:
Pass {
[Name]
[Tags]
[RenderSetup]
// Other code
}
我们可以定义该Pass的名称,例如:
Name "MyPassName"
通过这个名称,我们可以使用 ShaderLab 的 UsePass 命令来直接使用其他 Unity Shader 中的 Pass。如:
UsePass "MyShader/MYPASSNAME"
这样可提高代码的复用性。需要注意,由于Unity 内部会把所有Pass的名称换成大写字母的表示,因此,在使用 UsePass命令时必须使用大写形式。
Pass中同样可以设置渲染状态,SubShader 的状态设置同样适用于 Pass。
Pass同样可以设置标签,但它的标签不同于SubShader 的标签。这些标签也是用于告诉渲染引擎我们希望怎么样来渲染该物体。标签类型如下:
标签类型 | 说明 | 例子 |
LightMode | 定义该Pass在Unity的渲染流水线中的角色 | Tags {"LightMode" = "ForwardBase"} |
RequireOptions | 用于指定当满足某些条件时才渲染该Pass,它的值是一个由空格分隔的字符串.目前,Unity支持的选项有:SoftVegetation.在后面的版本中,可能会增加更多的选项 | Tags {"RequireOptions" = "SoftVegetation"} |
除了上面普通的Pass定义之外, Unity Shader 还支持一些特殊的 Pass,以便进行代码复用或实现更复杂的效果。
- UsePass:如我们之前提到的一样,可以使用该命令来复用其他Unity Shader中的Pass。
- GrabPass:该Pass负责抓取屏幕并将结果存储在一张纹理中,以用于后续的Pass处理。
4.Fallback(留一条后路)
紧跟在各个SubShader语义块后面的,可以是一个Fallback指令。它用于告诉Unity,”如果上面所有的SubShader在这块显卡上都不能运行,那么就使用这个最低级的Shder吧!“
语义如下:
Fallback "name"
//或者
Fallback off
例子如下:
Fallback "VertexLit"
Fallback 还会影响阴影的投射。在渲染阴影纹理时,Unity会在每个 Unity Shader 中寻找一个阴影投射的Pass。通常情况下,我们不需要专门实现一个Pass,这是因为Fallback使用内置Shader中包含了这样一个通用的Pass。因此,每个Unity Shader正确设置Fallback是非常重要的。
PS:Unity Shader还有其他语义,如CustomEditor语义来扩展编辑界面;Category语义来对Unity Shader 中的命令进行分组。
Unity Shader的编写形式
我们可以使用以下3种形式来编写Unity Shader(表面着色器、顶点/片元着色器、固定函数着色器)。而且不管使用哪种形式,真正意义上的Shader代码都需要在 ShaderLab语义块中,如下:
Shader "MyShader" {
Properties {
//所需要的各种属性
}
SubShder {
//真正意义上的 Shader 代码会出现在这里
//表面着色器 (Surface Shader)或者
//顶点/片元着色器(Vertex/Fragment Shader)或者
//固定函数着色器(Fixed Function Shader)
}
SubShder {
//和上一个SubShder类似
}
}
1.表面着色器(宠儿)
表面着色器是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 语义块(而非Pass语义块)中的 CGPROGRAM 和 ENDCG之间。
原因是,表面着色器不需要开发者关心使用多少个 Pass、每个Pass如何渲染等问题,Unity会在背后为我们做好这些事情。我们要做的只是告诉它:”使用这些纹理去填充颜色,使用这个纹理去填充颜色,使用这个法线纹理去填充法线,使用Lambert光照模型,其他的都不要烦我!“。
CGPROGRAM 和 ENDCG之间的代码是用 Cg/HLSL编写的,也就是说,我们需要把Cg/HLSL语言嵌套在 ShaderLab语言中。
值得注意的是,这里的Cg/HLSL是Unity经过封装后提供的,它的语法和标准的Cg/HLSL语法几乎一样,但还是有细微不同,例如有些原生函数和用法Unity并没有支持。
2.顶点/片元着色器(最聪明)
在Unity中我们可以使用Cg/HLSL语言来编写 顶点/片元着色器,它们更加复杂,但灵活度也更高。
一个简单的顶点/片元着色器示例代码如下:
Shader "Custom/Simple VertexFragment Shader" {
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float vert(float4 v:POSITION): SV_POSITION {
return mul (UNITY_MATRIX_MVP,v);
}
fixed4 frag() : SV_Target {
return fixed4(1.0,0.0,0.0,1.0);
}
ENDCG
}
}
}
和表面着色器类似,顶点/片元着色器的代码也需要定义在 CGPROGRAM 和 ENDCG之间,但不同的是,顶点/片元着色器是写在Pass语义块内,而非SubShader内的。
原因是,我们需要自己定义每个Pass需要使用的Shader代码。虽然我们需要编写更多的代码,但带来的好处是灵活度很高。更重要的是,我们可以控制渲染的实现细节,同样这里的CGPROGRAM 和 ENDCG之间的代码也是使用Cg/HLSL编写的。
3.固定函数着色器(被抛弃的角落)
上面的两种 Unity Shader 形式都使用了可编程管线,而对于一些较旧的设备(其GPU仅支持DirectX7.0、OpenGL1.5或OpenGL ES 1.1),例如 IPHONE3,它们不支持可编程管线着色器,因此,这时候我们就需要使用固定函数着色器来完成渲染。这些着色器往往只可以完成非常简单的效果。
示例代码如下:
Shader "Tutorial/Basic" {
Properties {
_Color ("Main Color", Color) = (1,0.5,0.5,1)
}
SubShader {
Pass {
Material {
Diffuse [_Color]
}
Lighting On
}
}
}
可以看出,固定函数着色器的代码被定义在Pass语义块中,这些代码相当于Pass中的一些渲染设置,正如我们之前讲的一样。对于固定函数着色器来说,我们需要完全使用 ShaderLab 的语法 (即使用 ShaderLab 的渲染设置命令)来编写,而非使用Cg/HLSL。
PS:由于现在绝大数GPU都支持可编程的渲染管线,这种固定管线的编程方式已经逐渐被抛弃。
4.选择哪种Unity Shader 形式
作者建议:
- 除非有明确的需要要求必须要使用 固定函数着色器,例如在旧设备中,否则请使用 可编程管线的着色器,即表面着色器或者顶点/片元着色器。
- 如果想跟各种光源打交道,则可能更喜欢使用表面着色器,但需要小心在移动平台的性能表现。
- 如果你需要使用的光源数目非常少,例如只有一个平行光,那么使用顶点/片元着色器是一个更好的选择。
- 最重要的是,如果你有很多自定义的渲染效果,那么请选择顶点/片元着色器。
5.该书使用的 Unity Shader 形式
着重讲述使用顶点/片元着色器进行Unity Shader的编写。当然对于表面着色器来说,会在之后进行剖析。