什么是Shader?
Shader又称着色器,科学定义是“可编程图形管线”,如果从Unity中的工程角度思考,写Shader可以理解为我们的项目或者其中一个场景定义渲染方式,而制作一个Shader文件,一般是这一过程中的一环,往往是为某个物件定义其独特的渲染效果。
Shader文件的结构
这里从Unity工程中典型的单一Shader文件出发,尝试以初学者视角快速理解其结构,即具体代码块的功能,以窥Shader的运作方式,文章可能有所详略,相关名词可自行了解。
Shader命名
事实上,当我们尝试在Unity里新建一个Shader文件时会发现,这里提供了几种不同的模板(如果读过那本很出名的入门精要,往往会觉得无所适从,因为Unity5.3之前的版本是只有一种Shader可以创建的)。不过可以说明的是,我个人学习时,是先创建包括了顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)的Unlit Shader,而非Unity提供的标准表面着色器(那玩意甚至不能写PASS块),这样阅读起来会没有障碍。
如果不改变它的命名,创建文件后打开,可以发现开头已经有了默认的命名:归类在Unlit文件中的NewUnlitShader文件。具体的命名应该根据Shader的种类和工程实践的需求进行,这里不再赘述。
Shader "Unlit/NewUnlitShader"
在对材质赋予Shader时,也可以用同样的路径找到这个Shader文件:
Properties语义块
正如你可能在别的教程中看到的一样,Shader文件由一个个的语义块组成,而properties语义块定义一些我们需要控制的变量(被支持的变量类型有限,可以自行查询)类似于Unity脚本中的public变量,可以在交互界面中改变其属性,例如:
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
以上代码定义了一个2D图像属性的变量,初始值是一张白色贴图,其格式为:
变量名 (“检视面板里显示的名称”, 变量类型)= 默认值
你可以在Shader的Inspector面板中改变它:
SubShader语义块
SubShader语义块就是我们需要编辑的主体了,在同一个Shader文件中,可以有多个SubShader语义块,但至少要有一个,你可以理解为:我撰写一个Shader,需要考虑到Unity在不同的平台该怎么渲染,并分别提供不同的SubShader块来帮助Unity完成渲染;如果我不想考虑这些事,但我的工程又可能在不同的环境中被使用,那么文件末尾还应当有这样的字段:
FallBack "Diffuse"
以上语句相当于指定了SubShader的缺省值,即如果没有找到可用的块,用Unity自带的Diffuse进行渲染。
Pass块
Pass块属于SubShader块,也是可以有多个同时存在,不过,我们可能需要更加详细地告诉Unity它在我们渲染中的角色:
Tags { "RenderType"="Opaque" }
这是Tags语句,指定Pass块在光照流水线中的作用,其具体含义暂不关心(学到这里还记不住,太多了)
CG代码片的开始、引用库和函数
终于要到我们Shader的关键部位:正式的CG代码片了,不过我们还需要做点事情:
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
CG代码片使用CGPEOGRAM和ENDCG来作为开始和结束标志(我没弄懂这有啥意义),之后,我们用#pragma指令指定顶点着色器和片元着色器的代码,这里分别是vert和frag函数(见下文);另外,为了使用一些很有用的Unity内置函数和变量,我们需要引入一些库文件,例如UnityCG文件。
声明变量
我们在CG代码片中声明变量,这包括我们需要用到的Properties块中的变量,应当重新声明;也包括我们CG代码计算中需要的局部变量,如这里重申了2D类型的_MainTex和一个float4类型的自定变量:
sampler2D _MainTex;
float4 _MainTex_ST;
定义顶点着色器结构体
我们应当定义两个结构体,用于顶点着色器的输入和输出(事实上输出结构体就是片元着色器的输入结构体,因此其中每个子类型都应当关注)
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
这些输入输出从哪来呢?在实际撰写中,这些结构体中的类型,几乎都来自Unity提供的变量,如标志渲染位置的POSITION;来自贴图纹理等的TEXCOORD;有些则是一些约定俗成的过程变量,如常用作顶点着色器输出和片元着色器输入的SV_POSITION。
顶点着色器函数
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
这是一个非常典型的顶点着色器函数(来自默认Unlit Shader模板),在初学阶段我们不关心每个语句的语义,而是尝试剖析其结构:它使用了我们前面定义的输入结构体类型appdata,中间经过了一些输入和内置变量之间的运算,最终返回了v2f类型给片元着色器。
片元着色器函数
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
同样的,片元着色器拿到v2f类型的输入,经过Unity的自建函数UNITY_APPLY_FOG(用于处理雾效,暂不研究),得到输出col,它是一个fixed格式变量(实际上是颜色,整个Shader是一个不考虑灯光效果的漫反射Shader)
总结
好的,大功告成!现在阅读了一个完整的Shader,我们大概了解了写一个Shader需要做些什么,也能一窥Unity Shader应该学习的东西(包括常用变量类型及其数据结构,内建函数,约定俗成的写法,不同着色器间的交流等等)。