Unity3D着色器简介
发表于2015-07-28
第一部分
我们可以肯定地说unity3D已经使游戏开发更容易上手了。但毫无疑问着色器编程对很多人来说还是有难度。着色器是一段专门为在GPU上运行而编写的程序,但它却常常披着神秘的面纱。归根结底地说它就是将你的3D模型的三角形画出来的程序。如果你想让你的游戏看起来与众不同,学习如何编写着色器是很重要的。unity3d也将着色器用于后期处理,因此对2D游戏来说着色器同样也很重要。本系列教程将循序渐进的介绍着色器编程,目标读者为对着色器知之甚少或毫无概念的开发者们。
介绍
下图粗略展示了在Unity3D渲染工作流中扮演同一角色的三个不同的实体:
3D模型本质上来说就是一个叫做顶点的3D坐标的集合。它们连接起来构成了三角形。每个顶点可以包含一些其它信息,如颜色、它的指向(叫做法线)和一些用来将纹理映射到它自身的坐标(叫做UV数据)等。
没有材质的模型不能被渲染。材质包含一个着色器及其属性值。因此不同的材质可以共享相同的着色器,但可以使用不同的数据。
着色器剖析
Unity3d支持两种不同种类的着色器:表面着色器以及片段顶点着色器。其实有第三种——固定函数着色器,但是他们现在已经被废弃了而且本系列的教程不会介绍。不管哪种着色器满足你的需求,所有着色器的构造都是差不多的:
[C#] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | Shader "MyShader" { Properties { // The properties of your shaders // - textures // - colours // - parameters // ... } SubShader { // The code of your shaders // - surface shader // OR // - vertex and fragment shader // OR // - fixed function shader } } |
你可以有多个SubShader,它们一个接着一个。它们包含了操作GPU的真实指令。Unity3d会试着按照顺序执行它们,直到GPU找到一个与你的显卡相匹配程序才开始真正的执行。因为你可以在同一个文件中用不同类型的着色器适配不同类型的平台,所以这个特性非常有用。
属性
着色器的属性在某种程度上类似于C#脚本中的公共字段,他们将会在你材质的检视面板中显示,你可以改变他们。与脚本不同,材质属于资源:当游戏运行时对材质在编辑器中的改变是永久性的。即使当游戏停止之后,你将会发现你对材质的改变还存留在材质中。
下面的这一小段程序包含了着色器中所有属性的基本类型:
[C#] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 | Properties { _MyTexture ( "My texture" , 2D) = "white" {} _MyNormalMap ( "My normal map" , 2D) = "bump" {} // Grey _MyInt ( "My integer" , Int) = 2 _MyFloat ( "My float" , Float) = 1.5 _MyRange ( "My range" , Range(0.0, 1.0)) = 0.5 _MyColor ( "My colour" , Color) = (1, 0, 0, 1) // (R, G, B, A) _MyVector ( "My Vector4" , Vector) = (0, 0, 0, 0) // (x, y, z, w) } |
2D类型,用于3、4行,表示这个参数是纹理。他们可以被初始化为白色、黑色或灰色。你也可以用bump来说明这个纹理将会被用来当做法线贴图。在这种情况下,它的颜色将自动初始化为#808080,这种颜色用来说明完全没有凹凸。向量和颜色通常有4个元素(分别是XYZW和RGBA)。
上面的图片说明了当着色器被附着到一个材质中时,这些属性在检视视图中是以什么样子呈现的。
遗憾的是,以上这些知识对使用我们的属性是完全不够的。属性部分其实就是unit3d用来提供一个从检视面板接触到着色器内部隐藏属性的方法。在着色器的真实代码部分也就是子着色器部分,这些属性将会被定义。
[C#] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | SubShader { // Code of the shader // ... sampler2D _MyTexture; sampler2D _MyNormalMap; int _MyInt; float _MyFloat; float _MyRange; half4 _MyColor; float4 _MyVector; // Code of the shader // ... } |
纹理使用的类型是sampler2D,向量是32位的float4,颜色通常是16位的half4。用于书写着色器的语言(Cg/HLSL)是非常迂腐的:变量的名字必须与前面定义的名字完全相同。但是类型却不必相同:你将_MyRange声明为一个half而不是float不会报错。更令人疑惑不解的是如下事实:如果你能够定义一个向量类型的属性,但是此属性与一个float2变量关联,那么多出来的两个值将会被unity3d忽略。
渲染顺序
正如前所提到的,SubShader包含了这个着色器的真实代码,是由很接近C语言的Cg / HLSL语言写成的。不严格地说,着色器主体部分在图像的每一个像素都会执行一次,因此着色器对的性能要求很严格。由于GPU结构的限制,每个着色器中可以执行的指令数目是有上限的。虽然可以通过将计算划分到几个通道中分别执行避免这个限制,但本教程不会介绍该技术。
一个着色器的程序体如下面这样:
[C#] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 | SubShader { Tags { "Queue" = "Geometry" "RenderType" = "Opaque" } CGPROGRAM // Cg / HLSL code of the shader // ... ENDCG } |
4-14行包含了真正的CG代码,此部分由CGPROGRAM和ENDCG指令标记。
在真正的主体之前的第三行引入了标签的概念。标签是一种告诉Unity3d我们写的着色器有哪些属性的方式。例如,渲染顺序(Queue)和它将如何被渲染(RenderType)。
当渲染三角形时,GPU通常将他们按照到摄像机的距离分类,因此距离摄像机近的三角形将被先绘制。这种方法通常在渲染固体几何形状时显得游刃有余,但是对透明的物体渲染却显得力不从心。这就是为什么unity3d允许指定标签Queue来控制每种材质的渲染顺序了。Queue接受正整数(数字越小,绘制的越快);同时可以使用如下记忆标签:
Background (1000): used for backgrounds andskyboxes,Background (1000):用来绘制背景和天空盒子Geometry (2000): the default label used formost solid objects,Geometry(2000):用来绘制固体物体的默认标签Transparent (3000): used for materials withtransparent properties, such glass, fire, particles and water;Transparent (3000): 用来绘制有透明属性的材质,例如玻璃、火焰、粒子和水Overlay (4000): used for effects such aslens flares, GUI elements and texts.Overlay (4000):用来绘制特效,例如镜头耀斑、GUI元素和文字
unity3d同时也允许指定相对顺序,例如Background+2也就是1002。弄乱绘制顺序会产生如下异常情况:即使一个物体应该被其他的模型遮挡也会被绘制出来。
ZTest
然而记住很重要的一点:透明的对象不是必须显示在几何对象之上的。GPU会默认执行深度测试ZTest,它可以避免绘制隐藏的像素。GPU使用了一块与它正在渲染的屏幕尺寸大相同的内存来完成ZTest。每一个像素包含了它所在的被绘制对象的深度(与摄像机的距离)。如果我们要绘制一个比当前深度大的像素,那么这个像素将被忽略。无论像素的深度是多少,一旦它被其他对象挡住,ZTest会将它剔除。
表面&顶点和片段
最后要说的部分就是着色器的真正代码了。在讲之前,我们要先决定用哪种类型的着色器。本节将会粗略展示着色器是什么样子的,但是不会解释。表面、顶点和片断着色器会在后续教程中深入讲解。
表面着色器
当你想模拟的材质会被光以一种真实的方式影响时,你就需要一个表面着色器了。表面着色器将光如何被反射的计算隐藏起来,并且可以在surf函数中指定类似反射率、法线、反射强度等等直观属性。这些值会被用于一个光照模型中,然后输出每个像素的最终RGB值。或者你也可以编写自己的光照模型,但只有非常高级的效果才有这种需求。
一个典型的表面着色器的Cg代码如下:
[C#] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | CGPROGRAM //使用兰伯特光照模型 #pragma surface surf Lambert sampler2D _MainTex; // 输入纹理 struct Input { float2 uv_MainTex; }; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; } ENDCG |
第5行输入了一个纹理,然后这个纹理在第12行被设置成了材质的Albedo属性。这个着色器使用了Lambertian光照模型(第三行)。Lambertian光照模型是一个非常经典的模型,它可以用来模拟光在一个物体上反射的情况。只用albedo属性的着色器通常被称为漫反射着色器。
顶点和片段着色器
顶点和片段着色器像GPU渲染三角形一样工作,而且没有内置的关于光如何表现的概念。模型的几何体首先通过一个叫做vert的函数改变它的顶点。然后每个三角形将会通过frag函数决定每个像素的RGB值。他们对2D特效、后期处理和特殊的3D效果很有用,而这些领域如果用表面着色器实现将非常复杂。
如下的顶点和片段着色器简单地将一个对象变成了统一的红色,而且没有光照:
[C#] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag struct vertInput { float4 pos : POSITION; }; struct vertOutput { float4 pos : SV_POSITION; }; vertOutput vert(vertInput input) { vertOutput o; o.pos = mul(UNITY_MATRIX_MVP, input.pos); return o; } half4 frag(vertOutput output) : COLOR { return half4(1.0, 0.0, 0.0, 1.0); } ENDCG } |
15-17行将顶点从他们原来的3D空间转换到了最终的屏幕上的2D坐标。Unity3d引入了UNITY_MATRIX_MVP的概念将转换的数学过程隐藏了起来。紧接着,在第22行给每一个像素赋值为红色。记住顶点和片段着色器的Cg部分需要放在Pass中。与简单的表面着色器不同,表面着色器在不在Pass中都可以。
结论
本教程简单地介绍了两种在unit3d中可用的着色器然后分别解释了何时使用它们。接下来将会有四篇教程解释如何实现这些着色器。另外还有一篇教程将会介绍屏幕着色器,它可以用于2D图像的后期处理。