概述
在Unity中,我们需要配合使用材质(Material)
和Unity Shader
才能达到效果。在开发过程中,最常见的流程是:
- 创建一个材质
- 创建一个Unity Shader,并且把它赋给上一步中创建的材质
- 把材质赋给要渲染的对象(GameObject)
- 在材质面板中调整Unity Shader的属性来达到理想的效果
Unity Shader定义了渲染所需的代码
(即顶点着色器,片元着色器等),属性
(如使用哪一些纹理)和指令
(渲染和标签设置等)。而材质则允许我们调节这些属性,并将其赋给相应的模型。
Unity Shader的基础:ShaderLab
Shader "ShaderName" {
Properties {
// 属性
}
SubShader {
// 显卡A使用的子着色器
}
SubShader {
// 显卡B使用的子着色器
}
Fallback "VertexLit"
}
Unity在背后会根据使用的平台来把这些结构编译成真正的代码和Shader文件,而开发者们只需要和Unity Shader打交道即可。从设计上来说,ShaderLab类似于Direct3D Effects(.FX)
语言,它们都定义了要显示一个材质所需的所有东西,而不仅仅是着色器代码。
Unity Shader的结构
Shader名字
每一个Unity Shader文件的第一行都需要通过Shader
语义来定义该Unity Shader的名字。这个名字由一个字符串来定义,例如"ShaderName"
。当为材质选择Unity Shader时,这些名称就会出现在材质面板的下拉列表里。通过在字符串中添加斜杠("/"),还可以控制Unity Shader在材质面板中出现的位置,方便后续的项目管理。例如:
Shader "Custom/ShaderName" { }
那么这个Unity Shader在材质面板中的位置就是:Custom -> ShaderName。
材质和Unity Shader的桥梁: Properties
Properties语义块中包含了一系列属性(Property)
,这些属性将会出现在材质面板中。例如:
Properties {
Name ("Display Name", PropertyType) = DefaultValue
}
开发者们声明这些属性是为了在材质面板中能够方便的调整各种材质属性。如果我们需要在Unity Shader中访问它们,就需要使用每个属性的名字(Name)
。在Unity中,这些属性的名字通常又一个下划线开始。显示的名字(Display Name)
则是出现在材质面板上的名字,这个名字和属性得名字可以不一样。我们还需要为属性指定它的类型(PropertyType)
。除此之外,我们还需要为每一个属性指定一个默认值,在我们第一次把该Unity Shader赋给某个材质时,材质面板上显示的就是这些默认值。
Shader "Custom/ShaderLabProperties" {
Properties {
// Numbers and Sliders
_Int("Int", Int) = 2
_Float("Float", Float) = 1.5
_Range("Range", Range(0.0, 5.0)) = 3.0
// Colors and Vectors
_Color("Color", Color) = (1, 1, 1, 1)
_Vector("Vector", Vector) = (2, 3, 6, 1)
// Textures
_2D("2D", 2D) = "" {}
_3D("3D", 3D) = "black" {}
_Cube("Cube", Cube) = "white" {}
}
}
对于Int
,Float
,Range
这些数字类型的属性,它们的默认值就是一个单独的数字;对于Color
和Vector
这类型的属性,默认值是用圆括号包围的四维向量;对于2D
,3D
,Cube
这三种纹理类型,默认值的定义则稍微复杂,它们的默认值是通过一个字符串后跟着一个花括号来指定的,其中,字符串要么是空的,要么是内置的纹理名称,如"white"
,"black"
,"gray"
或者"bump"
。另外,为了在Shader中可以访问到这些属性,我们需要在CG代码片段
中定义和这些属性类型相匹配的变量。需要说明的是,即使我们不在Properties语义块中声明这些属性,也可以直接在CG代码片中定义变量。因此,Properties语义块的作用仅仅是为了让这些属性可以出现在材质面板中。
关键部位:SubShader
每一个Unity Shader文件中可以包含多个SubShader语义块,但最少要有一个。当Unity需要加载这个Unity Shader时,Unity会扫描所有的SubShader语义块,然后选择第一个能够在目标平台上运行的SubShader。如果都不支持的话,Unity就会使用Fallback语义指定的Unity Shader。这样做也方便开发人员优化代码,因为不同的显卡具有不同的能力,一些旧的显卡仅能支持一定数目的操作指令,而一些高级的更新的显卡可以支持更多的指令数。在这种情况下,我们希望在久的显卡上使用计算难度较低的着色器,而在高级的显卡上使用计算复杂度较高的着色器,以便提供更出色的画面表现。
SubShader语义块内通常包括:
SubShader {
// Optional
[Tags]
// Optional
[RenderSetup]
Pass {
}
// Optional Other Pass
}
SubShader中定义了一系列Pass
以及可选状态([RenderSetup])
和标签([Tags])
设置。每个Pass定义了一次完整的渲染流程,但如果Pass的数目过多,往往会造成渲染性能的下降。因此,我们应尽量使用最小数目的Pass。
状态和标签同样可以在Pass中声明。不同的是,SubShader中的一些标签设置是特定的。也就是说,这些标签设置和Pass中使用的标签是不一样的。而对于状态设置来说,其使用的语法是相同的。但是,如果我们在SubShader进行了这些设置,那么将会用于所有的Pass。
渲染状态设置([RenderSetup])
状态名称 | 设置指令 | 解释 |
---|---|---|
Cull |
Cull Back/Front/Off |
设置剔除模式 |
ZTest |
ZTest Less/Greater/LEqual/GEqual/Equal/NotEqual/Always |
设置深度测试时使用的函数 |
ZWrite |
ZWrite On/Off |
开启/关闭深度写入 |
Blend |
Blend SrcFactor DstFactor |
开启并设置混合模式 |
Subshader中定义了一系列渲染状态的设置命令,这些指令可以设置显卡的各种状态。当在SubShader中设置了上述渲染状态时,将会应用到所有的Pass中。如果我们不想应用到所有的Pass,我们可以在Pass语义块中单独进行设置。
标签设置([Tags])
SubShader的标签(Tags)是一个键值对(Key/Value Pair)
,它的键和值都是字符串类型。这些键值对是SubShader和渲染引擎之间的沟通桥梁。它们用来告诉Unity的渲染引擎:我希望怎样以及何时渲染这个对象。
标签的结构如下:
Tags { "TagName1" = "Value1" "TagName2" = "Value2" }
Queue
Tags { "Queue" = "Transparent" }
控制渲染顺序,指定该物体属于哪一个渲染队列,通过这种方式可以保证所有的透明物理可以在所有的不透明物体后面被渲染,我们也可以自定义使用的渲染队列来控制物体的渲染顺序。
RenderType
Tags { "RenderType" = "Opaque" }
对着色器进行分类,例如这是一个不透明的着色器或者是一个透明的着色器。这可以被用于着色器替换(Shader Replacement)功能。
DisableBatching
Tags { "DisableBatching" = "True" }
一些SubShader在使用Unity的批处理功能时会出现问题,例如使用了模型空间下的坐标来进行顶点动画。这个时候可以通过该标签来直接指明是否对该SubShader使用批处理。
ForceNoShadowCasting
Tags { "ForceNoShadowCasting" = "True" }
控制使用该SubShader的物体是否会投射阴影。
IgnoreProjector
Tags { "IgnoreProjector" = "True" }
如果该标签值为"True",那么使用该SubShader的物体将不会受Projector的影响。通常用于半透明物体。
CanUseSpriteAtlas
Tags { "CanUseSpriteAtlas" = "False" }
当该SubShader是用于精灵(Sprite)
时,将该标签设为"False"。
PreviewType
Tags { "PreviewType" = "Plane" }
指明材质面板将如何预览该材质。默认情况下,材质将会显示为一个球形,我们可以通过把该标签的值设为"Plane","SkyBox"来改变预览模型。
Pass语句块
Pass语句块包含的语义如下:
Pass {
[Name]
[Tags]
[RenderSetup]
// Other code
}
首先我们可以在Pass中定义该Pass的名称,例如:
Name "PassName"
通过这个明晨,我们可以使用ShaderLab的UsePass
命令来直接使用其它Unity Shader中的Pass。例如:
UsePass "ShaderName/PASSNAME"
这样可以提高代码的复用性。需要注意的是,由于Unity内部会把所有的Pass的名字转换成大些字母的表示,因此,在使用UsePass命令时必须使用大写形式的名字。
其次,我们可以对Pass设置渲染状态。SubShader的状态设置同样适用于Pass。另外,在Pass中我们还可以使用固定管线的着色器命令
。
Pass同样可以设置标签,但是它的标签不同于SubShader标签。这些标签也是用于告诉渲染引擎我们希望怎样渲染物体。
LightMode
Tags { "LightMode" = "ForwardBase" }
定义该Pass在Unity的渲染流水线中角色。
RequireOptions
Tags {"RequireOptions" = "SoftVegetation"}
用于指定当满足某些条件才渲染该Pass,它的值是一个由空格分割的字符串。目前,Unity支持的选项由SoftVegetation。
后路:Fallback
紧跟在SubShader语义块后面的,可以是一个Fallback指令。它用于告诉Unity "如果上面所有的SubShader在这块显卡上都不能运行,那么就使用这个最低级的Shader"。
Fallback "name"
// or
Fallback Off
Unity的新玩意:表面着色器
表面着色器(Surface 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语义块(而非Pass语义块中)
中的CGPROGRAM
和ENDCG
之间。原因是,表面着色器不需要开发者关心使用多少个Pass,每个Pass如何渲染等问题,Unity会在背后为我们做好这些事情。我们要做的只是告诉它:“嘿,使用这些纹理区填充颜色,使用这个法线纹理去填充法线,使用Lambert光照模型,其它的别来烦我!”。
CGPROGRAM和CGEND之间的代码使用CG/HLSL编写的。也就是说,我们需要把CG/HLSL语言嵌套在ShaderLab语言中。值得注意的是,这里的CG/HLSL是Unity经封装后提供的,它的语法和标准的CG/HLSL几乎一样,但还是有细微区别,例如,有些原生的函数和用法Unity没有提供支持。
Unity的顶点/片元着色器
除了表面着色器,在Unity中我们可以使用CG/HLSL语言来编写顶点/片元着色器,它们更加复杂,但灵活性也更高。
Shader "Custom/Simple VertexFragment Shader" {
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 v : POSITION) : SV_POSITION {
return mul(UNITY_MATRIX_MVP, v);
}
fixed4 frag() : SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
}
和表面着色器类似,顶点/片元着色器的代码也需要定义在CGPROGRAM和ENDCG之间,但不同的是,顶点/片元着色器是写在Pass语义块内,而非SubShader内部。原因是,我们需要在每一个Pass中定义需要使用的Shader代码。虽然我们可能需要编写更多代码但是这样可以带来更高的灵活性。
特别注意
Unity Shader != 真正的Shader
- 在传统的Shader中,我们仅可以编写特定类型的Shader,例如,顶点着色器,片元着色器。而在Unity Shader中,我们可以在同一个文件中包含需要的顶点着色器和片元着色器代码。
- 在传统的Shader中,我们无法设置一些渲染设置,例如是否开启混合,深度测试等。这些是开发者们在另外的代码中自行设置的。而在Unity Shader中,我们通过一行代码就可以完成这些设置。
- 在传统的Shader中,我们需要编写冗长的代码来设置着色器的输入输出,要小心地处理这些输入输出的位置对应关系等。而在Unity Shader中,我们只需要在特定语句块中声明一些属性,就可以依靠材质来方便的改变这些属性。