【OpenGL】11-光照

参考教程

【中文版】基础光照

【英文版】Basic Lighting

学习心得

  终于是开始接触一些较为复杂的东西了,这篇主要是讲解光照实现的原理(漫反射光和镜面光)以及他们的计算方式。

  首先,我们需要作出一个适合检测光照的模型,在此我实现了一个有着自转与公转的光源——正方体模型,最终效果如下:

  点此去Youtube观看

  今天我们需要实现的是漫反射光,和镜面反射光。漫反射光是什么呢,首先我们需要介绍这几个概念,环境光漫反射光镜面反射光

  环境光,我们生活中很少有完全漆黑的时候,大部分情况下我们都可以模糊的看到物体,这个时候我们没有明确的光源,我们称之为环境光。在环境光下,我们只是能够勉强的看到物体,但并不是很清楚,例如:

WX20180221-224145@2x

  忽略掉那个没有产生作用的光源,我们可以模糊的看到一坨东西,但是并不能很清楚的看到颜色(因为我注释了光源生效的代码)。

  当漫发射光生效后,我们已经可以清楚的看到物体,以及辨别他的颜色(请注意,不管我们能否看到,物体是存在颜色的):

2222

  那么漫反射到底是什么呢,当一束光照到一个物体上时,因为物体本身颜色决定了他能吸收某个色的光和反射某个的光,同时我们就可以看到他的颜色。而漫反射重要的地方在于判断一束光是否照射到一个物体上,在这里我们需要拿出初中物理的两个概念,入射角法线

6a63f6246b600c33139841aa114c510fd8f9a1eb

  途中P为法线,A为入射光线,A与P的夹角即是入射角。看到这里想必你已经大概猜出来了,我们用来判断是否光照射到上边的依据就是入射角。当入射角大于90°时,说明光线没有照射到这里。而根据入射角的角度,光的强度也是不一样的。

  明白了原理,我们便可以用OpenGL中Shader提供好的方法来模拟漫反射来对物体进行着色。首先我们需要得到物体的法线,我们在给出正方体坐标的时候一并给出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
float vertices[] = {
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,

-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,

-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,

0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,

-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,

-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f
};

  这个数组中,每六个值为一组,前三个存储坐标,后三个存储法线向量,可以看出由于我们的长方体有六个边,所以总共有六个不同的法线向量。

1
2
3
4
5
6
7
8
9
10
// normal attribute
glVertexAttribPointer(
1,
3,
GL_FLOAT,
GL_FALSE,
6 * sizeof(float),
(void *) (3 * sizeof(float))
);
glEnableVertexAttribArray(1);

  将这些发现向量绑定到顶点数组里,然后可以在顶点着色器中操作,

1
2
3
4
5
6
7
8
9
10
...
layout (location = 1) in vec3 aNormal;
...
out vec3 Normal;
...
void main(){
...
Normal = mat3(transpose(inverse(model))) * aNormal;
...
}

  可以看到,我们将顶点坐标中的法线向量读出来,然后进行通过一个 inverse(矩阵的逆) 操作和 transpose (矩阵的转置)操作得到的三级矩阵与其相乘并传值给片段着色器。而做这多余的操作也是有原因的:

  首先,法向量只是一个方向向量,不能表达空间中的特定位置。同时,法向量没有齐次坐标(顶点位置中的w分量)。这意味着,位移不应该影响到法向量。因此,如果我们打算把法向量乘以一个模型矩阵,我们就要从矩阵中移除位移部分,只选用模型矩阵左上角3×3的矩阵(注意,我们也可以把法向量的w分量设置为0,再乘以4×4矩阵;这同样可以移除位移)。对于法向量,我们只希望对它实施缩放和旋转变换。

  其次,如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了。因此,我们不能用这样的模型矩阵来变换法向量。下面的图展示了应用了不等比缩放的模型矩阵对法向量的影响:

img

  每当我们应用一个不等比缩放时(注意:等比缩放不会破坏法线,因为法线的方向没被改变,仅仅改变了法线的长度,而这很容易通过标准化来修复),法向量就不会再垂直于对应的表面了,这样光照就会被破坏。

  修复这个行为的诀窍是使用一个为法向量专门定制的模型矩阵。这个矩阵称之为法线矩阵(Normal Matrix),它使用了一些线性代数的操作来移除对法向量错误缩放的影响。如果你想知道这个矩阵是如何计算出来的,建议去阅读这个文章

  法线矩阵被定义为「模型矩阵左上角的逆矩阵的转置矩阵」。真是拗口,如果你不明白这是什么意思,别担心,我们还没有讨论逆矩阵(Inverse Matrix)和转置矩阵(Transpose Matrix)。注意,大部分的资源都会将法线矩阵定义为应用到模型-观察矩阵(Model-view Matrix)上的操作,但是由于我们只在世界空间中进行操作(不是在观察空间),我们只使用模型矩阵。

  在顶点着色器中,我们可以使用inverse和transpose函数自己生成这个法线矩阵,这两个函数对所有类型矩阵都有效。注意我们还要把被处理过的矩阵强制转换为3×3矩阵,来保证它失去了位移属性以及能够乘以vec3的法向量。

  在漫反射光照部分,光照表现并没有问题,这是因为我们没有对物体本身执行任何缩放操作,所以并不是必须要使用一个法线矩阵,仅仅让模型矩阵乘以法线也可以。可是,如果你进行了不等比缩放,使用法线矩阵去乘以法向量就是必不可少的了。

  即使是对于着色器来说,逆矩阵也是一个开销比较大的运算,因此,只要可能就应该避免在着色器中进行逆矩阵运算,它们必须为你场景中的每个顶点都进行这样的处理。用作学习目这样做是可以的,但是对于一个对效率有要求的应用来说,在绘制之前你最好用CPU计算出法线矩阵,然后通过uniform把值传递给着色器(像模型矩阵一样)。

  传给片段着色器之后,我们就获得了最后计算着色的法线,那么还需要计算入射向量,这个也比较简单,通过对片段位置和光源的位置相减就可以获得,而片段位置可以通过模型矩阵与定点坐标相乘获得:

1
2
3
4
5
6
7
8
9
10
11
12
...
layout(location=0) in vec3 aPos;
...
uniform mat4 model;

out vec3 FragPos;
...
void main(){
....
FragPos = vec3(model * vec4(aPos, 1.0));
...
}

  在顶点着色器里完成这些操作并传值给片段着色器,便可以获得入射向量。

  最后,通过 dot 方法得到入射角,并使其不会产生负数(使用max,当dot得到负数时给他赋值0),如下:

1
2
3
4
vec3 norm = normalize(Normal);                  //单位化
vec3 lightDir = normalize(lightPos - FragPos); //单位化
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor

  当计算光照时我们通常不关心一个向量的模长或它的位置,我们只关心它们的方向。所以,几乎所有的计算都使用单位向量完成,因为这简化了大部分的计算(比如点乘)。所以当进行光照计算时,确保你总是对相关向量进行标准化,来保证它们是真正地单位向量。忘记对向量进行标准化是一个十分常见的错误。

  最后我们将漫反射后计算的颜色加上环境光颜色(一般为光源颜色的几十分之一倍,看具体情况)赋值给片段着色器的输出值即可。

1
FragColor = vec4((diffuse + 0.1) * lightColor),1.0f);

WX20180221-232646@2x

  处理完漫反射后我们可以看看镜面光照,和漫反射光照一样,镜面光照也是依据光的方向向量和物体的法向量来决定的,但是它也依赖于观察方向,例如玩家是从什么方向看着这个片段的。镜面光照是基于光的反射特性。如果我们想象物体表面像一面镜子一样,那么,无论我们从哪里去看那个表面所反射的光,镜面光照都会达到最大化。你可以从下面的图片看到效果:

2222222

  我们通过反射法向量周围光的方向来计算反射向量。然后我们计算反射向量和视线方向的角度差,如果夹角越小,那么镜面光的影响就会越大。它的作用效果就是,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光。

  观察向量是镜面光照附加的一个变量,我们可以使用观察者世界空间位置和片段的位置来计算它。之后,我们计算镜面光强度,用它乘以光源的颜色,再将它加上环境光和漫反射分量。

  我们使用摄像机坐标作为观察者坐标,在片段着色器中设立一个Uniform变量,并在OpenGL代码里将摄像机坐标传入进去

1
uniform vec3 viewPos;

  定义一个镜面的反射强度,并用片段坐标减去观察点坐标得到我们的视线方向,通过 reflect 方法得到反射向量(传入参数为入射向量和法线向量,注意入射向量的方向是从光源到片段点)

1
2
3
float specularStrength = 0.5;
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(FragPos-lightPos, norm);

  最后计算镜面分量,并将其与之前的漫反射光分量和环境光分量加起来乘光源颜色,就得到了最后要赋值的片段颜色:

1
2
3
4
5
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;

vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result , 1.0);

  最终效果看Youtube

0%