OpenGL自制3D游戏引擎入门X

Lighting 基础光照

一、颜色

  1. RGB三个值就可以组合出任意一种我们想要的颜色

  2. 我们日常生活中看到的颜色实际上是物体反射的颜色(即那些无法被物体吸收的颜色),这一颜色反射定理直接被运用在图形领域,我们在OpenGL中创建一个光源时,也会给光源一个颜色(例如白色,因为太阳光为白色复合光)。当我们把光源的颜色与物体的颜色值相乘,所得到的就是这个物体反射的颜色

     glm::vec3 lightColor(1.0f, 1.0f, 1.0f);
     glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
     glm::vec3 result = lightColor * toyColor; // = (1.0f, 0.5f, 0.31f);
    

二、创建光照场景

  1. 我们需要创建一个表示光源的立方体,因此需要新开一个VAO(当然也可以沿用被投光物体的VAO并利用model矩阵做一些变换就行,但之后会频繁对顶点数据等进行修改,为了不影响光源,建议新创建一个VAO)

     unsigned int lightVAO;
     glGenVertexArrays(1, &lightVAO);
     glBindVertexArray(lightVAO);
     // 只需要绑定VBO不用再次设置VBO的数据,因为箱子的VBO数据中已经包含了正确的立方体顶点数据
     glBindBuffer(GL_ARRAY_BUFFER, VBO);
     // 设置灯立方体的顶点属性(对我们的灯来说仅仅只有位置数据)
     glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
     glEnableVertexAttribArray(0);
    
  2. 以及定义片段着色器:从uniform变量中接受物体颜色和光源颜色并相乘

     #version 330 core
     out vec4 FragColor;
    
     uniform vec3 objectColor;
     uniform vec3 lightColor;
    
     void main()
     {
         FragColor = vec4(lightColor * objectColor, 1.0);
     }
    
  3. 当然我们需要为光源创建另外一套着色器,保证其能够在其他光照着色器发生改变时不受影响

     #version 330 core
     out vec4 FragColor;
    
     void main()
     {
         FragColor = vec4(1.0); // 将向量的四个分量全部设置为1.0
     }
    

三、基础光照概述

  1. 现实的光照极其复杂,因此OpenGL的光照使用简化后的模型,对实际情况进行近似,这样更容易处理且效果也不错
  2. **冯氏光照模型(Phong Lighting Model)**主要由三个分量组成:
    • 环境光照(Ambient Lighting):物体几乎永远不会是黑暗的,因为总有一些光照如日月,远处的光等,因此我们需要一个环境光照常量,它永远会给物体一些颜色
    • 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量,物体越是正对着光源,这一分量越亮
    • 镜面光照(Specular Lighting):模拟有光泽物体上的亮点,镜面光照的颜色相比于物体的颜色更倾向于光的颜色

四、环境光照

  1. 光的一个属性是它可以向很多方向发散并反弹,从而能够达到不是非常直接临近的点,因此光能够在其它表面上反射,对一个物体产生间接影响,如果要考虑这种情况则需要要用到全局光照(Global Illumination)这一算法,但这种算法开销高昂且复杂,我们暂时不考虑,我们这里用到的是简化后的环境光照模型

  2. 将环境光照添加到场景:用光的颜色乘以一个非常小的常量环境因子,再乘以物体的颜色,然后将最终结果最为片段的颜色

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

五、漫反射光照

  1. 漫反射光照使物体上与光线方向越接近的片段能从光源处获得更多的亮度

    如上图所示,光线与片段之间是有一定的角度的,如果光线垂直于物体表面,这束光对物体的影响会最大化,为了测量光线与片段之间的角度,我们引入**法向量(Normal Vector)**,要求得两向量之间的角度,只需要点乘即可计算出来(两单位向量之间夹角越小,点乘结果越倾向于1,当两向量夹角为90度时点乘变为0,这符合夹角越大,光照对片段的影响越小这一情况)
    Tip:为了得到两向量的夹角余弦值,我们需要对所有向量进行标准化

  2. 法向量是垂直于平面的向量,但顶点本身没有平面,因此需要借助周围的顶点来计算这一顶点的表面。我们可以使用叉乘对立方体的所有顶点计算法向量,但是由于我们用到的立方体不是复杂情况,所有我们可以手动将法线数据添加到顶点数据中并更新顶点着色器

     #version 330 core
     layout (location = 0) in vec3 aPos;
     layout (location = 1) in vec3 aNormal;
     ...
    
  3. 更新顶点属性指针后我们还需要更新顶点属性指针,因为我们的光源也使用同一个顶点数组,但并不需要添加法向量,因此我们只改变步长参数忽略后三个float值即可

  4. 现在我们对每个顶点都有了法向量,但我们仍然需要光源的位置向量和片段的位置向量,由于光源的位置是一个静态变量,我们可以在片段着色器中将其声明为uniform,然后在渲染循环中(渲染循环外也可以,因为是静态的)更新uniform

  5. 最后我们还需要片段的位置,我们会在世界空间中进行所以的光照计算,因此我们需要一个在世界空间中的顶点位置,通过把顶点位置属性乘以模型矩阵来将其变换到世界空间坐标

     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;
     }
    

Tip:当计算光照时我们通常只关心它们的方向,所以几乎所有的计算都使用单位向量完成,可以简化大部分的计算。所以进行光照计算时应该确保对相关向量进行标准化保证其是单位向量

  1. 下一步对法向量和光线方向进行点乘,计算光源对当前片段实际的漫反射影响,得到的结果再乘以光的颜色,得到漫反射分量。如果两个向量之间的角度大于90度,点乘结果变为负数,因此使用max函数返回两个参数中较大的一个,从而保证漫反射分量不会变为负数

     float diff = max(dot(norm, lightDir), 0.0);
     vec3 diffuse = diff * lightColor;
    
  2. 现在我们有了环境光分量与漫反射分量,将其相加再与物体颜色相乘得到最后的输出颜色

     vec3 result = (ambient + diffuse) * objectColor;
     FragColor = vec4(result, 1.0);
    
法线变换
  1. 现在我们已经把法向量从顶点着色器传到了片段着色器,目前片段着色器都是在世界空间坐标中进行的,我们应当将法向量转化为世界空间坐标,但并不能简单乘以一个模型矩阵就能搞定。首先法向量是一个方向向量,因此如果我们打算把法向量乘以一个模型矩阵,就应该从矩阵中移除位移部分,只选取模型矩阵左上角3x3的矩阵(当然也可以把法向量的w分量设置为0,然后乘以4x4的矩阵也不是不可以)

  2. 其次,如果模型矩阵进行了不等比缩放,顶点的改变会导致法向量不再与表面垂直,修复这一行为的诀窍是使用一个为法向量专门定制的模型矩阵法线矩阵(Normal Matrix),它使用一些线性代数的操作移除对法向量错误缩放的影响

    法线矩阵被定义为
    模型矩阵左上角的逆矩阵(Inverse Matrix)的转置矩阵(Transpose Matrix)

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

     Normal = mat3(transpose(inverse(model))) * aNormal;
    
  4. 即使对于着色器来说,逆矩阵也是一个开销比较大的运算,因此尽可能避免在着色器中进行逆矩阵运算,为了效率考虑,应当在绘制之前就在CPU计算出法线矩阵,然后通过uniform传递给着色器

六、镜面光照

  1. 镜面光照也是依据光的方向向量和物体的法向量来决定的,同时它依赖于观察方向。镜面光照是基于光的反射特性,物体表面像一面镜子一样,那么无论从哪个方向观察镜面所反射的光,镜面光照都会达到最大化

  2. 通过反射法向量周围的光来计算反射向量,然后我们计算反射向量和视线方向的角度差,如果夹角越小,那么镜面光的影响越大,当从光被物体所反射的那个方向观察时,我们会看到一个高光。观察向量是镜面光照附加的一个变量,我们可以使用观察者世界空间位置和片段的位置来计算它。之后计算镜面光强度,乘以光源颜色,加上环境光和漫反射分量

  3. 为了得到观察者的世界空间坐标,我们简单地使用摄像机位置坐标代替,把另一个uniform添加到片段着色器,把相应的摄像机位置坐标传给片段着色器:

     uniform vec3 viewPos;
     lightingShader.setVec3("viewPos", camera.Position);
    
  4. 定义镜面强度(Specular Intensity)变量,给镜面高光一个中等亮度颜色,计算视线方向向量以及对应的沿着法线轴的反射向量并算出镜面分量

     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;
    
  5. 需要注意的是对lightDir向量进行取反,reflect函数要求第一个向量是从光源指向片段位置的向量,但lightDir正好相反(之前定义lightDir时是lightPos - FragPos)。第二个参数是一个法向量,我们直接提供已标准化的norm向量

  6. 同样,max函数确保视线方向与反射方向的点乘不是负值,并取其32次幂。此处32代表高光的反光度(Shininess)。一个物体反光度越高,反射光线的能力越强,散射越少,高光点就会越小

  7. 最后将镜面光照加到环境光分类与漫反射分量中,得到结果乘以物体颜色即可得到最终效果

     vec3 result = (ambient + diffuse + specular) * objectColor;
     FragColor = vec4(result, 1.0);
    
(下接 OpenGL入门XI)

夏のXオルタちゃんwithパーカー Pid:63793035


OpenGL自制3D游戏引擎入门X
https://baifabaiquan.cn/2021/04/04/OpenGL入门X/
作者
白发败犬
发布于
2021年4月4日
许可协议