Unity 表面着色器为开发者提供了一层简单快速的编写 Shader 的方式,对开发者来说隐藏了光照模型这些复杂的的概念,但是有时候 Unity 自带的光照模型往往不能满足我们的需求,而需要自己定义光照模型。所以接下来就一起看看 Unity 中常见的光照模型函数。
光照模型是一个用许多光照属性(反射率、高光等)来计算每个像素点上最终的着色函数。光照模型有很多种类,比如早期游戏引擎中的标准光照模型,以及后来的 BRDF 光照模型,还有基于物理的 BRDF 模型等等。
Lambert 光照模型
粗糙的物体表面向各个方向等强度地反射光的现象叫作漫反射,产生这种现象的表面体称为理想漫反射体(Lambert 反射体)。
当场景中仅存在环境光(给予物体在每个顶点处的光照是一样的,如平行光)时,表面某点处的光强:
- Iad = kd x Ia
- Ia 是环境光的强度
- kd 为材质对环境光的反射系数(0 < kd < 1)
当场景中仅有方向光(入射角度不同光线方向也不同,比如聚光灯)的存在,表面某点处的光强:
- Iad = kd * Il * cosθ
- Il 是方向光的强度
- kd 为材质对环境光的反射系数(0 < kd < 1)
- θ 是入射光方向和顶点法线的夹角。当夹角为 0°,说明入射光平行于法线(垂直于表面),此时反射强度最大;当夹角为 90° 时,说明入射光同表面顶点切线平行,此时物体不会反射任何光线。
cosθ 等价于顶点单位法向量 N 与从顶点指向光源的单位向量 L 的点积,所以有 Iad = kd * Il * (N·L)
当场景中两种光线都存在时,表面某点处的光强:
Idiff = kd * Ia + kd * Il * (N·L) = kd * Ia + kd * Il * dot(N, L)
接着我们来看看 Unity 中的 Lambert 光照模型的源码:
inline fixed4 UnityLambertLight (SurfaceOutput s, UnityLight light) { fixed diff = max (0, dot (s.Normal, light.dir)); fixed4 c; c.rgb = s.Albedo * light.color * diff; c.a = s.Alpha; return c; }
可以看出最终的颜色输出的计算就是根据某点本来的颜色值和当前场景中的光强度以及(N·L)的积,其中 N 和 L 的点积只取了大于 0 的部分,因为当值为负时此时光照方向是表面的背面,对于不透明的物体来说不会被渲染。
Half Lambert 光照模型
Half Lambert 用来给在比较暗的区域显示物体。实现也很简单,代码如下:
inline half4 LightingCustomLambert(SurfaceOutput s, half3 lightDir, half atten) { // Lambert half diffLight = dot(s.Normal, lightDir); // half Lambert diffLight = diffLight * 0.5 + 0.5; half4 c; c.rgb = s.Albedo * _LightColor0.rgb * (diffLight * atten * 1); c.a = s.Alpha; return c; }
可以看出在 Lambert 的基础上,通过 diffLight = diffLight * 0.5 + 0.5,使得 diffLight 变大了从而增强了在光线暗的区域的视觉效果。
Phong 光照模型
物理世界的某些物体看起来并不会像 Lambert 光照模型描述的那样简单。比如光滑的金属表面被光照射时,某一点处会有很强的反射光(肉眼可见的光斑)。这是因为在接近镜面反射角的某个区域内,反射了入射光的大部分光强,这种现象称作镜面反射。
在镜面反射中相对于漫反射增加了一个观察角度,Phong 光照模型就是基于光照的方向和用户的视角方向进行计算的。
Phong 光照模型表面某点处的镜面反射光强:
- Is = ks * Il * (R·V) ^ p
- ks 是材质的镜面反射系数 Il 是光强R 为反射光的方向V 表示从顶点到视点的方向p 是高光指数,p 越大反射越集中,当慢慢视线方向偏离反射方向光线开始慢慢衰减,反之 p 越小观察到的光斑区域也就越小,反射光强度也很弱。
在上面的数学模型中,R 应该是我们最陌生的变量。如何求得反射光方向 R?
R + L = 2 * N * (N·L) = > R = 2 * N * (N·L) - L
Is = ks * Il * ((2 * N * (N·L) - L)·V) ^ p
因为在 Phong 光照模型中,我们需要视角方向作为一个输入参数来计算最终的输出值,所以在自定义光照模型函数时,如果需要用到镜面反射,那么我们需要在函数的输入参数中增加视角方向 viewDir,如下:
inline fixed4 PhongLight (SurfaceOutput s, half3 viewDir, UnityLight light)
Phong 光照模型时假设反射型表面的最终光照强度取决于两个因素:漫反射颜色和光线的反射值,所以有:
- I = D + s
- D 是根据 Lambert 计算的漫反射部分
- S 就是上面我们介绍的镜面反射部分
BlinnPhong 光照模型
Blinn 是一种高效计算和模拟高光的方式,它是通过视角方向和光线方向构成的半角向量完成的。BlinnPhong 光照模型则是混合和了 Lambert 的漫反射和 Blinn 计算高光的模式,渲染有时比 Phong 高光更柔和、更平滑,此外它的处理速度相当快。
上面说到,在 BlinnPhong 中我们使用视角方向和光线方向构成的半角向量来计算高光。让我们来看看 BlinnPhong 光照模型表面某点处的光强计算公式:
- Ibp = ks * Il * (N·H) ^ p
- ks 是材质的镜面反射系数
- Il 是光强
- N 为入射点的单位法向量
- H 表示光线方向和视角方向的半角向量
- p 是高光指数,p 越大反射越集中,当慢慢视线方向偏离反射方向光线开始慢慢衰减,反之 p 越小观察到的光斑区域也就越小,反射光强度也很弱。
这里的 H 该怎么计算? 很简单,这里的半角向量 H 就是视角方向 V 和光线方向 L 叠加之后归一化的值:
H = normalize(V, L)
与 Phong 光照模型比较,BlinnPhong 光照模型计算大致一样,不过由于引入了光线方向和视角方向的半角向量来计算使得不用计算反射光,使得计算更加简单快速,使用视角方向和光线方向构成的半角向量来模拟反射向量,事实上这中方式比 Phong 光照模型中在物理上更加精准。
同样,最后我们也来看看 Unity 中的 BlinnPhong 光照模型的代码:
inline fixed4 UnityBlinnPhongLight (SurfaceOutput s, half3 viewDir, UnityLight light) { half3 h = normalize (light.dir + viewDir); fixed diff = max (0, dot (s.Normal, light.dir)); float nh = max (0, dot (s.Normal, h)); float spec = pow (nh, s.Specular*128.0) * s.Gloss; fixed4 c; c.rgb = s.Albedo * light.color * diff + light.color * _SpecColor.rgb * spec; c.a = s.Alpha; return c; }
上面的代码中首先计算了光线方向和视角方向的半角向量 h,接着计算了 Lambert 光照模型中计算光强的乘法因子 diff,然后又计算了法向量和 h 的点积 nh,最后通过指数计算得到了高光乘法因子 spec,最终输出就是 Lambert 光照模型得到的漫反射值以及 BlinnPhong 光照模型得到的高光反射值的和。
参考
《Unity 5.x Shaders and Effects Cookbook》
https://blog.csdn.net/taoqilin/article/details/52800702
本文转自: 卢俊的Blog,转载此文目的在于传递更多信息,版权归原作者所有。