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. #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进行相应调整才能得到真正的纹理坐标。

最后,我们计算得到顶点的初始颜色——即光源对该顶点的影响。在我们的第一个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的效果如下(啦啦啦,又是小苹果+呆萌小怪兽的组合~~~):

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. 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效果如下:

在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的计算。效果如下:

写在最后

本文里对处理光源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入门的更多相关文章

  1. 【Unity Shaders】Vertex & Fragment Shader入门

    写在前面 三个月以前,在一篇讲卡通风格的Shader的最后,我们说到在Surface Shader中实现描边效果的弊端,也就是只对表面平缓的模型有效.这是因为我们是依赖法线和视角的点乘结果来进行描边判 ...

  2. 【Unity Shaders】Mobile Shader Adjustment —— 为手机定制Shader

    本系列主要參考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同一时候会加上一点个人理解或拓展. 这里是本书全部的插图.这里是本书所需的代码和 ...

  3. 【Unity Shaders】Mobile Shader Adjustment—— 什么是高效的Shader

    本系列主要参考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同时会加上一点个人理解或拓展. 这里是本书所有的插图.这里是本书所需的代码和资源 ...

  4. 【Unity Shaders】Shader中的光照

    写在前面 自己写过Vertex & Fragment Shader的童鞋,大概都会对Unity的光照痛恨不已.当然,我相信这是因为我们写得少...不过这也是由于官方文档对这方面介绍很少的缘故, ...

  5. unity shader入门(一):基本结构话痨版

    unity shader 有三种形式:表面着色器(Surface Shader),顶点/片元着色器(Vertex/Fragment Shader),固定函数着色器(Fixed Function Sha ...

  6. 【Unity Shaders】学习笔记——SurfaceShader(一)认识结构

    [Unity Shaders]学习笔记——SurfaceShader(一)认识结构 转载请注明出处:http://www.cnblogs.com/-867259206/p/5595747.html 写 ...

  7. 【Unity Shaders】法线纹理(Normal Mapping)的实现细节

    写在前面 写这篇的目的是为了总结我长期以来的混乱.虽然题目是"法线纹理的实现细节",但其实我想讲的是如何在shader中编程正确使用法线进行光照计算.这里面最让人头大的就是各种矩阵 ...

  8. 【Unity Shaders】Alpha Test和Alpha Blending

    写在前面 关于alpha的问题一直是个比较容易摸不清头脑的事情,尤其是涉及到半透明问题的时候,总是不知道为什么A就遮挡了B,而B明明在A前面.这篇文章就总结一下我现在的认识~ Alpha Test和A ...

  9. 【Unity Shaders】Unity里的雾效模拟

    写在前面 熟悉Unity的都知道,Unity可以进行基本的雾效模拟.所谓雾效,就是在远离我们视角的方向上,物体看起来像被蒙上了某种颜色(通常是灰色).这种技术的实现实际上非常简单,就是根据物体距离摄像 ...

随机推荐

  1. 华为DHCP-重要

    近日遇到遇到控制器和wac对接的一些问题.尤其是地址池这块排查起来比较费事,且这些命令不容易找到,以下是能经常用到的命令. 1,查看ip是否冲突: (看下conflict字段) 2,防止冲突命令:   ...

  2. C语言 百炼成钢8

    //题目22:两个乒乓球队进行比赛,各出三人.甲队为a,b,c三人,乙队为x,y,z三人.已抽签决定 //比赛名单.有人向队员打听比赛的名单.a说他不和x比,c说他不和x, z比,请编程序找出 //三 ...

  3. [转]在 Eclipse 中嵌入 NASA World Wind Java SDK

    使用此开源 SDK 开发 GIS 应用程序 NASA 开发的开源 World Wind Java (WWJ) SDK 为地理信息系统(Geographic Information Systems,GI ...

  4. python爬虫中文网页cmd打印出错问题解决

    问题描述 用python写爬虫,很多时候我们会先在cmd下先进行尝试. 运行爬虫之后,肯定的,我们想看看爬取的结果. 于是,我们print... 运气好的话,一切顺利.但这样的次数不多,更多地,我们会 ...

  5. GEOS库学习之五:与GDAL/OGR结合使用

    要学习GEOS库,肯定绕不开地理方面的东西.如果需要判断的两个多边形或几何图形,不是自己创建的,而是来自shapefile文件,那就得将GEOS库和GDAL/OGR库结合使用了.实际上只需要OGR就行 ...

  6. MATLAB中提高fwrite和fprintf函数的I/O性能

    提高fwrite和fprintf函数的I/O性能 http://www.matlabsky.com/thread-34861-1-1.html     今天我们将讨论下著名的fwrite(fprint ...

  7. 系统级I/O

    Unix I/O 输入操作是从I/O设备拷贝数据到主存,而输出操作是从主存拷贝数据到I/O设备. 一个文件就是一个字节序列. 所有的I/O设备,如网络.磁盘.和终端,都被模型化为文件,而所有的输入和输 ...

  8. 实验5 简单嵌入式WEB服务器实验 实验报告 20135303 20135326

    北京电子科技学院(BESTI) 实     验    报     告 课程:信息安全系统设计基础                班级:  1353 姓名:20135303 魏昊卿 学号:2013532 ...

  9. 配置sonar、jenkins进行持续审查

    本文以CentOS操作系统为例介绍Sonar的安装配置,以及如何与Jenkins进行集成,通过pmd-cpd.checkstyle.findbugs等工具对代码进行持续审查. 一.安装配置sonar ...

  10. 『随笔』C# 程序 修改 ConfigurationManager 后,不重启 刷新配置

    基本共识: ConfigurationManager 自带缓存,且不支持 写入. 如果 通过 文本写入方式 修改 配置文件,程序 无法刷新加载 最新配置. PS. Web.config 除外:Web. ...