一、颜色

现实世界中有无数种颜色,每一个物体都有它们自己的颜色。计算机通过离散的、有限的数值来模拟真实世界中(无限)的颜色,所以并不是所有现实世界中的颜色都可以用数值来表示的。然而我们仍能通过数值来表现出非常多的颜色,甚至你可能都不会注意到与现实的颜色有任何的差异。颜色可以数字化的由红色(Red)、绿色(Green)和蓝色(Blue)三个分量组成,它们通常被缩写为RGB。仅仅用这三个值就可以组合出任意一种颜色。
我们在现实生活中看到某一物体的颜色并不是这个物体真正拥有的颜色,而是它所反射的(Reflected)颜色。换句话说,那些不能被物体所吸收(Absorb)的颜色(被拒绝的颜色)就是我们能够感知到的物体的颜色。例如,太阳光能被看见的白光其实是由许多不同的颜色组合而成的(如下图所示)。如果我们将白光照在一个蓝色的玩具上,这个蓝色的玩具会吸收白光中除了蓝色以外的所有子颜色,不被吸收的蓝色光被反射到我们的眼中,让这个玩具看起来是蓝色的。下图显示的是一个珊瑚红的玩具,它以不同强度反射了多个颜色。
OpenGL_光照 - 图1
白色的阳光实际上是所有可见颜色的集合,物体吸收了其中的大部分颜色。它仅反射了代表物体颜色的部分,被反射颜色的组合就是我们所感知到的颜色(此例中为珊瑚红)。
这些颜色反射的定律被直接地运用在图形领域。当我们在OpenGL中创建一个光源时,我们希望给光源一个颜色。在上一段中我们有一个白色的太阳,所以我们也将光源设置为白色。当我们把光源的颜色与物体的颜色值相乘,所得到的就是这个物体所反射的颜色(也就是我们所感知到的颜色)。让我们再次审视我们的玩具(这一次它还是珊瑚红),看看如何在图形学中计算出它的反射颜色。我们将这两个颜色向量作分量相乘,结果就是最终的颜色向量了:

  1. glm::vec3 lightColor(1.0f, 1.0f, 1.0f); // 光源,白色
  2. glm::vec3 toyColor(1.0f, 0.5f, 0.31f); // 玩具颜色(珊瑚红)
  3. glm::vec3 result = lightColor * toyColor; // = (1.0f, 0.5f, 0.31f);
  4. // 物理意义:表示光源照射到玩具之后,我们看到的玩具颜色。
  5. // 玩具的每个颜色分量,表示反射光源的对应颜色分量的百分比。
  6. glm::vec3 lightColor(0.0f, 1.0f, 0.0f); // 绿色光源
  7. glm::vec3 toyColor(1.0f, 0.5f, 0.31f); // 玩具颜色(珊瑚红)
  8. glm::vec3 result = lightColor * toyColor; // = (0.0f, 0.5f, 0.0f);深绿色
  9. // 光源在红色和蓝色分量上为0,所以并没有反射红蓝色。
  10. glm::vec3 lightColor(0.33f, 0.42f, 0.18f); // 深橄榄绿色光源
  11. glm::vec3 toyColor(1.0f, 0.5f, 0.31f); // 玩具颜色(珊瑚红),反射光源颜色分量百分比:
  12. // 红色分量:1.0,吸收0%,反射100%
  13. // 绿色分量:0.5,吸收50%,反射50%
  14. // 蓝色分量;0.31,吸收69%,反射31%
  15. glm::vec3 result = lightColor * toyColor; // = (0.33f, 0.21f, 0.06f);

我们可以定义物体的颜色为物体从一个光源反射各个颜色分量的大小

二、光照

实践项目:https://github.com/JackieLong/OpenGL/tree/main/project_light_test
现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是我们有限的计算能力所无法模拟的。因此OpenGL的光照使用的是简化的模型,对现实的情况进行近似,这样处理起来会更容易一些,而且看起来也差不多一样。这些光照模型都是基于我们对光的物理特性的理解。其中一个模型被称为冯氏光照模型

(一)冯氏光照模型

Phong Lighting Model,冯氏光照模型的主要结构由3个分量组成:

  • 环境光照(Ambient)
    • 模拟这种现象:即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。使用环境光照常量给物体一些颜色。
    • 通俗理解,我们在黑暗的地方,看到的物体的颜色。
  • 漫反射光照(Diffuse)
    • 物体上与光线方向越接近的地方能从光源处获得更多的亮度。模拟光源对物体的方向性影响(Directional Impact)。物体的某一部分越是正对着光源,它就会越亮。
    • 视觉上最显著的分量,基本上定调了物体的颜色。
  • 镜面反射光照(Specular)
    • 模拟有光泽物体上面出现的亮点,比如手电筒照到箱子上,钢铁在发亮,而木板则没有。
    • 镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。

      1、环境光照

      环境光就好比一种全局的光,可以照射到所有物体表面。比如我们在完全无光的时候,也能看出点物体的颜色,完全黑色的话就有点假。 ```cpp // ** 在片段着色器中

uniform vec3 lightColorambient; // 光“材质属性之一”:环境光分量 uniform vec3 objectColor_ambient; // 物体材质属性之一:环境光分量物体颜色环境光分量(材质中的环境光属性)

void main() { vec3 ambient = lightColor_ambient * objectColor_ambient;

  1. FragColor = vec4(ambient, 1.0); // 物体在“无光”的情况下的颜色

}

  1. ![](https://cdn.nlark.com/yuque/0/2021/png/461452/1610078280645-b148e5e6-bbea-40bd-9c95-cba324e2392d.png#align=left&display=inline&height=469&margin=%5Bobject%20Object%5D&originHeight=469&originWidth=600&size=0&status=done&style=none&width=600)
  2. <a name="MvSxN"></a>
  3. ### 2、漫反射光照
  4. <a name="1jSHS"></a>
  5. #### 数学模型
  6. ![](https://cdn.nlark.com/yuque/0/2021/png/461452/1609922446951-6539d951-0f05-4b32-bfb7-91510627a2f4.png#align=left&display=inline&height=322&margin=%5Bobject%20Object%5D&originHeight=322&originWidth=447&size=0&status=done&style=none&width=447)<br />已知:<br />光颜色![](https://cdn.nlark.com/yuque/__latex/3867029b0c86f7b4bad4258e1eb61e0e.svg#card=math&code=%5CLarge%20c_%7Blight%7D&height=20&width=48),<br />光源位置![](https://cdn.nlark.com/yuque/__latex/6e02a9e18a151948e1012e6455f19d19.svg#card=math&code=%5CLarge%20v_%7Blight%7D&height=20&width=49),<br />顶点(片段)位置![](https://cdn.nlark.com/yuque/__latex/ac4c210d058135f51dbd13c517185f01.svg#card=math&code=%5CLarge%20v_%7Bvertex%7D&height=18&width=61),<br />顶点(片段)位置的(单位)法向量![](https://cdn.nlark.com/yuque/__latex/3ea7fe24b091853a6851337deeeb76be.svg#card=math&code=%5CLarge%20v_%7Bnormal%7D&height=18&width=70),<br />顶点(片段)位置的物体颜色![](https://cdn.nlark.com/yuque/__latex/ca72065d0989fb980b31071526c889ab.svg#card=math&code=%5CLarge%20c_%7Bobject%7D&height=20&width=57)。<br />眼睛在顶点(片段)位置的法线方向上往法线反方向看。<br />求眼睛所看到的,顶点(片段)位置的最终颜色![](https://cdn.nlark.com/yuque/__latex/44ddce1e053c8d915aa551c931ace12e.svg#card=math&code=%5CLarge%20c_%7Bfinal%7D&height=21&width=53)。
  7. <a name="dLkgx"></a>
  8. #### 计算过程
  9. 设![](https://cdn.nlark.com/yuque/__latex/bc7c1280d0f0b818dc0587da0abb8d4b.svg#card=math&code=%5CLarge%20c_%7Bdiffuse%7D&height=21&width=72)为光在顶点位置法线方向的分量(漫反射),则有:<br />![](https://cdn.nlark.com/yuque/__latex/718ee3c4636d038febd5cec21d0bdc5a.svg#card=math&code=%5CLarge%20%5Cbegin%7Barray%7D%7Bl%7D%0A%20c_%7Bdiffuse%7D%20%26%3D%20c_%7Blight%7D%2A%5Ccos%20%5Ctheta%20%5C%5C%26%3D%20c_%7Blight%7D%2A%5Cfrac%7B%28v_%7Blight%7D-v_%7Bvertex%7D%29%20%5Ccdot%20v_%7Bnormal%7D%7D%7B%7Cv_%7Blight%7D%20-%20v_%7Bvertex%7D%7C%7Cv_%7Bnormal%7D%7C%7D%0A%5Cend%7Barray%7D&height=84&width=375),然后有:<br />![](https://cdn.nlark.com/yuque/__latex/5651b0a4c04c83c774be57a20131546a.svg#card=math&code=%5CLarge%20%5Cbegin%7Barray%7D%7Bl%7D%5C%0Ac_%7Bfinal%7D%20%26%3D%20%28c_%7Blight%7D%2A%5Ccos%5Ctheta%29%2Ac_%7Bobject%7D%20%5C%5C%0A%26%3D%28c_%7Blight%7D%2A%5Cfrac%7B%28v_%7Blight%7D-v_%7Bvertex%7D%29%20%5Ccdot%20v_%7Bnormal%7D%7D%7B%7Cv_%7Blight%7D%20-%20v_%7Bvertex%7D%7C%7Cv_%7Bnormal%7D%7C%7D%29%2Ac_%7Bobject%7D%0A%5Cend%7Barray%7D&height=84&width=458)<br />设光的方向为单位向量![](https://cdn.nlark.com/yuque/__latex/fc3c36eb723a07107dab339f463517a3.svg#card=math&code=%5CLarge%20v_%7Bdir%7D%20%3D%20%5Cfrac%7Bv_%7Blight%7D%20-%20v_%7Bvertex%7D%7D%7B%7Cv_%7Blight%7D%20-%20v_%7Bvertex%7D%7C%7D&height=60&width=232),则有:<br />![](https://cdn.nlark.com/yuque/__latex/88e6710414fa341f02ffcf90c3702675.svg#card=math&code=%5CLarge%20c_%7Bfinal%7D%20%3D%20c_%7Blight%7D%2A%28v_%7Bdir%7D%5Ccdot%20v_%7Bnormal%7D%29%2Ac_%7Bobject%7D&height=30&width=378)
  10. <a name="wl0P2"></a>
  11. #### 法向量
  12. 法向量是一个垂直于顶点表面的(单位)向量。由于顶点本身并没有表面(它只是空间中一个独立的点),我们利用它周围的顶点来计算出这个顶点的表面。我们能够使用叉乘对立方体所有的顶点计算法向量。<br />上面的各种向量都必须在同一个坐标系中,一般通过模型矩阵转换到世界坐标系中,这里存在一个法线向量的矩阵变换问题,并不能简单乘以模型矩阵而正确得到,因为模型矩阵可能存在非等比缩放,这时法线的方向和大小都会发生改变。我们需要一个**法线矩阵**来消除这种缩放对法线带来的影响([法线矩阵的计算](http://www.lighthouse3d.com/tutorials/glsl-tutorial/the-normal-matrix/))。<br />法线矩阵![](https://cdn.nlark.com/yuque/__latex/1e9ab2bf42342b360f17dacf08d412c6.svg#card=math&code=%5CLarge%20M_%7Bnormal%7D%20%3D%20%28M_%7Bmodel%283%5Ctimes%203%29%7D%27%5E%7B-1%7D%29%5ET&height=40&width=262),![](https://cdn.nlark.com/yuque/__latex/05597f4ddf1044c3633ed58ea20fa8db.svg#card=math&code=%5Clarge%203%5Ctimes%203&height=18&width=45)表示是模型矩阵左上角3x3的部分,也就是说去除了平移变换效果,是因为法线向量一般就是基向量用来表示方向,不存在位置一说,也就没有平移变换一说了。
  13. <a name="8y4CV"></a>
  14. #### 程序设计
  15. ```cpp
  16. // 在顶点着色器中
  17. layout(location = 0) vec3 pos; // 从内存中载入的顶点数据
  18. layout(location = 1) vec3 normal; // 顶点的法向量
  19. uniform mat4 model; // 模型矩阵
  20. uniform mat4 view; // 观察矩阵
  21. uniform mat4 projection; // 投影矩阵
  22. out vec3 outPos; // 顶点在坐标系中的位置
  23. out vec3 outNormal; // 经过模型变换后的法向量
  24. void main()
  25. {
  26. gl_Position = projection * view * model * vec4(pos,1.0);
  27. outPos = vec3(model * vec4(pos,1.0)) ;
  28. outNormal = mat3(transpose(inverse(model))) * normal; // 矩阵的逆运算开销大,可以通过uniform传入
  29. }
  1. // 在片段着色器中
  2. uniform vec3 lightColor_diffuse; // 光“材质属性之一”:漫反射分量
  3. uniform vec3 lightPos; // 光源位置
  4. in vec3 outPos; // 顶点的位置(世界坐标)
  5. in vec3 outNormal; // 顶点法向量
  6. vec3 objectColor_diffuse; // 物体材质属性之一:漫反射分量
  7. void main()
  8. {
  9. vec3 lightDir = normalize(lightPos - outPos); // 光射向的反方向
  10. float cos_theta = max(lightDir * outNormal, 0.0); // 单位向量点乘得到夹角余弦值,max约束为非负。
  11. vec3 diffuse = light_diffuse * cos_theta * objectColor_diffuse; // 漫反射光照
  12. FragColor = vec4(diffuse, 1.0);
  13. }

视觉效果

OpenGL_光照 - 图2

3、镜面反射光照

数学模型

OpenGL_光照 - 图3
已知:
光颜色OpenGL_光照 - 图4
光源位置OpenGL_光照 - 图5
顶点(片段)位置OpenGL_光照 - 图6
顶点(片段)位置的(单位)法向量OpenGL_光照 - 图7
顶点(片段)位置的物体颜色OpenGL_光照 - 图8
图中,OpenGL_光照 - 图9为光在顶点位置处反射之后的基向量。
眼睛位置OpenGL_光照 - 图10,与顶点的连线和OpenGL_光照 - 图11的夹角为OpenGL_光照 - 图12

镜面强度(Specular Intensit)OpenGL_光照 - 图13,反射强度,可以反射多少百分比的光。
反光度OpenGL_光照 - 图14(2~256,2的次幂,一般取32),越大,高光点半径越小。

求眼睛所看到的,顶点(片段)位置的最终颜色OpenGL_光照 - 图15。(坐标系为世界坐标系)

计算过程

OpenGL_光照 - 图16
OpenGL_光照 - 图17的计算比较简单,这里暂时不做进一步计算(主要是因为GLSL中的reflect函数可以直接计算出)

反光度OpenGL_光照 - 图18的值的影响效果如下:
OpenGL_光照 - 图19

程序设计

  1. // 在顶点着色器中
  2. layout(location = 0) vec3 pos; // 从内存中载入的顶点数据
  3. layout(location = 1) vec3 normal; // 顶点的法向量
  4. uniform vec3 model; // 模型矩阵
  5. uniform vec3 view; // 观察矩阵
  6. uniform vec3 projection; // 投影矩阵
  7. out vec3 outPos; // 顶点在坐标系中的位置
  8. out vec3 outNormal; // 经过模型变换后的法向量
  9. void main()
  10. {
  11. gl_Position = vec4(projection * view * model * pos);
  12. outPos = vec3(model * vec4(pos,1.0)) ;
  13. outNormal = mat3(transpose(inverse(model))) * normal; // 矩阵的逆运算开销大,可以通过uniform传入
  14. }
  1. // 在片段着色器中
  2. uniform vec3 lightColor_specular; // 光“材质属性之一”:镜面反射分量
  3. uniform float shininess; // 反光度
  4. uniform vec3 lightPos; // 光源位置
  5. uniform vec3 viewPos; // 眼睛位置(摄像机位置)
  6. in vec3 outPos; // 顶点的世界坐标
  7. in vec3 outNormal; // 顶点法向量
  8. vec3 objectColor_specular; // 物体材质属性之一:镜面反射分量
  9. void main()
  10. {
  11. vec3 vecR = reflect(outPos - lightPos, outNormal); // 光的反射,计算结果是光的反射向量
  12. vecR = normalize(vecR); // 标准化
  13. vec3 vecEye = normalize(viewPos - outPos); // 顶点到眼睛的基向量(视线的反方向)
  14. float cos_theta = max(vecR * vecEye, 0.0); // 单位向量点乘得到夹角余弦值,max约束为非负。
  15. vec3 specular = lightColor_specular * pow(cos_theta, shininess) * objectColor_specular;
  16. FragColor = vec4(specular, 1.0);
  17. }

OpenGL_光照 - 图20

(二)Gourand着色和冯氏着色

有没有发现,前面光照计算是在片段着色器中完成,这就是冯氏着色器,如果放在顶点着色器中计算就叫做Gourand着色器。
这两者的区别在于,一个顶点着色器可能对应要执行多个片段着色器,比如三个顶点画一个三角形,顶点只有3个,但是屏幕上要显示这个三角形远远不止3个像素。假如在顶点着色器中计算光照,则计算出的是三角形三个顶点的光照颜色,片段着色器会根据这三个顶点的光照值进行插值计算,因此得到的光照值讲成线性分布。假如在片段着色器中计算光照,则计算出的是每个像素的光照,光照效果将非常逼真。
所以Gourand着色(Gourand Shading)可以叫做逐顶点着色器,冯氏着色(Phong Shading)可以叫做逐片段着色器。
显然前者的计算量要远小于后者。

三、材质(Material)

实践项目:https://github.com/JackieLong/OpenGL/tree/main/project_material_test

(一)本质

材质只是指物体的这三种反射能力:环境光反射、漫反射、镜面反射。比如钢铁会反光(较强的镜面反射能力),而木头不会(很弱的镜面反射能力)。
所以通过以下三个分量即可定义一个材质属性:

  • 环境光照分量(Ambient Lighting)
    • 定义了环境光照下,物体的颜色,一般和物体本身颜色相同。
  • 漫反射光照分量(Diffuse Lighting)
    • 漫反射光照下,物体的颜色,一般和环境光照相同。
  • 镜面光照分量(Specular Lighting)
    • 设置的是镜面光照对物体的颜色影响(或者甚至可能反射一个物体特定的镜面高光颜色)
    • 其中有一个反光度(Shininess),影响高光点的半径,越大,半径越小,光点越小。

材质属性的光照计算如下:

  1. // 在着色器中
  2. struct Material // 材质属性
  3. {
  4. vec3 ambient; // 环境光照分量
  5. vec3 diffuse; // 漫反射光照分量
  6. vec3 specular; // 镜面光照分量
  7. float shininess; // 反光度
  8. }
  9. uniform vec3 lightColor;
  10. uniform Material material;
  11. void main()
  12. {
  13. vec3 ambient = lightColor * material.ambient;
  14. vec3 diffuse = lightColor * (material.diffuse * cos_theta); // cos_theta:光射向的反向和法线的夹角余弦值
  15. vec3 specular = lightColor * (material.specular * pow(cos_theta, material.shininess)); // cos_theta:顶点(片段)到眼睛连线 与光在顶点处反射后的方向的夹角的余弦值。
  16. FragColor = vec4(ambient + diffuse + specular, 1.0);
  17. }

(二)常见材质

http://devernay.free.fr/cours/opengl/materials.html

Name Ambient(环境光分量) Diffuse(漫反射分量) Specular(镜面反射分量) Shininess(反光度)
emerald 0.0215 0.1745 0.0215 0.07568 0.61424 0.07568 0.633 0.727811 0.633 0.6
jade 0.135 0.2225 0.1575 0.54 0.89 0.63 0.316228 0.316228 0.316228 0.1
obsidian 0.05375 0.05 0.06625 0.18275 0.17 0.22525 0.332741 0.328634 0.346435 0.3
pearl 0.25 0.20725 0.20725 1 0.829 0.829 0.296648 0.296648 0.296648 0.088
ruby 0.1745 0.01175 0.01175 0.61424 0.04136 0.04136 0.727811 0.626959 0.626959 0.6
turquoise 0.1 0.18725 0.1745 0.396 0.74151 0.69102 0.297254 0.30829 0.306678 0.1
brass 0.329412 0.223529 0.027451 0.780392 0.568627 0.113725 0.992157 0.941176 0.807843 0.21794872
bronze 0.2125 0.1275 0.054 0.714 0.4284 0.18144 0.393548 0.271906 0.166721 0.2
chrome 0.25 0.25 0.25 0.4 0.4 0.4 0.774597 0.774597 0.774597 0.6
copper 0.19125 0.0735 0.0225 0.7038 0.27048 0.0828 0.256777 0.137622 0.086014 0.1
gold 0.24725 0.1995 0.0745 0.75164 0.60648 0.22648 0.628281 0.555802 0.366065 0.4
silver 0.19225 0.19225 0.19225 0.50754 0.50754 0.50754 0.508273 0.508273 0.508273 0.4
black plastic 0.0 0.0 0.0 0.01 0.01 0.01 0.50 0.50 0.50 .25
cyan plastic 0.0 0.1 0.06 0.0 0.50980392 0.50980392 0.50196078 0.50196078 0.50196078 .25
green plastic 0.0 0.0 0.0 0.1 0.35 0.1 0.45 0.55 0.45 .25
red plastic 0.0 0.0 0.0 0.5 0.0 0.0 0.7 0.6 0.6 .25
white plastic 0.0 0.0 0.0 0.55 0.55 0.55 0.70 0.70 0.70 .25
yellow plastic 0.0 0.0 0.0 0.5 0.5 0.0 0.60 0.60 0.50 .25
black rubber 0.02 0.02 0.02 0.01 0.01 0.01 0.4 0.4 0.4 .078125
cyan rubber 0.0 0.05 0.05 0.4 0.5 0.5 0.04 0.7 0.7 .078125
green rubber 0.0 0.05 0.0 0.4 0.5 0.4 0.04 0.7 0.04 .078125
red rubber 0.05 0.0 0.0 0.5 0.4 0.4 0.7 0.04 0.04 .078125
white rubber 0.05 0.05 0.05 0.5 0.5 0.5 0.7 0.7 0.7 .078125
yellow rubber 0.05 0.05 0.0 0.5 0.5 0.4 0.7 0.7 0.04 .078125

OpenGL_光照 - 图21

(三)光的“材质”

光源对环境光、漫反射和镜面光分量也具有不同强度。可以对光设置类似的材质属性:

  1. // 在着色器中
  2. struct Material // 材质属性
  3. {
  4. vec3 ambient; // 环境光照分量
  5. vec3 diffuse; // 漫反射光照分量
  6. vec3 specular; // 镜面光照分量
  7. float shininess; // 反光度
  8. }
  9. struct Light
  10. {
  11. vec3 pos;
  12. vec3 ambient; // 一般比较低,不希望环境颜色太明显
  13. vec3 diffuse; // 通常设置为光的颜色,比如比较明亮的白色vec3(0.5)
  14. vec3 specular; // 一般为vec3(1.0)
  15. }
  16. uniform Material material;
  17. uniform Light light;
  18. void main()
  19. {
  20. vec3 ambient = lightColor.ambient * (material.ambient);
  21. vec3 diffuse = lightColor.diffuse * (material.diffuse * cos_theta); // cos_theta:光射向的反向和法线的夹角余弦值
  22. vec3 specular = lightColor.specular * (material.specular * pow(cos_theta, material.shininess)); // cos_theta:顶点(片段)到眼睛连线 与光在顶点处反射后的方向的夹角的余弦值。
  23. FragColor = vec4(ambient + diffuse + specular, 1.0);
  24. }
  25. // ***************C++中
  26. // 一般光源属性的数值方法
  27. void main()
  28. {
  29. glm::vec3 lightColor; // 光源颜色
  30. glm::vec3 diffuseColor = glm::vec3(0.5f) * lightColor; // 满反射强度
  31. glm::vec3 ambientColor = glm::vec3(0.2f) * diffuseColor; // 环境光为漫反射的百分比
  32. glm::vec3 specularColor = glm::vec(1.0f); // 一般为1.0,最大化强度发光
  33. }

四、光照贴图

实践项目:https://github.com/JackieLong/OpenGL/tree/main/project_lighting_map_test
从上面的学习我们知道物体的光照颜色计算如下:

  1. void main()
  2. {
  3. vec3 ambient = lightColor.ambient * (material.ambient);
  4. vec3 diffuse = lightColor.diffuse * (material.diffuse * cos_theta); // cos_theta:光射向的反向和法线的夹角余弦值
  5. vec3 specular = lightColor.specular * (material.specular * pow(cos_theta, material.shininess)); // cos_theta:顶点(片段)到眼睛连线 与光在顶点处反射后的方向的夹角的余弦值。
  6. FragColor = vec4(ambient + diffuse + specular, 1.0);
  7. }

这里有一个明显的缺陷,将物体的材质定义为一个整体,这明显限制了三个光照分量各自设置的灵活性。
这些光照分量本质也是颜色,完全可以通过纹理技术来进行精细化控制。我们可以引入漫反射贴图(Diffuse Map)镜面反射贴图(Specular Map)来代替,为什么没有环境光?通过漫反射可以间接地控制环境光分量(见光的“材质”)。
大致原理如下:

  1. struct Material
  2. {
  3. // vec3 ambient; // 可以通过diffuse间接控制,一般就是一样的。
  4. sampler2D diffuse; // 漫反射贴图,GL_TEXTURE0,则为0
  5. sampler2D specular; // 镜面反射贴图,GL_TEXTURE1,则为1
  6. float shininess; // 反光度
  7. }
  8. uniform Material material;
  9. in vec2 texCoord; // 纹理坐标
  10. void main()
  11. {
  12. vec3 m_diffuse = vec3(texture(material.diffuse, texCoord));
  13. vec3 m_ambient = m_diffuse;
  14. vec3 m_specular = vec3(texture(material.specular, texCoord));
  15. vec3 ambient = lightColor.ambient * (m_ambient);
  16. vec3 diffuse = lightColor.diffuse * (m_diffuse * cos_theta); // cos_theta:光射向的反向和法线的夹角余弦值
  17. vec3 specular = lightColor.specular * (m_specular * pow(cos_theta, material.shininess)); // cos_theta:顶点(片段)到眼睛连线 与光在顶点处反射后的方向的夹角的余弦值。
  18. FragColor = vec4(ambient + diffuse + specular, 1.0);
  19. }

其实应该叫“材质”贴图,毕竟是材质的贴图。
还有其他贴图用于增强细节:法线/凹凸贴图(Normal/Bump Map)、反射贴图(Reflect Map)。

五、投光物

将光投射(cast)到物体的光源叫做投光物(Light Caster)。

(一)平行光

  • 所有光线方向平行
  • 没有光源位置

最常见的太阳光。
光源在很远(无限远)的位置,不论物体和/或者观察者的位置,看起来好像所有的光都来自于同一个方向。
当我们使用一个假设光源处于无限远处的模型时,它就被称为定向光,因为它的所有光线都有着相同的方向,它与光源的位置是没有关系的。

OpenGL_光照 - 图22

  1. struct DirLight {
  2. // vec3形式
  3. vec3 dir; // 定向光的方向都相同,固定不变。
  4. // 光的射线方向。
  5. // vec3 position; // 方向固定,不需要位置了。
  6. // vec4形式
  7. vec4 dir; // w = 1.0,则为有位置的光源。
  8. // w = 0.0,则为无位置的光,也就是定向光。
  9. // 下面照旧
  10. vec3 ambient;
  11. vec3 diffuse;
  12. vec3 specular;
  13. };
  14. uniform DirLight sunLight;
  15. void main()
  16. {
  17. vec3 lightDir = normalize(-sunLight.dir); // 一般取反,因为计算中一般是需要顶点到光源的方向向量。
  18. ......
  19. }

OpenGL_光照 - 图23

(二)点光源

  • 有光源位置
  • 朝所有方向发光
  • 光线随距离衰减

灯泡、火把、打火机。
OpenGL_光照 - 图24

衰减

随着光线传播距离的增长逐渐削减光的强度通常叫做衰减(Attenuation)。衰减公式如下:
OpenGL_光照 - 图25

  • OpenGL_光照 - 图26:顶点(片段)离光源距离
  • 可配置项
    • 常数项:OpenGL_光照 - 图27,一般为1.0,保证分母不小于1。
    • 一次项:OpenGL_光照 - 图28,光强以线性衰减。
    • 二次项:OpenGL_光照 - 图29,光强以二次衰减,在距离较小时,比一次项影响小得多。

函数特性如下:
OpenGL_光照 - 图30
随距离增大急剧衰减,到一定距离后,衰减速度变得很慢,到100基本上就为0了。

如何配置项

没什么技巧,一般靠经验。来自Ogre3D的Wiki的一些参考值如下:

覆盖半径 常数项 一次项 二次项
7 1.0 0.7 1.8
13 1.0 0.35 0.44
20 1.0 0.22 0.20
32 1.0 0.14 0.07
50 1.0 0.09 0.032
65 1.0 0.07 0.017
100 1.0 0.045 0.0075
160 1.0 0.027 0.0028
200 1.0 0.022 0.0019
325 1.0 0.014 0.0007
600 1.0 0.007 0.0002
3250 1.0 0.0014 0.000007

一般到100的足够了。

程序设计如下:

  1. struct PointLight
  2. {
  3. vec3 pos;
  4. vec3 ambient;
  5. vec3 diffuse;
  6. vec3 specular;
  7. float constant; // 常数项
  8. float linear; // 一次项
  9. float quadratic; // 二次项
  10. }
  11. uniform PointLight light;
  12. in vec3 fragPos; // 顶点在世界坐标中位置
  13. void main()
  14. {
  15. float distance = length(light.pos - fragPos); // 距离光源
  16. float attenuation = 1.0 / (light.constant + // 衰减
  17. light.linear * distance +
  18. light.quadratic * (distance * distance));
  19. vec3 ambient = light.ambient * attenuation; // 计算光的衰减
  20. vec3 diffuse = light.diffuse * attenuation;
  21. vec3 specular = light.specular * attenuation;
  22. ......
  23. }

(三)聚光

  • 有光源位置
  • 特定方向,不是所有方向
  • 有范围:切光角(Cutoff Angle)

OpenGL_光照 - 图31
OpenGL_光照 - 图32:从片段指向光源的向量
OpenGL_光照 - 图33OpenGL_光照 - 图34OpenGL_光照 - 图35的夹角。OpenGL_光照 - 图36时,则被照亮。
OpenGL_光照 - 图37:切光角,指定了聚光半径。
OpenGL_光照 - 图38:聚光的方向。
以手电筒为例,摄像机的位置就是光源位置。

  1. struct SpotLight
  2. {
  3. vec3 pos; // 光源位置(手电筒时,就是摄像机位置)
  4. vec3 dir; // 聚光方向(手电筒朝向,也就是摄像机朝向)
  5. float cutoff; // 切光角的余弦值(注意)
  6. ......
  7. }
  8. uniform SpotLight flashLight; // 手电筒
  9. void main()
  10. {
  11. float cos_theta = normalize(flashLight.dir) * normalize((fragPos - flashLight.pos));
  12. if(cos_theta > flashLight.cutoff) // 在聚光半径内,手电筒照到的地方
  13. {
  14. // 执行光照计算
  15. }
  16. else // 手电筒照射地方以外
  17. {
  18. // 比如环境光
  19. }
  20. }

效果如下:
OpenGL_光照 - 图39
光圈边缘太明显,来个渐变模糊。可以在再加一个外圈,进行插值渐变光照计算。

  1. struct SpotLight
  2. {
  3. vec3 pos; // 光源位置(手电筒时,就是摄像机位置)
  4. vec3 dir; // 聚光方向(手电筒朝向,也就是摄像机朝向)
  5. float cutOff; // 切光角的余弦值(注意)
  6. float outerCutOff; // 外圈切光角的余弦值。
  7. ......
  8. }
  9. uniform SpotLight light; // 手电筒
  10. void main()
  11. {
  12. float cos_theta = normalize(light.dir) * normalize((fragPos - light.pos));
  13. float eplison = light.cutOff - light.outerCutOff;
  14. float intensity = clamp((cos_theta - light.outerCutOff) / eplison, 0.0, 1.0);
  15. ...
  16. // 将不对环境光做出影响,让它总是能有一点光
  17. diffuse *= intensity;
  18. specular *= intensity;
  19. ...
  20. }

六、多光源

  1. out vec4 FragColor;
  2. void main()
  3. {
  4. vec3 outputColor;
  5. outputColor += calcDirLight(...); // 计算定向光,比如太阳光
  6. for(int i = 0; i < num_of_point_lights; i++)
  7. outputColor += calcPointLight(...); // 计算点光源,多个火把
  8. outputColor += calcSpotLight(...); // 计算聚光
  9. FragColor = vec4(outputColor, 1.0);
  10. }
  11. vec3 calcDirLight(...)
  12. {
  13. }
  14. vec3 calcPointLight(...)
  15. {
  16. }
  17. vec3 calcSpotLight(...)
  18. {
  19. }