第六章 片元着色器和抓取通道

到目前为止,我们都在折腾 表面着色器(Surface Shaders) 。它的设计初衷是简化我们的着色器编码工作,为艺术家提供一个有意义的工具。但是如果想让我们的着色器知识更上一层楼,我们就要前往 顶点(Vertex)片元(Fragment) 着色器的知识岛屿冒险啦。


在这一章节,我们将会学习下面的一些知识点:

  • 理解顶点和片元着色器
  • 使用抓取通道
  • 实现一个玻璃效果的着色器
  • 在2D游戏中实现水效果的着色器

介绍

表面着色器(Surface Shaders) 相比,顶点片元 着色器少了一些诸如,光是如何在物体表面反射的物理属性信息。所谓有失必有得,这样的话顶点和片元着色器就没有了物理规则的限制并且特别适合实现非真实的效果。这个章节将集中讲抓取通道的技术,这些技术可以让着色器来模拟形变效果。


理解顶点和片元着色器

理解顶点和片元着色器最好的方法就是你自己亲自创建一个。在这个知识点我们将展示如何编写一个这样的着色器,该着色器简单的将一张纹理应用到一个模型上并且通过给定的颜色进行乘积运算,效果就如同下图一样:
diagram


这里展示的着色器非常的简单,只是作为学习其他顶点和片元着色器基础。


  • 始前准备
    对于这个知识点,我们将需要一个新的着色器。我们按照下面的步骤来:

    • 1.创建一个新的着色器。
    • 2.创建一个新的材质并且把着色器应用于该材质。
  • 操作步骤
    在前面的所有章节中,我们总是能在 表面着色器(Surface Shaders) 的基础上进行修改。但在这里就不能再那样做了,因为表面着色器和片元着色器在结构上是不一样的。我们需要做如下的修改:

    1. 删除着色器上的所有属性,然后用下面的属性替换:
    Color ("Color", Color) = (1,0,0,1) // Red
    _MainTex ("Base texture", 2D) = "white" {}
    
    1. 删除 SubShader 块中的所有代码,然后用下面的代码替换:
    Pass 
    {
    	CGPROGRAM
    	#pragma vertex vert
    	#pragma fragment frag
    	half4 _Color;
    	sampler2D _MainTex;
    	struct vertInput 
    	{
    		float4 pos : POSITION;
    		float2 texcoord : TEXCOORD0;
    	};
    	struct vertOutput 
    	{
    		float4 pos : SV_POSITION;
    		float2 texcoord : TEXCOORD0;
    	};
    	vertOutput vert(vertInput input) 
    	{
    		vertOutput o;
    		o.pos = UnityObjectToClipPos(input.pos);
    		o.texcoord = input.texcoord;
    		return o;
    	}
    	half4 frag(vertOutput output) : COLOR
    	{
    		half4 mainColour = tex2D(_MainTex, output.texcoord);
    		return mainColour * _Color;
    	}
    	ENDCG 
    }
    

    后面所有的顶点和片元着色器都会以此为基础。


  • 原理介绍
    正如它的名字所提示的那样,顶点和片元着色器的工作步骤分为两步。首先,模型会通过一个 顶点函数(vertex function) ;之后,获得的结果会输入一个 片元函数(fragment function) 。这两个函数都直接用 pragma 进行声明: #pragma vertex vert #pragma fragment frag 在这个例子中,我们简单的给这两个函数取名为 vertfrag
    从概念上来讲, 片元(fragments) 跟像素关系紧密;片元这个术语常用于指代那些绘制像素时所必须的数据集。这也是为何顶点和片元着色器常被叫做 像素着色器(Pixel Shaders) 的原因。 在着色器代码中,顶点函数接收来自于定义的结构体 vertInput 中的输入数据:
struct vertInput 
{
   float4 pos : POSITION;
   float2 texcoord : TEXCOORD0;
};

这个结构体的名字可以随便取,但是它里面的内容却不行。其中的每一项都必须使用 绑定语义(binding semantic) 进行修饰。这是Cg语言的一个特性,允许我们标记变量从而能让这些变量能被一些确切的数据初始化,比如 法线向量(normal vectors)顶点位置(normal vectors)POSITION 这个绑定语义是说当 vertInput 这个结构体输入到顶点函数中时,pos 将会包含当前顶点的位置。这个跟表面着色器中 appdata_full 结构体中的 vertex 项有些类似。两者的主要区别在于 pos 表示的是模型上的坐标(3D物体),这样的话我们需要手动把它转换成视口坐标(屏幕上的坐标)。


注意
在表面着色器中顶点函数一般只用于修改模型的几何形状。而在顶点着色器和片元着色器中,如果要将模型的坐标投影到屏幕上,顶点函数是必须的。


这种转换背后的数学原理超出了该章节的范围。然而我们可以通过Unity提供的一个特殊矩阵:UNITY_MATRIX_MVP,让它对 pos 进行乘运算从而得到一个这样的变换。它通常就是提到的 模型视图投影变换矩阵(model-view-projection matrix) ,并且它对于找到顶点在屏幕中的位置来说是必不可少的:

vertOutput o;
o.pos = mul(UNITY_MATRIX_MVP, input.pos);

注意
如果你使用的是比较新的Unity引擎版本,那么o.pos = mul(UNITY_MATRIX_MVP, input.pos);这行代码会被Unity自动替换成o.pos = UnityObjectToClipPos(input.pos);
另一个初始化数据 textcoord ,它使用了 TEXCOORD0 绑定语义来获取第一张纹理的UV数据。之后就没有额外的处理了并且这个值可以直接传递给 片元函数(fragment function)
o.texcoord = input.texcoord;
Unity会帮我们初始化 vertInput,而需要我们初始化的是 vertOutput 。尽管如此,它里面的内容依然需要用绑定语义修饰:

struct vertOutput 
{
   float4 pos : SV_POSITION;
   float2 texcoord : TEXCOORD0;
};

一旦顶点函数初始化 vertOutput 后,这个结构就会传递给片元函数。模型的主纹理采样就会跟提供的颜色值进行乘积


可以看到,顶点着色器和片元着色器并没有材质的物理学属性;相比于表面着色器,它们的工作方式更接近于GPU架构。


  • 额外内容
    顶点着色器和片元着色器最难理解的一方面就是绑定语义。可以使用的绑定语义不仅数量多并且它们所表达的意义还跟上下文有关。

    输入语义
    下表的绑定语义可以在 vertInput 中使用,这是Unity给顶点函数提供的结构。被这些语义修饰的部分将会被自动初始化:

    绑定语义描述
    POSITION, SV_POSITION一个顶点在世界坐标中的位置(物体空间)
    NORMAL顶点的法线,相对于世界坐标来说的(不是相对于摄像机)
    COLOR, COLOR0, DIFFUSE, SV_TARGET保存在顶点中的颜色信息
    COLOR1, SPECULAR保存在顶点中的次要颜色信息(通常是高光反射)
    TEXCOORD0, TEXCOORD1, …, TEXCOORDi保存在顶点中的 0-i 的UV数据

    输出语义
    当进行绑定的时候,会在 vertOutput 结构中使用对应的语义;它们不能保证这些部分会被自动初始化。于此相反的;得由我们自己去给它们初始化。编译器会尽可能确保这些部分会被正确的数据初始化:

    绑定语义描述
    POSITION, SV_POSITION, HPOS顶点在摄像机坐标中的位置(裁剪空间,每个维度从0-1取值)
    COLOR, COLOR0, COL0, COL,SV_TARGET正面主要颜色
    COLOR1, COL1正面次要颜色
    TEXCOORD0, TEXCOORD1, …,TEXCOORDi, TEXi保存在顶点中的 0-i 的UV数据
    WPOS窗口中的基于像素的坐标(起点在左下角)

    如果出于某种原因,你需要某个包含不同类型数据的部分,你就可以从众多可用的 TEXCOORD 中选择一个去修饰它。编译器不允许某个部分没有修饰语义。


  • 相关补充
    你可以参考英伟达官网的手册 NVIDIA Reference Manual,里面有其他的可以在Cg中使用的绑定语义可供参考。