我们可以放心地讲,Unity对于很多人来说已经使游戏开发变得更加容易。但其中毫无疑问仍然有很长的路要走的就是Shader编程。提起Shader人们通常会觉得它很神秘,其实 Shader就是专门用于在GPU上运行的程序,它最终会绘制你3D模型中的所有三角形。如果你想给你的游戏添加特别的外观或效果,那么学习如何编写Shader程序是至关重要的。Unity也会使用Shader来做后期处理(postprocessing),或者将它们作为2D游戏至关重要的组成元素。本教程简单介绍Shader编程,主要面向那些对Shader几乎没有任何知识的开发人员。
简介
下图展示了三个在Unity的渲染工作流中起着重要作用的实体元素:
从本质上来说,3D模型是由一系列3D坐标组成的集合,这些3D坐标在这里被称作顶点(vertex).这些顶点连接在一起以组成三角形。每个顶点可以包含一些其它的信息,比如,这个顶点的颜色、这个顶点指向的方向(被称作法线方向)以及一些用来映射这个顶点的纹理的坐标(被称作UV数据)。
模型的渲染不能没有材质(Material),材质是对Shader以及属性的一层包装。因此,不同的材质可以有相同的Shader,但是对Shader上的属性设置不同的值。
Shader的组成结构
Unity提供了两种不同类型的Shader:表面Shader和顶点片段Shader。其实还存在第三种Shader,固定管线功能Shader,但是它已经是过时的Shader并且在这里我们不会对它进行阐述。不管是哪种Shader,它们的组成结构都是相同的:
你可以有多个SubShader程序段,一个接着另一个。它们包含了在GPU上执行的实际指令。Unity将会尝试顺序地执行它们,直到找到能在你显卡运行,即与你显卡兼容的SubShader程序段。当为不同的平台编写代码时这将会是很有用的,因为平台可以适应一个Shader文件中相同Shader的不同版本。
The properties
Shader中的属性在某种程度上和C#脚本中的公有字段是一样的,它们将会在材质的Inspector面板上显示,你可以对这些属性值进行调节。但Shader的属性与C#脚本的公有字段又有一些不同:当在游戏运行时对一个材质的属性值进行改变将会是永久的,即使停止运行游戏,你会发现这种改变将会一直保存在你的材质中。
下面的程序段展示了你将会在一个Shader中用到的所有基本属性类型:
用于_MyTexture和_MyNormalMap的2D类型定义了这些参数是纹理贴图。它们可以被初始化为white, black 或者 gray。你也可以用Bump属性类型定义一个用于法线贴图的纹理。在这种情况下,它被自动地初始化为颜色值 #808080,表示根本没有法线贴图。Vector和Color属性类型总是有四个元素(分别是XYZW和RGBA)。
下面的图片展示了一旦Shader被赋给一个材质,这些属性在Inspector面板是如何显示的:
不幸的是,这样使用属性是不够的。这些属性实际上是被Unity用来从Inspector面板访问Shader中对应的隐藏变量。这些变量仍然需要在Shader的实体程序中被定义,这个实体程序段被包含在SubShader的程序段中。
对于纹理贴图所使用的类型是sampler2D,向量是使用了32bits的 float4类型,颜色一般是16 bits 的half4类型。编写Shader的程序语言Cg / HLSL中:变量名字必须和前面属性中定义的名字一致,但是类型不必一致。将_MyRange的声明从float类型替换为half类型,你将不会的到任何的错误提示。令人困惑的是如果你定义了一个Vector类型的属性,但是它和一个float2类型的变量进行连接,向量额外的两个元素将会被Unity忽略。
渲染顺序
我们已经提到,SubShader的程序段部分包含了Shader中实际可执行的程序,它是由和C语言很像的Cg / HLSL语言所编写。从广义上来说,Shader的主体程序为你图片的每个像素所执行;性能表现在这里来说是至关重要的。由于GPU的硬件结构,您可以在Shader中执行的指令数量是有限制的。通过多个Pass通道中分开进行计算可以避免这个问题,但是在本教程中对此不会进行讲解。
一个Shader的主体结构,通常是这样的:
实际的Cg代码包含在由CGPROGRAM和ENDCG指令表示的部分中。
在实际的主体代码之前,标签的概念已经被介绍过了。标签(Tag)用来告诉Unity3D我们写的shader的某些属性。例如,shader渲染的顺序(Queue)和应该如何被渲染(RenderType)。
当显然三角形时,GPU通常会根据它们与摄像机的距离,对它们进行排序,以便距离远一些的三角形被最先绘制。这种方法在渲染固态几何体时一般来说是足够了的,但是在渲染透明物体时会失败。这也是Unity为什么会允许指定队列(Queue)标签去控制每种材质的渲染顺序。。Queue接受正整数(越小越先绘制),也可以使用:
当渲染三角形时,GPU通常根据它们离摄像机的距离,远的先绘制。这在渲染不透明的几何形状时够用了,但是透明物体将失败。这也是为什么Unity3D运行指定Queue标签,可以控制每个材质的渲染顺序。Queue接受正整数(越小越先绘制),预定义(mnemonic)的标签也可以使用:
* Background (1000): 被用于背景和天空盒;
* Geometry (2000): 默认的标签,被用于大多数的几何物体;
* Transparent (3000): 被用于有透明属性的材质,例如玻璃,火焰,粒子和水;
* Overlay (4000): 被用于像lens flares这样的效果, GUI 元素以及文本显示.
Unity也允许指定相对的顺序,例如Background+2,定义了一个值为1002的渲染队列。搞乱了Queue值会导致恶劣的情况,一个对象总是被绘制,即使它应该被其他模型遮挡住了。
Z深度测试
记住,一个透明属性的物体不一定总是显示在一个固态几何属性的物体之上。默认情况下,GPU执行了一种叫做ZTest的测试,它能够停止绘制被遮挡住的像素。原理是,它使用了一个额外的缓冲区,其大小与渲染屏幕的缓存相同。每个像素包含绘制对象在该像素的深度(离相机的距离)。如果我们要绘制一个像素比当前深度更大,像素就被丢弃。ZTest剪裁被其它对象遮挡住的像素,无论他们绘制到屏幕上的顺序。
表面Shader VS 顶点片段Shader
表面Shader
当材质需要根据光照模拟实际效果时,那么你就需要一个表面shader。表面shader在函数surf中隐藏光线如何被反射的计算,并允许指定“直观”的属性,如反照率、法线、反射率等。然后将这些值插入到光照模型,将输出每个像素的最终RGB值。或者,当需要非常高级的效果时,你也可以编写自己的光照模型
一个典型的表面shader的Cg代码如下所示:
在这个例子中,通过sampler2D _MainTex这一行指定了输入的纹理;之后在这个surf函数中被用于设置材质的反射率属性(Albedo)。
Shader使用了一种叫做Lambertian光照模型,它是一种模拟光如何在一个对象物体上反射的光照模型。Shader只使用了反射率属性(albedo)通常被称作漫反射
顶点片段Shader
顶点片元Shader的工作方式和GPU渲染三角形很接近,并且没有光如何表现的内置概念。模型的几何结构首先会通过一个叫做vert的函数,这个函数可以改变模型的顶点。之后,各个三角形会通过另一个被叫做frag的函数,这个函数决定了每一个像素的最终RGB颜色。顶点片元Shader用于渲染对于表面Shader来说,过于复杂的渲染效果,例如2D效果,后期处理以及3D特效。
下面的顶点片元Shader将一个对象物体简单地变为均匀的红色,并且没有光照效果:
vert函数将顶点从它的局部坐标系转化为最终屏幕上的2D坐标。Unity引进了UNITY_MATRIX_MVP来隐藏背后复杂的数学运算。在这之后,frag函数返回值,给每一个像素一个红色的颜色值。要记住和表面Shader不一样的是,顶点片段shader的Cg部分需要写在Pass块中。
总结
本教程简单地介绍了Unity中两种类型的Shader,并且阐述了什么情况下该具体使用哪一种类型的Shader。