传统经验光照模型


物体为什么呈现各种各样的颜色是因为,光照射到物体表面时,物体对光会发生反射、透射、吸收、衍射、折射、和干涉等物理情况,也就是说,即便我们想渲染出一束光打在一个石膏球上那么一个简单的场景,在物理上也是非常复杂,更不要说在游戏中试试渲染。因此大佬们基于各种物理现象的实验归纳总结出了一些基于物理的渲染公式(PBR,Physically Based Redenring),但也是比较复杂且开销较大,在以前的硬件设备上是无法很好应用的。那么在以前当然也是有一些光照模型的,这些模型被称为传统经验光照模型,这些简化公式也是有着很好的效果的。就算法理论基础来说,光照模型分为基于物理理论的模型和经验模型,而从使用效果来说,则分为局部光照和全局光照。

一个简单粗暴的定义,在局部光照模型中我们只关心直接光照部分:

光线与物体相交,通常会发生吸收(absorption)和散射(scattering)两种情况。吸收不改变光线的方向,但会改变光线的密度和颜色;散射只改变光线的方向,不改变光的密度和颜色。其中,当散射光线进入到物体内部,这种现象被称为折射(refraction)或透射(transmission);当散射光线到物体外部,会被称为反射(reflection)。对于不透明物体,光线进入物体内部会继续与内部更小的颗粒相交,最终有一些光线被物体吸收,另一些则重新发射出物体表面。

漫反射与镜面反射用来区分这两种不同的散射情况,漫反射代表有多少光线被折射,吸收和散射出表面,镜面反射呈现了光线被反射的效果。

1.漫反射(Diffuse)

经典的兰伯特定律(Lambert'law),反射光线的强度,物体表面发现与光线方向的夹角的余弦值,这两者成正比。C light代表光源颜色,M diffuse代表材质的漫反射颜色。N^,L^代表归一化后的物体表面法线和光源方向,两者点乘结果是(-1,1)但是颜色范围是(0,1)所以负值部分是没有意义的,因此用max把负值截断为0。用通俗的话来讲就是,物体表面法线与光源方向有多接近,越接近越明亮。

 

 1 Shader "Custom/LambertShader"
 2 {
 3    Properties
 4     {
 5         _Diffuse ("Diffuse",Color) = (1,1,1,1)
 6         _LightColor ("LightCol",Color) = (1,1,1,1)
 7     }
 8     SubShader
 9     {
10   
11         Pass
12         {
13             Tags{ "LightMode" = "ForwardBase"}
14             CGPROGRAM
15             #pragma vertex vert
16             #pragma fragment frag
17             #include "UnityCG.cginc"
18             #include "Lighting.cginc"
19  
20             float4 _Diffuse;
21             float4 _LightColor;
22  
23             struct appdata
24             {
25                 float4 vertex : POSITION;
26                 float3 normal : NORMAL;
27             };
28             struct v2f
29             {
30                 float4 pos : SV_POSITION;
31                 float3 nDirWS : TEXCOORD0;
32             };
33             v2f vert (appdata v)
34             {
35                 v2f o = (vert)0;
36                 o.pos = UnityObjectToClipPos(v.vertex);
37                 o.nDirWS = UnityObjectToWorldNormal(v.normal);
38                 return o;
39             }
40             fixed4 frag (v2f i) : SV_Target
41             {
42                 float3 nDir = i.nDirWS;
43                 float3 lDir = normalize(_WorldSpaceLightPos0.xyz);              
44                 float Lambert = max(0.0,dot(nDir,lDir)) ;
45                 float3 diffuse = _LightColor.rgb * _Diffuse.rgb * Lambert;
46                 return float4(diffuse,1.0);
47             }
48             ENDCG
49         }
50     }
51 }

 上图即是兰伯特模型的结果,但是有个问题,背光面实在是太黑了与我们的认知不符(因为现实中还有间接光照,这里咱不讨论)。在早期的游戏里看到这种光照模型会感到一团黑,假如光源在角色侧面,那么角色正面就会一团黑。于是Valve在半衰期Half-Life中稍微改进了一下兰伯特模型:

半兰伯特模型并没有使用max截断负值,而是让点乘结果乘上一a值再加上一个b值做映射,一般来说a和b都是0.5,这么一来渲染效果便明亮了许多。但是半兰伯特模型是没有物理理论依托的,纯粹是看起来不错,那就这么做了,所以再次说明此为是经验模型。

 

2.镜面反射/高光反射(Specular)

我们这里用Phong模型来计算镜面反射,r代表光线根据物体表面法线计算得出的光线反射方向,v代表我们的视角反向也就是摄像机的方向(注意是从物体表面朝着摄像机),M specular代表材质镜面反射的颜色,M gloss(或shininess)代表材质的光泽度。

 其中M gloss直白来说控制着,这个物体表面有多光滑,越光滑高光点集中,越粗糙高光点越分散。左图mloss=10,右图mgloss=35。

Blin-Phong模型,对Phong模型的改进:

Phong模型所需向量中,计算r和v计算量略多(相较于Blin-Phong模型),作为改进Blin-Phong只需的h(半程向量),计算量减少且容易获得。

就效果来说,两者也是有些许区别的,下左图为Phong,下右图为Blin-Phong。可以看出Blin-Phong表现更柔和,Phong有明显的明暗交界。

 这是因为Phong模型考虑的是反射方向与视方向的夹角,当两者超过九十度时,负值就会被max截断为0,实际上即便超过九十度也是会有影响的。

 而Blin-Phong考虑的是半程向量与法线的夹角,无论观察者从哪个角度看,两者夹角都不会超过九十度。

需要注意的是,同一观察角度下,半程向量与法线夹角通常小于反射方向与视方向的夹角,因此使用Blin-Phong时要想获得跟Phong差不多的效果需要把mgloss调整为2-4倍。

以下例子中,phong为8.0;Blin-Phong为32.0

 1 Shader "Custom/LambertShader"
 2 {
 3    Properties
 4     {
 5         _Shininess ("Shininess",float) = 10.0;
 6         //_Diffuse ("Diffuse",Color) = (1,1,1,1)
 7         _LightColor ("LightCol",Color) = (1,1,1,1)
 8     }
 9     SubShader
10     {
11   
12         Pass
13         {
14             Tags{ "LightMode" = "ForwardBase"}
15             CGPROGRAM
16             #pragma vertex vert
17             #pragma fragment frag
18             #include "UnityCG.cginc"
19             #include "Lighting.cginc"
20  
21             float4 _Diffuse;
22             float4 _LightColor;
23             float _Shininess;
24  
25             struct appdata
26             {
27                 float4 vertex : POSITION;
28                 float3 normal : NORMAL;
29             };
30             struct v2f
31             {
32                 float4 posCS : SV_POSITION;
33                 float4 posWS : TEXCOORD0;
34                 float3 nDirWS : TEXCOORD1;
35             };
36             v2f vert (appdata v)
37             {
38                 v2f o = (vert)0;
39                 o.posCS = UnityObjectToClipPos(v.vertex);
40                 o.posWS = mul(Unity_ObjectToWorld,v.vertex);
41                 o.nDirWS = UnityObjectToWorldNormal(v.normal);
42                 return o;
43             }
44             fixed4 frag (v2f i) : SV_Target
45             {
46                 //准备计算所需的向量
47  
48                 //世界空间下法线方向
49                 float3 nDir = i.nDirWS;
50                 //世界空间下光源方向
51                 float3 lDir = normalize(_WorldSpaceLightPos0.xyz);
52                 //计算视方向              
53                 float3 vDir  = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz);
54                 //计算光线反射方向
55                 //float3 rDir = normalize(reflect(-lDir,nDir);
56                 //计算半程向量
57                 float3 hDir = normalize(vDir + lDir);
58                  
59                 //计算Bling-Phong光照模型
60                 //float Lambert = max(0.0,dot(nDirWS,lDir)) ;
61                 //float3 diffuse = _LightColor.rgb * _Diffuse.rgb * Lambert;
62                 float3 finalRGB = _LightColor * _SpecularColor * pow(max(0.0,dot(nDir,hDir)),_shininess);
63                 return float4(finalRGB,1.0);
64             }
65             ENDCG
66         }
67     }
68 }

3.环境光照(Ambient)

在局部光照模型中,我们没有计算间接光,但是环境光照是个很重要的部分,我们可以用一种简单的方式来考虑环境光照。

比如下面例子,英雄联盟里面的野区,基本上野区都是被绿色植物覆盖,加上moba游戏类型,英雄,野怪等角色实际上占屏幕像素很小的一部分,大多时候我们注意不到也不会去注意角色身上的光照细节,因此我们可以近似影响野怪的环境光就是植被的绿色,尤其是野怪的下半身。而野怪的头顶是否也受到天空的蓝色影响呢?这里就是我们的解决方法,简单的单色环境光。

 

1 //身体不同部分受不同单色环境光影响
2 //模型法线向上部分,代表头顶
3 float upMask = max(0.0,nDir.g);
4 //模型法线向下部分
5 float downMask = max(0.0,-nDir.g);
6 //模型法线周围部分
7 float sideMask = 1.0 - upMask - downMask;
8 //各个部分分别乘各自部分的颜色
9 float3 envCol = _EnvUpCol * upMsk + _EnvDownMask * downMask + _EnvSideCol * sideMask

我们输出一下法线作为颜色看看:

 

当然用法线其他通道输出也是可以的,看个人习惯,我们输出一下法线xyz作为颜色rgb看看:

可以看到,绿通道代表上部。

参考资料

1.Unity Shader入门精要--冯乐乐

2.https://developer.valvesoftware.com/wiki/Half_Lambert

3.https://learnopengl-cn.github.io/05%20Advanced%20Lighting/01%20Advanced%20Lighting/