http://blog.csdn.net/candycat1992/article/details/40212735

三个月以前,在一篇讲卡通风格的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定义:

 

  1. Pass {  
  2.     Tags { "LightMode" = "Vertex" }  
  3.     Cull Back  
  4.     Lighting OnCGPROGRAM  
  5.       
  6.     #pragma vertex vert  
  7.     #pragma fragment frag  
  8.       
  9.     #pragma multi_compile_fwdbase  
  10.       
  11.     #include "UnityCG.cginc"  
  12.       
  13.     // More code here  
  14.       
  15.     ENDCG  
  16. }  

 

 

 

在上面的代码里,我们定义了一个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。而它需要得到模型的相关信息作为输入,因此,我们定义下面的结构:

 

  1. struct a2v {  
  2.     float4 vertex : POSITION;  
  3.     float3 normal : NORMAL;  
  4.     float4 texcoord : TEXCOORD0;  
  5. };  


这个结构定义依赖某些语法,即那些“: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的输入。

 

  1. struct v2f {  
  2.     float4 pos : POSITION;  
  3.     float2 uv : TEXCOORD0;  
  4.     float3 color : TEXCOORD1;  
  5. };  


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

 

 

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

 

 

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

 

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

 

  1. v2f vert(a2v v) {  
  2.     v2f o;  
  3.     o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  4.     o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);  
  5.     o.color = ShadeVertexLights(v.vertex, v.normal);          
  6.     return o;  
  7. }  

 

第一行,我们定义了输出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:

 

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


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

 

 

 

完整代码

 

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

 

  1. Shader "Custom/VertexLit" {  
  2.     Properties {  
  3.         _MainTex ("Base (RGB)", 2D) = "white" {}  
  4.     }  
  5.     SubShader {  
  6.         Tags { "RenderType"="Opaque" }  
  7.         LOD 300  
  8.           
  9.         Pass {  
  10.             Tags { "LightMode" = "Vertex" }  
  11.               
  12.             Cull Back  
  13.             Lighting On  
  14.               
  15.             CGPROGRAM  
  16.               
  17.             #pragma vertex vert  
  18.             #pragma fragment frag  
  19.               
  20.             #include "UnityCG.cginc"  
  21.               
  22.             sampler _MainTex;  
  23.             float4 _MainTex_ST;  
  24.               
  25.             struct a2v {  
  26.                 float4 vertex : POSITION;  
  27.                 float3 normal : NORMAL;  
  28.                 float4 texcoord : TEXCOORD0;  
  29.             };  
  30.               
  31.             struct v2f {  
  32.                 float4 pos : POSITION;  
  33.                 float2 uv : TEXCOORD0;  
  34.                 float3 color : TEXCOORD1;  
  35.             };  
  36.               
  37.             v2f vert(a2v v) {  
  38.                 v2f o;o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  39.                 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);  
  40.                 o.color = ShadeVertexLights(v.vertex, v.normal);  
  41.                 return o;  
  42.             }  
  43.               
  44.             float4 frag(v2f i) : COLOR {  
  45.                 float4 c = tex2D(_MainTex, i.uv);  
  46.                 c.rgb = c.rgb * i.color * 2;  
  47.                 return c;  
  48.             }  
  49.               
  50.             ENDCG  
  51.         }  
  52.     }   
  53.     FallBack "Diffuse"  
  54. }  

 

 

 

这样,我们就完成了第一个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我们设置光源数据。

 

  1. Pass {  
  2.     Tags { "LightMode" = "ForwardBase" }  


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

 

 

  1. #pragma multi_compile_fwdbase  


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

 

 

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

 

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

 

  1. struct a2v {  
  2.     float4 vertex : POSITION;  
  3.     float3 normal : NORMAL;  
  4.     float4 texcoord : TEXCOORD0;  
  5.     float4 tangent : TANGENT;  
  6. };  


这里我们添加了一个新的变量,其语义是: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的输出里添加新的变量——光源方向。

 

  1. struct v2f {  
  2.     float4 pos : POSITION;  
  3.     float2 uv : TEXCOORD0;  
  4.     float2 uv2 : TEXCOORD1;  
  5.     float3 lightDirection : TEXCOORD2;  
  6.     LIGHTING_COORDS(3,4)  
  7. };  

 


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

 

 

 


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

 

 

The Vertex Program

 

 

 

  1. v2f vert(a2v v) {  
  2.     v2f o;  
  3.       
  4.     TANGENT_SPACE_ROTATION;  
  5.     o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));  
  6.     o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  7.     o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);  
  8.     o.uv2 = TRANSFORM_TEX(v.texcoord, _MainTex);  
  9.       
  10.     TRANSFER_VERTEX_TO_FRAGMENT(o);  
  11.     return o;  
  12. }  

 

 

在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

 

 

 

  1. float4 frag(v2f i) : COLOR {  
  2.     float4 c = tex2D(_MainTex, i.uv);  
  3.     float3 n = UnpackNormal(tex2D(_BumpTex, i.uv2));  
  4.       
  5.     float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;  
  6.       
  7.     float atten = LIGHT_ATTENUATION(i);  
  8.       
  9.     // Angle to the light  
  10.     float diff = saturate(dot(n, normalize(i.lightDirection)));  
  11.     lightColor += _LightColor0.rgb * (diff * atten);  
  12.       
  13.     c.rgb = lightColor * c.rgb * 2;  
  14.   
  15.     return c;  
  16. }  

 

 

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

 

 

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

 

  1. uniform float4 _LightColor0;  



 

完整代码

 

最后完整的代码如下:

 

  1. Shader "Custom/DiffuseNormal" {  
  2.     Properties {  
  3.         _MainTex ("Base (RGB)", 2D) = "white" {}  
  4.         _BumpTex ("Bump Texture", 2D) = "white" {}  
  5.     }  
  6.     SubShader {  
  7.         Tags { "RenderType"="Opaque" }  
  8.         LOD 300  
  9.       
  10.         Pass {  
  11.             Tags { "LightMode" = "ForwardBase" }  
  12.               
  13.             Cull Back  
  14.             Lighting On  
  15.               
  16.             CGPROGRAM  
  17.               
  18.             #pragma vertex vert  
  19.             #pragma fragment frag  
  20.               
  21.             #pragma multi_compile_fwdbase  
  22.               
  23.             #include "UnityCG.cginc"  
  24.             #include "Lighting.cginc"  
  25.             #include "AutoLight.cginc"  
  26.               
  27.             sampler _MainTex;  
  28.             sampler _BumpTex;  
  29.               
  30.             float4 _MainTex_ST;  
  31.             float4 _BumpTex_ST;  
  32.               
  33.             struct a2v {  
  34.                 float4 vertex : POSITION;  
  35.                 float3 normal : NORMAL;  
  36.                 float4 texcoord : TEXCOORD0;  
  37.                 float4 tangent : TANGENT;  
  38.             };  
  39.               
  40.             struct v2f {  
  41.                 float4 pos : POSITION;  
  42.                 float2 uv : TEXCOORD0;  
  43.                 float2 uv2 : TEXCOORD1;  
  44.                 float3 lightDirection : TEXCOORD2;  
  45.                 LIGHTING_COORDS(3,4)  
  46.             };  
  47.   
  48.             v2f vert(a2v v) {  
  49.                 v2f o;  
  50.                   
  51.                 TANGENT_SPACE_ROTATION;  
  52.                 o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));  
  53.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  54.                 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);  
  55.                 o.uv2 = TRANSFORM_TEX(v.texcoord, _MainTex);  
  56.                   
  57.                 TRANSFER_VERTEX_TO_FRAGMENT(o);  
  58.                 return o;  
  59.             }  
  60.               
  61.             float4 frag(v2f i) : COLOR {  
  62.                 float4 c = tex2D(_MainTex, i.uv);  
  63.                 float3 n = UnpackNormal(tex2D(_BumpTex, i.uv2));  
  64.                   
  65.                 float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;  
  66.                   
  67.                 float atten = LIGHT_ATTENUATION(i);  
  68.                   
  69.                 // Angle to the light  
  70.                 float diff = saturate(dot(n, normalize(i.lightDirection)));  
  71.                 lightColor += _LightColor0.rgb * (diff * atten);  
  72.                   
  73.                 c.rgb = lightColor * c.rgb * 2;  
  74.   
  75.                 return c;  
  76.             }  
  77.               
  78.             ENDCG  
  79.         }  
  80.     }   
  81.     FallBack "Diffuse"  
  82. }  

 

 

 

Shader效果如下:

 

 

 

在Forward Mode中处理Multiple Lights

 

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

 

这基本上只需要两步:

 

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

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

因此,我们把之前pass的代码再粘贴一遍,来创建一个新的pass,但要把tag改成:
  1. Tags { "LightMode" = "ForwardAdd" }  
 
并且更改#pragma指令:
  1. #pragma multi_compile_fwdadd  


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

然后,我们移除掉第二个pass对UNITY_LIGHTMODEL_AMBIENT的处理,因为我们已经在第一个pass中处理过这个值了。我们最后的代码如下:
  1. Shader "Custom/DiffuseNormal" {  
  2.     Properties {  
  3.         _MainTex ("Base (RGB)", 2D) = "white" {}  
  4.         _BumpTex ("Bump Texture", 2D) = "white" {}  
  5.     }  
  6.     SubShader {  
  7.         Tags { "RenderType"="Opaque" }  
  8.         LOD 300  
  9.       
  10.         Pass {  
  11.             Tags { "LightMode" = "ForwardBase" }  
  12.               
  13.             Cull Back  
  14.             Lighting On  
  15.               
  16.             CGPROGRAM  
  17.               
  18.             #pragma vertex vert  
  19.             #pragma fragment frag  
  20.               
  21.             #pragma multi_compile_fwdbase  
  22.               
  23.             #include "UnityCG.cginc"  
  24.             #include "Lighting.cginc"  
  25.             #include "AutoLight.cginc"  
  26.               
  27.             sampler _MainTex;  
  28.             sampler _BumpTex;  
  29.               
  30.             float4 _MainTex_ST;  
  31.             float4 _BumpTex_ST;  
  32.               
  33.             struct a2v {  
  34.                 float4 vertex : POSITION;  
  35.                 float3 normal : NORMAL;  
  36.                 float4 texcoord : TEXCOORD0;  
  37.                 float4 tangent : TANGENT;  
  38.             };  
  39.               
  40.             struct v2f {  
  41.                 float4 pos : POSITION;  
  42.                 float2 uv : TEXCOORD0;  
  43.                 float2 uv2 : TEXCOORD1;  
  44.                 float3 lightDirection : TEXCOORD2;  
  45.                 LIGHTING_COORDS(3,4)  
  46.             };  
  47.   
  48.             v2f vert(a2v v) {  
  49.                 v2f o;  
  50.                   
  51.                 TANGENT_SPACE_ROTATION;  
  52.                 o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));  
  53.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  54.                 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);  
  55.                 o.uv2 = TRANSFORM_TEX(v.texcoord, _MainTex);  
  56.                   
  57.                 TRANSFER_VERTEX_TO_FRAGMENT(o);  
  58.                 return o;  
  59.             }  
  60.               
  61.             float4 frag(v2f i) : COLOR {  
  62.                 float4 c = tex2D(_MainTex, i.uv);  
  63.                 float3 n = UnpackNormal(tex2D(_BumpTex, i.uv2));  
  64.                   
  65.                 float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;  
  66.                   
  67.                 float atten = LIGHT_ATTENUATION(i);  
  68.                   
  69.                 // Angle to the light  
  70.                 float diff = saturate(dot(n, normalize(i.lightDirection)));  
  71.                 lightColor += _LightColor0.rgb * (diff * atten);  
  72.                   
  73.                 c.rgb = lightColor * c.rgb * 2;  
  74.   
  75.                 return c;  
  76.             }  
  77.               
  78.             ENDCG  
  79.         }  
  80.           
  81.         Pass {  
  82.             Tags { "LightMode" = "ForwardAdd" }  
  83.               
  84.             Cull Back  
  85.             Lighting On  
  86.             Blend One One  
  87.               
  88.             CGPROGRAM  
  89.               
  90.             #pragma vertex vert  
  91.             #pragma fragment frag  
  92.               
  93.             #pragma multi_compile_fwdadd  
  94.               
  95.             #include "UnityCG.cginc"  
  96.             #include "Lighting.cginc"  
  97.             #include "AutoLight.cginc"  
  98.               
  99.             sampler _MainTex;  
  100.             sampler _BumpTex;  
  101.               
  102.             float4 _MainTex_ST;  
  103.             float4 _BumpTex_ST;  
  104.               
  105.             struct a2v {  
  106.                 float4 vertex : POSITION;  
  107.                 float3 normal : NORMAL;  
  108.                 float4 texcoord : TEXCOORD0;  
  109.                 float4 tangent : TANGENT;  
  110.             };  
  111.               
  112.             struct v2f {  
  113.                 float4 pos : POSITION;  
  114.                 float2 uv : TEXCOORD0;  
  115.                 float2 uv2 : TEXCOORD1;  
  116.                 float3 lightDirection : TEXCOORD2;  
  117.                 LIGHTING_COORDS(3,4)  
  118.             };  
  119.   
  120.             v2f vert(a2v v) {  
  121.                 v2f o;  
  122.                   
  123.                 TANGENT_SPACE_ROTATION;  
  124.                 o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));  
  125.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  126.                 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);  
  127.                 o.uv2 = TRANSFORM_TEX(v.texcoord, _MainTex);  
  128.                   
  129.                 TRANSFER_VERTEX_TO_FRAGMENT(o);  
  130.                 return o;  
  131.             }  
  132.               
  133.             float4 frag(v2f i) : COLOR {  
  134.                 float4 c = tex2D(_MainTex, i.uv);  
  135.                 float3 n = UnpackNormal(tex2D(_BumpTex, i.uv2));  
  136.                   
  137.                 float3 lightColor = float3(0);  
  138.                   
  139.                 float lengthSq = dot(i.lightDirection, i.lightDirection);  
  140.                 float atten = LIGHT_ATTENUATION(i);  
  141.                   
  142.                 // Angle to the light  
  143.                 float diff = saturate(dot(n, normalize(i.lightDirection)));  
  144.                 lightColor += _LightColor0.rgb * (diff * atten);  
  145.                   
  146.                 c.rgb = lightColor * c.rgb * 2;  
  147.   
  148.                 return c;  
  149.             }  
  150.               
  151.             ENDCG  
  152.         }  
  153.     }   
  154.     FallBack "Diffuse"  
  155. }  


 
我们在场景里放置两个光源——一个平行光,用于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),那么千万不要自己尝试去写一个函数出来。。。某些内置的变量实在是不知道它们什么时候工作、怎么工作。。。

posted on 2015-05-21 10:37  _Sin  阅读(1717)  评论(0编辑  收藏  举报