冯氏光照模型
基础光照模型
现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是我们有限的计算能力所无法模拟的。因此OpenGL的光照使用的是简化的模型,对现实的情况进行近似,这样处理起来会更容易一些,而且看起来也差不多一样。这些光照模型都是基于我们对光的物理特性的理解。其中一个模型被称为冯氏光照模型(Phong Lighting Model)。冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。下面这张图展示了这些光照分量看起来的样子:
- 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
- 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
- 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。
环境光照
光通常都不是来自于同一个光源,而是来自于我们周围分散的很多光源,即使它们可能并不是那么显而易见。光的一个属性是,它可以向很多方向发散并反弹,从而能够到达不是非常直接临近的点。所以,光能够在其它的表面上反射,对一个物体产生间接的影响。考虑到这种情况的算法叫做全局照明(Global Illumination)算法,但是这种算法既开销高昂又极其复杂。
由于我们现在对那种又复杂又开销高昂的算法不是很感兴趣,所以我们将会先使用一个简化的全局照明模型,即环境光照。正如你在上一节所学到的,我们使用一个很小的常量(光照)颜色,添加到物体片段的最终颜色中,这样子的话即便场景中没有直接的光源也能看起来存在有一些发散的光。
把环境光照添加到场景里非常简单。我们用光的颜色乘以一个很小的常量环境因子,再乘以物体的颜色,然后将最终结果作为片段的颜色:
void main()
{
float ambientStrength = 0.1;//环境常量因子
vec3 ambient = ambientStrength * lightColor;//光照*环境因子
vec3 result = ambient * objectColor;//物体颜色*环境光照
FragColor = vec4(result, 1.0);
}
漫反射模型
环境光照本身不能提供最有趣的结果,但是漫反射光照就能开始对物体产生显著的视觉影响了。漫反射光照使物体上与光线方向越接近的片段能从光源处获得更多的亮度。为了能够更好的理解漫反射光照,请看下图:
图左上方有一个光源,它所发出的光线落在物体的一个片段上。我们需要测量这个光线是以什么角度接触到这个片段的。如果光线垂直于物体表面,这束光对物体的影响会最大化(译注:更亮)。为了测量光线和片段的角度,我们使用一个叫做法向量(Normal Vector)的东西,它是垂直于片段表面的一个向量(这里以黄色箭头表示),我们在后面再讲这个东西。这两个向量之间的角度很容易就能够通过点乘计算出来。
注意,为了(只)得到两个向量夹角的余弦值,我们使用的是单位向量(长度为1的向量),所以我们需要确保所有的向量都是标准化的,否则点乘返回的就不仅仅是余弦值了(见[变换](https://learnopengl-cn.github.io/01 Getting started/07 Transformations/))。
点乘返回一个标量,我们可以用它计算光线对片段颜色的影响。不同片段朝向光源的方向的不同,这些片段被照亮的情况也不同。
所以,计算漫反射光照需要什么?
- 法向量:一个垂直于顶点表面的向量。
- 定向的光线:作为光源的位置与片段的位置之间向量差的方向向量。为了计算这个光线,我们需要光的位置向量和片段的位置向量。
法向量
法向量是一个垂直于顶点表面的(单位)向量。由于顶点本身并没有表面(它只是空间中一个独立的点),我们利用它周围的顶点来计算出这个顶点的表面。我们能够使用一个小技巧,使用叉乘对立方体所有的顶点计算法向量,但是由于3D立方体不是一个复杂的形状,所以我们可以简单地把法线数据手工添加到顶点(vertexShader)数据中。(通过VBO 读到顶点着色器中)
#version 330 core//vertexShader
layout (location = 0) in vec3 aPos;//位置信息
layout (location = 1) in vec3 aNormal;//法向量信息
所有光照的计算都是在片段着色器里进行,所以我们需要将法向量由顶点着色器传递到片段着色器。我们这么做:
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
Normal = aNormal;
}
接下来,在片段着色器(fragmentShader)中定义相应的输入变量:
in vec3 Normal;
计算漫反射
到此,我们已经拥有法向量,我们还需要光照的位置即lightPos,通过uniform传入参数
uniform vec3 lightPos;
然后在渲染循环中(渲染循环的外面也可以,因为它不会改变)更新uniform。我们使用在前面声明的lightPos向量作为光源位置:
lightingShader.setVec3("lightPos", lightPos);
如此一来我们有了光照的位置。
最后,我们还需要片段的位置。我们会在世界空间中进行所有的光照计算,因此我们需要一个在世界空间中的顶点位置。我们可以通过把顶点位置属性乘以模型矩阵(不是观察和投影矩阵)来把它变换到世界空间坐标。这个在顶点着色器中很容易完成,所以我们声明一个输出变量,并计算它的世界空间坐标:
out vec3 FragPos;
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
FragPos = vec3(model * vec4(aPos, 1.0));//从模型坐标系转换到世界坐标系中,然后输出到fragmentShader
Normal = aNormal;
}
最后,在片段着色器中添加相应的输入变量。
in vec3 FragPos;
现在,所有需要的变量都设置好了,我们可以在片段着色器中添加光照计算了。
我们需要做的第一件事是计算光源和片段位置之间的方向向量。前面提到,光的方向向量是光源位置向量与片段位置向量之间的向量差。我们能够简单地通过让两个向量相减的方式计算向量差。我们同样希望确保所有相关向量最后都转换为单位向量,所以我们把法线和最终的方向向量都进行标准化:
利用光源在世界坐标系下的向量减去目标点在世界坐标系下的向量就能得到一个向量,该向量为从光源指向目标点。然后利用该向量和法向量求点积就能得到该向量在该法向量上的分量,也就是光照的强度。
vec3 norm = normalize(Normal);//获取法向量并且标准化成单位向量
vec3 lightDir = normalize(lightPos - FragPos);//光照方向向量,该向量由光源出发,指向顶点(向量相减,指向被减)
float diff = max(dot(norm, lightDir), 0.0);//如果光照强度小于0 则取0
vec3 diffuse = diff * lightColor;//漫反射的系数乘上光照
现在我们有了环境光分量和漫反射分量,我们把它们相加,然后把结果乘以物体的颜色,来获得片段最后的输出颜色。
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);
白色为光源。
补充
现在我们已经把法向量从顶点着色器传到了片段着色器。可是,目前片段着色器里的计算都是在世界空间坐标中进行的。所以,我们是不是应该把法向量也转换为世界空间坐标?基本正确,但是这不是简单地把它乘以一个模型矩阵就能搞定的。
首先,法向量只是一个方向向量,不能表达空间中的特定位置。同时,法向量没有齐次坐标(顶点位置中的w分量)。这意味着,位移不应该影响到法向量。因此,如果我们打算把法向量乘以一个模型矩阵,我们就要从矩阵中移除位移部分,只选用模型矩阵左上角3×3的矩阵(注意,我们也可以把法向量的w分量设置为0,再乘以4×4矩阵;这同样可以移除位移)。对于法向量,我们只希望对它实施缩放和旋转变换。
其次,如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了。因此,我们不能用这样的模型矩阵来变换法向量。下面的图展示了应用了不等比缩放的模型矩阵对法向量的影响:
每当我们应用一个不等比缩放时(注意:等比缩放不会破坏法线,因为法线的方向没被改变,仅仅改变了法线的长度,而这很容易通过标准化来修复),法向量就不会再垂直于对应的表面了,这样光照就会被破坏。
修复这个行为的诀窍是使用一个为法向量专门定制的模型矩阵。这个矩阵称之为法线矩阵(Normal Matrix),它使用了一些线性代数的操作来移除对法向量错误缩放的影响。
法线向量变换矩阵
假设在变换前的图像为:
变换后的图像为:
显然直接通过法向量直接乘变换矩阵是错误的。
现在假设从图1变换到图2的变换矩阵为M 则变换后的T‘ = MT。现假设有一个正确变换的法线变换矩阵G,这个G就是我们需要求解的。它满足 N’ = GN. 故有以下变换
(
G
N
)
.
(
M
T
)
=
0
(GN).(MT)=0
(GN).(MT)=0
向量的点积可以转换为两个矩阵的乘积
Normal = mat3(transpose(inverse(model))) * aNormal;//法线向量矩阵
为此即算完整的漫反射模型。
完整的顶点着色器的代码为 vertexShader
#version 330 core
layout (location = 0) in vec3 aPos;//位置
layout (location = 1) in vec3 aNormal;//法向量
out vec3 FragPos;//顶点在世界坐标系下的位置
out vec3 Normal;//法向量
uniform mat4 model;//从模型坐标系转换到世界坐标系
uniform mat4 view;//从世界坐标系转换到相机坐标系
uniform mat4 projection;//从相机坐标系转换到透视坐标系
void main()
{
FragPos = vec3(model * vec4(aPos, 1.0));//顶点坐标
Normal = mat3(transpose(inverse(model))) * aNormal;//法线变换
gl_Position = projection * view * vec4(FragPos, 1.0);//顶点位置,转换到透视坐标系的顶点位置
}
镜面光照
镜面高光(Specular Highlight)加进来,这样冯氏光照才算完整。
和漫反射光照一样,镜面光照也是依据光的方向向量和物体的法向量来决定的,但是它也依赖于观察方向,例如玩家是从什么方向看着这个片段的。镜面光照是基于光的反射特性。如果我们想象物体表面像一面镜子一样,那么,无论我们从哪里去看那个表面所反射的光,镜面光照都会达到最大化。你可以从下面的图片看到效果:
我们通过反射法向量周围光的方向来计算反射向量。然后我们计算反射向量和视线方向的角度差,如果夹角越小,那么镜面光的影响就会越大。它的作用效果就是,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光。
观察向量是镜面光照附加的一个变量,我们可以使用观察者世界空间位置和片段的位置来计算它。之后,我们计算镜面光强度,用它乘以光源的颜色,再将它加上环境光和漫反射分量。
为了得到观察者的世界空间坐标,我们简单地使用摄像机对象的位置坐标代替(它当然就是观察者)。所以我们把另一个uniform添加到片段着色器,把相应的摄像机位置坐标传给片段着色器:
uniform vec3 viewPos;
现在我们已经获得所有需要的变量,可以计算高光强度了。首先,我们定义一个镜面强度(Specular Intensity)变量,给镜面高光一个中等亮度颜色,让它不要产生过度的影响。
float specularStrength = 0.5;
vec3 viewDir = normalize(viewPos - FragPos);//视角方向
vec3 reflectDir = reflect(-lightDir, norm);//光照方向和视角方向相反,需要取一个负号
需要注意的是我们对lightDir
向量进行了取反。reflect
函数要求第一个向量是从光源指向片段位置的向量,但是lightDir
当前正好相反,是从片段指向光源(由先前我们计算lightDir
向量时,减法的顺序决定)。为了保证我们得到正确的reflect
向量,我们通过对lightDir
向量取反来获得相反的方向。第二个参数要求是一个法向量,所以我们提供的是已标准化的norm
向量。
剩下要做的是计算镜面分量。下面的代码完成了这件事:
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
我们先计算视线方向与反射方向的点乘(并确保它不是负值),然后取它的32次幂。这个32是高光的反光度(Shininess)。一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。在下面的图片里,你会看到不同反光度的视觉效果影响:
我们不希望镜面成分过于显眼,所以我们把指数保持为32。剩下的最后一件事情是把它加到环境光分量和漫反射分量里,再用结果乘以物体的颜色:
vec3 result = (ambient + diffuse + specular) * objectColor; FragColor = vec4(result, 1.0);
整体的fragmentShader为:
#version 330 core
out vec4 FragColor;
in vec3 Normal; //法向量
in vec3 FragPos; //位置点
uniform vec3 lightPos; //光源位置
uniform vec3 viewPos; //观察者位置
uniform vec3 lightColor;//光源颜色
uniform vec3 objectColor;//物体颜色
void main()
{
//冯氏着色器
// ambient
float ambientStrength = 0.1;//环境光系数
vec3 ambient = ambientStrength * lightColor;//环境光强度
// diffuse 漫反射
vec3 norm = normalize(Normal);//法向量归一化
vec3 lightDir = normalize(lightPos - FragPos);//光照方向
float diff = max(dot(norm, lightDir), 0.0);//计算在法向量上的投影
vec3 diffuse = diff * lightColor;//漫反射的大小
// specular 镜面反射
float specularStrength = 0.5;
vec3 viewDir = normalize(viewPos - FragPos);//观察者的方向向量
vec3 reflectDir = reflect(-lightDir, norm); //计算镜面反射的值
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);
}
小结
本章节主要讨论的是光照模型,主要是对shader的编写以及对各个向量之间的转换关系。建议在熟悉好各个坐标系的转换关系,以及各个向量变换关系后再仔细研读。本文仅仅作为自身的一个学习记录。详细可以访问openGL