一,光照的基本概念
运行 OpenGL 程序在屏幕上显示的最终颜色,受场景中光线的特性以及物体反射和吸收光的属性(即材质)影响。光线可能来自特定的位置与方向,也可能是散布在整个场景中(环境光);而物体表面能够吸收,反射光线,有些物体本身还能够发射光线,物体的这些属性被称为材质。在 OpenGL 中,物体的材质属性通过反射不同方向的环境光,漫反射光,镜面光的RGB颜色来表示的。光照计算就是将发射光,泛射光,漫反射光以及镜面高光四个成分分别计算,然后再累加起来。
光特性
发射光(emission):由物体自身发射的光。如果物体本身不发光,则无此属性;
环境光(ambient):就是哪些在环境中进行了充分散射的光,而无法分辨其方向的光。光线在物体表面上向各个方向上均匀泛射,场景中的物体都会泛射光,这些泛射光又会照射到其他物体上继续被泛射,直到光子能量耗尽为止,这样整个场景中散布着这样的泛射光。在编程时,可通过设置一个颜色常量来表示环境光或使用 ambient occlusion map (环境闭包贴图)来处理环境光。在 OpenGL 中,全局环境光的强度为 (0.2, 0.2, 0.2, 1.0),这弱弱的白色全局环境光确保即使没有额外的光源,场景中的物体依然是可见的。
漫反射光(diffuse):光线来自某个方向,但在物体表面上向各个方向上反射,无论在何处观察,散射光看上去亮度都相同。我们之所以能看到物体,就是因为物体将入射的光然后向各个方向反射(所以称之为漫反射)。物体的漫反射材质属性对物体的颜色起着决定性作用。
镜面高光(specular):光线来自一个特定的方向,然后在物体表面上以一个特定的方向反射出去(镜面反射)。正是由于镜面高光让物体看起来有光泽,如台球,金属表面的光泽。镜面反射与物体的镜面反射材质属性有很大关系,如金属表面能产生很高的镜面发射,而地毯几乎没有镜面反射。在 OpenGL 中,镜面光的强度可通过光泽度(shininess)来调节。
材质属性
物体的材质属性取决于物体反射的红绿蓝光的比例。与光源的特性相似,材质也分为泛射材质,漫反射材质,镜面材质和反射材质,分别表示反射相应类型光的反射率。
泛射材质:环境光的反射率影响物体的整体颜色,因为直射到物体上的漫反射光最亮,而没有被直射的物体的环境反射光最明显。一个物体的泛射光反射率受全局环境光和来自光源的环境光的双重影响。泛射光的反射率不受观察点点位置的影响。
漫反射材质:漫反射的反射率对物体的颜色起着最重要的作用,它受入射的漫反射光颜色以及入射光与法线的夹角的影响,但不受观察点位置的影响。
镜面反射材质:表面较为光洁的物体由于反射光线而会产生亮斑(看起来有光泽),在这种亮斑被称为镜面反射光。在 OpenGL 中物体对光的镜面反射材质属性决定了光泽的颜色、大小和亮度。镜面反射光的强度还取决于观察点的位置,当观察点正好处于入射光的反射光线上,亮斑的亮度到达最大值。
发射材质:前面提到的三种材质都是被动地反射来自外界的光线,而有些物体本身可以发射光。在 OpenGL 中是通过给物体设置发射材质颜色来模拟的,物体表面的发射颜色可以增加物体的强度,但不受任何光源的影响。此外,在整个场景中,发射光并没有被当作一种额外的光照。
二,光照的计算
前面说了,光照效果是由发射光,环境光,漫反射光以及镜面高光四部分组成,这四部分各自独立计算,然后再累加起来得到最终的光照效果。下面就来详细介绍这些组成部分是如何计算的。
发射光计算
前面说过,在 OpenGL 中是给物体材质设置发射颜色来模拟发射光的,因此它的计算非常简单:
发射颜色 = 物体的发射材质颜色
环境光计算
前面说到,一般我们是设置一个颜色常量来表示环境光(或来自 ambient occlusion map),因此环境光的计算也是很简单的。
环境颜色 = 光源的环境光颜色 × 物体的环境材质颜色
漫反射光计算
漫反射如下图所示:
漫反射颜色 = 光源的漫反射光颜色 × 物体的漫反射材质颜色 × DiffuseFactor
其中漫反射因子 DiffuseFactor 是光线与顶点法线向量的点积:
DiffuseFactor = max(0, dot(N, L))
在图形学中,点积几何意义其实就是表示两个向量之间夹角的 cos 值。因此这个公式直观地揭示了漫反射的规律:顶点法线正对入射光线,漫反射效果最强,顶点法线背对入射光线(角度大于等于90度)就完全没有漫反射效果。
注意:在光照计算中,顶点法线必须是经过规范化的(normalize)。
我们可以设置一个颜色常量来表示漫反射材质颜色,来得到平滑颜色的表面。但有些物体表面是很粗糙的,如裂缝,隆起,刮痕等。为了获得这样的粗糙表面效果,可以使用 displaced polygon 技术,但这需要大量的计算,效率低下。另外一种更为高效的方式就是使用凹凸贴图(bump map)。凹凸贴图就是一种纹理,其内容包含编码在RGB颜色空间中的经过扰动的顶点法线。在使用凹凸贴图进行漫反射计算时时,首先和前面一样用正常法线计算出 NormalDiffuseFactor,再根据扰动的顶点法线计算出 PerturbedDiffuseFactor,然后将两者相乘作为最终的 DiffuseFactor。如下图所示:
普通漫反射 | 凹凸贴图 |
![]() | ![]() |
镜面反射
镜面反射如下图所示:
镜面反射颜色 = 光源的镜面光颜色 × 物体的镜面材质颜色 × SpecularFactor
SpecularFactor = power(max(0, dot(N, H)), shininess)
H = normalise(L + E)
与漫反射不同,镜面反射受观察者的位置影响,这一点在上面的计算公式中可以清楚地看出来。H 向量是视线向量 E 与光线向量 L 的半向量(注意:它经过规划化的),其几何意义就是视线与光线夹角的平分线。而 H 和 N 的点积的几何意义就是说这个平分线与法线的夹角的 cos 值,然后将这个 cos 值进行 shininess 次乘方计算得到最终的镜面反射因子 Specularfactor。 这里的关键点在于视线与光线的平分线与法线的点积计算上,这表示,当视线与光线在表面处的反射光线夹角(可看成是N与H的夹角)越少时,镜面反射效果最明显(角度越小,cos 值越大)。
一般可使用常量颜色作为镜面材质颜色,但这样得到的效果有时候并不理想。同漫反射一样,我们也可以使用镜面贴图来获得更好的镜面反射效果。镜面贴图通过控制物体表示上特定像素允许的镜面反射强度来获得较为真实的效果。
普通镜面反射 | 镜面贴图纹理 | 镜面贴图反射 |
![]() | ![]() | ![]() |
衰减因子
光源发射的光线在其传播过程中,会与空气中的其他粒子碰撞,其能量会逐渐衰减。在 OpenGL 中,这是通过将光照强度乘以随传播距离变化的衰减因子来模拟实现的。这个衰减因子的计算公式如下:
衰减因子 = 1.0/(距离衰减常量 + 线性衰减常量 × 距离 + 二次衰减常量 × 距离的平方)
其中距离衰减常量,线性衰减常量和二次衰减常量均为常量值。
注意:环境光,漫反射光和镜面光的强度都会受随着距离的增大而衰减,只有发射光和全局环境光的强度不会受此影响。
聚光灯因子
在这里,我们讨论聚光灯的发射光照计算(也即位置型光源:如台灯,相对方向性光源:如太阳)。聚光灯就是朝某个特定发射发射光线的光源。你可以想象下漆黑的夜晚里,一个手电筒给与你光明的这个场景,手电筒就是一个很好的聚光灯示例。聚光灯的计算分为两部分:在光线照射角度范围之外的部分被忽略,只有在照射角度范围之内的部分需要计算。
聚光灯夹角cos值 = power(max(0, dot(单位光源向量, 单位光线向量)), 聚光灯指数)
其中单位光线向量是从光源指向顶点的单位向量,聚光灯指数表示聚光灯的亮度程度。前面说过点积的几何意义就是表示角度的,这里聚光灯因子就表示光源向量与光线向量之间的夹角。
而为了模拟真实聚光灯光环效果,在照射角度范围之内与之外的接壤处,设置一个渐变过渡区域,以避免光照从有到无巨变:
无过渡 | 有过渡 |
![]() | ![]() |
增加过渡区之后的计算如下:
内环是完全光照的,而过渡区域(中环)是从完全光照到完全没有光照的渐变过渡,外环是完全没有光照的。
聚光灯因子 = clamp((外环的聚光灯角度cos值 - 当前顶点的聚光灯角度cos值)/(外环的聚光灯角度cos值 - 内环聚光灯角度cos值), 0, 1)
clamp 函数是将聚光灯因子限定在[0, 1]之间,这样,内环是完全光照的,而中环是从完全光照到没有光照的渐变过渡,外环是没有光照的。因此:
聚光灯效果 = 聚光灯光源颜色 × 聚光灯因子
光照计算的终极公式
光照颜色 = 发射颜色 + 全局环境颜色 + (环境颜色 + 漫反射颜色 + 镜面反射颜色) × 聚光灯效果 × 衰减因子
如果场景中有多个光源(包括环境光),那么分别计算来每个光源的光照颜色,然后把这些光照颜色累加即可。如果物体不发射光,则没有发射颜色这一成分。
三,高洛德着色(Gouraud Shading)与冯氏着色(Phong Shading)
在图形渲染中有两种着色方式,高洛德着色与冯氏着色。高洛德着色也被称为Per-Vertex着色,它是在顶点着色阶段对顶点进行颜色计算,然后在光栅化阶段对这些顶点颜色进行线性插值形成片元的颜色;冯氏着色也被称为Per-Pixel像素着色,它是在片元着色阶段对每一个片元(像素)进行颜色计算。无疑,插值的颜色效果没有针对每一个片元进行颜色计算的效果好(除非你的图元切分到像素近似大小,不过这样 GPU 肯定吃不消,计算量巨大!)。
Gouraud 着色 | Phong 着色 |
![]() | ![]() |
四,结语
光照是个很复杂的话题,尤其是多光源,阴影的计算。在这篇文章中,我详细介绍了不同类型光的性质以及物体的材质属性,以及光照计算的过程。这些都是光照的基石,掌握了这些才有可能继续攀登高峰。在下一篇文章中,我将演示一个 Gouraud 着色的光照示例。