美文网首页
OpenGL #06 Phong Lighting Model

OpenGL #06 Phong Lighting Model

作者: MisakiMel | 来源:发表于2019-10-23 13:43 被阅读0次

      趁着这段还没开始毕设的黄金闲暇时间,沉下心来学点自己想学的东西,例如重拾OpenGL就是不错的决定。在暑假学完基本光照之后,由于准备秋招的原因就放下了OpenGL的学习,但是现在想重拾就发现,之前学的基本光照已经忘了七七八八了。那只好重新学一遍了,而学基本光照那会并没有写学习总结发到简书上,正好现在重新学可以顺便把其补回来。
      本节学习的重点是冯氏光照模型(Phong Lighting Model),但在此之前,我们可以先来了解一下OpenGL中关于颜色的感念,这有助于我们了解该光照模型。

    颜色


      在现实生活中,每一个物体都拥有自己的颜色,于OpenGL而言,这也一样,每一个物体对象,都会有自己的颜色数据,虽然其无法提供与现实世界一样多的颜色种类,但我们仍能通过数值来表现出足够多的颜色,毕竟人眼能识别的颜色也是有限的。这个数值就是由红色(Red)、绿色(Green)、蓝色(Blue)三个分量组成,通常缩写为RGB。例如我们可以定义一个珊瑚红:glm::vec3 coral(1.0f, 0.5f, 0.31f);
      这就是颜色的全部了?不,事情没有这么简单,在现实生活中,我们能看到一个物体呈现什么颜色,取决的不是这个物体真正的颜色,而是其所反射的颜色。我们知道太阳光由多种不同颜色的光组合而成(就可见光而言)。如果我们将白光照在一个珊瑚色的玩具上,这个蓝色的玩具会吸收白光中除了珊瑚色以外的所有子颜色,不被吸收的珊瑚色光被反射到我们的眼中,让这个玩具看起来是珊瑚色的。但我们刚才给出的RGB数据可知,珊瑚色是一种混合颜色,所以其反射的颜色也是混合的颜色,由红绿蓝三种混合颜色而成,如果我们用的不是白光,而是纯蓝光,那么它看起来不会是它本身的它颜色,而是蓝色,因为其反射的只是该混合颜色的蓝色部分。这就是取决的不是这个物体真正的颜色,而是其所反射的颜色的含义。
      那么如何将这个颜色反射的定律运用到图形领域呢?其实关键词就只有一个,就是向量乘运算*,但在我们熟知的数学向量中,不存在乘运算啊,只有点乘·和叉乘x的说法,在数学领域,这的确不存在,但在图形学领域,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);
    

      我们可以看到玩具的颜色吸收了白色光源中很大一部分的颜色,但它根据自身的颜色值对红、绿、蓝三个分量都做出了一定的反射。这也表现了现实中颜色的工作原理。
      这个颜色理论已经足够了,我们可以可以开始学习Phong Lighting Model了。

    Phong Lighting Model


      现实世界的光照的极其复杂的,而且会受到诸多因素的影响,这是我们有限的计算能力所无法模拟的。因此OpenGL用的基于经验总结而成的简化模型,这样处理起来更容易一些,而且效果也尽如人意。光照模型有很多,冯氏光照模型(Phong Lighting Model)是比较简单的一个。冯氏光照模型主要结构由3个分量组成:环境(Ambient)漫反射(Diffuse)镜面(Specular)光照。

    • 环境光照(Ambient Lighting)

      往复杂了想,一个物体所接收的光,绝对不会是来自同一个光源,而是周围分散的多个光源,因为光能够在很多物体表面上反射,从而影响到其他物体。有对应与这种复杂情况的光照计算算法:全局照明(Global Illumination)算法,但开销高昂又极其复杂。
      而环境光照就是这种复杂情况的简化处理,我们把这些环境影响的光简化为来自一个光源的光,一个很小常量光照颜色,添加到物体片段的最终颜色中,这样即使物体没有受直接光源的照射,也会有一点发散光的感觉。
      为了举个例子,我们需要先建立一个光照场景,场景内有一个珊瑚色物体,无直接光源,但有环境光照影响。则我们需要一个物体颜色变量objectColor,一个光颜色变量ambientColor,物体顶点数据是沿用之前的正方体的,所以顶点着色器为:

    #version 330 core
    layout (location = 6) in vec3 aPos;   // 位置变量的属性位置值为 6
    
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;
    
    void main()
    {
        gl_Position = projection * view * model * vec4(aPos.x, aPos.y, aPos.z, 1.0f);
    }
    

      然后在片段着色器里定义两个uniform变量,用来记录物体的颜色和光的颜色,然后所有片段输出同一种颜色,而 环境光照的影响则是用光的颜色乘以一个很小的常量环境因子,再乘以物体的颜色,然后将最终结果作为片段的颜色:

    #version 330 core
    out vec4 fragColor;  
    
    uniform vec3 objectColor;
    uniform vec3 lightColor;
    
    void main()
    {
        float ambientStrength = 0.2;
        vec3 ambient = ambientStrength * lightColor;
    
        vec3 result = ambient * objectColor;
        fragColor = vec4(result, 1.0);
    }
    

      那么,我们把珊瑚色传给objectColor,把白光传给lightColor:

            shader.setVec3("lightColor", 1.0f, 0.5f, 0.31f);
            shader.setVec3("objectColor", 1.0f, 1.0f, 1.0f);
    
      其他诸如顶点数据、MVP矩阵的数据这里就不做列举,因为这不是重点。

      我们可以看到冯氏光照的第一个阶段已经应用到物体上了,这个物体非常的暗,但也不是完全黑的,在我看来把常量环境因子设为0.1,模仿的是在漆黑环境下依稀看到物体轮廓的这种感觉,而这种情况下确实是由环境远处微弱发散光造成的(如月光)。

    • 漫反射光照(Diffuse Lighting)

      我们知道,光的入射方向与物体表面法向量方向越靠近,其出射光的强度越强,即一束光垂直打在一个物体的表面,其反射光的亮度最大,而一束光与物体表面平行,那么反射光亮度为0。漫反射光照表示就是这么一种情况,光的入射方向与物体表面法向量方向的关系影响其反射光(反射颜色)的强度。



      图片左上方有一个光源,它所发出光落在一个片段上。我们要测量的就是这个光想是以什么角度入射到这个片段上的。如果光线垂直于表面,那么这束光对这个片段的影响最大(更亮)。为了测量入射光线方向与片段法向量方向的角度,我们需要点乘,如果点乘结果越大(越接近1)说明两向量的夹角越趋近于90度,如果结果越小(越接近0)说明两向量夹角越趋近于0度,利用点乘结果反映两向量的角度。
      注意,为了(只)得到两个向量夹角的余弦值,我们使用的是单位向量(长度为1的向量),所以我们需要确保所有的向量都是标准化的,否则点乘返回的就不仅仅是余弦值了。
      为了计算漫反射光照,我们需要什么?

    • 与入射光方向相反的单位方向向量(即图中黑线),可由光源位置与片段位置之间的向量差获得,那么我们需要光源的位置向量和片段的位置向量
    • 当前片段的法向量,即垂直顶点表面的向量。

    法向量

      顶点本身并没有表面,所以单独一个顶点的法向量没什么意义,如果一个顶点在一个平面内,该平面的法向量就是该顶点的法向量,我们可以利用顶点周围的点来计算出该顶点的表面。由于我们这里用的3D立方体不是复杂的形状,所以我们简单地把法线数据加入到顶点数据中,在顶点着色器设置一个法向量变量,并更新顶点的链接方式。

    float vertices[] = {
        //顶点坐标            //顶点的法向量
        -0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
         ...
    }
    
    //顶点着色器里
    #version 330 core
    layout (location = 6) in vec3 aPos;   // 位置变量的属性位置值为 6 
    layout (location = 7) in vec3 aNormal; // 法变量的属性位置值为 7
    ...
    
    //链接顶点属性
    glVertexAttribPointer(6, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 6, (void*)0);
    glEnableVertexAttribArray(6);
    glVertexAttribPointer(7, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 6, (void*)(sizeof(float) * 3));
    glEnableVertexAttribArray(7);
    

      我们打算所有的光照计算都放在片段着色器里,所以把在顶点着色器的法向量和顶点的位置变量传递给片段着色器,顶点的位置变量可由顶点坐标乘以模型矩阵转为世界空间坐标而得到。

    #version 330 core
    layout (location = 6) in vec3 aPos;   // 位置变量的属性位置值为 6
    layout (location = 7) in vec3 aNormal; // 颜色变量的属性位置值为 7
    
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;
    
    out vec3 Normal;
    out vec3 fragPos;
    void main()
    {
        gl_Position = projection * view * model * vec4(aPos.x, aPos.y, aPos.z, 1.0f);
        Normal = aNormal;
        fragPos = vec3(model * vec4(aPos, 1.0f));
    }
    

    然后在片段着色器里定义相应的输入变量:in vec3 Normal;in vec3 fragPos;

    计算漫反射光照

      顶点位置变量有了,顶点的法向量也有了,差的就是光源的位置变量了,我们在片段着色器中顶一个uniform变量lightPos记录光源的位置变量信息,在得到光源的位置变量后,两者相减得到光线的方向向量(入射方向的反方向),方向向量再与法向量点乘,得到漫反射分量。大概的思路就是这样,但有一些细节需要注意,让我们先看看代码:

        shader.setVec3("lightPos", 1.2f, 1.0f, 2.0f);
    
    #version 330 core
    out vec4 fragColor;  
    
    uniform vec3 objectColor;
    uniform vec3 lightColor;
    uniform vec3 lightPos;
    
    in vec3 Normal;
    in vec3 fragPos;
    void main()
    {
        float ambientStrength = 0.1;
        vec3 ambient = ambientStrength * lightColor;
    
        vec3 lightDir = normalize(lightPos - fragPos);
        float diff = max(dot(lightDir, normalize(Normal)),0.0);
        vec3 diffuse = lightColor * diff;
    
        vec3 result = (ambient + diffuse)* objectColor;
    
        fragColor = vec4(result, 1.0);
    }
    

      可以看到,在计算光线的方向向量时,我们使用了一个函数叫normalize(),这是一个将向量标准化的函数,因为当计算光照时我们不考虑一个向量的摸长,只考虑它的方向,所以为了简化计算,也为了防止计算错误,在进行光照计算时,对相关向量进行标准化是最应该做的。
      另外,在进行余弦值计算时,我们使用了max()函数对其进行最小值限定,如果其夹角超过90度,余弦值为负数,这会使得漫反射分量变为负数,这是没有意义的,所以使用max()函数防止这种情况发生。
      在有了环境光分量和漫反射分量之后,我们把它们相加,然后把结果乘以物体的颜色,来获得片段最后的输出颜色。



      可以看到,在法向量不同的表面,其反射颜色的情况也不尽相同,说明漫反射光照已经应用到物体上,但由于我并没有把光源实体化(仅提供了一个光源位置),所以并不知道光源在视窗的哪个位置上,为了更能体现漫反射光照带来的效果,接下来把光源实体化:
    我们需要

    1. 定义一个新的VAO,为了从简处理,我们用的依旧是正方体的顶点数据,所以该VAO绑定之前已经创建好的VBO就好了:
            //光源实体化
            unsigned int lightVAO;
            glGenVertexArrays(1, &lightVAO);
            glBindVertexArray(lightVAO);
            // 只需要绑定VBO不用再次设置VBO的数据,因为箱子的VBO数据中已经包含了正确的立方体顶点数据
            glBindBuffer(GL_ARRAY_BUFFER, VBO);
            glVertexAttribPointer(6, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 6, (void*)0);
            glEnableVertexAttribArray(6);
    
    1. 为该光源定义一个新的顶点着色器和片段着色器,顶点着色器将顶点坐标与MVP矩阵相乘即可,而片段着色器固定输出白光颜色就好:
    //VertexShaderSourceOfLight.vert
    #version 330 core
    layout(location = 6) in vec3 aPos;
    
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;
    
    void main()
    {
        gl_Position = projection * view * model * vec4(aPos, 1.0f);
    }
    
    //FragmentShaderSourceOfLight.frag
    #version 330 core
    out vec4 FraColor;
    
    void main()
    {
        FraColor = vec4(1.0);
    }
    
    1. 定义一个新的shader对象
        Shader lampShader("VertexShaderSourceOfLight.vert", "FragmentShaderSourceOfLight.frag");
    
    1. 在渲染循环里激活shader,为顶点着色器传递MVP矩阵,并渲染。
        while (!glfwWindowShouldClose(window))
        {
            ...
            lampShader.use();
            modelMat = glm::mat4(1.0f);
            modelMat = glm::translate(modelMat, lightPos);
            modelMat = glm::scale(modelMat, glm::vec3(0.2f));
            glUniformMatrix4fv(glGetUniformLocation(lampShader.ID, "model"), 1, GL_FALSE, glm::value_ptr(modelMat));
            glUniformMatrix4fv(glGetUniformLocation(lampShader.ID, "view"), 1, GL_FALSE, glm::value_ptr(viewMat));
            glUniformMatrix4fv(glGetUniformLocation(lampShader.ID, "projection"), 1, GL_FALSE, glm::value_ptr(projMat));
                
            glBindVertexArray(lightVAO);
            glDrawArrays(GL_TRIANGLES, 0, 36);
            ...
        }
    
      如果没问题,就可以看到效果了:

      要注意的是,把光源实体化并不意味着该立方体具有发光功能,从计算漫反射中可以看出,我们只需要光源的位置就能计算被照射物体的漫反射,而光源的实体化只是为了更好的视觉效果,无助于计算漫反射。

    • 镜面光照(Specular Lighting)

      漫反射计算的是入射角对亮度的影响,那么镜面光照计算的就是反射和玩家视线的夹角对亮度的影响。


      就是说,假设某点的反射光是散射光,那么观察者视线与该散射光的中线(就是以入射光线和法向量计算出的出射光线)越靠近,摄像机获得的亮度就越大,反之越小。其效果就是当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光。
      为了计算镜面光照,显然我们需要反射向量和观察向量(也就是观察者视线),观察向量需要顶点位置和观察者的位置,顶点位置已知,而观察者位置我们用摄像机对象的位置坐标代替,这样就能轻易得到观察向量了。
      首先,我们需要在片段着色器里
    定义一个uniform变量来记录观察者的位置坐标:uniform vec3 viewPos;
    在main函数里
    把摄像机对象的位置坐标传递给它:shader.setVec3("viewPos", camera.Position);
    在片段着色器里
    两者相减,就得到了观察向量了:vec3 viewDir = normalize(viewPos - fragPos);
      至于反射向量,OpenGL提供了一个函数帮助我们计算,只要我们喂给该函数一入射向量,一法向量,就可就得反射向量。要注意的是入射向量有方向上的要求,是入射方向,与我们求得的lightDir方向相反:vec3 reflectDir = reflectDir(-lightDir, Normal);
      至此,两个所需向量已经获得,按照计算漫反射的经验我们只需将它们点乘然后限制最小值即可float spec = max(dot(viewDir, reflectDir), 0.0);,我们可以看看效果:

      可以看到立方体该面的上方亮度比下方的更亮,这就是镜面反射造成的,但效果不够明显,我们可以考虑使用幂运算的方法使效果更明显。我们知道点乘就是求余弦值,如果两向量夹角小,其余弦值越大,夹角大,其余弦值越小,但不会低于0,那么对点乘结果使用幂运算的话,会导致夹角小的与夹角大的结果产生很大的差距,而所取的幂次方越大,其差距越大,高亮效果越明显(效果不明显就是因为差距太小):

      例如0.9*0.9 = 0.81 ,0.3*0.3 = 0.09,0.72的差距比0.6大。
      我们可以看看对结果取32次幂会有怎样的效果,OpenGL提供了pow函数做幂运算:
        float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
        vec3 specular = lightColor * spec * 0.5;
    
        vec3 result = (ambient + diffuse + specular)* objectColor;
    

      可以看到立方体该面的右上角有一高光(观察角度与刚才差不多),效果就很明显了。
      最后给出整个片段着色器的代码:

    #version 330 core
    out vec4 fragColor;  
    
    uniform vec3 objectColor;
    uniform vec3 lightColor;
    uniform vec3 lightPos;
    uniform vec3 viewPos;
    
    in vec3 Normal;
    in vec3 fragPos;
    void main()
    {
        float ambientStrength = 0.1;
        vec3 ambient = ambientStrength * lightColor;
    
        vec3 lightDir = normalize(lightPos - fragPos);
        float diff = max(dot(lightDir, normalize(Normal)),0.0);
        vec3 diffuse = lightColor * diff;
    
        vec3 viewDir = normalize(viewPos - fragPos);
        vec3 reflectDir = reflect(-lightDir, normalize(Normal));
        float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
        vec3 specular = lightColor * spec * 0.5;
    
        vec3 result = (ambient + diffuse + specular)* objectColor;
    
        fragColor = vec4(result, 1.0);
    }
    

    相关文章

      网友评论

          本文标题:OpenGL #06 Phong Lighting Model

          本文链接:https://www.haomeiwen.com/subject/nakqvctx.html