Press "Enter" to skip to content

OpenGL(9)之基础光照

 

0.前言

 

世间之大,还有许多未知需要去探索,还有许多已知需要去深入,所以努力吧,不要耗费自己的光阴。

 

1.基础光照

 

现实世界光照及其复杂,本文所描述的是OpenGL的光照是对其进行了相当的简化,是对现实情况的近似模拟,这样处理起来就会更加容易一些。

 

而这些光照模型都是基于光的物理特性而衍生出来的一种近似等效。

 

其中最为研究比较广泛的是冯氏光照模型(Phong Lighting Model),主要结构由3个分量组成:

环境光照(Ambient Lighting): 处于黑暗情况下,世界上仍然会有一些光亮(月亮,远光源),因此物体几乎不会是完全黑暗使用环境光常量,永远会给物体一些颜色.(就如同人生不可能总是暗淡无光);

 

漫反射光照(Diffuse Lighting): 物体的某一部分越是正对光源,就会越发铮亮(直面惨淡的人生,也就不会害怕黑夜,光明就会在前方,共勉);

 

镜面光照(Specular Lighting): 模拟有光泽物体上面的出现亮点,镜面光照的颜色相比于物体的颜色会更加倾向于光的颜色。(人生路上,会有许多声音,始终还是坚持心中所想。);

 

2.环境光照

 

光通常来讲不是来自同一个光源,而是来自周围分散的光源,并向很多方向发散反弹,从而能够达到不是非常直接临近的点,所以光能够在其他表面上反射,对一个物体产生间接的影响(导致在该物体上的光线亮度的不同加成,呈现不同的光照效果)。

 

对于上述的情况考虑的算法通常是全局照明算法,此算法开销高且复杂,因此使用一种简化的全局照明模型,即环境光照,将环境光添加到场景,这样做的话场景没有直接光源也能看起来会存在一些发散的光。

 

将环境光添加到场景的方法:使用光的颜色乘以一个很小的常量环境因子(环境光),再乘以物体的颜色,然后将最终结果作为片段的颜色:

 

void main(){
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor;
FragColor = vec4(result,1.0);
}

如上图:发现环境光立方体非常暗,由于应用了环境光照,也不完全暗淡无光,光源立方体没有受影响。

 

3.漫反射光照

 

漫反射较之环境光会对物体产生显着的视觉效果,漫反射光照会使得物体上与光线方向越接近的片段,能从光源处获取更多的亮度。

如上图,左上方的一个光源,发出的光线落在物体的一个片段上,在这里需要测量该光线是以什幺角度接触这个片段的,如果光线垂直于物体表面即

 

与法向量平行,那幺此光源对物体的影响也就最大,使得该片段亮度越强(其实就是这种光照效果的呈现)。

 

为了求出光线和片段之间的角度,需要使用到法向量的概念,其是垂直于片段表面的一个向量,通过光照向量与法向量,二者之间通过点乘即可求出他们之间的角度。

 

总结:当两个单位向量的夹角越小,其点乘结果就越倾向于1,当两个向量的夹角为90度,点乘结果为0,这样来说对于$$\theta$$ 其值越大,光对片段颜色影响也就越小(即对物体造成的亮度就比较弱)

 

计算漫反射光照的条件

法向量:一个垂直于顶点表面的向量
定向的光线:作为光源位置与片段位置之间向量差的方向向量,(光的位置向量、片段的位置向量)。

4.法向量

 

法向量是一个垂直于顶点表面(这个表面是由顶点所构成的,并不能说明法向量与顶点的点乘结果是正交或者平行的)的向量,由于顶点本身没有表面(只是空间中一个独立的点),因此可利用其周围的顶点来计算出这个顶点的表面。

 

方式:通过对立方体的所有顶点使用叉乘计算出法向量。(一个立方体由6个平面组成,每个平面由6个顶点组成即(两个三角形组成一个平面),因此总共需要36个顶点组成一个立方体)。

 

关于叉乘知识可参考: 变换

 

这里通过将法线数据添加到顶点数据中,如下:

 

float vertices[] = {
// position  // normal
    -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
};

 

由于上述对顶点数组添加了额外的数据,因此需要进行顶点着色器的更新:

 

#version 330 core
layout (location =0 ) in vec3 aPos;
layout (location =1 ) in vec3 aNormal;
...

 

上述每个顶点都添加了一个法向量并更新了顶点着色器,因此还需要进行顶点属性的更新,注意:光源使用了同样的顶点数据(是为了将光源映射为物体的表面)作为其顶点数据,但是光源的着色器并没有使用新添加的法向量,不需要更新光源的着色器或者属性配置,但是必须修改一下顶点属性指针来适应新的顶点数组大小。

 

glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,6*sizeof(float),(void*)0);
glEnableVertexAttribArray(0);

 

上述代码片段步长为6,是为了使用每个顶点的前三个,并且忽略后三个而设计的。

 

所有的光照计算都是在片段着色器中进行,因此需要将法向量由顶点着色器传递到片段着色器(由顶点着色器输出到片段着色器中);

 

out vec3 Normal;
void main(){
gl_Position = projection * view * model * vec4(aPos,1.0);
Normal = aNormal;
}

 

在片段着色器中定义相应的输入变量;

 

in vec3 Normal;

 

5.计算漫反射光照

 

上述步骤已经为每个顶点分配了法向量,但是仍然需要知道光源的位置向量以及片段的位置向量,由于光源的位置是一个静态变量,因此可以在片段着色器中将其声明为uniform

 

uniform vec3 lightPos;

 

接着在渲染过程更新uniform

 

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));
Normal = aNormal;
}

 

最后在片段着色器中添加相应的输入变量

 

in vec3 FragPos;

 

光的方向向量是光源的位置向量与片段位置向量之间的向量差,通过两个向量相减的方式计算向量差,还需要确保所有相关向量都转换为单位向量,因此需要将法线和最终的方向向量进行标准化:

 

//标准化的法向量
vec3 norm = normalize(Normal);
//光的方向向量
vec3 lightDirection = normalize(lightPos - FragPos);

 

接下来需要计算光源对当前片段实际产生的漫反射影响(其实就是相应的亮暗程度)对norma和lightDirection向量进行点乘,计算的结果值再乘以光的颜色,得到漫反射分量,两个向量之间的角度越大,漫反射分量越小(也就是越暗);

 

float diff = max(dot(norm,ligthDirection),0.0);
vec3 diffuse = diff * lightColor;

 

通过上述的环境光分量和漫反射分量,将其相加,然后将结果乘以物体的颜色,得出片段最后的输出颜色。

 

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

由上述环境光和漫反射操作使得图中的立方体的真实感大大加强了。

 

6.法线矩阵

 

由上述操作,已经将法向量从顶点着色器传到了片段着色器,当前的片段着色器的计算是在世界空间坐标中进行的,但是不能单纯的讲法向量乘以一个模型矩阵将其转换到世界空间中,原因如下:

 

首先,法向量只是一个方向向量,不能表达空间中的特定的位置,同时法向量没有齐次坐标(w分量),意味着位移操作不能够影响法向量; 如果把法向量乘一个模型矩阵,就必须从矩阵中移除位移部分,只选用模型矩阵左上角3*3的矩阵(也可讲法向量的w分量设置为0,再乘以模型矩阵(4*4)同样可以完成位移移除,对法向量实施缩放和旋转变换是允许的。

 

其次,如果模型矩阵执行了非均匀缩放的话,顶点的改变会导致法向量不再垂直于表面,当法向量不再垂直于对应的表面,这样光照就会被破坏。

如何修复上述不再垂直表面的这个行为呢?可以为该法向量专门定制一个模型矩阵,该矩阵用来解决上述缩放的问题,被称为法线矩阵. 使用线性代数的操作来移除对法向量错误缩放的影响。

 

在顶点着色器中,可以使用inverse和transpose函数来生成这个法线矩阵,注意生成的法线矩阵还需要强制转换为3*3矩阵,用来保证其失去位移属性以及能乘以vec3的法向量。

 

Normal = mat3(transpose(inverse(model))) * aNormal;

 

小结:在漫反射光照部分,如果没有对物体进行任何缩放操作,并不是必须要使用一个法线矩阵,仅仅让模型矩阵乘以法线即可,如果进行了**不等比(均匀)**缩放,使用法线矩阵(处理之后的模型矩阵)乘以法线量就必不可少了。

 

7.镜面光照

 

介绍了上述的两个阶段,接着讲冯氏光照的最后一个阶段: 镜面光照 。

 

镜面光照依据光的方向向量和物体的法向量来决定,但是还需要依赖观察方向(即用户从哪个角度来观察这个片段)。

由上述图像,我们通过反射法向量周围光的方向来计算反射向量(其中的~R1), 当计算反射向量与视线方向的角度越小,则镜面光的影响就会越大(效果就是高亮的光)。

 

其中涉及的变量如下:

观察(视线方向)向量:可使用观察者世界空间位置和片段的位置来计算。
光的方向向量
环境光分量
漫反射分量
镜面分量:通过视线方向向量与光的方向向量进行点乘
镜面强度
光源的颜色

 

    1. 得到观察者的世界空间坐标,简单的使用摄像机对象的位置代替(其也被作为观察者),定义一个unirofm添加到片段着色器中,把相应的摄像机位置坐标传给片段着色器。

 

 

uniform vec3 viewPos;
 lightingShader.setVec3("viewPos",camera.Position);

 

 

    1. 定义一个镜面强度变量用于计算高光强度,给一个中等亮度颜色值;如果设置为1.0f,会得到一个非常亮的镜面光分量。

 

 

float specularStrength = 0.5;

 

 

    1. 计算视线方向向量,以及反射向量

 

 

// 视线方向向量
vec3 viewDirection  = normalize(viewPos - FragPos);
 //反射向量 = 光的方向向量 与 法向量
 vec3 reflectDirection = reflect(-lightDirection,norm)

 

 

    1. 计算镜面分量:计算视线方向与反射方向的点乘,然后取其32次幂。其中32为高光的反光度(Shininess). 一个物体的反光度越高,反射光的能力越强,散射就会越少,高光点就会越小。效果就会越逼真。

 

 

float spec = pow(max(dot(viewDirection,reflectDirection),0.0),32);
vec3 specular = specularStrength * spec * lightColor;

 

    1. 将镜面分量加到环境光分量和漫反射分量中来,最后得到组合光的效果。

 

 

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

 

最后呈现就如下图所示了,确实组合光照的效果就比其中任意一个分量的效果显得真实。

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注