第四章 开始Unity Shader学习之旅(2)
1. 强大的援手:Unity提供的内置文件和变量
上面,我们讲述了如何在Unity中编写一个基本的顶点/片元着色器的过程。顶点/片元着色器的复杂之处在于,很多事情都要我们“亲力亲为”,例如我们需要自己转换法线方向,自己处理光照、阴影等。为了方便开发者的编码过程,Unity提供了很多内置文件,这些文件包含了很多提前预定义的函数、变量和宏等。如果读者在学习其它人编写的UnityShader代码时,遇到了一些从未见到的变量、函数,而又无法找到对应的声明和定义,那么很有可能就是这些代码使用了Unity内置文件提供的函数和变量。
下面我们给出这些文件和变量的预览。
1.1 内置的包含文件
包含文件(include file),是类似于c++中头文件的一种文件。在Unity中,它们的文件后缀是.cginc。在编写Shader时,我们可以使用#include指令把这些文件包含进来,这样我们就可以使用Unity为我们提供的一些非常有用的变量和帮助函数,例如:
CGPROGRAM
//......
#include "UnityCG.cgnic"
//......
ENDCG
那么,这些文件在哪里呢?我们可以在官方网站(http://unity3d.com/cn/get-unity/download/archive)上选择下载->内置着色器,来直接下载这些文件,下图显示了由官网压缩包得到的文件。
从图中可以看出,从官网下载的文件中包含了多个文件夹。其中,CGIncludes文件夹中包含了所有的内置文件;DefaultResources文件夹中包含了一些内置组件或功能所需要的Unity Shader,例如一些GUI元素使用的Shader;DefaultResourcesExtra则包含了所有Unity中内置的Unity Shader;Editor文件夹目前只包含了一个脚本文件,它用于定义Unity5引入的StandardShader所使用的的材质面板。这些文件都是非常好的参考资料,在我们想要学习内置着色器的实现或是寻找内置函数的实现时,都可以在这里找到内部实现。但在本节中,我们只关注CGIncludes文件夹下的相关文件。
我们也可以从Unity的应用程序中直接找到CGIncludes文件夹。它的位置是:Unity的安装路径/Data/CGIncludes.。
下表给出了CGIncludes中主要包含的文件以及它们的主要用处。
可以看出,有一些文件即便是我们没有使用#include指令,它们也是会被自动包含进来的,例如UnityShaderVariables.cginc。因此,在前面的例子中,我们可以直接使用UNITY_MATRIX_MVP变量来进行顶点变换。除了上表列出的包含文件外,Unity5引入了许多重要的包含文件,如UnityStandardBRDF.cginc、UnityStandardCore.cginc等,这些包含文件用于实现基于物理的渲染,我们会在后面说道。
UnityCG.cginc是我们最常接触的一个包含文件,在后面的学习中,我们将使用很多文件提供的结构体和函数,为我们的编写提供方便。例如我们可以直接使用UnityCg.cginc中预定义的结构体作为顶点着色器的输入和输出。下表给出了一些结构体的名称和包含的变量。
强烈建议读者找到UnityCG.cginc文件并查看上述结构体的声明,这样的过程可以帮助我们快速的理解Unity中一些内置变量的工作原理。
除了结构体外,UnityCG.cginc也提供了一些常用的帮助函数。下表给出了一些函数名和它们的描述。
我们建议读者在UnityCG.cginc文件找到这些函数的定义,并尝试理解它们。一些函数我们完全可以自己实现,例如 UnityObjectToWorldDir和 UnityWorldToObjectDir,这两个函数实际上就是对方向矢量进行了一次坐标空间变换。而UnityCG.cginc文件可以帮我们提高代码的复用率。UnityCG.cginc还包含了很多宏,在后面的学习中,我们就会遇到它们。
1.2 内置的变量
我们在以前给出了一些用于坐标变换和摄像机参数的内置变量。除此之外,Unity还提供了用于访问时间、光照、雾效和环境光等目的的变量。这些内置变量大多位于UnityShaderVariables.cginc中,与光照有关的内置变量还会位于Lighting.cginc、AutoLight.cginc等文件中。当我们在后面的学习中遇到这些变量时再进行详细的讲解。
2. Unity提供的Cg/HLSL语义
读者在平时的Shader学习中可能经常看到,在顶点着色器和片元着色器的输入输出变量后还有一个冒号以及一个全部大写的名称,例如SV_POSITION、POSITION、COLOR0。这些大写的名字是什么呢?它们有什么用呢?
2.1 什么是语义
实际上,这些是Cg/HLSL提供的语义(semantics)。语义实际上就是一个赋给Shader输入和输出的字符串,这个字符串表达了这个参数的含义。通俗地讲,这些语义可以让Shader知道从哪里读取数据,并把数据输出到哪里,它们在Cg/HLSL的Shader流水线中是不可或缺的。需要注意的是,Unity并没有支持所有的语义。
通常情况下,这写输入输出并不需要有特别的意义,也就是说,我们可以自行决定这些变量的用途。例如在上面的代码中,顶点着色器的输出结构体中我们用Color0语义去描述color变量。color变量本身存储了什么,Shader流水线并不关心。
而Unity为了方便对模型数据的传输,对一些语义进行了特别含义的规定。例如,在顶点着色器中的输入结构体a2v用TEXCOORD0来描述texcoord,Unity会识别TEXCOORD0的语义,以把模型的第一组纹理坐标填充到texcoord中。需要注意的是,即便语义的名称一样,如果出现的位置不同,含义也不同。例如TEXCOORD0即可用于描述顶点着色器的输入结构体a2v,也可用于描述输出结构体v2f。但在输入结构体a2v中,TEXCOORD0有特别的含义,即把模型的第一组纹理坐标存储在该变量中,而在输出结构体v2f中,TEXCOORD0修饰的变量含义就可以由我们来决定。
在DirectX 10以后,有了一种新的语义类型,就是系统数值语义(system-value semantics)。这类语义是以SV开头的,SV代表的含义就是系统数值(system-value)。这些语义在渲染流水线中有特殊的含义。例如在上面的代码中,我们使用SV_POSITION语义去修饰顶点着色器的输出变量pos,那么就表示pos包含了可用于光栅化的变换后的顶点坐标(即齐次裁剪空间中的坐标)。用这些语义描述的变量是不可以随便赋值的,因为流水线需要它们来完成特定的目的,例如渲染引擎会把用SV_POSITION修饰的变量经过光栅化后显示在屏幕上。读者有时可能会看到同一个变量在不同的Shader里面使用了不同的语义修饰。例如一些Shader会使用POSITION而非SV_POSITION来修饰顶点着色器的输出。SV_POSITION是DirectX 10中引入的系统数值语义,在绝大多数平台上,它和POSITION语义是等价的,但在某些平台(如索尼PS4)上必须使用SV_POSITION来修饰顶点着色器的输出,否则无法让Shader正常工作。同样的例子还有COLOR和SV_Target。因此为了让我们的Shader有更好的跨平台性,对于这些有特殊含义的变量我们最好使用SV开头的语义进行修饰。
2.2 Unity支持的语义
下表总结了从应用阶段传递模型数据给顶点着色器时Unity使用的常用语义。这些语义虽然没有使用SV开头,但在Unity内部赋予了它们特殊的含义。
其中TEXCOORDn中的n的数目是和Shader Model有关的,例如一般在Shader Model 2(即Unity默认编译到的Shader Model版本)和Shader Model 3中,n等于8,而在Shader Model 4和Shader Model 5中,n等于16。通常情况下,一个模型的纹理坐标组数一般不超过2,即我们往往只使用TEXCOORD0和TEXCOORD1。在Unity内置的数据结构体appdata_full中,它最多使用6个坐标纹理组。
下表总结了从顶点着色器到片元着色器阶段Unity支持的常用语义。
上面的语义中,除了SV_POSITION有特别含义外,其它语义对变量的含义没有明确要求,也就是说,我们可以存储任意值带这些语义描述变量中。通常,如果我们需要把一些自定义的数据从顶点着色器传递给片元着色器,一般选用TEXCOORD0等。
下表给出了Unity中支持的片元着色器的输出语义。
2.3 如何定义复杂的变量类型
上面提到的语义绝大部分用于描述标量或矢量类型的变量,例如fixed2、float、float4、fixed4等。下面的代码给出了一个使用语义来修饰不同类型变量的例子。
struct v2f{
float4 pos:SV_POSITION;
fixed3 color0:COLOR0;
fixed4 color1 :COLOR1;
half value0:TEXCOORD0;
float2 value1:TEXCOORD1;
};
关于何时使用哪种类型的变量,我们会在后面给出一些建议,但要注意的是,一个语义可以使用的寄存器只能处理4个浮点值(float)。因此如果我们想要定义矩阵类型,例如float3×4、float4×4等变量就需要使用更多的空间。一种方法是,把这些变量拆分成多个变量,例如对float4×4的矩阵类型,我们可以拆分成4个float4类型的变量,每个变量存储了矩阵的一行数据。