这篇是系列教程的第三篇,最近工作比较紧,所以这个周六周日就自觉去加了刚回来就打开电脑补上这篇,这个系列的教程我会尽量至少保证一周写一篇的.如果大家看过我的上一篇教程《Esfog_UnityShader教程_UnityShader语法实例浅析》的话,相信已经对UnityShader有了一些了解了,我们从这篇开始就不会再专门纠缠语法了,一般都会在用到的时候特殊说明一下.如果你还对UnityShader的基础语法比较陌生,那么推荐看一下本系列的前两篇文章地址是http://www.cnblogs.com/Esfog/p/3562022.html.
漫反射DiffuseReflection
光照是图形渲染里的一个重要课题,处理的好坏直接影响了游戏展示给玩家的场景效果,做的越好越真实,给玩家代入感也就越强烈,一般的光照模型中包括4种:环境光,自发光,漫反射,高光。而光照处理里面两个最常见的课题也就是漫反射和镜面反射(高光),这一篇我们讨论漫反射,关于镜面反射的相关内容将在下一篇中讲解.提到漫反射大家一定不会陌生,因为我们在小学或是初中就一定知道了什么是漫反射和镜面反射了.不过为了下面讲解的让大家更容易理解,我还是简单的用自己的语言描述一下漫反射的概念.
如上图(图片来自网络),当光照射到物体表面时,由于物体表面的凹凸不平,导致光向各个方向反射出去,在理想状态下,我们认为光向各个方向反射的量是相同的,不难理解,这样就会无论我们从哪个角度观察物体,物体上的某一点看上去都是一样亮的.也就是说我们的眼睛从各个角度接收到来自这个点的光线量都是相同.在游戏里,摄像机也就相当于我们的眼睛,那我们要做的也就是计算出光照在物体表面后,在每个像素上的反射强度,无论我们从什么角度看,都让它始终保持这个值.原理就说到这里,如果你对我说的不理解,那也无妨,总之你知道漫反射就是无论你从哪个角度看,看到某个点的亮度都应是一样的就可以了.下面来我们来看具体的计算原理.
如上图(图片取自《Cg Programming in Unity》),要计算漫反射,我们需要知道两个量一个是物体表面的法线N,另一个是光的入射方向L(注意,我们这里考虑的只有平行光DirectionalLight,对于点光源等其他光源的计算方式略有不同,读者可自行搜索了解),
在写具体代码之前,我们先说明一下计算的原理,原理就是我们通过计算光的入射方向(这里所谓的入射其实是入射方向的反方向,之所以这样是为了方便计算)和物体表面向量的夹角来决定这点的光照强度,两个向量的夹角越小就说明越来越接近于关照直射,这时候当然反射的光也就越强,如果夹角大于等于90度那么反射的光强度越来越弱,超过90度就完全看不到了.额外说一点:也许你会纠结,前面明明说我们假设光在任何方向上的反射都是同量的,而且物体表面的凹凸不平也应该是平均的,那么为什么直射的地方就一定比其它地方看上去量的,说实话这个问题一开始纠结了我很久,它超出了我对Shader的理解范围,偏大到物理方面了,后来我个人认为可能是由于夹角越大的时候光在向各个方向反射的时候表面内部光来回传递所消耗的能量就越多,最后反射出去的也就越少了.这只是我的个人理解,如果哪位有更权威的解释,请评论告诉我,如果你压根没有把这当成一个我问题就不要去思考他了,我这个人比较爱钻牛角尖,什么都爱刨根问底,也不知道是好是坏.
那么在通过夹角来计算光的亮度之前,我们需要对N和L两个向量进行归一化处理,如果大家学过线性代数或者高中数学没忘干净的话,那么一定对它不陌生,如果你实在记不起来就去上网搜搜吧.在CG里对向量进行归一化的操作我们使用normalize函数,这是CG数学库提供给我们的.然后我们利用向量的点积公式N·L = |N|*|L|*cosθ(如果你对点积也不了解,我就不过分解释了,上网搜一下).由于我们刚刚对N和L进行了归一化,他们的模就是1了,那么N·L = cosθ了.也就是说我们直接对N和L进行点积运算会得到两个向量的夹角余弦值,我们知道如果加角θ越接近月0°那么N·L也就越接近于1,越接近月90°,N·L就越接近于0.所以我们只要把光的颜色乘以这个点积结果就会达到我们想要的角度越小光越强,角度越大光越弱的效果了.下面给出《The Cg Tutorial》中给出的漫反射计算公式:
diffuse = Kd * lightColor * max(N·L,0)
下面我们就来通过实际代码来讲述一下具体的计算过程.
1 Shader "Esfog/Diffuse" 2 { 3 Properties 4 { 5 _MainTex ("Base (RGB)", 2D) = "white" {} 6 } 7 SubShader 8 { 9 Pass 10 { 11 Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"} 12 CGPROGRAM 13 #pragma vertex vert 14 #pragma fragment frag 15 #include "UnityCG.cginc" 16 17 uniform sampler2D _MainTex; 18 uniform float4 _LightColor0; 19 struct VertexOutput 20 { 21 float4 pos:SV_POSITION; 22 float2 uv_MainTex:TEXCOORD0; 23 float3 normal:TEXCOORD1; 24 }; 25 26 VertexOutput vert(appdata_base input) 27 { 28 VertexOutput o; 29 o.pos = mul(UNITY_MATRIX_MVP,input.vertex); 30 o.uv_MainTex = input.texcoord.xy; 31 o.normal = normalize(mul(float4(input.normal,0),_World2Object)); 32 return o; 33 } 34 35 float4 frag(VertexOutput input):COLOR 36 { 37 float3 normalDir = normalize(input.normal); 38 float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); 39 float3 Kd = tex2D(_MainTex,input.uv_MainTex).xyz; 40 float3 diffuseReflection = Kd * _LightColor0.rgb * max(0,dot(normalDir,lightDir)); 41 return float4(diffuseReflection,1); 42 } 43 ENDCG 44 } 45 } 46 FallBack "Diffuse" 47 }
这里我假定大家已经看过我的上一篇教程或者有一定的Shader语法基础,所以不会像上一篇一样一行行解释,只选择与本篇相关的或者新出现的内容进行说明.
第11行 大家会发现我们的Tags里面比上一次多了一个"LightMode"="ForwardBase",这句话的作用其实是和Unity处理场景中的所有光源的方案有关系的,一般默认使用的处理方式为Forward(具体细节请参考官方文档Unity's Rendering behind the scenes一节的内容).如果处理方式为Forward那么当我们在Tags里写上这一句的时候,简单的理解为,当渲染这个物体的时候场景中的第一个平行光源的一些参数我们可以直接在这个Pass中使用.包括光的颜色_LightColor0,光的在世界空间的位置_WorldSpaceLightPos0等.如果你不适用这种方法的话你也可以通过在外部写一个脚本将光的一些参数和任何你想传送的变量传送到Shader中,具体方式我们在日后遇到相关问题的时候具体说.
第18行 float4 _LightColor0;我们定义了一个新的_LightColor0来接收存储光照的颜色,我们并没有在Properties中定义也没有通过外部脚本来传值,那它是做什么用的呢?其实由于我们上边在Tags中添加的新标签,所以_LightColor0会被Unity自动赋值为场景中第一个平行光的颜色(这也不一定,其实和光源的RenderMode属性有关,但如果你不做修改的话,那就是用第一个光源).
第23行 float3 normal:TEXCOORD1;我们在顶点着色器的返回结构中多添加了一个变量,看名知意我们要把顶点的发线经过插值后传到片段着色器中,我们之前说过TEXCOORD0~TEXCOORDX(具体视显卡能力而定),可以用来保存任何我们需要插值的内容,由于我们想在片段着色器中来计算物体表面的漫反射所以就需要将法线传过去.
那么有一个很值得思考的问题:"为什么要在片段着色器中计算漫反射呢?",这是个很好的问题,在到底在顶点着色器还是片段着色器中来进行光照的处理并无一个明确的规定,这是一个对于性能和效果的权衡,如果在顶点着色器中计算的话很显然我们的计算次数和顶点的数量一致,也就是很少,但是效果就不好,一般来说一个像素所在的三角片上的三个顶点对光源的捕捉有可能不足,就会导致最后用三个顶点计算出来的漫反射颜色来插值出的面片颜色就会不准确,也就会出现本该很亮的地方却不亮.所以说在顶点着色器中进行光照计算的性能会很高(因为计算次数远远小于片段着色器),但是效果差,而在片段着色器中计算则正相反,性能会很低,但是效果会很好(因为每个像素都是单独通过法线来计算的,而不是直接用顶点插值出来的).所以具体情况具体分析.
第31行o.normal = normalize(mul(float4(input.normal,0),_World2Object));这一句中我们把模型本身自带的顶点法线属性,先进行空间变换将其变换到世界空间中去,为什么要换到世界空间中呢,其实只要保证要进行操作的两个向量在同一个空间,那么具体是哪个空间并不重要,不过由于Unity为我们提供的大量参数都是在世界空间中的,所以我们就变换到世界空间中去吧,这里还有一点很重要,向量的空间变换与点不同,它要右乘上目标变换矩阵的逆的转置,具体的数学原因有些复杂,大家可以自己查一下,那么原来的目标矩阵式模型空间到世界空间unity中为我们提供了这个矩阵_Object2World,不过我们要的不是它,我们先来求他的逆,unity也为我们提供了_World2Object,其实严格上来讲这并不是_Object2World的逆,我们呢要将这个矩阵*unity_Scale.w之后再将矩阵的最右下角的数置为1.这其中的问题我只是在国外的论坛上看到过一些解释,我也无法很准确的表达出来,等以后我弄明白了再告诉大家,如果有人知道也请您一定告诉我,不过为什么这里我们没有进行刚才的几步操作呢,因为这两步操作都是针对缩放的,由于我们马上要对他进行归一化所以对我们只要他的方向正确就好,大小无所谓。这样我们得到了它的逆,我们要继续求它的转置,不过这个就不需要了,之前我们都是将矩阵放在mul函数的左边,而向量或点放在右侧,我们调换一下他们的位置就相当于乘上了转置(不明白就看看线性代数吧),由于mul要求点或者向量必须表示成4维的,所以我们将他转换成float4并将最后一位填0(原因后面有解释).上述结束以后我们进行了归一化,传给了顶点输出结构中相应变量,这样做只为了保证在插值的时候不同顶点之间的法线是在同一个标准下进行的.因为法线只起到方向的作用,如果有的顶点的法线长度太长,而有的很短,那么各个分量在进行差值的时候就会出现错误的结果,当然一般美术同学做出来的模型自带的法线属性都是单位向量,所以这里只是为了以防万一.
第37行 float3 normalDir = normalize(input.normal); 我们定义个了一个float3变量来保存插值后在当前片段上的法线,由于经过插值会使我们原本的单位向量不再是单位向量(如果你想知道具体原因,就想一下插值过程知识满足了各个分量的插值结果,但并不能保证整体上还是一个单位向量),所以我们需要再次进行归一化.
第38行float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);我们又定义了一个float3变量来保存入射光的方向(反方向),这里有一些需要说明的,Unity中的平行光源的位置是没有意义的,只有它的方向代表了光线的方向.而在3D数学中,点和向量的概念有时候是很模糊的.为了区分它们,在齐次空间中我们用一个4维的行(列)向量来表示它们,xyz分量都相同,只有w分量不同,点的w分量为1,向量的w分量为0,如果想知道具体原因的话,就去看看线性代数吧,这个不太好解释.而我们的平行光源其实他可以看做一个向量,他具体在哪没有意义,我们要的只是一个方向,_WorldSpaceLightPos0是Unity为我们赋值的一个表示场景中第一个平行光的位置(这里不一定是第一个, 解释通上面的光照颜色).由于位置无关,那么我们就直接取出他的前三个分量,把它看做一个由世界空间原点到光源位置的一个向量,然后我们再对他进行归一化最终就得到我们想要的结果了,也许这里我也解释的不是很清楚,我自己理解这里的时候也不是一下子就明白的.
第39行float3 Kd = tex2D(_MainTex,input.uv_MainTex).xyz;这个Kd就是我们在上面一开始提到的那个书中给出的计算漫反射的公式,其中Kd表示材质的漫反射颜色,这个说法很有迷惑感,我个人是这样理解的,根据物理上的说法,物体本身如果没有光照射,它是呈现不出任何的颜色的,而最终表现给眼睛的颜色实际上是光源找到物体表面没有被物体表面吸收而反射回来的颜色,那么我们这个Kd不如理解成将光吸收后反射出去的颜色成分(其中3个分量分别对应对RGB三种颜色值的反射量).如果你不纠结于此就当我没说.总之我们这里讲Kd直接赋成我们的纹理颜色就可以了,你就理解成物体本身就这个颜色也成.
第40行就是利用了我们一开始的公式将光照颜色计算出来,有三个地方需要说明,其中_LightColor是光源的颜色,这个在上面解释过了,至于为什么要和Kd相乘,我个人觉得这是和色彩处理相关的,因为相乘的情况下颜色的结合看起来是最自然的和真实世界中最相近.第二个地方就是dot(normalDir,lightDir),这个dot是Cg提供给我们来计算两个光源的点积的,为什么这么做一开始我们说过了,最后一点就是max函数,之所以将点积计算出来之后还要和0去取一个最大值,主要是为了避免当normal和lightdir的夹角大于90度的时候点积计算出现负值,会导致整个漫反射颜色计算出来是个负值进而导致整体的光照计算出现错误,当大于90度的时候漫反射颜色已经失去意义,所以需要用max函数保证不会出现负值.
(~ o ~)~好了系列教程的第三篇到此结束了,和前两篇基础的不同,这一次可能涉及到一些数学概念和Unity的东西所以我描述起来和大家理解起来都比较费劲,不过没什么东西是一蹴而就的,我给大家写一篇文章的背后,我自己都不知花了多久去弄清楚一个概念,去理解一个公式.总之希望大家不要因为一时的不理解而放弃学习和探索。你选择学习Shader就说明你一定是对游戏开发又更高追求的人,成功从来都是不易的,那些唾手可得的东西并没有什么值得让人骄傲和羡慕的.希望大家和我一道继续向前向前!
下面是两幅图来展示一下效果:
上面是使用的我们上篇教程所讲述的直接使用贴图颜色的效果
上面这张是使用了我们上面写的Shader.有了明暗效果是不是看上去更真实了一些了,当然你可能觉得效果并没有什么特别好,因为漫反射要和其他光照处理一起使用才更加完美.我们这里只用了漫反射,连环境光都没有使用,所以并不是特别理想.
add:(2015/4/8)
添加一个关于法线为什么乘以逆的转置的一个简单推导
尊重他人智慧成果,欢迎转载,请注明作者esfog,原文地址http://www.cnblogs.com/Esfog/p/3577412.html.