1. 光照
显示世界中,光照环境往往是相对复杂的。因为假设太阳作为世界的唯一光源,那么太阳光照在物体A上A将阳光进行反射后,A又做为一个新的光源共同作用于另一个物体B。所以于B来讲光源是复杂的。然而这只是其中一个因素,受制于天气、温度等其他情况我们需要考虑的因素更多。在OpenGL中我们仅考虑一些简单的模型,对现实情况进行一个近似的模拟。这一节中我们主要介绍一下冯氏光照模型
。冯氏光照模型中主要有3个分量构成:
- 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
- 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
- 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。
接下来我们将逐一讲解这三个分量。
2. 环境光照
上文中提到了,真实世界中的光源情况是复杂的。如果对系统中的每一个光源都进行考虑,这种算法叫做全局照明算法
。但是这种算法既开销高昂又极其复杂。我们使用环境光照
来简化这个概念,即使用一个很小的常量光照颜色,叠加到实际光照颜色中。
我们看到此处直接用环境颜色与物体颜色相乘。这是因为当前阶段我们还没有添加光源。
3. 漫反射光照
当光线照射在平面上时会发生镜面反射,这是一个光学常识。但事实上真实世界中的物体表面一般都不是完全的平面而是凹凸不平的(微观上的凹凸不平),此时将发生漫反射。漫反射的结果就是,你不仅可以从光源的镜像角度可以观察到物体,从各个角度你都可以观察得到。不过漫反射的强度区域光源射入平面的夹角有关。通过实验,我们观察到当射入角度与平面法线夹角越小时,漫反射强度越强。
我们要在GL中模拟漫反射效果。从上述的叙述中我们知道,漫反射的关键因素在于法线夹角。那么也就是说,我们在GL中需要两点,第一个是法向量
,第二个是光线射入的角度
。
法向量是相对于平面而言的,于点是没有意义的。而同一点在不同平面中对应的法向量也是不同的。这就决定了我们需要改造我们的顶点数据,传一组法向量给顶点着色器,此外绘制VAO时也不能以EBO绘制,而应该以VBO绘制。
虽然对灯的着色器使用不能完全利用的顶点数据看起来不是那么高效,但这些顶点数据已经从箱子对象载入后开始就储存在GPU的内存里了,所以我们并不需要储存新数据到GPU内存中。这实际上比给灯专门分配一个新的VBO更高效了。
4. 计算漫反射光照
顶点数据中我们传入了法向量,我们还需要一个角度。顶点着色器中我们是可以拿到顶点数据的,这时只要我们拿到光源的位置即可以计算出光的方向了,标准化以后就是方向向量了。光源的位置一般是相对固定的,我们可以用uniform变量在外界对着色器进行赋值。记得将光源坐标和顶点坐标都转化为世界坐标哦,或者保证光源坐标、顶点坐标和法向量在同一坐标系统内
也可。法向量和方向向量都记得要标准化,这样他们点乘的结果才是两个向量的夹角余弦值。
最后,如果夹角余弦值小于零,我们认为这时无意义的,他将造成我们的漫反射分量为负值,而真实世界中,仅可能是无影响,而不会是负影响。所以我们小于0时我们取0。
片段着色器中的代码大概是这个样子的:
还有一件事,之前说过,要保证光源坐标、顶点坐标和法向量在同一坐标系统内
。一般情况下我们会将他们转化为世界坐标,因为这更符合我们的直觉。我们要把法向量也转换到世界空间内。顶点数据中,你所写的法向量直觉上应该是局部坐标,所以要经过模型矩阵转换。此外,模型矩阵中我们也可能对物体进行非比例变化,这将导致我们的法向量发现也发生改变。所以在把法向量从局部坐标转化至世界坐标时,我们要经过法线矩阵
的转换。我们可以通过逆矩阵和转置矩阵来变化模型矩阵。所以顶点着色器中我们的代码大概是这个样子的:
其中inverse()是求逆矩阵函数,transpose是求转置矩阵函数。
5. 镜面光照
那么,最后我们只要再将镜面反射叠加进去就好了。
跟漫反射的原理差不多,我们看到物体的颜色跟物体表面镜面反射的光线与我们观察的视角的夹角有关。当夹角越小时,那么镜面光的影响越大。与我们的效果就是,我们将看到一个高光。
那么我们还是需要两个方向向量,第一个是镜面光的方向向量
,第二个是观察角度的方向向量
。
镜面光的方向我们可以根据光源方向及法向量通过反射计算出来。观察方向则是我们摄像机的位置与观察点所构成的向量。
所以我们的两个向量大概是这个样子的:
我们看到,观察方向需要我们传入一个摄像机位置,我们需要在渲染循环中将摄像机的世界坐标传给片段着色器。
计算反射光时,我们使用的是reflect函数。这个函数需要的是传入一个从光源指向平面的向量以及平面的法向量。这里之所以我们传入-lightDir是因为我们之前计算的lightDir是从平面指向光源的。
这里我们最好给一个镜面强度系数,以免当光源垂直照向平面时,我们的镜面光分量过于明亮。
两个向量获取后我们就可以计算他们的夹角来计算我们的镜面光分量了。
首先我们看镜面系数的计算方式,点乘求夹角余弦值后取大于0的值,然后调用pow函数。pow是取这个系数的指定次幂。我们指定了32,代表我们指定了他的反光度
是32。一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。在下面的图片里,你会看到不同反光度的视觉效果影响:
至此我们计算出镜面系数,用光源颜色乘镜面系数乘镜面强度系数后即可获得镜面分量。
最后的最后,我们将环境光照、漫反射光照及镜面光照进行叠加后,与物体颜色相乘,即可获得物体在冯氏光照光源影响下所展示的颜色了。
在光照着色器的早期,开发者曾经在顶点着色器中实现冯氏光照模型。在顶点着色器中做光照的优势是,相比片段来说,顶点要少得多,因此会更高效,所以(开销大的)光照计算频率会更低。然而,顶点着色器中的最终颜色值是仅仅只是那个顶点的颜色值,片段的颜色值是由插值光照颜色所得来的。结果就是这种光照看起来不会非常真实,除非使用了大量顶点。
在顶点着色器中实现的冯氏光照模型叫做Gouraud着色(Gouraud Shading),而不是冯氏着色(Phong Shading)。记住,由于插值,这种光照看起来有点逊色。冯氏着色能产生更平滑的光照效果。