前两个章节我们讲述了冯氏光照模型的环境光和漫反射光,本节我们学习组成冯氏光照的最后一个因素:镜面高光。
镜面高光现象
大家小时候应该都做过这样的恶作剧,上课的时候拿一面镜子,对准某个同学,慢慢调整镜子的角度,直到反射的太阳光照在对方的脸上,然后引起该同学的极度不适。
有没有想过为什么会有这种现象?
有的同学答了,这是因为镜子的反射光正好进入了同学的眼睛里。
说的没错,那假如我拿一件衣服来反射太阳光,能不能达到同样的效果。
很多同学脱口而出:不能。
是的,可是大家有没有想过为什么不能?
也许大家会出于直觉回答,因为衣服不反光,镜子反光。其实也对,但不太严谨。事实上衣服也反光,只是衣服表面过于粗糙,光线被散射到了各个方向,不能集中射向一个方向,导致进入人眼的光线强度大大削弱。镜子就不同了,镜子比较光滑,光线反射方向比较统一,进入人眼的光线强度也就越多,从而产生刺眼的效果。
冯氏光照模型使用镜面高光分量
来模拟这种现象。
镜面高光的表示与计算
与漫反射分量相同,镜面高光也是根据光线的入射方向向量和法向量来决定的,只不过镜面高光还需要依赖视线的观察方向,也就是眼睛是从什么方向观察的物体。
视线方向向量与反射光向量的之间的夹角越小,夹角余弦值就会越大,那么人眼感受到的光照就会越强,反之,光照越暗。因此,我们使用夹角的余弦值表示镜面高光因子,然后再用镜面高光因子乘以光线颜色即可求出镜面高光分量:
- 1、首先需要求出反射光向量
reflectDirection
和人眼视线方向向量viewDirection
。 - 2、归一化两个向量。
- 3、求出两个归一化向量的点积,得到镜面高光因子。
- 4、将上一步求出的高光因子乘以光线颜色,得到镜面高光分量。
本节我们使用 GLSL 内置的反射向量算法reflect(inVec, normal)
,其中 inVec
为入射向量,方向由光源指向入射点,normal
为入射点的法向量。
如何实现镜面高光
接下来,我们开始编码实现镜面高光效果。
计算反射光向量
反射光向量在片元着色器中实现,参照上一节漫反射分量的计算,我们已经有了光源位置u_LightPosition
和入射点位置v_Position
,所以可以求得入射光向量:
//求出入射光向量
vec3 lightDirection = v_Position - u_LightPosition
切记,在使用GLSL 的reflect 函数计算反射光向量时,一定要确保入射光向量的方向是从光源位置指向入射点位置。
有了入射光向量,我们还需要入射点的法向量v_Normal
,这个值已经从顶点着色器中插值化后传到片元着色器了,所以我们可以直接拿来用:
vec3 reflectDirection =reflect(normalize(lightDirection), normalize(v_Normal));
这样就求出了反射光向量,接下来我们计算视线观察向量。
计算视线观察向量
我们将入射点到观察者的方向向量定义为视线观察向量,为了计算这个向量,我们需要知道入射点的位置以及观察者的位置,入射点的位置我们有了,现在需要观察者的位置,我们将人眼在世界坐标系下的坐标作为观察者位置,然后将其用 uniform
变量的形式传递到片元着色器中。
因此我们的片元着色器要增加一个 uniform
变量接收观察者坐标。
// 观察者坐标。
uniform vec3 viewPosition;
有了观察者坐标,我们就可以计算出视线观察向量了。
// 视线观察向量
vec3 viewDirection = viewPosition - v_Position;
计算镜面高光因子
前面求出了视线观察向量和反射光向量,接下来我们就可以计算镜面高光因子了。
首先,归一化视线观察向量
和反射光向量
viewDirection = normalize(viewDirection);
reflectDirection = normalize(reflectDirection);
然后计算这两个向量的点积,这里要注意一点,就是如果这两个向量的点积为负数,则说明视线观察向量和反射光向量大于 90 度,是没有反射光进入眼睛的,所以我们使用 max
函数取点积和 0 之间的最大值。
// 镜面高光因子
float specialFactor = dot(viewDirection, reflectDirection);
// 如果为负值,一律设置为 0。
specialFactor = max(specialFactor, 0.0);
完整的片元着色器程序如下:
precision mediump float;
varying vec4 v_Color;
uniform vec3 u_LightColor;
uniform float u_AmbientFactor;
uniform vec3 u_LightPosition;
varying vec3 v_Position;
varying vec3 v_Normal;
uniform vec3 u_ViewPosition;
void main(){
// 环境光分量
vec3 ambient = u_AmbientFactor * u_LightColor; //环境光分量
// 光线照射向量
vec3 lightDirection = v_Position - u_LightPosition;
// 归一化光线照射向量
lightDirection= normalize(lightDirection);
// 漫反射因子
float diffuseFactor = dot(normalize(lightDirection), normalize(v_Normal));
// 如果大于 90 度,则无光线进入人眼,漫反射因子设置为0。
diffuseFactor = max(diffuseFactor, 0.0);
// 漫反射光照
vec3 diffuseLightColor =u_LightColor * diffuseFactor;
// 归一化视线观察向量
vec3 viewDirection = normalize(v_Position - u_ViewPosition);
//反射向量
vec3 reflectDirection = reflect(-lightDirection, normalize(v_Normal));
// 初始化镜面光照因子
float specialFactor = 0.0;
// 如果有光线进入人眼。
if(diffuseFactor > 0.0){
specialFactor = dot(normalize(viewDirection), normalize(reflectDirection));
specialFactor = max(specialFactor,0.0);
}
// 计算镜面光照分量
vec3 specialLightColor = u_LightColor * specialFactor * 0.5;
// 计算总光照
vec3 outColor = ambient + diffuseLightColor + specialLightColor;
// 将物体自身颜色乘以总光照,即人眼看到的物体颜色。
gl_FragColor = v_Color * vec4(outColor, 1);
}
加入光照之后,我们的着色器代码就变多了,但其实并不复杂,仅仅是取值
、计算
、赋值
操作而已。
JavaScript 部分
镜面光照我们需要为着色器传递一个人眼观察位置,所以我们的JavaScript 部分需要修改:
// 获取着色器全局变量 `u_ViewPosition`
var u_ViewPosition = gl.getUniformLocation(program, 'u_ViewPosition');
将人眼观察位置放置在 z 轴正方向 10 位置,即物体的前面。
var uniforms = {
eyeX: 0,
eyeY: 0,
eyeZ: 10
};
gl.uniform3f(u_ViewPosition, uniforms['eyeX'], uniforms['eyeY'], uniforms['eyeZ']);
完整的代码大家可以参见这里,我们比较下加入镜面高光前后的效果。
无镜面高光时:
添加镜面高光后:
观察上面两幅图,我们能很直观地看到添加镜面高光后,球体正中央有一个明晃晃的光圈,符合真实世界中的场景。
反光度
但是,这个刺眼的光圈面积太大了,我们需要给它添加一个称为反光度
(shininess)的参数约束光圈的大小,一个物体的反光度越大,反光率就越强,散射的光就越少,我们看到的高光面积就越小。
我们定义一个u_Shininess
的变量表示物体的反光度,然后用前面求得的高光因子乘以 2 的shininess次幂作为最终的高光因子。这样就可以让我们的光圈变得小一些。
求幂计算可以通过GLSL 内置的公式 pow(2, shininess)求得。
specialFactor = max(specialFactor, 0.0);
specialFactor = pow(specialFactor, u_Shininess);
一般情况下,我们设置物体的反光度为 32 就可以了,但是特殊场景下,效果可能不理想,这时候,我们就需要根据实际情况调整反光度了。
我们将反光度设置成 32,看下增加反光度后的效果:
这会效果明显好多了,我们可以看到光圈点很小了,大家可以点击此处查看我做的 demo,调节反光度看下效果。
Blinn-Phong光照模型
冯氏光照模型不仅能够很好的近似真实光照,而且性能也相当高。但是 这种光照在某些场景下仍然有些缺陷,大家观察前面没有添加反光度时的图片,应该能发现高光光圈边缘有一圈很明显的暗灰色断痕,但大家再看一下增加反光度后的效果,却没发现这种现象。这是为什么呢?
产生这个问题的原因是,在高光边缘部位,由于人眼视线向量和反射光向量夹角大于90度,那么夹角的余弦值便小于 0,按照冯氏光照模型的镜面光照算法,夹角余弦值小于 0 时, 我们的镜面高光分子系数就会用 0 来代替。所以高光边缘部位及以外的部分就没有了镜面光照分量,试想一下,如果反光度越小,镜面高光区域就越大,那高光区域边缘部位漫反射光的分量所占比重就会比较小,在高光边缘部位就会产生一种较大的亮度差,给人一种暗灰色断痕的感觉。反之,反光度越小,光圈越小,相应地,光圈周围漫反射光的分量所占比重就比较大,所以不会在高光边缘产生过大的亮度差。
如下图,反射光线和视线观察向量之间的夹角γ 大于90度,所以此时镜面高光分量为 0。
其实,这种观察角度,镜面高光分量还是应该有的,只是值比较小而已。所以,出现了 Blin Phong 光照模型,这种光照模型不再利用反射向量,而是采用了半程向量
,半程向量是视线和反射光之间夹角的一半方向上的单位向量,利用半程向量和法向量之间的夹角余弦来表示镜面高光因子,半程向量和法向量之间的夹角越小,镜面高光分量越大,如下图所示:
实现 Blin Phong 光照
我们在冯氏光照代码的基础上加以修改,实现 Blin 光照模型。与冯氏光照模型不同的是,我们需要半程向量,半程向量该如何求呢?
按照向量的计算规则,半程向量只需要我们将视线观察向量和反射向量相加,然后将得出的结果归一化就可以求出了。
// 计算半程向量
vec3 halfVector = normalize(reflectDirection + viewDirection);
// 计算高光因子
float specialFactor = dot(normalize(v_Nomral), halfVector);
利用 GLSL 的内置函数,我们就很容易的求出来了。
从冯氏光照模型进化成 Blin光照模型,我们只需要改动这么一处就可以了,是不是觉得很简单呢?
算法是很简单,但是我希望大家还是能够把算法背后的原理搞清楚。这才是大家学习的目的。
好了,我们比较一下反光度同时为 1 的时候,冯氏光照和 Blin 光照之间的差别。
冯氏光照效果:
Blin 光照效果:
可以看到,采用 Blin光照模型
后, 镜面高光区域过度的更加自然。
回顾
至此,我们就讲完了冯氏光照模型,以及为了解决冯氏光照的缺陷而引入的 Blin 光照模型
。
本节也涉及到了许多向量矩阵之间的计算,大家多加练习,不要被这些计算搞晕了。
接下来我们进入下一个环节的学习,对 GLSL 的语法进行一个总结。