Unity Shaders Vertex & Fragment Shader入门

时间:2022-06-06 16:50:37

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

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

Unity Shaders  Vertex & Fragment 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):

Unity Shaders  Vertex & Fragment Shader入门

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

我们来分析下它的流程。首先,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. #pragma vertex vert
  6. #pragma fragment frag
  7. #pragma multi_compile_fwdbase
  8. #include "UnityCG.cginc"
  9. // More code here
  10. ENDCG
  11. }

在上面的代码里,我们定义了一个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进行相应调整才能得到真正的纹理坐标。
Unity Shaders  Vertex & Fragment Shader入门

最后,我们计算得到顶点的初始颜色——即光源对该顶点的影响。在我们的第一个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. Pass {
  9. Tags { "LightMode" = "Vertex" }
  10. Cull Back
  11. Lighting On
  12. CGPROGRAM
  13. #pragma vertex vert
  14. #pragma fragment frag
  15. #include "UnityCG.cginc"
  16. sampler _MainTex;
  17. float4 _MainTex_ST;
  18. struct a2v {
  19. float4 vertex : POSITION;
  20. float3 normal : NORMAL;
  21. float4 texcoord : TEXCOORD0;
  22. };
  23. struct v2f {
  24. float4 pos : POSITION;
  25. float2 uv : TEXCOORD0;
  26. float3 color : TEXCOORD1;
  27. };
  28. v2f vert(a2v v) {
  29. v2f o;o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
  30. o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
  31. o.color = ShadeVertexLights(v.vertex, v.normal);
  32. return o;
  33. }
  34. float4 frag(v2f i) : COLOR {
  35. float4 c = tex2D(_MainTex, i.uv);
  36. c.rgb = c.rgb * i.color * 2;
  37. return c;
  38. }
  39. ENDCG
  40. }
  41. }
  42. FallBack "Diffuse"
  43. }

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

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

Unity Shaders  Vertex & Fragment 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 Shaders  Vertex & Fragment Shader入门
 

但是,光源在哪里呢?

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. TANGENT_SPACE_ROTATION;
  4. o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));
  5. o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
  6. o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
  7. o.uv2 = TRANSFORM_TEX(v.texcoord, _MainTex);
  8. TRANSFER_VERTEX_TO_FRAGMENT(o);
  9. return o;
  10. }

在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. float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;
  5. float atten = LIGHT_ATTENUATION(i);
  6. // Angle to the light
  7. float diff = saturate(dot(n, normalize(i.lightDirection)));
  8. lightColor += _LightColor0.rgb * (diff * atten);
  9. c.rgb = lightColor * c.rgb * 2;
  10. return c;
  11. }

在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. Pass {
  10. Tags { "LightMode" = "ForwardBase" }
  11. Cull Back
  12. Lighting On
  13. CGPROGRAM
  14. #pragma vertex vert
  15. #pragma fragment frag
  16. #pragma multi_compile_fwdbase
  17. #include "UnityCG.cginc"
  18. #include "Lighting.cginc"
  19. #include "AutoLight.cginc"
  20. sampler _MainTex;
  21. sampler _BumpTex;
  22. float4 _MainTex_ST;
  23. float4 _BumpTex_ST;
  24. struct a2v {
  25. float4 vertex : POSITION;
  26. float3 normal : NORMAL;
  27. float4 texcoord : TEXCOORD0;
  28. float4 tangent : TANGENT;
  29. };
  30. struct v2f {
  31. float4 pos : POSITION;
  32. float2 uv : TEXCOORD0;
  33. float2 uv2 : TEXCOORD1;
  34. float3 lightDirection : TEXCOORD2;
  35. LIGHTING_COORDS(3,4)
  36. };
  37. v2f vert(a2v v) {
  38. v2f o;
  39. TANGENT_SPACE_ROTATION;
  40. o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));
  41. o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
  42. o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
  43. o.uv2 = TRANSFORM_TEX(v.texcoord, _MainTex);
  44. TRANSFER_VERTEX_TO_FRAGMENT(o);
  45. return o;
  46. }
  47. float4 frag(v2f i) : COLOR {
  48. float4 c = tex2D(_MainTex, i.uv);
  49. float3 n = UnpackNormal(tex2D(_BumpTex, i.uv2));
  50. float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;
  51. float atten = LIGHT_ATTENUATION(i);
  52. // Angle to the light
  53. float diff = saturate(dot(n, normalize(i.lightDirection)));
  54. lightColor += _LightColor0.rgb * (diff * atten);
  55. c.rgb = lightColor * c.rgb * 2;
  56. return c;
  57. }
  58. ENDCG
  59. }
  60. }
  61. FallBack "Diffuse"
  62. }

Shader效果如下:

Unity Shaders  Vertex & Fragment 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. Pass {
  10. Tags { "LightMode" = "ForwardBase" }
  11. Cull Back
  12. Lighting On
  13. CGPROGRAM
  14. #pragma vertex vert
  15. #pragma fragment frag
  16. #pragma multi_compile_fwdbase
  17. #include "UnityCG.cginc"
  18. #include "Lighting.cginc"
  19. #include "AutoLight.cginc"
  20. sampler _MainTex;
  21. sampler _BumpTex;
  22. float4 _MainTex_ST;
  23. float4 _BumpTex_ST;
  24. struct a2v {
  25. float4 vertex : POSITION;
  26. float3 normal : NORMAL;
  27. float4 texcoord : TEXCOORD0;
  28. float4 tangent : TANGENT;
  29. };
  30. struct v2f {
  31. float4 pos : POSITION;
  32. float2 uv : TEXCOORD0;
  33. float2 uv2 : TEXCOORD1;
  34. float3 lightDirection : TEXCOORD2;
  35. LIGHTING_COORDS(3,4)
  36. };
  37. v2f vert(a2v v) {
  38. v2f o;
  39. TANGENT_SPACE_ROTATION;
  40. o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));
  41. o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
  42. o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
  43. o.uv2 = TRANSFORM_TEX(v.texcoord, _MainTex);
  44. TRANSFER_VERTEX_TO_FRAGMENT(o);
  45. return o;
  46. }
  47. float4 frag(v2f i) : COLOR {
  48. float4 c = tex2D(_MainTex, i.uv);
  49. float3 n = UnpackNormal(tex2D(_BumpTex, i.uv2));
  50. float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;
  51. float atten = LIGHT_ATTENUATION(i);
  52. // Angle to the light
  53. float diff = saturate(dot(n, normalize(i.lightDirection)));
  54. lightColor += _LightColor0.rgb * (diff * atten);
  55. c.rgb = lightColor * c.rgb * 2;
  56. return c;
  57. }
  58. ENDCG
  59. }
  60. Pass {
  61. Tags { "LightMode" = "ForwardAdd" }
  62. Cull Back
  63. Lighting On
  64. Blend One One
  65. CGPROGRAM
  66. #pragma vertex vert
  67. #pragma fragment frag
  68. #pragma multi_compile_fwdadd
  69. #include "UnityCG.cginc"
  70. #include "Lighting.cginc"
  71. #include "AutoLight.cginc"
  72. sampler _MainTex;
  73. sampler _BumpTex;
  74. float4 _MainTex_ST;
  75. float4 _BumpTex_ST;
  76. struct a2v {
  77. float4 vertex : POSITION;
  78. float3 normal : NORMAL;
  79. float4 texcoord : TEXCOORD0;
  80. float4 tangent : TANGENT;
  81. };
  82. struct v2f {
  83. float4 pos : POSITION;
  84. float2 uv : TEXCOORD0;
  85. float2 uv2 : TEXCOORD1;
  86. float3 lightDirection : TEXCOORD2;
  87. LIGHTING_COORDS(3,4)
  88. };
  89. v2f vert(a2v v) {
  90. v2f o;
  91. TANGENT_SPACE_ROTATION;
  92. o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));
  93. o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
  94. o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
  95. o.uv2 = TRANSFORM_TEX(v.texcoord, _MainTex);
  96. TRANSFER_VERTEX_TO_FRAGMENT(o);
  97. return o;
  98. }
  99. float4 frag(v2f i) : COLOR {
  100. float4 c = tex2D(_MainTex, i.uv);
  101. float3 n = UnpackNormal(tex2D(_BumpTex, i.uv2));
  102. float3 lightColor = float3(0);
  103. float lengthSq = dot(i.lightDirection, i.lightDirection);
  104. float atten = LIGHT_ATTENUATION(i);
  105. // Angle to the light
  106. float diff = saturate(dot(n, normalize(i.lightDirection)));
  107. lightColor += _LightColor0.rgb * (diff * atten);
  108. c.rgb = lightColor * c.rgb * 2;
  109. return c;
  110. }
  111. ENDCG
  112. }
  113. }
  114. FallBack "Diffuse"
  115. }
 
我们在场景里放置两个光源——一个平行光,用于ForwardBase Pass的计算,一个Point Light,用于ForwardAdd Pass的计算。效果如下:
Unity Shaders  Vertex & Fragment Shader入门

写在最后

本文里对处理光源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),那么千万不要自己尝试去写一个函数出来。。。某些内置的变量实在是不知道它们什么时候工作、怎么工作。。。