【UnityShader从零开始】(4)漫反射效果

发表于2015-08-14
评论0 4.5k浏览
系列教程第四篇,本来打算昨天写的,有些小偷懒就今天写了,这一期我们来讨论一下关于镜面反射的基本原理和具体代码。这一篇是承接着上一篇《Esfog_UnityShader教程_漫反射DiffuseReflection》来讲述的,如果你还没有看或者对漫反射不是很了解的话,建议点击链接去看一下,这样子有助于你更好的理解本篇教程。

镜面反射SpecularReflection

上一篇关于漫反射的讲解中,我们说过光照处理里面两个最常见的课题也就是漫反射和镜面反射(高光)。那么这节我们就来说一下镜面反射,顺带把环境光也说一下,这样做出来的效果更好一些。类似于上一篇,我们还是来先说明一下镜面反射(为了方面,下面一律称高光)。

如上图(图片来自网络),这两种反射给人的直观感受,我相信大家一定在生活中有所了解,上次我们说过漫反射的效果让我们对物体的某一个位置,无论我们从哪个位置观察它,我们看到的亮度都是一样的。而镜面反射则不然,你从不同的位置去观察物体的同一个点的受光照情况,看到的结果是不一样的,因为光照的反射不再是各个方向的反射量均匀了,而是在与反射光线与视线的偏差大小有着反比关系,如果你的视线与反射光线重合,那么看到的将是最亮的,如果角度大于90度那将看不到高光效果。一个比较好的例子就是在阳光下我们观察表面很光滑的汽车,你会发现汽车外壳上的高光区域会随着你观察位置的不同而移动和变化。

如上图(图片取自《Cg Programming in Unity》),要计算镜面反射,我们需要知道的东西可能要比漫反射多一些,一共4个向量,光的入射方向L(反方向,下同),表面法线N,观察的目标位置到摄像机的向量V(视线),以及反射光线R,和漫反射一样,我们这里只考虑平行光的情况,其他类型的光源于其有所不同,大家自行了解,这里就不赘述了,如果有地方用到了,也可以到时候具体说明。

参考着上面这幅图,我们来说明一下它的计算原理,前面提到其实最终决定我们看到的光的强弱的是视线V和反射向量R,这两个向量的夹角越小说明越接近光线经过物体表面的反射直接反射到我们的眼睛(摄像机)。两者的关系很类似与上一篇漫反射中入射光线L和法线N的关系。所以我们也通过V·R = |V|*|R|*cosθ。大家通过这个公式也应该可以明白为什么上面说镜面反射对于物体的同一个点你在不同位置观察看到的结果并不一样了吧。既然这里我们只用到了两个向量就可以决定最终的影响高光的因子,那为什么前面说需要四个向量呢,那是因为反射光线R是需要通过法线N和入射光线L进行计算得来的。

我们先来说明一下如何通过N和L来计算得到R。

如图所示(图片取自网友butwang博客),不难看出我们只要计算出一个向量s然后对其乘以2就可以得到2s然后根据向量的加法规则,我们可以利用L+2s = R来获取最终结果。那么首先来求向量s,要计算s就要利用L和L在N上的投影向量,因为N·L = |N|*|L|*cosθ,若N为单位向量,则|N| = 1所以N·L = |L|*cosθ,L在N上的投影距离为N·L,然后再将结果乘以N的单位向量,所以我们要先将N规范化才行,我们假设N就是规范化后的单位向量那么,L在N上的投影向量则为(L·N)*N,那么通过向量减法我们可以计算出s = (L·N)*N - L,进而计算出R = 2s + L = 2((L·N)*N-L) +L = 2N(N·L) - L。不过这么繁琐的逻辑,Cg已经帮我们搞定了,我们只需要通过reflect(L,N)函数就可以计算出反射向量,第一个参数是光的入射反向(注意,这里是真正的入射方向,不是反方向),第二个参数是法向量。

下面给出《The Cg Tutorial》中给出的镜面反射计算公式(它的公式和下面的略有不同,我做了下变化,效果都是一样的):

先来解释一下这个公式,这个Ks是材质的反射颜色,和上一篇中漫反射公式里的Kd有些相似,Kd一般设置成贴图颜色即可,但是Ks一般不可以,我个人理解它是用来设置物体表面受到高光的时候应该呈现的一种高光颜色,如果你有相应的高光贴图,那么你可以利用Ks来读取贴图颜色来为不同的位置呈现不同的高光颜色,如果没有的话,默认设置成纯白就可以了,lightColor就是光源的颜色没什么好说的。facing这个要说一下,我们计算光照强度的时候用了V和R,但有的时候V和R虽然角度小于90度属于有效范围,但是这时候如果L和N的夹角已经大于90度了,实际上这个物体不应该在收到这个光照影响了,但是如果我们只看V和R,那么可能通过reflect函数计算出来的R向量与V的夹角不一定会大于90度,这样子我们就会使本不应该受到光照的地方受到了光照。所以我们加了一个facing变量,如果N和L夹角大于90度,则facing为0,否则为1。而对于max(V·R,0)和我们上节的max(N·L,0)差不多,这里就不说了,在max的外层还有一个指数shininess,这个是用来调整光泽度的,shininess越大,说明物体的表面越光泽,那么你看到的亮斑就越小越集中,否则越大越分散。为什么这么来实现,我也不太清楚,一般设为10较为适合。

前面提到,我们要顺带说一下环境光的问题,这里简单描述一下,现实生活中,一个真实的物体除了从光源出直接接受光照之外,还会受到周围其它物体反射出去的光,有时候即使物体本身并没有受到光源直接照射,也会呈现出一定的亮度。在渲染中我们把这些受到其它物体反射所得到的光统称为环境光。在unity中场景中所有的物体使用统一的环境光,在Edit->Render Setting->Ambient Light你可以设置它,一般比较微弱。它无法与真实世界的效果相媲美,只是一种大致的模拟效果。下面给出《The Cg Tutorial》中给出的环境光计算公式:

其中Ka是材质关于环境光的系数,这个我理解成和漫反射中的Kd保持一致就行了,globalAmbient这个就是我们刚才设置的漫反射颜色。

最终我们要把这些颜色加在一起作为最终的结果 Color = ambient + diffuse + specular。下面看一下具体代码吧。
Shader "Esfog/SpecularReflection"
{
    Properties 
    {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _SpecColor("SpecularColor",Color) = (1,1,1,1)
        _Shininess("Shininess",Float) = 10
    }
    SubShader 
    {
        Pass
        {
            Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma target 5.0
            uniform float4 _LightColor0;
            uniform sampler2D _MainTex;
            uniform float _Shininess;
            uniform float4 _SpecColor;
            struct VertexOutput 
            {
                float4 pos:SV_POSITION;
                float4 posWorld:TEXCOORD0;
                float3 normal:TEXCOORD1;
                float2 uv:TEXCOORD2;
            };
            VertexOutput vert(appdata_base input)
            {
                VertexOutput o;
                o.pos = mul(UNITY_MATRIX_MVP,input.vertex);
                o.posWorld = mul(_Object2World,input.vertex);
                o.normal = normalize(mul(float4(input.normal,0.0),_World2Object).xyz);
                o.uv = input.texcoord.xy;
                return o;
            }
            float4 frag(VertexOutput input):COLOR
            {
                float3 normalDir = normalize(input.normal);
                float3 viewDir = normalize(float3(_WorldSpaceCameraPos - input.posWorld));
                float4 Kd = tex2D(_MainTex,input.uv);
                float4 Ks = _SpecColor;
                float4 Ka = Kd;
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                float3 ambientLighting = Ka.rgb * UNITY_LIGHTMODEL_AMBIENT.rgb;
                float3 diffuseReflection = Kd.rgb * _LightColor0.rgb * max(0.0,dot(normalDir,lightDir));
                float facing;
                if(dot(normalDir,lightDir)<=0)
                {
                    facing = 0;
                }
                else
                {
                    facing = 1;
                }
                float3 SpecularReflection = facing * _LightColor0.rgb * _SpecColor.rgb * pow(max(0,dot(reflect(-lightDir,normalDir),viewDir)),_Shininess);
                return float4(ambientLighting + diffuseReflection + SpecularReflection,1);
            }
            ENDCG    
        }
    } 
    FallBack "Diffuse"
}
有的地方和漫反射的基本相同,我只解释这节中的新地方。

第6~7行在Properties中我们定义了一个新的变量_Shininess,float类型的,和Color类型的_SpecColor,其中_Shininess用来调节物体表面的光泽度。原理在前面解释过了。_SpecColor用来调整高光反射颜色,如果你有高光贴图的话这里就改成2d类型。

第21~22行这两个变量我们在后面要使用,所以声明一下和Properties中的关联。

第26行,我们在VertexOuput中多定义了一个变量,由于我们要计算视线向量,所以必须知道物体在世界空间中的位置(这个不一定,你可以在任何其他空间计算,只要参与计算的两个向量在一个空间就可以).

第35行,直接对模型空间的点左乘_Object2World矩阵转到世界空间就可以得到顶点在世界空间的位置了。

第45~47行为分别为漫反射,高光,环境光的反射系数赋值,注意高光与另外两者的区别。

第49行利用上面的环境光计算公式计算出环境光,其中UNITY_LIGHTMODEL_AMBIENT是Unity提供给我们直接获取场景中环境光颜色的变量。

第51~59行计算镜面反射公式中的facing,原理前面说过。

第60行利用镜面反射公式计算出高光颜色。有两点要注意,第一是给reflect函数传入射光的时候,传的不是反方向,而是真正的入射方向,所以要给我们之前方式计算出来的lightDir前面加个负号,第二点就是Cg中通过pow来计算指数,这个函数一般的语言都会提供,大家应该见过。

第61行把我们计算的漫反射,高光,环境光加在一起作为最终的颜色就可以了。

(~ o ~)~系列教程的第四篇到此结束了,结合前一篇我们大致上对最基本的光照模型有了一些了解,下一篇也许会结合两者做一个实例,或者继续讲其他的地方,之后的内容可能会用到更多的数学原理和思考方法,其实Shader难得不是语法,难得是理解背后的真像,去探索背后的知识,我觉得比仅仅会使用要收获的更多。说实话,我不是一个随便的人,所以写帖子总是力求能写好,写透彻一些,这也让我每写一篇文章都要花费大量的时间,但这其中也让我自己在写教程的过程中能有所收获,发现自己理解不到位的地方,即使补充,如果这些内容能给大家带来一些启发,我也觉得我没有白写。谢谢大家的支持。

继续用上篇中的模型来大概展示一下效果。

上面是使用上篇中漫反射的效果

上面是使用了本篇课的ambient+diffuse+specular的效果。质感一下子就出来了,没有加高光贴图,要么会效果更好。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引