UnityShader 基础
UnityShader 概述
一对好兄弟:材质和UnityShader
总体来说,在Unity中我们需要配合使用材质(Material)和Unity Shader才能达到需要的效果。一个最常见的流程是:
- (1)创建一个材质;
- (2)创建一个Unity Shader,并把它赋给上一步中创建的材质;
- (3)把材质赋给要渲染的对象;
- (4)在材质面板中调整Unity Shader的属性,以得到满意的效果。
Unity Shader定义了渲染所需的各种代码(如顶点着色器和片元着色器)、属性(如使用哪些纹理等)和指令(渲染和标签设置等),而材质则允许我们调节这些属性,并将其最终赋给相应的模型。
Unity 中的材质
Unity中的材质需要结合一个GameObject的Mesh或者Particle Systems组件来工作
Unity 中的Shader
Unity一共提供了4种Unity Shader模板供我们选择——Standard Surface Shader, Unlit Shader, Image Effect Shader以及Compute Shader。其中,Standard Surface Shader会产生一个包含了标准光照模型(使用了Unity 5中新添加的基于物理的渲染方法,详见第18章)的表面着色器模板,Unlit Shader则会产生一个不包含光照(但包含雾效)的基本的顶点/片元着色器,Image Effect Shader则为我们实现各种屏幕后处理效果(详见第12章)提供了一个基本模
Unity Shader本质上就是一个文本文件。和Unity中的很多外部文件类似,Unity Shader也有导入设置(Import Settings)面板,在Project视图中选中某个Unity Shader即可看到
Unity Shader的导入设置面板
在该面板上,我们可以在Default Maps中指定该Unity Shader使用的默认纹理。当任何材质第一次使用该Unity Shader时,这些纹理就会自动被赋予到相应的属性上。在下方的面板中,Unity会显示出和该Unity Shader相关的信息,例如它是否是一个表面着色器(Surface Shader)、是否是一个固定函数着色器(Fixed Function Shader)等,还有一些信息是和我们在Unity Shader中的标签设置(详见3.3.3节)有关,例如是否会投射阴影、使用的渲染队列、LOD值等
我们可以通过单击Show generated code按钮来打开一个新的文件,在该文件里将显示Unity在背后为该表面着色器生成的顶点/片元着色器
Unity Shader的基础:ShaderLab
Unity Shader是Unity为开发者提供的高层级的渲染抽象层。图3.6显示了这样的抽象。Unity希望通过这种方式来让开发者更加轻松地控制渲染。
一个Unity Shader的基础结构如下所示:
Shader "ShaderName" {
Properties {
// 属性
}
SubShader {
// 显卡A使用的子着色器
}
SubShader {
// 显卡B使用的子着色器
}
Fallback "VertexLit"
}
Unity Shader的结构
每个Unity Shader文件的第一行都需要通过Shader语义来指定该Unity Shader的名字
Shader "Custom/MyShader" { }
材质和Unity Shader的桥梁:Properties
Properties语义块中包含了一系列属性(property),这些属性将会出现在材质面板中。
Properties {
Name ("display name", PropertyType) = DefaultValue
Name ("display name", PropertyType) = DefaultValue
// 更多属性
}
每个属性的名字(Name)。在Unity中,这些属性的名字通常由一个下划线开始。显示的名称(display name)则是出现在材质面板上的名字。我们需要为每个属性指定它的类型(PropertyType)
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) = "" {}
_Cube ("Cube", Cube) = "white" {}
_3D ("3D", 3D) = "black" {}
}
FallBack "Diffuse"
}
重量级成员:SubShader
每一个Unity Shader文件可以包含多个SubShader语义块,但最少要有一个。当Unity需要加载这个Unity Shader时,Unity会扫描所有的SubShader语义块,然后选择第一个能够在目标平台上运行的SubShader。如果都不支持的话,Unity就会使用Fallback语义指定的Unity Shader。
SubShader语义块中包含的定义通常如下:
SubShader {
// 可选的
[Tags]
// 可选的
[RenderSetup]
Pass {
}
// Other Passes
}
SubShader中定义了一系列Pass以及可选的状态([RenderSetup])和标签([Tags])设置。每个Pass定义了一次完整的渲染流程,但如果Pass的数目过多,往往会造成渲染性能的下降。因此,我们应尽量使用最小数目的Pass。状态和标签同样可以在Pass声明。不同的是,SubShader中的一些标签设置是特定的。也就是说,这些标签设置和Pass中使用的标签是不一样的。而对于状态设置来说,其使用的语法是相同的。但是,如果我们在SubShader进行了这些设置,那么将会用于所有的Pass。
·状态设置
当在SubShader块中设置了上述渲染状态时,将会应用到所有的Pass。如果我们不想这样(例如在双面渲染中,我们希望在第一个Pass中剔除正面来对背面进行渲染,在第二个Pass中剔除背面来对正面进行渲染),可以在Pass语义块中单独进行上面的设置。
SubShader的标签
SubShader的标签(Tags)是一个键值对(Key/Value Pair),它的键和值都是字符串类型。这些键值对是SubShader和渲染引擎之间的沟通桥梁。它们用来告诉Unity的渲染引擎:SubShader我希望怎样以及何时渲染这个对象。
Tags { "TagName1" = "Value1" "TagName2" = "Value2" }
Pass语义块
Pass {
[Name]
[Tags]
[RenderSetup]
// Other code
}
Name "MyPassName"
通过这个名称,我们可以使用ShaderLab的UsePass命令来直接使用其他Unity Shader中的Pass。
UsePass "MyShader/MYPASSNAME"
这样可以提高代码的复用性。需要注意的是,由于Unity内部会把所有Pass的名称转换成大写字母的表示,因此,在使用UsePass命令时必须使用大写形式的名字。
留一条后路:Fallback
紧跟在各个SubShader语义块后面的,可以是一个Fallback指令。它用于告诉Unity, “如果上面所有的SubShader在这块显卡上都不能运行,那么就使用这个最低级的Shader吧!”
Fallback "name"
// 或者
Fallback Off
Unity Shader的形式
在Unity中,我们可以使用下面3种形式来编写Unity Shader。而不管使用哪种形式,真正意义上的Shader代码都需要包含在ShaderLab语义块中,如下所示:
Shader "MyShader" {
Properties {
// 所需的各种属性
}
SubShader {
// 真正意义上的Shader代码会出现在这里
// 表面着色器(Surface Shader)或者
// 顶点/片元着色器(Vertex/Fragment Shader)或者
// 固定函数着色器(Fixed Function Shader)
}
SubShader {
// 和上一个SubShader类似
}
}
Unity的宠儿:表面着色器
表面着色器(Surface Shader)是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会在背后为我们做好这些事情。
CGPROGRAM和ENDCG之间的代码是使用CG/HLSL编写的,也就是说,我们需要把CG/HLSL语言嵌套在ShaderLab语言中。值得注意的是,这里的CG/HLSL是Unity经封装后提供的,它的语法和标准的CG/HLSL语法几乎一样,但还是有细微的不同
最聪明的孩子:顶点/片元着色器
在Unity中我们可以使用CG/HLSL语言来编写顶点/片元着色器(Vertex/Fragment Shader)
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,0.0,0.0,1.0);
}
ENDCG
}
}
}
和表面着色器类似,顶点/片元着色器的代码也需要定义在CGPROGRAM和ENDCG之间,但不同的是,顶点/片元着色器是写在Pass语义块内,而非SubShader内的。原因是,我们需要自己定义每个Pass需要使用的Shader代码。虽然我们可能需要编写更多的代码,但带来的好处是灵活性很高。更重要的是,我们可以控制渲染的实现细节。同样,这里的CGPROGRAM和ENDCG之间的代码也是使用CG/HLSL编写的。
Unity Shader ! = 真正的Shader
在Unity里,Unity Shader实际上指的就是一个ShaderLab文件——硬盘上以.shader作为文件后缀的一种文件。
在Unity Shader(或者说是ShaderLab文件)里,我们可以做的事情远多于一个传统意义上的Shader。
- ·在传统的Shader中,我们仅可以编写特定类型的Shader,例如顶点着色器、片元着色器等。而在Unity Shader中,我们可以在同一个文件里同时包含需要的顶点着色器和片元着色器代码。
- ·在传统的Shader中,我们无法设置一些渲染设置,例如是否开启混合、深度测试等,这些是开发者在另外的代码中自行设置的。而在Unity Shader中,我们通过一行特定的指令就可以完成这些设置。
- ·在传统的Shader中,我们需要编写冗长的代码来设置着色器的输入和输出,要小心地处理这些输入输出的位置对应关系等。而在Unity Shader中,我们只需要在特定语句块中声明一些属性,就可以依靠材质来方便地改变这些属性。而且对于模型自带的数据(如顶点位置、纹理坐标、法线等), Unity Shader也提供了直接访问的方法,不需要开发者自行编码来传给着色器。
从本质上来讲,Unity Shader只有两种形式:顶点/片元着色器
扩展阅读
Unity官网上关于Unity Shader方面的文档正在不断补充中,由于Unity封装了很多功能和细节,因此,如果读者在使用Unity Shader的过程中遇到了问题可以去到官方文档(http://docs.unity3d.com/Manual/SL-Reference.html)中查看。除此之外,Unity也提供了一些简单的着色器编写教程(http://docs.unity3d.com/Manual/ShaderTut1.html,http://docs.unity3d.com/Manual/ShaderTut2.html)。由于在Unity Shader中,绝大多数可编程管线的着色器代码是使用CG语言编写的,读者可以在NVIDIA提供的CG文档(http://http.developer.nvidia.com/CG/)中找到更多的内容。NVIDIA同样提供了一个系列教程(http://http.developer.nvidia.com/CGTutorial/cG_tutorial_chapter01.html)来帮助初学者掌握CG的基本语法。