LearnOpenGL笔记->入门->(基础光照-材质-光照贴图)

让我们开始吧!!;

光照

一.基础光照

现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是我们有限的计算能力所无法模拟的。因此OpenGL的光照使用的是简化的模型,对现实的情况进行近似,这样处理起来会更容易一些,而且看起来也差不多一样。这些光照模型都是基于我们对光的物理特性的理解。其中一个模型被称为冯氏光照模型(Phong Lighting Model)。冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。下面这张图展示了这些光照分量看起来的样子:

  • 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。

  • 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。

  • 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。

二.环境光照

光通常都不是来自于同一个光源,而是来自于我们周围分散的很多光源,即使它们可能并不是那么显而易见。光的一个属性是,它可以向很多方向发散并反弹,从而能够到达不是非常直接临近的点。所以,光能够在其它的表面上反射,对一个物体产生间接的影响。考虑到这种情况的算法叫做全局照明(Global Illumination)算法,但是这种算法既开销高昂又极其复杂。

由于我们现在对那种又复杂又开销高昂的算法不是很感兴趣,所以我们将会先使用一个简化的全局照明模型,即环境光照。正如你在上一节所学到的,我们使用一个很小的常量(光照)颜色,添加到物体片段的最终颜色中,这样子的话即便场景中没有直接的光源也能看起来存在有一些发散的光

把环境光照添加到场景里非常简单。我们用光的颜色乘以一个很小的常量环境因子,再乘以物体的颜色,然后将最终结果作为片段的颜色

#version 330 core
out vec4 FragColor;

uniform vec3 objectColor;
uniform vec3 lightColor;

void main()
{
    //加入了
    float ambientStrength = 0.2f;
    vec3 ambient = ambientStrength * lightColor;
    vec3 result = ambient * objectColor;
    FragColor = vec4(result,1.0f);
}

如果将ambientStrength改为0.1,则结果如下:

对比发现物体的颜色更加暗了,作为灯源的白色正方体没有影响的原因在于用的是另外一个着色器!!!;

三.漫反射光照

环境光照本身不能提供最有趣的结果,但是漫反射光照就能开始对物体产生显著的视觉影响了。漫反射光照使物体上与光线方向越接近的片段能从光源处获得更多的亮度。为了能够更好的理解漫反射光照,请看下图:

图左上方有一个光源,它所发出的光线落在物体的一个片段上。我们需要测量这个光线是以什么角度接触到这个片段的。如果光线垂直于物体表面,这束光对物体的影响会最大化(译注:更亮)。为了测量光线和片段的角度,我们使用一个叫做法向量(Normal Vector)的东西,它是垂直于片段表面的一个向量(这里以黄色箭头表示),我们在后面再讲这个东西。这两个向量之间的角度很容易就能够通过点乘计算出来。

你可能记得在变换那一节教程里,我们知道两个单位向量的夹角越小,它们点乘的结果越倾向于1。当两个向量的夹角为90度的时候,点乘会变为0。这同样适用于θθ越大,光对片段颜色的影响就应该越小。

【注意,为了(只)得到两个向量夹角的余弦值我们使用的是单位向量(长度为1的向量),所以我们需要确保所有的向量都是标准化的,否则点乘返回的就不仅仅是余弦值了(见变换。】

点乘返回一个标量,我们可以用它计算光线对片段颜色的影响。不同片段朝向光源的方向的不同,这些片段被照亮的情况也不同。

所以计算漫反射光照需要什么?

  • 法向量:一个垂直于顶点表面的向量

  • 定向的光线:作为光源的位置与片段的位置之间向量差的方向向量。为了计算这个光线,我们需要光的位置向量和片段的位置向量

四.法向量

法向量是一个垂直于顶点表面的(单位)向量由于顶点本身并没有表面(它只是空间中一个独立的点),我们利用它周围的顶点来计算出这个顶点的表面。我们能够使用一个小技巧,使用叉乘对立方体所有的顶点计算法向量,但是由于3D立方体不是一个复杂的形状,所以我们可以简单地把法线数据手工添加到顶点数据中。更新后的顶点数据数组可以在这里找到。试着去想象一下,这些法向量真的是垂直于立方体各个平面的表面的(一个立方体由6个平面组成)。

由于我们向顶点数组添加了额外的数据,所以我们应该更新光照的顶点着色器:

#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);

//物体的顶点属性配置-----;

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);//这里的步长为获得下一个属性值,应该右移六个长度的单位;
glEnableVertexAttribArray(0);
//法线顶点属性;
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

我们只想使用每个顶点的前三个float,并且忽略后三个float,所以我们只需要把步长参数改成float大小的6倍就行了。

虽然对灯的着色器使用不能完全利用的顶点数据看起来不是那么高效,但这些顶点数据已经从箱子对象载入后开始就储存在GPU的内存里了,所以我们并不需要储存新数据到GPU内存中。这实际上比给灯专门分配一个新的VBO更高效了。

所有光照的计算都是在片段着色器里进行,所以我们需要将法向量由顶点着色器传递到片段着色器。我们这么做:

首先在顶点着色器中:

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

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

out vec3 Normal;//顶点着色器要输出Normal值;

void main()
{
    gl_Position = projection*view*model*vec4(aPos,1.0);
    Normal = aNormal;//输出的Normal值等价于传入时顶点的法线值;
}

接下来,在片段着色器中定义相应的输入变量:

#version 330 core
out vec4 FragColor;

in vec3 Normal;//输入来自顶点着色器输出的参数变量;

uniform vec3 objectColor;
uniform vec3 lightColor;

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

五.计算漫反射光照

我们现在对每个顶点都有了法向量,但是我们仍然需要光源的位置向量和片段的位置向量。由于光源的位置是一个静态变量,我们可以简单地在片段着色器中把它声明为uniform

uniform vec3 lightPos;

然后在渲染循环中(渲染循环的外面也可以,因为它不会改变)更新uniform。我们使用在前面声明的lightPos向量(全局变量)作为光源位置:

lightingShader.setVec3("lightPos", lightPos);
//或者
glUniform3f(glGetUniformLocation(lightCubeshader.ID, "lightPos"), lightPos.x,lightPos.y,lightPos.z);

最后,我们还需要片段的位置我们会在世界空间中进行所有的光照计算,因此我们需要一个在世界空间中的顶点位置。我们可以通过把顶点位置属性乘以模型矩阵model(不是观察和投影矩阵)来把它变换到世界空间坐标这个在顶点着色器中很容易完成,所以我们声明一个输出变量,并计算它的世界空间坐标:(顶点着色器)

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

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

out vec3 Normal;
out vec3 FragPos;//输出一个通过model矩阵转换到世界空间的顶点;

void main()
{
    gl_Position = projection*view*model*vec4(aPos,1.0);
    Normal = aNormal;
    FragPos = vec3(model*vec4(aPos,1.0));//添加的第二步;
}

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

#version 330 core
out vec4 FragColor;

in vec3 Normal;
in vec3 FragPos;//添加了这一行,为一顶点在世界空间中的坐标!!;

uniform vec3 objectColor;
uniform vec3 lightColor;

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

现在,所有需要的变量都设置好了,我们可以在片段着色器中添加光照计算了!!!;

我们需要做的第一件事是计算光源和片段位置之间的方向向量。前面提到,光的方向向量是光源位置向量与片段位置向量之间的向量差。你可能记得在变换教程中,我们能够简单地通过让两个向量相减的方式计算向量差。我们同样希望确保所有相关向量最后都转换为单位向量,所以我们把法线和最终的方向向量都进行标准化[注意不要忘了标准化,为了计算后面的角度]:

vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);

此时的片段着色器代码(物体的着色程序中的片段着色器!)为:

in vec3 Normal;
in vec3 FragPos;

uniform vec3 lightPos;//光源的位置;

uniform vec3 objectColor;
uniform vec3 lightColor;

void main()
{
    float ambientStrength = 0.1f;
    vec3 ambient = ambientStrength * lightColor;
    vec3 result = ambient * objectColor;
    FragColor = vec4(result,1.0f);
//--------------------以下为新添加的部分----------------
    vec3 norm = normalize(Normal);//法线初始化;
    vec3 lightDir = normalize(lightPos-FragPos);//此为关照的向量;
}

【当计算光照时我们通常不关心一个向量的模长或它的位置,我们只关心它们的方向。所以,几乎所有的计算都使用单位向量完成,因为这简化了大部分的计算(比如点乘)。所以当进行光照计算时,确保你总是对相关向量进行标准化,来保证它们是真正地单位向量。忘记对向量进行标准化是一个十分常见的错误。】

下一步,我们对normlightDir向量进行点乘计算光源对当前片段实际的漫反射影响结果值再乘以光的颜色,得到漫反射分量两个向量之间的角度越大,漫反射分量就会越小

float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
//片段着色器变成这样子;
#version 330 core
out vec4 FragColor;

in vec3 Normal;
in vec3 FragPos;

uniform vec3 lightPos;//光源的位置;

uniform vec3 objectColor;
uniform vec3 lightColor;

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

    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos-FragPos);
//-----------///
    //计算漫反射分量;
    //首先进行漫反射系数的获取,这个其实就是两个向量夹角的cos值!!;
    float diff = max(0.0f,dot(norm,lightDir));
    //再获取漫反射分量!!;
    vec3 diffuse = diff * lightColor;
}

现在我们有了环境光分量和漫反射分量我们把它们相加,然后把结果乘以物体的颜色,来获得片段最后的输出颜色。

#version 330 core
out vec4 FragColor;

in vec3 Normal;
in vec3 FragPos;

uniform vec3 lightPos;//光源的位置;

uniform vec3 objectColor;
uniform vec3 lightColor;

void main()
{
    float ambientStrength = 0.1f;
    vec3 ambient = ambientStrength * lightColor;

    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos-FragPos);

    //计算漫反射分量;
    //首先进行漫反射系数的获取,这个其实就是两个向量夹角的cos值!!;
    float diff = max(0.0f,dot(norm,lightDir));
    //再获取漫反射分量!!;
    vec3 diffuse = diff * lightColor;
//------------------------------//
    vec3 result = (ambient+diffuse) * objectColor;
    FragColor = vec4(result,1.0f);

}

输出如下:

好耶!!;

现在我们已经把法向量从顶点着色器传到了片段着色器。可是,目前片段着色器里的计算都是在世界空间坐标中进行的。所以,我们是不是应该把法向量也转换为世界空间坐标?基本正确,但是这不是简单地把它乘以一个模型矩阵就能搞定的。

首先,法向量只是一个方向向量,不能表达空间中的特定位置。同时,法向量没有齐次坐标(顶点位置中的w分量)。这意味着,位移不应该影响到法向量。因此,如果我们打算把法向量乘以一个模型矩阵,我们就要从矩阵中移除位移部分,只选用模型矩阵左上角3×3的矩阵注意,我们也可以把法向量的w分量设置为0,再乘以4×4矩阵;这同样可以移除位移)。对于法向量,我们只希望对它实施缩放和旋转变换。

其次,如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了。因此,我们不能用这样的模型矩阵来变换法向量。下面的图展示了应用了不等比缩放的模型矩阵对法向量的影响:

每当我们应用一个不等比缩放时(注意:等比缩放不会破坏法线,因为法线的方向没被改变,仅仅改变了法线的长度,而这很容易通过标准化来修复),法向量就不会再垂直于对应的表面了,这样光照就会被破坏。

修复这个行为的诀窍是使用一个为法向量专门定制的模型矩阵。这个矩阵称之为法线矩阵(Normal Matrix),它使用了一些线性代数的操作来移除对法向量错误缩放的影响。如果你想知道这个矩阵是如何计算出来的,建议去阅读这个文章

法线矩阵被定义为「模型矩阵左上角3x3部分的逆矩阵的转置矩阵」。真是拗口,如果你不明白这是什么意思,别担心,我们还没有讨论逆矩阵(Inverse Matrix)和转置矩阵(Transpose Matrix)。注意,大部分的资源都会将法线矩阵定义为应用到模型-观察矩阵(Model-view Matrix)上的操作,但是由于我们只在世界空间中进行操作(不是在观察空间),我们只使用模型矩阵。

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

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

矩阵求逆是一项对于着色器开销很大的运算,因为它必须在场景中的每一个顶点上进行,所以应该尽可能地避免在着色器中进行求逆运算。以学习为目的的话这样做还好,但是对于一个高效的应用来说,你最好先在CPU上计算出法线矩阵,再通过uniform把它传递给着色器(就像模型矩阵一样)。

六.镜面反射

如果你还没被这些光照计算搞得精疲力尽,我们就再把镜面高光(Specular Highlight)加进来,这样冯氏光照才算完整。

和漫反射光照一样,镜面光照也决定于光的方向向量和物体的法向量,但是它也决定于观察方向,例如玩家是从什么方向看向这个片段的。镜面光照决定于表面的反射特性。如果我们把物体表面设想为一面镜子,那么镜面光照最强的地方就是我们看到表面上反射光的地方。你可以在下图中看到效果:

我们通过根据法向量翻折入射光的方向来计算反射向量。然后我们计算反射向量与观察方向的角度差,它们之间夹角越小,镜面光的作用就越大。由此产生的效果就是,我们看向在入射光在表面的反射方向时,会看到一点高光。

观察向量是我们计算镜面光照时需要的一个额外变量,我们可以使用观察者的世界空间位置和片段的位置来计算它。之后我们计算出镜面光照强度,用它乘以光源的颜色,并将它与环境光照和漫反射光照部分加和。

我们选择在世界空间进行光照计算,但是大多数人趋向于更偏向在观察空间进行光照计算。在观察空间计算的优势是,观察者的位置总是在(0, 0, 0),所以你已经零成本地拿到了观察者的位置。然而,若以学习为目的,我认为在世界空间中计算光照更符合直觉。如果你仍然希望在观察空间计算光照的话,你需要将所有相关的向量也用观察矩阵进行变换(不要忘记也修改法线矩阵)。

要得到观察者的世界空间坐标,我们直接使用摄像机的位置向量即可(它当然就是那个观察者)。那么让我们把另一个uniform添加到片段着色器中,并把摄像机的位置向量传给着色器:

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

现在我们已经获得所有需要的变量,可以计算高光强度了。首先,我们定义一个镜面强度(Specular Intensity)变量,给镜面高光一个中等亮度颜色,让它不要产生过度的影响。

float specularStrength = 0.5;

如果我们把它设置为1.0f,我们会得到一个非常亮的镜面光分量,这对于一个珊瑚色的立方体来说有点太多了。下一节教程中我们会讨论如何合理设置这些光照强度,以及它们是如何影响物体的。下一步,我们计算视线方向向量,和对应的沿着法线轴的反射向量:

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

我们现在为冯氏光照计算了全部的光照分量。根据你的视角,你可以看到类似下面的画面:

while (!glfwWindowShouldClose(window))
    {
        float currentime = glfwGetTime();
        deltatime = currentime - lastime;
        lastime = currentime;

        //准备输入:
        processInput(window);

        //每一次更新缓冲颜色,之后继续画下一帧;
        glClearColor(0.0f, 0.05f, 0.05f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);//清除颜色缓冲单元;
        glClear(GL_DEPTH_BUFFER_BIT);//清除深度缓存冲单元;

        绑定两个纹理,要先激活没一个纹理单元;
        //glActiveTexture(GL_TEXTURE0);
        //glBindTexture(GL_TEXTURE_2D, texture1);//绑定这个纹理到当前激活的纹理单元;
        //glActiveTexture(GL_TEXTURE1);//激活纹理单元1;
        //glBindTexture(GL_TEXTURE_2D, texture2);
        启用Shader对象shader的程序启动函数;

        //创建三个变换矩阵;
            //先初始化三个单位矩阵;
        glm::mat4 projection = glm::mat4(1.0f);
        glm::mat4 view = glm::mat4(1.0f);
        glm::mat4 model = glm::mat4(1.0f);

        //创建一个随时间旋转的矩阵:
        lightshader.useProgram();//启用着色器程序;
        //设置物体着色程序中的片段着色器中的objectcolor以及lightcolor;
        glUniform3f(glGetUniformLocation(lightshader.ID, "objectColor"), 1.0f, 0.5f, 0.31f);
        glUniform3f(glGetUniformLocation(lightshader.ID, "lightColor"), 1.0f, 1.0f, 1.0f);
        //光源的位置;

        glUniform3f(glGetUniformLocation(lightshader.ID, "lightPos"), 1.0f+sin(glfwGetTime())*2.0f, 1.0+cos(glfwGetTime())*2.0f, lightPos.z);
        //人眼的位置viewPos;
        glUniform3f(glGetUniformLocation(lightshader.ID, "viewPos"), cameraPos.x,cameraPos.y,cameraPos.z);

        view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
        projection = glm::perspective(glm::radians(fov), (float)800 / (float)600, 0.1f, 100.0f);

        //将变换矩阵传入着色器;注意到由于变换随时发生,要每一次迭代都进行,因此要放在里面;

        glUniformMatrix4fv(glGetUniformLocation(lightshader.ID, "model"), 1, GL_FALSE, glm::value_ptr(model));
        glUniformMatrix4fv(glGetUniformLocation(lightshader.ID, "view"), 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(glGetUniformLocation(lightshader.ID, "projection"), 1, GL_FALSE, glm::value_ptr(projection));

        glBindVertexArray(VAO);//绑定VAO;
        glDrawArrays(GL_TRIANGLES, 0, 36);

        lightCubeshader.useProgram();//启用另外一个着色器程序;画光源;

        model = glm::mat4(1.0f);//重新初始化model矩阵;
        model = glm::translate(model, glm::vec3(1.0f + sin(glfwGetTime()) * 2.0f, 1.0 + cos(glfwGetTime()) * 2.0f, lightPos.z));//物体移动到光源的位置;
        model = glm::scale(model, glm::vec3(0.2f));//光源缩小;
        //传入着色器;
        glUniformMatrix4fv(glGetUniformLocation(lightCubeshader.ID, "model"), 1, GL_FALSE, glm::value_ptr(model));
        glUniformMatrix4fv(glGetUniformLocation(lightCubeshader.ID, "view"), 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(glGetUniformLocation(lightCubeshader.ID, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
        // 绘制灯立方体对象
        glBindVertexArray(lightVAO);
        glDrawArrays(GL_TRIANGLES, 0, 36);//位置0,画36个点;

        //更新缓冲;
        glfwSwapBuffers(window);
        //进行检查;
        glfwPollEvents();
    }

上面为作业1,可以看到随着物体的移动的光照变化;

材质

一.材质

在现实世界里,每个物体会对光产生不同的反应。比如,钢制物体看起来通常会比陶土花瓶更闪闪发光,一个木头箱子也不会与一个钢制箱子反射同样程度的光。有些物体反射光的时候不会有太多的散射(Scatter),因而产生较小的高光点,而有些物体则会散射很多,产生一个有着更大半径的高光点。如果我们想要在OpenGL中模拟多种类型的物体,我们必须针对每种表面定义不同的材质(Material)属性

在上一节中,我们定义了一个物体和光的颜色,并结合环境光与镜面强度分量,来决定物体的视觉输出。当描述一个表面时,我们可以分别为三个光照分量定义一个材质颜色(Material Color):环境光照(Ambient Lighting)、漫反射光照(Diffuse Lighting)和镜面光照(Specular Lighting)。通过为每个分量指定一个颜色,我们就能够对表面的颜色输出有细粒度的控制了。现在,我们再添加一个反光度(Shininess)分量,结合上述的三个颜色,我们就有了全部所需的材质属性了

#version 330 core
struct Material {
    vec3 ambient;//ambient材质向量定义了在环境光照下这个表面反射的是什么颜色,通常与表面的颜色相同
    vec3 diffuse;//diffuse材质向量定义了在漫反射光照下表面的颜色。
    vec3 specular;//specular材质向量设置的是表面上镜面高光的颜色(或者甚至可能反映一个特定表面的颜色)
    float shininess;//shininess影响镜面高光的散射/半径。
}; 

uniform Material material;

在片段着色器中,我们创建一个结构体(Struct)来储存物体的材质属性。我们也可以把它们储存为独立的uniform值,但是作为一个结构体来储存会更有条理一些。我们首先定义结构体的布局(Layout),然后简单地以刚创建的结构体作为类型声明一个uniform变量。

如你所见,我们为冯氏光照模型的每个分量都定义一个颜色向量。ambient材质向量定义了在环境光照下这个表面反射的是什么颜色,通常与表面的颜色相同。diffuse材质向量定义了在漫反射光照下表面的颜色。漫反射颜色(和环境光照一样)也被设置为我们期望的物体颜色。specular材质向量设置的是表面上镜面高光的颜色(或者甚至可能反映一个特定表面的颜色)。最后,shininess影响镜面高光的散射/半径。

有这4个元素定义一个物体的材质,我们能够模拟很多现实世界中的材质。devernay.free.fr中的一个表格展示了一系列材质属性,它们模拟了现实世界中的真实材质。下图展示了几组现实世界的材质参数值对我们的立方体的影响:

可以看到,通过正确地指定一个物体的材质属性,我们对这个物体的感知也就不同了。效果非常明显,但是要想获得更真实的效果,我们需要以更复杂的形状替换这个立方体。在模型加载章节中,我们会讨论更复杂的形状。

搞清楚一个物体正确的材质设定是个困难的工程,这主要需要实验和丰富的经验。用了不合适的材质而毁了物体的视觉质量是件经常发生的事。

让我们试着在着色器中实现这样的一个材质系统。

二.设置材质

我们在片段着色器中创建了一个材质结构体的uniform,所以下面我们希望修改一下光照的计算来遵从新的材质属性。由于所有材质变量都储存在一个结构体中,我们可以从uniform变量material中访问它们:

void main(){    
    // 环境光
    vec3 ambient = lightColor * material.ambient;

    // 漫反射 
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = lightColor * (diff * material.diffuse);

    // 镜面光
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);  
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    vec3 specular = lightColor * (spec * material.specular);  

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

可以看到,我们现在在需要的地方访问了材质结构体中的所有属性,并且这次是根据材质的颜色来计算最终的输出颜色的。物体的每个材质属性都乘上了它们各自对应的光照分量。

我们现在可以通过设置适当的uniform来设置应用中物体的材质了。GLSL中一个结构体在设置uniform时并无任何区别,结构体只是充当uniform变量们的一个命名空间所以如果想填充这个结构体的话,我们必须设置每个单独的uniform但要以结构体名为前缀

lightingShader.setVec3("material.ambient",  1.0f, 0.5f, 0.31f);
lightingShader.setVec3("material.diffuse",  1.0f, 0.5f, 0.31f);
lightingShader.setVec3("material.specular", 0.5f, 0.5f, 0.5f);
lightingShader.setFloat("material.shininess", 32.0f);

我们将环境光和漫反射分量设置成我们想要让物体所拥有的颜色,而将镜面分量设置为一个中等亮度的颜色,我们不希望镜面分量过于强烈。我们仍将反光度保持为32。

现在我们能够轻松地在应用中影响物体的材质了。运行程序,你会得到像这样的结果:

不过看起来真的不太对劲?

2.1光的属性

这个物体太亮了。物体过亮的原因是环境光、漫反射和镜面光这三个颜色对任何一个光源都全力反射。光源对环境光、漫反射和镜面光分量也分别具有不同的强度。前面的章节中,我们通过使用一个强度值改变环境光和镜面光强度的方式解决了这个问题。我们想做类似的事情,但是这次是要为每个光照分量分别指定一个强度向量。如果我们假设lightColorvec3(1.0),代码会看起来像这样:

vec3 ambient  = vec3(1.0) * material.ambient;
vec3 diffuse  = vec3(1.0) * (diff * material.diffuse);
vec3 specular = vec3(1.0) * (spec * material.specular);

所以物体的每个材质属性对每一个光照分量都返回了最大的强度。对单个光源来说,这些vec3(1.0)值同样可以对每种光源分别改变,而这通常就是我们想要的。现在,物体的环境光分量完全地影响了立方体的颜色,可是环境光分量实际上不应该对最终的颜色有这么大的影响,所以我们会将光源的环境光强度设置为一个小一点的值,从而限制环境光颜色:

vec3 ambient = vec3(0.1) * material.ambient;//环境光强度不能太大;

我们可以用同样的方式影响光源的漫反射和镜面光强度。这和我们在上一节中所做的极为相似,你可以认为我们已经创建了一些光照属性来影响各个光照分量。我们希望为光照属性创建类似材质结构体的东西:

struct Light {
    vec3 position;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

uniform Light light;

一个光源对它的ambientdiffusespecular光照分量有着不同的强度。环境光照通常被设置为一个比较低的强度,因为我们不希望环境光颜色太过主导。光源的漫反射分量通常被设置为我们希望光所具有的那个颜色,通常是一个比较明亮的白色。镜面光分量通常会保持为vec3(1.0),以最大强度发光注意我们也将光源的位置向量加入了结构体。

和材质uniform一样,我们需要更新片段着色器:

//更新着色器,加入光照强度的变化;
vec3 ambient  = light.ambient * material.ambient;
vec3 diffuse  = light.diffuse * (diff * material.diffuse);
vec3 specular = light.specular * (spec * material.specular);

我们接下来在应用中设置光照强度:

lightingShader.setVec3("light.ambient",  0.2f, 0.2f, 0.2f);
lightingShader.setVec3("light.diffuse",  0.5f, 0.5f, 0.5f); // 将光照调暗了一些以搭配场景
lightingShader.setVec3("light.specular", 1.0f, 1.0f, 1.0f); 

现在我们已经调整了光照对物体材质的影响,我们得到了一个与上一节很相似的视觉效果。但这次我们有了对光照和物体材质的完全掌控:

改变物体的视觉效果现在变得相对容易了。让我们做点更有趣的事!

2.2不同的光源颜色

到目前为止,我们都只对光源设置了从白到灰到黑范围内的颜色,这样只会改变物体各个分量的强度,而不是它的真正颜色。由于现在能够非常容易地访问光照的属性了,我们可以随着时间改变它们的颜色,从而获得一些非常有意思的效果。由于所有的东西都在片段着色器中配置好了,修改光源的颜色非常简单,并立刻创造一些很有趣的效果:

https://learnopengl-cn.github.io/img/02/03/materials.mp4

你可以看到,不同的光照颜色能够极大地影响物体的最终颜色输出。由于光照颜色能够直接影响物体能够反射的颜色(回想颜色这一节),这对视觉输出有着显著的影响。

我们可以利用sin和glfwGetTime函数改变光源的环境光和漫反射颜色,从而很容易地让光源的颜色随着时间变化:


glm::vec3 lightColor;
lightColor.x = sin(glfwGetTime() * 2.0f);
lightColor.y = sin(glfwGetTime() * 0.7f);
lightColor.z = sin(glfwGetTime() * 1.3f);

glm::vec3 diffuseColor = lightColor   * glm::vec3(0.5f); // 降低影响
glm::vec3 ambientColor = diffuseColor * glm::vec3(0.2f); // 很低的影响
//以下将光照变化的强度加入片段着色器;
lightshader.setVec3("light.ambient", ambientColor);
lightshader.setVec3("light.diffuse", diffuseColor);

下面是代码框架;

三.片段着色器

//相比于上面的着色器,这里添加了材质和光照分量的强度调整;
#version 330 core
out vec4 FragColor;

in vec3 Normal;
in vec3 FragPos;

uniform vec3 lightPos;//光源的位置;要设置与全局变量光源位置一致;

uniform vec3 objectColor;
uniform vec3 lightColor;

uniform vec3 viewPos;//人眼的位置;

struct Material {
    vec3 diffuse;
    vec3 specular;
    vec3 ambient;
    float shininess;
};

//定义光照属性,用于调整光照;
struct Light{
    vec3 Position;
    vec3 diffuse;
    vec3 specular;
    vec3 ambient;
};

uniform Material material;
uniform Light light;

void main()
{
     // 环境光
    vec3 ambient = lightColor * material.ambient;

    // 漫反射 
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0f);
    vec3 diffuse = lightColor * (diff * material.diffuse);

    // 镜面光
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);  
    float spec = pow(max(dot(viewDir, reflectDir), 0.0f), material.shininess);
    vec3 specular = lightColor * (spec * material.specular);  

    //对三种光的分量进行调整;
    diffuse = light.diffuse*diffuse;
    specular = light.specular * specular;
    ambient = light.ambient * ambient;

    vec3 result = ambient + diffuse + specular;//可以得到最终到我们眼睛的物体材质的颜色;
    FragColor = vec4(result, 1.0f);
}

光照贴图

本节将引入漫反射镜面光贴图(Map),可以实现物体对漫反射分量以及镜面光分量有较为准确的把控;

一.漫反射贴图

我们希望通过某种方式对物体的每个片段单独设置漫反射颜色。有能够让我们根据片段在物体上的位置来获取颜色值的系统吗?

这可能听起来很熟悉,而且事实上这个系统我们已经使用很长时间了。这听起来很像在之前教程中详细讨论过的纹理,而这基本就是这样:一个纹理。我们仅仅是对同样的原理使用了不同的名字:其实都是使用一张覆盖物体的图像,让我们能够逐片段索引其独立的颜色值。在光照场景中,它通常叫做一个漫反射贴图(Diffuse Map),它是一个表现了物体所有的漫反射颜色的纹理图像

为了演示漫反射贴图,我们将会使用下面的图片,它是一个有钢边框的木箱.

在着色器中使用漫反射贴图的方法和纹理教程中是完全一样的。但这次我们会将纹理储存为Material结构体中的一个sampler2D。我们将之前定义的vec3漫反射颜色向量替换为漫反射贴图。

【【注意sampler2D是所谓的不透明类型(Opaque Type),也就是说我们不能将它实例化,只能通过uniform来定义它。如果我们使用除uniform以外的方法(比如函数的参数)实例化这个结构体,GLSL会抛出一些奇怪的错误。这同样也适用于任何封装了不透明类型的结构体。】】

我们也移除了环境光材质颜色向量,因为环境光颜色在几乎所有情况下都等于漫反射颜色,所以我们不需要将它们分开储存:

struct Material {
    sampler2D diffuse;
    vec3      specular;
    float     shininess;
}; 
...
in vec2 TexCoords;

//以上是文档中的写法,我们也可以不删除材质的环境光分量;

当然你也可以保留这个环境光的vec3但整个物体仍只能拥有一个环境光颜色。如果想要对不同片段有不同的环境光值,你需要对环境光值单独使用另外一个纹理。

注意我们将在片段着色器中再次需要纹理坐标,所以我们声明一个额外的输入变量。接下来我们只需要从纹理中采样片段的漫反射颜色值即可:

vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));

不要忘记将环境光的材质颜色设置为漫反射材质颜色同样的值。

vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));

为了让它正常工作,我们还需要使用纹理坐标更新顶点数据,将它们作为顶点属性传递到片段着色器,加载材质并绑定材质到合适的纹理单元。

更新后的顶点数据可以在这里找到。顶点数据现在包含了顶点位置、法向量和立方体顶点处的纹理坐标。让我们更新顶点着色器来以顶点属性的形式接受纹理坐标,并将它们传递到片段着色器中

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
...
out vec2 TexCoords;

voidmain(){
    ...
    TexCoords = aTexCoords;
}

记得去更新两个VAO的顶点属性指针来匹配新的顶点数据,并加载箱子图像为一个纹理。在绘制箱子之前,我们希望将要用的纹理单元赋值到material.diffuse这个uniform采样器,并绑定箱子的纹理到这个纹理单元:

lightingShader.setInt("material.diffuse", 0);
...
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, diffuseMap);

使用了漫反射贴图之后,细节再一次得到惊人的提升,你的箱子看起来可能像这样:

片段着色器代码如下:

#version 330 core
out vec4 FragColor;

in vec3 Normal;
in vec3 FragPos;
in vec2 TexCroods;


//uniform vec3 objectColor;
//uniform vec3 lightColor;

struct Material {
    sampler2D diffuse;
    vec3 specular;
    //vec3 ambient;
    float shininess;
};

//定义光照属性,用于调整光照;
struct Light{
    vec3 position;//光源位置;//光源的位置;要设置与全局变量光源位置一致;
    vec3 diffuse;
    vec3 specular;
    vec3 ambient;
};

uniform vec3 viewPos;//人眼的位置;
uniform Material material;
uniform Light light;

void main()
{
    // 漫反射 
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(light.position - FragPos);
    float diff = max(dot(norm, lightDir), 0.0f);
    vec3 diffuse = light.diffuse * diff * texture(material.diffuse,TexCroods).rgb;

     // 环境光
    vec3 ambient = light.ambient * texture(material.diffuse,TexCroods).rgb;

    // 镜面光
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);  
    float spec = pow(max(dot(viewDir, reflectDir), 0.0f), material.shininess);
    vec3 specular = light.specular * (spec * material.specular);  

    vec3 result = ambient + diffuse + specular;//可以得到最终到我们眼睛的物体材质的颜色;
    FragColor = vec4(result, 1.0f);
}

二.镜面光贴图

你可能会注意到,镜面高光看起来有些奇怪,因为我们的物体大部分都是木头,我们知道木头不应该有这么强的镜面高光的。我们可以将物体的镜面光材质设置为vec3(0.0)来解决这个问题,但这也意味着箱子钢制的边框将不再能够显示镜面高光了,我们知道钢铁应该是有一些镜面高光的。所以,我们想要让物体的某些部分以不同的强度显示镜面高光

我们同样可以使用一个专门用于镜面高光的纹理贴图。这也就意味着我们需要生成一个黑白的(如果你想得话也可以是彩色的)纹理,来定义物体每部分的镜面光强度。[白色主要对应钢铁边框,而黑色主要对应内部的木头材质]下面是一个镜面光贴图(Specular Map)的例子:

镜面高光的强度可以通过图像每个像素的亮度来获取。镜面光贴图上的每个像素都可以由一个颜色向量来表示,比如说黑色代表颜色向量vec3(0.0),灰色代表颜色向量vec3(0.5)在片段着色器中,我们接下来会取样对应的颜色值并将它乘以光源的镜面强度一个像素越「白」,乘积就会越大,物体的镜面光分量就会越亮。

【注意到如果是黑的部分,则代表向量为vec3(0.0f),所以乘出来之后木头部分是没有高光颜色的】;

由于箱子大部分都由木头所组成,而且木头材质应该没有镜面高光,所以漫反射纹理的整个木头部分全部都转换成了黑色。箱子钢制边框的镜面光强度是有细微变化的,钢铁本身会比较容易受到镜面高光的影响,而裂缝则不会。

(从实际角度来说,木头其实也有镜面高光,尽管它的反光度(Shininess)很小(更多的光被散射),影响也比较小,但是为了教学目的,我们可以假设木头不会对镜面光有任何反应。)

使用PhotoshopGimp之类的工具,将漫反射纹理转换为镜面光纹理还是比较容易的,只需要剪切掉一些部分,将图像转换为黑白的,并增加亮度/对比度就好了。

三.采样镜面光贴图

镜面光贴图和其它的纹理非常类似,所以代码也和漫反射贴图的代码很类似。记得要保证正确地加载图像并生成一个纹理对象。由于我们正在同一个片段着色器中使用另一个纹理采样器,我们必须要对镜面光贴图使用一个不同的纹理单元(见纹理),所以我们在渲染之前先把它绑定到合适的纹理单元上:

//加载并生成纹理,之后将纹理传入一个新的纹理单元;
lightingShader.setInt("material.specular", 1);
...
//将镜面光贴图传入一个新的纹理单元;
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, specularMap);

接下来更新片段着色器的材质属性,让其接受一个sampler2D而不是vec3作为镜面光分量

struct Material {
    sampler2D diffuse;
    sampler2D specular;//接收存储镜面光贴图的纹理单元的采样器;
    float     shininess;
};

最后我们希望采样镜面光贴图,来获取片段所对应的镜面光强度:

vec3 ambient  = light.ambient  * vec3(texture(material.diffuse, TexCoords));
vec3 diffuse  = light.diffuse  * diff * vec3(texture(material.diffuse, TexCoords));  
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));//高光修改
FragColor = vec4(ambient + diffuse + specular, 1.0);

下面是我的完整代码:

片段着色器代码

#version 330 core
out vec4 FragColor;

in vec3 Normal;
in vec3 FragPos;
in vec2 TexCroods;

struct Material {
    sampler2D diffuse;
    sampler2D specular;
    float shininess;
};

//定义光照属性,用于调整光照;
struct Light{
    vec3 position;//光源的位置;要设置与全局变量光源位置一致;
    vec3 diffuse;
    vec3 specular;
    vec3 ambient;
};

uniform vec3 viewPos;//人眼的位置;
uniform Material material;
uniform Light light;

void main()
{
    // 漫反射 
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(light.position - FragPos);
    float diff = max(dot(norm, lightDir), 0.0f);
    vec3 diffuse = light.diffuse * diff * texture(material.diffuse,TexCroods).rgb;

     // 环境光
    vec3 ambient = light.ambient * texture(material.diffuse,TexCroods).rgb;

    // 镜面光
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);  
    float spec = pow(max(dot(viewDir, reflectDir), 0.0f), material.shininess);
    vec3 specular = light.specular * spec * texture(material.specular,TexCroods).rgb; //修改; 

    vec3 result = ambient + diffuse + specular;//可以得到最终到我们眼睛的物体材质的颜色;
    FragColor = vec4(result, 1.0f);
}

main.cpp代码

#include <glad/glad.h> 
#include <GLFW/glfw3.h>
#include <iostream>
#include<E:\OpenGl\练习1.1\3.3.shader_class\shader s.h>
//以下三行为glm的头文件代码;
#include <E:\OpenGl\glm\glm-master\glm\glm.hpp>
#include <E:\OpenGl\glm\glm-master\glm\gtc\matrix_transform.hpp>
#include <E:\OpenGl\glm\glm-master\glm\gtc\type_ptr.hpp>

#define STB_IMAGE_IMPLEMENTATION
#include <E:/OpenGl/stb_image.h/stb-master/stb_image.h>//这两行代码加入了stb_image库;

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_back(GLFWwindow* window, double xoffset, double yoffset);

//三个调整摄像机位置的全局变量;
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);

float deltatime = 0.0f;//上一帧与这一帧的时间差;
float lastime = 0.0f;//上一帧的时间;

//用来存储上一帧鼠标的位置!,设置为屏幕中心;
float lastX = 400.0;
float lastY = 300.0;

//仰俯角和偏航角;
float pitch = 0.0f;
float yaw = -90.0f;//从下往上;

float fov = 45.0f;//视域角;

glm::vec3 lightPos(1.2f, 1.0f, 2.0f);//声明一个光源,表示光源在空间中的位置;


int main()
{

    //先进行初始化glfw;
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本设置为3;
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本设置为3;
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow* window = glfwCreateWindow(800, 600, "MY OPENGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Fail to create a window" << std::endl;
        glfwTerminate();//释放资源;
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    //创建完告诉将上下文设置为进程上下文;

    //以下两步用于摄像机操作中的设置,由于是窗口的操作,因此放在此处!!;
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);//告诉GLFW隐藏光标并捕捉他;
    glfwSetCursorPosCallback(window, mouse_callback);//此句代码当发生鼠标移动时,就会调用mouse_callback函数改变两个欧拉角,
    //进而改变cameraFront方向向量,进而可以实现3D旋转;
    //还要对视域角fov做出变化,可以进行放大缩小;
    glfwSetScrollCallback(window, scroll_back);//当滚动鼠标滚轮的时候,我们就可以通过调用该函数来改变fov,进而改变透视投影矩阵,
    //以此来进一步形成放大和缩小!!;


    //对glad进行初始化;
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Fail to initnite glad" << std::endl;
        return -1;
    }
    //引入着色器类,着色器被封装到了class Shader里面;
    //这里要创建两个着色器程序,分别用在物体和光源上面;

    Shader lightshader("2.3.shader.vs", "2.3.shader.fs");
    Shader lightCubeshader("2.3.shader.vs.light", "2.3.shader.fs.light");//用于光源的着色器程序;


    float vertices[] = {
        // positions          // normals           // texture coords
        -0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  1.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  0.0f, 0.0f,

        -0.5f, -0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   0.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   0.0f, 0.0f,

        -0.5f,  0.5f,  0.5f, -1.0f,  0.0f,  0.0f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f, -1.0f,  0.0f,  0.0f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f, -1.0f,  0.0f,  0.0f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f, -1.0f,  0.0f,  0.0f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f, -1.0f,  0.0f,  0.0f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f, -1.0f,  0.0f,  0.0f,  1.0f, 0.0f,

         0.5f,  0.5f,  0.5f,  1.0f,  0.0f,  0.0f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f,  0.0f,  0.0f,  1.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  1.0f,  0.0f,  0.0f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  1.0f,  0.0f,  0.0f,  0.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  1.0f,  0.0f,  0.0f,  0.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f,  0.0f,  0.0f,  1.0f, 0.0f,

        -0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,  1.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,  1.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,  1.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,  0.0f, 1.0f,

        -0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f,  0.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f,  0.0f, 1.0f
    };


    unsigned int VAO, VBO;
    glGenVertexArrays(1, &VAO);//创建VAO;
    glGenBuffers(1, &VBO);//创建VBO;
    //glGenBuffers(1, &EBO);
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    //glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    //glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    //链接顶点属性;
    //位置属性;
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);//这里的步长为获得下一个属性值,应该右移六个长度的单位;
    glEnableVertexAttribArray(0);
    //法线顶点属性;
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    //纹理顶点数据;
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
    glEnableVertexAttribArray(2);//启用顶点的纹理属性;

    //这里先去除纹理,以后要加入;
    //shader.useProgram();
    //shader.setInt("Tex1", 0);//采样器1对应纹理单元0;
    //shader.setInt("Tex2", 1);

    glEnable(GL_DEPTH_TEST);//深度测试;


    //下面要再创建一个lightVAO,用来存放光源Cube的顶点指针;
    unsigned int lightVAO;
    glGenVertexArrays(1, &lightVAO);
    glBindVertexArray(lightVAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);//这里仅需要绑定VBO即可,数据在前面已经传入VBO;
    //链接顶点属性;
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);//启用顶点属性;


    //加入纹理;
    unsigned int diffuseMap;
    glGenTextures(1, &diffuseMap);//创建一个纹理对象;
    glBindTexture(GL_TEXTURE_2D, diffuseMap);
    //设置环绕方式;
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    //设置过滤方式;
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    
    //加载纹理;
    stbi_set_flip_vertically_on_load(true);//防止发生上下颠倒;
    int width, height, nrchannels;
    unsigned char* data = stbi_load("E:/OpenGl/textures/container2.png", &width, &height, &nrchannels, 0);
    if (data)
    {
        //记住生成纹理的函数glTexImage2D;
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
        //生成纹理后还要生成一系列多级渐远纹理;
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    else
    {
        std::cout << "Fail to load a image!" << std::endl;
    }
    stbi_image_free(data);//不要忘了删除图像数据;

    //镜面光贴图;
    unsigned int specularMap;
    glGenTextures(1, &specularMap);
    glBindTexture(GL_TEXTURE_2D, specularMap);
    //设置镜面光贴图纹理的环绕和过滤模式;
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    //加载纹理;
    stbi_set_flip_vertically_on_load(true);
    int width1, height1, nrchannels1;
    unsigned char * data1 = stbi_load("E:/OpenGl/textures/container2_specular.png", &width1, &height1, &nrchannels1, 0);
    if (data1)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data1);
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    stbi_image_free(data1);

    lightCubeshader.useProgram();

    lightshader.useProgram();
    lightshader.setInt("material.diffuse", 0);//找到material.diffuse的位置,并将纹理单元0传给它,就实现了将纹理传入片段着色器;
    lightshader.setInt("material.specular", 1);//找到material.specular的位置,并将纹理单元传给它;

    //准备引擎:
    while (!glfwWindowShouldClose(window))
    {
        float currentime = glfwGetTime();
        deltatime = currentime - lastime;
        lastime = currentime;

        //准备输入:
        processInput(window);

        //每一次更新缓冲颜色,之后继续画下一帧;
        glClearColor(0.0f, 0.05f, 0.05f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);//清除颜色缓冲单元;
        glClear(GL_DEPTH_BUFFER_BIT);//清除深度缓存冲单元;

        //创建三个变换矩阵;
            //先初始化三个单位矩阵;
        glm::mat4 projection = glm::mat4(1.0f);
        glm::mat4 view = glm::mat4(1.0f);
        glm::mat4 model = glm::mat4(1.0f);

        //创建一个随时间旋转的矩阵:
        lightshader.useProgram();//启用着色器程序;

        //人眼的位置viewPos;
        glUniform3f(glGetUniformLocation(lightshader.ID, "viewPos"), cameraPos.x, cameraPos.y, cameraPos.z);

        view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
        projection = glm::perspective(glm::radians(fov), (float)800 / (float)600, 0.1f, 100.0f);

        //将变换矩阵传入着色器;注意到由于变换随时发生,要每一次迭代都进行,因此要放在里面;

        glUniformMatrix4fv(glGetUniformLocation(lightshader.ID, "model"), 1, GL_FALSE, glm::value_ptr(model));
        glUniformMatrix4fv(glGetUniformLocation(lightshader.ID, "view"), 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(glGetUniformLocation(lightshader.ID, "projection"), 1, GL_FALSE, glm::value_ptr(projection));

        //设置光照分量;和位置;
        glUniform3f(glGetUniformLocation(lightshader.ID, "light.ambient"), 0.2f,0.2f,0.2f);
        glUniform3f(glGetUniformLocation(lightshader.ID, "light.specular"), 2.0f, 2.0f, 2.0f);
        glUniform3f(glGetUniformLocation(lightshader.ID, "light.diffuse"), 0.5f,0.5f,0.5f);
        //光源的位置;
        glUniform3f(glGetUniformLocation(lightshader.ID, "light.position"), lightPos.x, lightPos.y, lightPos.z);

        //将自己定义的材质输入着色器;
        lightshader.setFloat("material.shininess", 64.0f);

        //漫反射贴图(漫反射传入贴图的颜色);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, diffuseMap);//绑定之后该纹理就到了纹理单元里面了;

        //镜面光贴图;
        glActiveTexture(GL_TEXTURE1);//启用下一个纹理单元;
        glBindTexture(GL_TEXTURE_2D, specularMap);

        glBindVertexArray(VAO);//绑定VAO;
        glDrawArrays(GL_TRIANGLES, 0, 36);




        lightCubeshader.useProgram();//启用另外一个着色器程序;画光源;

        model = glm::mat4(1.0f);//重新初始化model矩阵;
        model = glm::translate(model, glm::vec3(lightPos.x,lightPos.y,lightPos.z));//物体移动到光源的位置;
        model = glm::scale(model, glm::vec3(0.2f));//光源缩小;
        //传入着色器;
        glUniformMatrix4fv(glGetUniformLocation(lightCubeshader.ID, "model"), 1, GL_FALSE, glm::value_ptr(model));
        glUniformMatrix4fv(glGetUniformLocation(lightCubeshader.ID, "view"), 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(glGetUniformLocation(lightCubeshader.ID, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
        // 绘制灯立方体对象
        glBindVertexArray(lightVAO);
        glDrawArrays(GL_TRIANGLES, 0, 36);//位置0,画36个点;

        //更新缓冲;
        glfwSwapBuffers(window);
        //进行检查;
        glfwPollEvents();
    }
    //结束要删除VAO,VBO,以及删除着色器程序; 
    glDeleteVertexArrays(1, &VAO);
    glDeleteVertexArrays(1, &lightVAO);
    glDeleteBuffers(1, &VBO);
    //glDeleteProgram(shaderProgram);//删除着色器程序;
    //结束清楚资源;
    glfwTerminate();
    return 0;
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    glViewport(0, 0, width, height);
}
void processInput(GLFWwindow* window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//如果按下的键为回车键;
        glfwSetWindowShouldClose(window, true);
    float cameraSpeed = 2.5f * deltatime;//移动速度;
    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        cameraPos += cameraUp * cameraSpeed;
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        cameraPos -= cameraUp * cameraSpeed;
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        cameraPos -= cameraSpeed * glm::normalize(glm::cross(cameraFront, cameraUp));
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        cameraPos += cameraSpeed * glm::normalize(glm::cross(cameraFront, cameraUp));
}

bool firstMouse = true;

void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    //计算鼠标距上一帧的偏移量。
        //把偏移量添加到摄像机的俯仰角和偏航角中。
        //对偏航角和俯仰角进行最大和最小值的限制。
        //计算方向向量。
    if (firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;//否则每一次都会进行循环;
    }
    //1.计算鼠标距上一帧的偏移量。
    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos;
    lastX = xpos;
    lastY = ypos;//更新存储的上一帧的值;
    float sensitivity = 0.1f;//设置灵敏度;
    xoffset *= sensitivity;
    yoffset *= sensitivity;

    //2.把偏移量添加到摄像机的俯仰角和偏航角中。
    pitch = pitch + yoffset;
    yaw = yaw + xoffset;

    //3.对偏航角和俯仰角进行最大和最小值的限制

    if (pitch > 89.0f)
        pitch = 89.0f;
    if (pitch < -89.0f)
        pitch = -89.0f;
    //计算方向向量。
    glm::vec3 direction;
    direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
    direction.y = sin(glm::radians(pitch));
    direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
    cameraFront = glm::normalize(direction);
}
void scroll_back(GLFWwindow* window, double xoffset, double yoffset)
{
    //我们要把fov限制在1.0到45.0之间!!;
    if (fov >= 1.0f && fov <= 45.0f)
    {
        fov -= yoffset;
    }
    if (fov >= 45.0f)
    {
        fov = 45.0f;
    }
    if (fov <= 1.0f)
    {
        fov = 1.0f;
    }
}

下面是加入镜面光贴图后的一张照片:

与前面进行对比,可以发现在边框上的高光明显多于内部木头材质的高光!!;

我们也可以使用法线/凹凸贴图(Normal/Bump Map)或者反射贴图(Reflection Map)给物体添加更多的细节,这一部分放在后面;

四.作业

作业3

使用漫反射贴图创建一个彩色而不是黑白的镜面光贴图,看起来是这样子,感觉不太真实......

好了就这样。

猜你喜欢

转载自blog.csdn.net/2201_75303014/article/details/128824284