首页 > 代码库 > 【Unity Shaders】Vertex & Fragment Shader入门

【Unity Shaders】Vertex & Fragment Shader入门


写在前面


三个月以前,在一篇讲卡通风格的Shader的最后,我们说到在Surface Shader中实现描边效果的弊端,也就是只对表面平缓的模型有效。这是因为我们是依赖法线和视角的点乘结果来进行描边判断的,因此,对于那些平整的表面,它们的法线通常是一个常量或者会发生突变(例如立方体的每个面),这样就会导致最后的效果并非如我们所愿。如下图所示:



因此,我们有一个更好的方法来实现描边效果,也就是通过两个pass进行渲染——首先渲染对象的背面,用黑色略微向外扩展一点,就是我们的描边效果;然后正常渲染正面即可。而我们应该知道,surface shader是不可以使用pass的。


如果我们想要使用上述方法实现描边,我们就需要写另一种shader——fragment shader。和surface shader相比,这种shader需要我们编写更多的代码,处理更多的事情,但也可以让我们更加了解shader是如何工作的。而之前的一篇文章也分析过,其实surface shader的背后也是生成了对应的vertex&fragment shader。


这篇文章主要参考了Unity Gems里的一篇文章,但正如文章评论里所说,有些技术比如求attenuation稳重方法已经“过时”,因此本文会对这类问题以及一些作者没有说清的问题给予说明。在查资料的时候,发现由于Unity背后做了太多事,定义了很多变量、函数和宏,而又没有给出详尽的使用说明,写起来实在太头大了。。。同样,本篇内容仅供参考。



Vertex & Fragment Shaders


Vertex & Fragment Shaders的工作流程如下图所示(简略版,来自Unity Gems):


所以,看起来也没那么难啦~我们只需要编写两个函数就可以喽~


我们来分析下它的流程。首先,vertex program收到系统传递给它的模型数据,然后把这些处理成我们后续需要的数据(但至少要包含这些顶点的位置信息)进行输出。其他的输出数据比如有,纹理的UV坐标以及其他需要传递给fragment program的数据。然后,系统对vertex program输出的顶点数据进行插值,并将插值结果传递给fragment program。最后,fragment program根据这些插值结果计算最后屏幕上的像素颜色。


在本篇文章,我们首先会学习编写一个简单的diffuse & diffuse bumped shader。然后再来具体看如何编写一个具有多个passes的shader。



Diffuse, Vertex Lit Fragment Shader


开始的开始,我们首先需要在SubShader中使用Pass {}关键字定义一个pass。一个Pass可以为该阶段定义一系列的tags。例如,我们可以剔除(Cull)背面或者正面,控制是否写入Z buffer等。我们的diffuser shader将会剔除背面。具体可见官网。


下面是我们的Pass定义:

		Pass {
			Tags { "LightMode" = "Vertex" }
			Cull Back
			Lighting OnCGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#pragma multi_compile_fwdbase
			
			#include "UnityCG.cginc"
			
			// More code here
			
			ENDCG
		}

在上面的代码里,我们定义了一个pass,设定LightMode为Vertex,告诉它打开光源并且剔除背面。然后,我们定义了CG程序的开头部分,指定了vertex和fragment programs的名字。最后,我们包含了Unity定义的一个文件,以便在后面的CG程序中可以使用某些函数和变量。


LightMode是个非常重要的选项,因为它将决定该pass中光源的各变量的值。如果一个pass没有指定任何LightMode tag,那么我们就会得到上一个对象残留下来的光照值,这并不是我们想要的。其他各个LightMode的具体含义可以参见官网(很重要,一定要去看,特别是对于每个Pass的细节解释,一定要点进去看!!!),这里做一个简单的解释。

  • LightMode=Vertex:会设置4个光源,并按亮度从明到暗进行排序,它们的值会存储在unity_LightColor[n], unity_LightPosition[n], unity_LightAtten[n]这些数组中。因此,[0]总会得到最亮的光源。

  • LightMode=ForwardBase: _LightColor0将会是主要的directional light的颜色。

  • LightMode=ForwardAdd:和上面一样, _LightColor0将是该逐像素光源的颜色。



Vertex Lit是什么


在我们写shader的时候有很多选择——我们可以定义多个passes,其中每一个pass处理一个光源,这样来处理所有的光源;或者我们选择逐顶点处理所有的光源(在一个pass里处理掉),然后再对它们进行插值。很明显,后面这种方式会快很多,因为它仅仅需要一个pass就可以了,而前一个方式需要更多的passes。


如果我们写了一个Vertex Lit shader,那么我们就会按照第二种方式那样,一次考虑所有的光源对顶点的影响。如果我们写了一个多passes的shader,那么它就会被多次调用,每次针对一个光源,考虑该光源对模型的影响。


对于Vertex Lit,Unity已经为我们编写了一些辅助函数,我们会在后面看到。



The Vertex Program


下面,我们正式开始编写代码。首先,我们需要定义vertex program。而它需要得到模型的相关信息作为输入,因此,我们定义下面的结构:

			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};

这个结构定义依赖某些语法,即那些“:XXX”样子的值。我们的变量叫什么并不重要,但这些“:XXX”语法则说明系统将使用哪些值去填充它们。这里,我们通过上述代码可以得到了model space中的顶点位置、法线方向以及纹理坐标。


在fragment shaders里,空间(spaces)的概念是非常重要的。空间重要是指坐标的相对位置。

  • 在model space中,坐标是相对于网格的原点(0,0,0)定义的。我们的vertex function需要把这些坐标转换到projection space中,即相对于摄像机的、真正被渲染的地方。
  • 在tangent space中,坐标是相对于模型的正面定义的——在处理法线纹理时我们使用这个space,这在后面会具体讲到。
  • 在world space中,坐标是相对于世界的原点(0,0,0)定义的。 
  • 在projection space中,坐标是相对于摄像机定义的,因此在这个space中,摄像机的位置就是(0,0,0)。
如果你读过一些关于shaders的文章,那么你大概会见过关于选择哪个space来照亮模型的理论。初学者往往会有点困惑,这实际上就是选择你要把光源方向、位置等数据转换到哪个坐标系中来进行相关运算,得到最终的像素值。希望在本篇的最后,你可以明白这些问题!

那么,在定义了vertex program的输入后,我们还需要定义它的输出。之前我们说过,vertex program的输出将会被插值用于生成像素,而这些插值后的值就是fragment program的输入。

			struct v2f {
				float4 pos : POSITION;
				float2 uv : TEXCOORD0;
				float3 color : TEXCOORD1;
			};

上面就是我们的输出。在这里,之前所说的语义就没有那么重要了——只有一个是必须的,即用POSITION标识的变量,这是把顶点坐标转换到projection space后的位置。我们输出的所有值(并且没有uniform限定词)都将在fragment program之前被插值。


注意:但对于DX11和Xbox360来说,必须要有语义说明,否则会报错。即需要为变量指定TEXCOORD1等位置。


出于性能的考虑,很显然我们应该尽可能在vertex function里进行更多的运算,这是因为vertex function是逐顶点调用的,而fragment function则是逐像素调用的。

下面是真正的vertex function,它把输入a2v转换成输出v2f(也是fragment function的输入)。

			v2f vert(a2v v) {
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.color = ShadeVertexLights(v.vertex, v.normal);		
				return o;
			}

第一行,我们定义了输出v2f的一个实例。然后把顶点的位置和Unity提前定义的一个矩阵UNITY_MATRIX_MVP(在UnityShaderVariables.cginc里定义)相乘,从而把顶点位置从model space转换到projection space。我们使用了矩阵乘法操作mul来执行这个步骤。


第二行,我们为给定的纹理计算其uv坐标,即根据mesh上的uv坐标来计算真正的纹理上对应的位置。我们使用了Unity.CG.cginc中的宏TRANSFORM_TEX来实现。


注意,要使用宏TRANSFORM_TEX,我们需要在shader中定义一些额外的变量,即必须定义一个名为_YourTextureName_ST (也就是你的纹理的名字加一个 _ST后缀)。这是因为宏TRANSFORM_TEX的定义为:#define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)。这是因为我们的纹理有Tiling和Offset参数,如下图中面板所示,因此需要对原mesh上的uv进行相应调整才能得到真正的纹理坐标。
最后,我们计算得到顶点的初始颜色——即光源对该顶点的影响。在我们的第一个shader中,我们使用一个名为ShadeVertexLights的函数,它的输入为模型的顶点和法线。这是一个内置的函数,它将考虑4个距离最近(若距离相等则按光源类型排序)的光源以及一个环境光(在Edit->Render Settings->Ambient Light里设置)。它的实现可以在UnityCG.cginc里找到。其他辅助函数可以详见官网。



The Fragment Shader


根据上述过程,系统会在每个顶点上调用vertex program,并将其输出在同一个几何图元上进行插值。下面,我们根据这些插值后的值来得到对应的像素值。下面是真正的fragment program:

			float4 frag(v2f i) : COLOR {
				float4 c = tex2D(_MainTex, i.uv);
				c.rgb = c.rgb * i.color * 2;
				return c;
			}

上述代码使用了surface shader中也很常见的纹理采样操作,来得到对应的纹理像素值。然后,将该纹理颜色和插值后的vertex function输出的顶点光颜色进行相乘,并把结果乘以2(否则颜色会太暗。)。最后,返回得到的像素值。



完整代码


最后,完整的Vertex Lit Diffuse代码如下:

Shader "Custom/VertexLit" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
 		LOD 300
 		
 		Pass {
			Tags { "LightMode" = "Vertex" }
			
			Cull Back
			Lighting On
			
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
			
			sampler _MainTex;
			float4 _MainTex_ST;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : POSITION;
				float2 uv : TEXCOORD0;
				float3 color : TEXCOORD1;
			};
			
			v2f vert(a2v v) {
				v2f o;o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.color = ShadeVertexLights(v.vertex, v.normal);
				return o;
			}
			
			float4 frag(v2f i) : COLOR {
				float4 c = tex2D(_MainTex, i.uv);
				c.rgb = c.rgb * i.color * 2;
				return c;
			}
			
			ENDCG
		}
	} 
	FallBack "Diffuse"
}

这样,我们就完成了第一个vertex & fragment shader。上述效果如果用surface shader可能只要几句话,但你渐渐会发现,虽然使用vertex & fragment shader会增加更多的代码量,但它能做的真是太多了!


上述shader的效果如下(啦啦啦,又是小苹果+呆萌小怪兽的组合~~~):




Diffuse Normal Map Shader


下面我们要向shader添加一个非常常见的法线纹理(Normal Texture)。



Normal Maps


如果你在Unity里使用过法线纹理的话,你应该知道在使用之前,你需要先把该纹理的类型设置成Normal,对吧?那么,到底为什么要这样呢?法线纹理跟其他纹理有什么不一样呢?


法线纹理具有以下性质

  • 它存储了模型表面的法线方向。有基于model space(肉眼看起来颜色比较丰富,有红色蓝色等)和基于tangent space(通常都是蓝色的)的两种法线纹理,而Unity常见的是后面一种法线纹理。

  • 由于法线向量中每一维的范围在(-1,1),因此我们需要把它重新映射到(0,255)。具体做法是把原值除以2再偏移0.5,最后乘以255。

  • 在存储的时候是压缩存储。因为法线纹理都是被正则化的,即是单位向量,模为1,所以实际上只需要存储该向量的两个维度就可以了,第三维可以用前两个推导出来。

  • 由于上一点,每一个维度占用16 bits,即每个rgba包含了两个维度的值。


当使用法线纹理的时候,我们需要在tangent space中处理光照对模型的影响。也就是说,我们需要把和计算光照对像素的影响的数据都转换到tangent space中,然后在这个坐标系中计算得到最终的颜色。而且,在这里我们实际上是计算了逐像素的光照,而不是像前一个shader那样是逐顶点的。


我们选择在tangent space计算光照是因为这种做法的计算量更少。我们只需要基于每个顶点,把光照信息(有时还需要观察点信息等)转换到tangent space,再对其进行插值即可。而另一种方式是在world space中处理光照,这意味着我们需要把法线纹理中的每一个法线转换到world space中,因此我们需要基于每个像素进行处理。和逐顶点的处理方式相比,这种方法显然需要更多的计算。
在Unity里转换到tangent space是比较容易的。下面,我们不会使用逐顶点的光照处理函数ShadeVertexLights,而是逐像素的处理光照。



照亮我们的模型


下面,我们将使用Lambert光照模型,也就是法线*光照方向*衰减*2。

在我们把需要的数据都转换到tangent space后,处理光照就变得非常简单了。可以用下图(来源:Unity Gems)来演示这样一个过程:



但是,光源在哪里呢?


Unity为我们提供了那些对模型有影响的光源(按重要度排序,例如距离远近、光照类型等)的位置、颜色和衰减等信息。


Unity使用了三个数据来定义顶点光源:unity_LightPosition,unity_LightAtten和unity_LightColor。例如[0]表示最重要的光源。


当我们编写一个multi-pass的光照模型(正如我们下面写的那样)时,我们只需要一次处理一个单独的光源,这种情况下,Unity同样定义了一个名为_WorldSpaceLightPos0的值,来帮助我们得到它的位置,并且还提供了一个非常有用的函数ObjSpaceLightDir,它可以计算得到该光源的方向。而为了得到该光源的颜色,我们可以在程序中包含“Lighting.cginc”文件,然后使用_LightColor0进行访问。



Forward Lighting(而非Vertex Lit)


在第一个shader里我们使用了vertex lights,而现在,我们来看下怎么为光源定义多个passes。那么,开始吧!


首先,我们需要更改Tags中的LightMode,让其值为ForwardBase,来让Unity我们设置光源数据。

		Pass {
			Tags { "LightMode" = "ForwardBase" }

然后,我们还需要添加#pragma指令:

#pragma multi_compile_fwdbase

这都是为了能让Unity各种内置数据、宏定义等可以正常工作。真的是很头大啊,至今官方也没有给出详细的参考资料。。。(Rant!!!)


然后,为了使用法线纹理我们需要定义两个变量,一个是名为_XXX的sampler2D变量,一个是名为_XXX_ST的float4变量(当然你还需要在Properties中定义一个名为_XXX的新属性)。


现在我们需要为vertex program定义新的输入:

			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
				float4 tangent : TANGENT;
			};

这里我们添加了一个新的变量,其语义是:TANGENT。我们会在把光源方向转换到tangent space中时需要这个变量。



Tangent Space转换


为了把向量从object space转换到tangent space,我们需要为顶点定义另外两个向量。通常对一个顶点来说,我们知道它的法线normal,而其中一个向量tangent是和normal正交的,另一个向量binormal则是normal和tangent的叉乘结果。有了这三个向量,我们就可以定义一个矩阵来执行到tangent space的转换。


幸运地是,UnityCG.cginc里定义了一个名为TANGENT_SPACE_ROTATION的宏,它提供了一个名为rotation的矩阵来把object space下的坐标转换到tangent space中。



Vertex到Fragment Programs的输出


在知道转换的方法后,我们需要在vertex function里计算tangent space下的光源方向,然后对其进行插值后传递给fragment function。因此,我们需要在vertex function的输出里添加新的变量——光源方向。

			struct v2f {
				float4 pos : POSITION;
				float2 uv : TEXCOORD0;
				float2 uv2 : TEXCOORD1;
				float3 lightDirection : TEXCOORD2;
				LIGHTING_COORDS(3,4)
			};


lightDirection将会存储插值后的光源方向向量。uv2将会存储法线纹理的纹理坐标。最后的LIGHTING_COORDS(3,4)是在AutoLight.cginc里定义的宏,它负责创建光源坐标,用于某些内置的光照计算。在下面计算光源的attenuation时,我们会需要这些值。



该shader只对directional lights和point lights有效。本例中我们没有考虑spotlight的角度。


The Vertex Program


			v2f vert(a2v v) {
				v2f o;
				
				TANGENT_SPACE_ROTATION;
				o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.uv2 = TRANSFORM_TEX(v.texcoord, _MainTex);
				
				TRANSFER_VERTEX_TO_FRAGMENT(o);
				return o;
			}

在vertex program里,我们使用了宏TANGENT_SPACE_ROTATION(在UnityCG.cginc里定义)来创建一个名为rotation的矩阵,并使用它把object space转换到tangent space中。


为了让这个宏能够正确处理我们的输入,vertex program的输入必须是一个名为v的结构体,并且它包含了一个名为normal的法线以及一个名为tangent的切线。这都是因为它的宏定义里指明了变量的名字的缘故。
然后,我们使用内置函数ObjSpaceLightDir(v.vertex)计算了在object space中光源(这时指的就是最重要的那个光源)的方向。随后,我们再把结果和新的rotation矩阵相乘,从而把方向从object space又转换到了tangent space。


下面几行,我们计算得到顶点在projection space中的位置以及纹理的uv坐标。


最后,我们使用了名为的TRANSFER_VERTEX_TO_FRAGMENT宏,它同样在AutoLight.cginc里定义,和上面v2f中的宏LIGHTING_COORDS协同工作,它会根据该pass处理的光源类型(spot?point?or directional?)来计算光源坐标的具体值,以及进行和shadow相关的计算等。



Directional和Point Lights


Unity把光源的位置存储在float4类型的_WorldSpaceLightPos0里,即_WorldSpaceLightPos0包含了4个元素。如果这个光源是directional,那么xyz就是这个光源的方向,而w(即最后一个元素)则是0;如果这时一个point light,那么xyz将表示光源的位置,而w则是1。那么,这些有什么影响呢?


这其实方便了ObjSpaceLightDir函数的计算过程。它首先将顶点的位置乘以光源位置的w元素,然后再用光源位置减去顶点的位置,来得到光源方向。因此,如果是一个directional light,我们相乘后就会得到0,即返回光源的xyz值(实际上就是光源的方向);如果是一个point light,我们就会得到顶点到光源的一个方向向量。



The Fragment Function


			float4 frag(v2f i) : COLOR {
				float4 c = tex2D(_MainTex, i.uv);
				float3 n = UnpackNormal(tex2D(_BumpTex, i.uv2));
				
				float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;
				
				float atten = LIGHT_ATTENUATION(i);
				
				// Angle to the light
				float diff = saturate(dot(n, normalize(i.lightDirection)));
				lightColor += _LightColor0.rgb * (diff * atten);
				
				c.rgb = lightColor * c.rgb * 2;

				return c;
			}

在fragment function里,我们首先从法线纹理里解压出法线。然后,我们使用Unity设置的环境光作为初始颜色值。随后,我们计算了衰减值,即光源距离的远近。这里,我们同样使用了AutoLight.cginc里的宏,即LIGHT_ATTENUATION,它同样会判断该pass处理的光源类型,然后得到光源的衰减率。


然后,我们把法线和光源方向进行点乘得到漫反射值,再和光源颜色以及衰减值结合起来,叠加到像素值上。为了得到光源的颜色,我们使用了_LightColor0——这需要我们在shader中包含“Lighting.cginc”文件。或者,我们也可以在shader中定义一个名为_LightColor0的变量,Unity会自行填充它的值。

uniform float4 _LightColor0;


完整代码


最后完整的代码如下:

Shader "Custom/DiffuseNormal" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_BumpTex ("Bump Texture", 2D) = "white" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 300
	
		Pass {
			Tags { "LightMode" = "ForwardBase" }
			
			Cull Back
			Lighting On
			
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#pragma multi_compile_fwdbase
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			
			sampler _MainTex;
			sampler _BumpTex;
			
			float4 _MainTex_ST;
			float4 _BumpTex_ST;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
				float4 tangent : TANGENT;
			};
			
			struct v2f {
				float4 pos : POSITION;
				float2 uv : TEXCOORD0;
				float2 uv2 : TEXCOORD1;
				float3 lightDirection : TEXCOORD2;
				LIGHTING_COORDS(3,4)
			};

			v2f vert(a2v v) {
				v2f o;
				
				TANGENT_SPACE_ROTATION;
				o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.uv2 = TRANSFORM_TEX(v.texcoord, _MainTex);
				
				TRANSFER_VERTEX_TO_FRAGMENT(o);
				return o;
			}
			
			float4 frag(v2f i) : COLOR {
				float4 c = tex2D(_MainTex, i.uv);
				float3 n = UnpackNormal(tex2D(_BumpTex, i.uv2));
				
				float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;
				
				float atten = LIGHT_ATTENUATION(i);
				
				// Angle to the light
				float diff = saturate(dot(n, normalize(i.lightDirection)));
				lightColor += _LightColor0.rgb * (diff * atten);
				
				c.rgb = lightColor * c.rgb * 2;

				return c;
			}
			
			ENDCG
		}
	} 
	FallBack "Diffuse"
}

Shader效果如下:




在Forward Mode中处理Multiple Lights


通过上面的学习,我们已经学会了如何处理一个光源,但仅仅是一个。要处理多光源,我们就需要编写另一个pass,并且使用新的tags来告诉Unity我们想要逐个处理光源。


这基本上只需要两步:

  • 一个pass处理第一个光源,就像我们上面做的那样

  • 然后定义更多的pass,来处理后续的光源,并把结果添加(add on)到前面的结果上

因此,我们把之前pass的代码再粘贴一遍,来创建一个新的pass,但要把tag改成:
Tags { "LightMode" = "ForwardAdd" }

并且更改#pragma指令:
#pragma multi_compile_fwdadd


然后添加一个新的命令来告诉Unity怎样混合前后两个pass的值:
Blend One One

然后,我们移除掉第二个pass对UNITY_LIGHTMODEL_AMBIENT的处理,因为我们已经在第一个pass中处理过这个值了。我们最后的代码如下:
Shader "Custom/DiffuseNormal" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_BumpTex ("Bump Texture", 2D) = "white" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 300
	
		Pass {
			Tags { "LightMode" = "ForwardBase" }
			
			Cull Back
			Lighting On
			
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#pragma multi_compile_fwdbase
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			
			sampler _MainTex;
			sampler _BumpTex;
			
			float4 _MainTex_ST;
			float4 _BumpTex_ST;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
				float4 tangent : TANGENT;
			};
			
			struct v2f {
				float4 pos : POSITION;
				float2 uv : TEXCOORD0;
				float2 uv2 : TEXCOORD1;
				float3 lightDirection : TEXCOORD2;
				LIGHTING_COORDS(3,4)
			};

			v2f vert(a2v v) {
				v2f o;
				
				TANGENT_SPACE_ROTATION;
				o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.uv2 = TRANSFORM_TEX(v.texcoord, _MainTex);
				
				TRANSFER_VERTEX_TO_FRAGMENT(o);
				return o;
			}
			
			float4 frag(v2f i) : COLOR {
				float4 c = tex2D(_MainTex, i.uv);
				float3 n = UnpackNormal(tex2D(_BumpTex, i.uv2));
				
				float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;
				
				float atten = LIGHT_ATTENUATION(i);
				
				// Angle to the light
				float diff = saturate(dot(n, normalize(i.lightDirection)));
				lightColor += _LightColor0.rgb * (diff * atten);
				
				c.rgb = lightColor * c.rgb * 2;

				return c;
			}
			
			ENDCG
		}
		
		Pass {
			Tags { "LightMode" = "ForwardAdd" }
			
			Cull Back
			Lighting On
			Blend One One
			
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#pragma multi_compile_fwdadd
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			
			sampler _MainTex;
			sampler _BumpTex;
			
			float4 _MainTex_ST;
			float4 _BumpTex_ST;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
				float4 tangent : TANGENT;
			};
			
			struct v2f {
				float4 pos : POSITION;
				float2 uv : TEXCOORD0;
				float2 uv2 : TEXCOORD1;
				float3 lightDirection : TEXCOORD2;
				LIGHTING_COORDS(3,4)
			};

			v2f vert(a2v v) {
				v2f o;
				
				TANGENT_SPACE_ROTATION;
				o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.uv2 = TRANSFORM_TEX(v.texcoord, _MainTex);
				
				TRANSFER_VERTEX_TO_FRAGMENT(o);
				return o;
			}
			
			float4 frag(v2f i) : COLOR {
				float4 c = tex2D(_MainTex, i.uv);
				float3 n = UnpackNormal(tex2D(_BumpTex, i.uv2));
				
				float3 lightColor = float3(0);
				
				float lengthSq = dot(i.lightDirection, i.lightDirection);
				float atten = LIGHT_ATTENUATION(i);
				
				// Angle to the light
				float diff = saturate(dot(n, normalize(i.lightDirection)));
				lightColor += _LightColor0.rgb * (diff * atten);
				
				c.rgb = lightColor * c.rgb * 2;

				return c;
			}
			
			ENDCG
		}
	} 
	FallBack "Diffuse"
}



我们在场景里放置两个光源——一个平行光,用于ForwardBase Pass的计算,一个Point Light,用于ForwardAdd Pass的计算。效果如下:



写在最后


本文里对处理光源attenuation的方法和Unity Gems里的方法不同,按原文里的做法在Unity 4.5(更早的版本不清楚)是无法得到正确的attenuation的,即把点光源拉进拉远不会对模型有任何影响,除非拉出了光源范围,这时会有一个不正常的明暗突变。为了找正确的方法真是麻烦啊。。。Unity关于shader的文档的确需要加强,而且在Unity里写Vertex & Fragment Shader绝对比想象中的难,有一条准则就是,如果它提供给里某些功能的函数(比如这里计算attenuation的方法,要4个步骤,#pragma multi_compile_fwdadd LIGHTING_COORDS TRANSFER_VERTEX_TO_FRAGMENT+ LIGHT_ATTENUATION),那么千万不要自己尝试去写一个函数出来。。。某些内置的变量实在是不知道它们什么时候工作、怎么工作。。。



【Unity Shaders】Vertex & Fragment Shader入门