文章目录
链接:Fantastic Tutorials on LearnOpenGL CN —— Basic Lighting
Introduction 简介
冯氏光照模型 主要结构由三个分量组成:
- 环境(Ambient)
- 漫反射(Diffuse)
- 镜面(Specular)
如下图,冯氏光照模型最终得到的就是三个分量混合而成的效果:
Ambient 环境光照
这是为了模拟这样的效果:即使是在黑暗的环境下,还是会有一些微弱的光源(例如月光,远处的光亮),这使得物体在某种程度上是可见的,也就是说,此时物体并非是完全黑色的,而是呈现出灰暗的色调。
但是通常,光源不是唯一的,并且,任何物体都会反射光。这就是全局光照(Global Lighting)算法所考虑的问题,而显然,这个算法是非常复杂且昂贵的。
通常我们采用这样的一个简化的模型:我们让片段着色器的颜色向量乘以一个常量环境因子来表示最终的颜色:
float ambientStength = 0.1;
// lightColor is the color of the light
vec3 ambient = ambientStrength * lightColor
// eventually, the output color is the multiply of light and objects' color
vec3 out = ambient * objectColor;
FragColor = vec4(out, 1.0);// opacity
最终效果:
Diffuse 漫反射光照
Explanation 解释
这是我们所习惯看到的(如果你对素描方面有所了解,那么你可能对这样的光照非常熟悉):靠近光源的亮面以及背光的暗面。
而这取决于光是以怎样的角度照射在平面上的,例如下图:
可以看到,光源(即灯泡)以一定的角度照射在下方橙色物体上平面的某个片段上,我们看到这里引入了一个重要的参照量:法向量(Normal Vector)。通过法向量和入射光的夹角可以方便我们计算光照对该片段的影响程度(垂直入射时夹角为
0
0
0,此时向量点乘的值最大,影响最大)。
Calculation 计算漫反射光照
计算需要:
- 在该片段垂直于物体表面的法向量
- 该片段的位置向量和光源的位置向量(减去可得入射光向量)
法向量
法向量可以在顶点数组中添加,然后我们需要重新告诉OpenGL如何解释顶点数组:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), 3 * sizeof(float));
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
而对于光源来说,可以使用同样顶点数组,但是法向量并不会参与光源的渲染,所以我们需要告诉OpenGL忽略法向量,即通过设置步长为6,跳过顶点数组中的法向量:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
教程中对这个问题作出了很好的解释:
之所以使用同样的数组,这是因为对于物体(立方体)的顶点数组在分配之后已经存储在GPU之中,这样比起我们重新为光源(灯泡)分配一个VBO来说显得更加高效,即使我们需要跳过不必要的法向量数据。
而颜色的计算是在片段着色器中进行的(这也是Phong和Gouraund的区别),因此我们需要将法向量数据从顶点着色器中传递给片段着色器:
// something else
...
out vec3 Normal;
void main()
{
// something else
...
Normal = aNormal;
}
然后片段着色器会有一个对应类型为in的变量:
...
in vec3 Normal;
...
片段着色器中的颜色计算
光源位置,这是一个静态变量:
uniform vec3 lightPos;
然后我们需要知道当前片段的位置,只需要将局部空间位置简单的乘以一个模型矩阵(将物体从),当然这需要我们在顶点着色器中完成:
...
out vec3 FragPos;
out vec3 Normal;
void main(){
...
FragPos = vec3(mdoel * vec4(aPos, 1.0f));
Normal = aNoraml;
对应的在片段着色器中有一个输入:
...
in vec3 FragPos;
...
接下来,我们便可以开始在片段着色器中添加光照计算了,首先通过片段的位置向量和光源的位置向量得到入射光向量:
vec3 norm = normalize(Normal);
vec3 lightDirection = normalize(lightPos - FragPos);
然后通过点乘来得到漫反射影响,然后乘以光源颜色得到漫反射分量:
// 如果夹角大于90度,即点乘小于0,则漫反射影响为0
float diffuseFactor = max(dot(norm, lightDirection), 0.0);
vec3 diffuse = diffuseFactor * lightColor;
结合前面的环境光照分量,最终得到的输出颜色是这样的:
// ambient's calculation
...
vec3 out = (ambient + diffuse) * objectColor;
FragColor = vec4(out, 1.0);
最终效果:
还有一件事
不等比缩放会导致法向量和平面的夹角发生变化,导致光照效果被破坏:
解决方法:通过一个法向量矩阵(一个专门为法向量设计的模型矩阵),这个矩阵称为法线矩阵(Normal Matrix),通过线性代数的操作来消除这个影响。
我们可以通过两个函数来实现——inverse(逆矩阵计算)和transpose:
Normal = mat3(transpose(inverse(model))) * aNormal;
在LearnOpenGL的教程中也有提到:
逆矩阵的计算的开销是非常大的,最好的做法是在将model矩阵传递给着色器之前交给CPU完成(而不是在GPU中)
当然,在这里我们并不会进行任何缩放操作(甚至是等比例缩放,如果你有兴趣可以自己添加),所以我们并不需要考虑这个问题。
Specular 镜面光照
特别是当光源距离物体非常近的时候,这样的光照效果会非常的明显,即我们非常熟悉的高光效果 —— 镜面高光(Specular Highlight)。相信部分同学会很有发言权,如果你经常坐在教室的两侧的话
如果我们在光线的反射光的方向观察的话,镜面高光的效果将会达到最大化,考虑前面的漫反射影响程度,因为此时我们视线和反射光的夹角
θ
\theta
θ 为0,点乘得到的值将会得到最大值:
因此,计算镜面光照我们需要:
- 观察位置和片段位置(摄像机位置)得到视线向量
- 光源位置和片段位置得到入射光向量,然后根据法线得到反射光向量
首先是观察位置,事实上就是摄像机的位置,这是一个静态变量:
uniform vec3 viewPos
然后我们计算视线向量和反射光向量:
vec3 viewDirection = normalize(viewPos - FraPos);
vec3 reflectDirection = normalize(reflect(-lightDirection, norm));
由于反射光的角度是从片段出发离开物体表面,而入射光是从光源到片段位置,所以我们需要取相反的方向关于法线进行reflect
操作,才能够得到方向正确的反射光。
然后,我们就可以开始计算镜面反射分量了:
float specularFactor =
pow(max(dot(viewDirection, reflectDirection), 0.0), 32);
vec3 specular = lightColor * specularFactor * specularIntensity;
其中:
specularIntensity
是一个镜面强度(Specular Intensity)变量,这个变量的值越大,反射光的强度就越大pow(..., 32)
表示反光度(Shininess),反光度越大,反射光越集中,亮点越小,对比度越大
最终我们的片段着色器的颜色将会是这样的:
...
vec3 out = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(out, 1.0);
最终效果会是这样的: