【OpenGL学习笔记⑧】——键盘控制正方体+光源【冯氏光照模型 光照原理 环境光照+漫反射光照+镜面光照】


✅ 重点参考了 LearnOpenGL CN 的内容,但大部分知识内容,小编已作改写,以方便读者理解。


文章目录

  • 零、 成果预览图
  • 一、 光照原理与投光物的配置
    • 1.1 光照原理
    • 1.2 投光物
  • 二、冯氏光照模型
  • 三、环境光照
  • 四、漫反射光照
    • 4.1 法向量
    • 4.2 光源发射的光线
    • 4.3 法向量与光线的夹角
  • 五、镜面光照
  • 六、主函数中光源的配置
  • 七、键盘控制正方体
  • 八、完整代码(主函数)
  • 九、运行结果
  • 十、参考附录:


光照 ☀️

上一篇文章链接: 【OpenGL学习笔记⑦】——键盘控制镜头的平移【3D正方体 透视投影 观察矩阵 对LookAt的理解】.
下一篇文章链接: 【OpenGL学习笔记⑨】——鼠标控制镜头 + 滚轮控制镜头缩放.
OpenGL总学习目录: https://blog.csdn.net/Wang_Dou_Dou_/article/details/121240714.


零、 成果预览图

在这里插入图片描述

  ● 说明:实现光照效果 + 依次实现正方体的 左移、右移、上移、下移、前进、后退 功能。



一、 光照原理与投光物的配置

1.1 光照原理

  ● 现实世界中颜色由红色(Red)、绿色(Green)和蓝色(Blue)三个分量组成,它们通常被缩写为 RGB。仅仅用这三个值就可以组合出任意一种颜色。

glm::vec3 color_Red(1.0f, 0.0f, 0.0f);			// 红色
glm::vec3 color_Green(0.0f, 1.0f, 0.0f);		// 绿色
glm::vec3 color_Blue(0.0f, 0.0f, 1.0f);			// 蓝色

  ● 我们在现实生活中看到某一物体的颜色并不是这个物体真正拥有的颜色,而是它所 反射的(Reflected) 颜色。换句话说,那些不能被物体所吸收(Absorb)的颜色(被拒绝的颜色)就是我们能够感知到的物体的颜色。

  ● 例如,太阳光(白光)其实是由许多不同的颜色光组合而成的(如下图所示)。如果我们将白光照在一个蓝色的玩具上,这个蓝色的玩具会吸收白光中除了蓝色以外的所有子颜色,不被吸收的蓝色光就被反射到我们的眼中,让这个玩具看起来是蓝色的。下图显示的是一个珊瑚红的玩具,它以不同强度反射了多个颜色:【主要反射了红色】

【OpenGL学习笔记⑧】——键盘控制正方体+光源【冯氏光照模型 光照原理 环境光照+漫反射光照+镜面光照】_第1张图片

  ● 这些颜色反射的定律被直接地运用在图形领域。当我们在 OpenGL 中创建一个光源时,我们希望给光源一个颜色。样例如下:

"样例一"
glm::vec3 light_Color(1.0f, 1.0f, 1.0f);		// 白光源
glm::vec3 object_Color(1.0f, 0.5f, 0.31f);		// 珊瑚红色
glm::vec3 result = lightColor * object_Color; 	// 结果仍然为珊瑚红色 =  (1.0f, 0.5f, 0.31f);

"样例二"
glm::vec3 light_Color(0.0f, 1.0f, 0.0f);		// 绿光源
glm::vec3 object_Color(1.0f, 0.5f, 0.31f);		// 珊瑚红色
glm::vec3 result = lightColor * toyColor; 		// 结果变为半绿色 = (0.0f, 0.5f, 0.0f);

  ◆ 说明:当我们把光源的颜色与物体的颜色值相乘,所得到的就是这个物体所反射的颜色。


1.2 投光物

  ● 在 OpenGL 中,我们想要将光源显示为可见的物体(就像限时中到太阳一样)。在这里我们依然沿用前面的 “3D正方体” 来承载光源,成为 “投光物”。

  ● 为了让 投光物 和 被照正方体 相互独立,我们需要给投光物单独开一个顶点着色器和一个片元着色器。

  ● 投光物的顶点着色器 light_v.txt:【和原先 3D 立方体的顶点着色器一样】

#version 330 core
layout (location = 0) in vec3 position;
uniform mat4 transform_2;						
uniform mat4 projection_2;
uniform mat4 view_2;
void main()
{
	gl_Position = projection_2 * view_2 * transform_2 * vec4(position, 1.0f);
}

  ● 投光物的片元着色器 light_f.txt:【白光源】

#version 330 core
out vec4 FragColor;
void main()
{
	FragColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);		// 白色光
}

  ● 我们希望 投光物摄像机 一样,成为一个封装类,方便我们创建和调用,故其我 创建了 Point_Light.h 如下:

#include "Shader.h"
#include 				// 根据自身情况进行设置
#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"

GLfloat vertices_2[] =
{	// 坐标
	...			// 为了节省篇幅, 此处省略. (同上一篇文章的立方体坐标)
};

class Point_Light
{
public:
	Point_Light()				// 构造函数
	{
		this->update();
	}

	void Draw(Shader &shader)	// 绘制函数
	{
		glBindVertexArray(light_VAO);
		glDrawArrays(GL_TRIANGLES, 0, 36);
		glBindVertexArray(0);
	}
		
	~Point_Light()				// 析构函数
	{
		glBindBuffer(GL_ARRAY_BUFFER, 0);		
		glBindVertexArray(0);					
		glDeleteVertexArrays(1, &light_VAO);
		glDeleteBuffers(1, &light_VBO);
	}

private:
	GLuint light_VAO, light_VBO;
	void update()
	{
		/* 设置顶点缓冲对象(VBO) + 设置顶点数组对象(VAO) */
		glGenVertexArrays(1, &light_VAO);
		glGenBuffers(1, &light_VBO);
		glBindVertexArray(light_VAO);
		glBindBuffer(GL_ARRAY_BUFFER, light_VBO);
		glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_2), vertices_2, GL_STATIC_DRAW);

		/* 设置链接顶点属性 */
		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
		glEnableVertexAttribArray(0);	
	}
};


二、冯氏光照模型

  ● 就像现实生活中的一样,不同光源的光照效果是及其不同的,比如:太阳与日光灯、手电筒与激光。不同光源有不同的光照模型,在这里,我们使用 OpenGL 中较为简单的光照模型:冯氏光照模型

  ● 冯氏光照模型的主要结构由 3 个分量组成而成:环境(Ambient)光照漫反射(Diffuse)光照镜面(Specular)光照。下面这张图展示了这些光照分量看起来的样子:

【OpenGL学习笔记⑧】——键盘控制正方体+光源【冯氏光照模型 光照原理 环境光照+漫反射光照+镜面光照】_第2张图片
  ◆ 说明
  ① 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
  ② 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)【后面将会重点讲解这个】。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
  ③ 镜面反射光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的本身颜色会更倾向于光的颜色。
  ④ 第四个图形就是把前三者结合在一起,得到的光照效果。



三、环境光照

  ● 有一种环境光照,用的是全局照明的算法,但太复杂了。这里呢,我们简单一点,使用一个很小的常量(光照)颜色,添加到物体片段着色器中,这样子的话即便场景中没有直接的光源也能看见一极其淡淡的光。

  ● 被照物体的片元着色器 shader_f.txt【理解版】:

#version 330 core
...								"为突出重点内容, 已省去部分内容"	
in vec3 ourColor;				// 传进来的颜色
out vec4 FragColor;				// 传出去的片元颜色
...								

void main()
{
	...
	float ambientStrength = 0.1;					// 常量(光照)颜色因子
	vec3 ambient = ambientStrength  * ourColor;		// 环境光照
	...
	
	FragColor = vec4(ambient + 漫反射光照 + 镜面反射光照, 1.0f) * ourColor;		// 三种光照的组合
}


四、漫反射光照

  ● 漫反射光照能让物体产生显著的视觉影响(就像那种,这边亮一点,那边暗一点的效果)。漫反射光照使物体上与光线方向越接近的片段能从光源处获得更多的亮度。为了能够更好的理解漫反射光照,请看下图:

【OpenGL学习笔记⑧】——键盘控制正方体+光源【冯氏光照模型 光照原理 环境光照+漫反射光照+镜面光照】_第3张图片

  ◆ 说明
    ① 图左上方有一个光源,它所发出的光线落在物体的一个片段上。我们需要测量这个光线是以什么角度接触到这个片段的(如图中的 θ θ θ φ φ φ)。如果光线垂直于物体表面,这束光对物体的影响会最大化(即最亮)。
    ② 为了测量光线和片段的角度,我们使用一个叫做 法向量(Normal Vector) 的东西,它是垂直于片段表面的一个向量(即图中的 N ‾ \overline{N} N ),我们在后面再讲这个东西。这两个向量之间的角度很容易就能够通过点乘计算出来。
    ③ 两个单位向量的夹角越小,它们点乘的结果越倾向于1。当两个向量的夹角为 90 度的时候,点乘会变为 0。这同样适用于 θ,θ 越大,光对片段颜色的影响就应该越小。

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

  ● 计算漫反射光照需要
  ① 法向量(Normal Vector):一个垂直于顶点表面的向量。
  ② 光源发射的光线:即图中黑色的那条线。为了计算这个光线,我们需要光的位置(向量)和片段的位置(向量)。
  ③ 法向量与光线的夹角:即图中的 θ θ θ φ φ φ


4.1 法向量

  ● 法向量是一个垂直于顶点表面的(单位)向量。由于顶点本身并没有表面(它只是空间中一个独立的点),我们利用它周围的顶点来计算出这个顶点的表面。由于 3D 立方体不是一个复杂的形状,所以我们可以简单地把法线数据手工添加到顶点数据中。

  ● 这些法向量垂直于立方体各个平面的表面的(一个立方体由 6 个平面组成)。

【OpenGL学习笔记⑧】——键盘控制正方体+光源【冯氏光照模型 光照原理 环境光照+漫反射光照+镜面光照】_第4张图片

  ● 3D正方体(不是投光物)的顶点属性数组改写如下:【注:和官方的不一样,因为我采用的是自己建立的一套世界坐标体系,详见【OpenGL学习笔记⑦】——键盘控制镜头的平移【3D正方体 透视投影 观察矩阵 对LookAt的理解】】

/* 编写各顶点位置 */
float vertices_1[] = {
	// x、y、z 坐标				// color				// normal
	-0.5f, -0.5f, -0.5f,		1.0f, 0.0f, 0.0f,		0.0f, 0.0f, -1.0f,	// red 红色面
	 0.5f, -0.5f, -0.5f,		1.0f, 0.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, -1.0f,
	 0.5f,  0.5f, -0.5f,		1.0f, 0.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, -1.0f,
	-0.5f, -0.5f, -0.5f,		1.0f, 0.0f, 0.0f,		0.0f, 0.0f, -1.0f,

	-0.5f, -0.5f,  0.5f,		0.0f, 1.0f, 0.0f,		0.0f, 0.0f, 1.0f,	// green 绿色面
	 0.5f, -0.5f,  0.5f,		0.0f, 1.0f, 0.0f,		0.0f, 0.0f, 1.0f,
	 0.5f,  0.5f,  0.5f,		0.0f, 1.0f, 0.0f,		0.0f, 0.0f, 1.0f,
	 0.5f,  0.5f,  0.5f,		0.0f, 1.0f, 0.0f,		0.0f, 0.0f, 1.0f,
	-0.5f,  0.5f,  0.5f,		0.0f, 1.0f, 0.0f,		0.0f, 0.0f, 1.0f,
	-0.5f, -0.5f,  0.5f,		0.0f, 1.0f, 0.0f,		0.0f, 0.0f, 1.0f,

	-0.5f,  0.5f,  0.5f,		0.0f, 0.0f, 1.0f,		1.0f, 0.0f, 0.0f, 	// blue 蓝色面(不是图中那种蓝)
	-0.5f,  0.5f, -0.5f,		0.0f, 0.0f, 1.0f,		1.0f, 0.0f, 0.0f,
	-0.5f, -0.5f, -0.5f,		0.0f, 0.0f, 1.0f,		1.0f, 0.0f, 0.0f,
	-0.5f, -0.5f, -0.5f,		0.0f, 0.0f, 1.0f,		1.0f, 0.0f, 0.0f,
	-0.5f, -0.5f,  0.5f,		0.0f, 0.0f, 1.0f,		1.0f, 0.0f, 0.0f,
	-0.5f,  0.5f,  0.5f,		0.0f, 0.0f, 1.0f,		1.0f, 0.0f, 0.0f,

	 0.5f,  0.5f,  0.5f,		1.0f, 1.0f, 0.0f,		-1.0f, 0.0f, 0.0f,	// yellow 黄色面
	 0.5f,  0.5f, -0.5f,		1.0f, 1.0f, 0.0f,		-1.0f, 0.0f, 0.0f,
	 0.5f, -0.5f, -0.5f,		1.0f, 1.0f, 0.0f,		-1.0f, 0.0f, 0.0f,
	 0.5f, -0.5f, -0.5f,		1.0f, 1.0f, 0.0f,		-1.0f, 0.0f, 0.0f,
	 0.5f, -0.5f,  0.5f,		1.0f, 1.0f, 0.0f,		-1.0f, 0.0f, 0.0f,
	 0.5f,  0.5f,  0.5f,		1.0f, 1.0f, 0.0f,		-1.0f, 0.0f, 0.0f,

	-0.5f, -0.5f, -0.5f,		1.0f, 0.0f, 1.0f,		0.0f, -1.0f, 0.0f, 	// purple 紫色面
	 0.5f, -0.5f, -0.5f,		1.0f, 0.0f, 1.0f,		0.0f, -1.0f, 0.0f,
	 0.5f, -0.5f,  0.5f,		1.0f, 0.0f, 1.0f,		0.0f, -1.0f, 0.0f,
	 0.5f, -0.5f,  0.5f,		1.0f, 0.0f, 1.0f,		0.0f, -1.0f, 0.0f,
	-0.5f, -0.5f,  0.5f,		1.0f, 0.0f, 1.0f,		0.0f, -1.0f, 0.0f,
	-0.5f, -0.5f, -0.5f,		1.0f, 0.0f, 1.0f,		0.0f, -1.0f, 0.0f,

	-0.5f,  0.5f, -0.5f,		0.0f, 1.0f, 1.0f,		0.0f, 1.0f, 0.0f,	// cyan 青蓝色面
	 0.5f,  0.5f, -0.5f,		0.0f, 1.0f, 1.0f,		0.0f, 1.0f, 0.0f,
	 0.5f,  0.5f,  0.5f,		0.0f, 1.0f, 1.0f,		0.0f, 1.0f, 0.0f,
	 0.5f,  0.5f,  0.5f,		0.0f, 1.0f, 1.0f,		0.0f, 1.0f, 0.0f,
	-0.5f,  0.5f,  0.5f,		0.0f, 1.0f, 1.0f,		0.0f, 1.0f, 0.0f,
	-0.5f,  0.5f, -0.5f,		0.0f, 1.0f, 1.0f,		0.0f, 1.0f, 0.0f
};

  ● 被照物体的顶点着色器 shader_v.txt【理解版】:

#version 330 core
...											"为突出重点内容, 已省去部分内容"	
layout(location = 2) in vec3 normal;		// 新加的法向量通道
...											

...
out vec3 FragNormal;		// 法向量输出通道, 将传给片元着色器
...

void main()
{
	...
	FragNormal = normal;	
	...
}

  ● 在后面我们将知道片段着色器里的计算都是在世界空间坐标中进行的。所以,在这里我们应该把法向量也转换为世界空间坐标。我们可以将其乘以一个 转换矩阵(Transform Matrix) 来搞定。

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

  ● 修复这个行为的方法是使用一个为法向量专门定制的矩阵。这个矩阵称之为 法线矩阵(Normal Matrix),它使用了一些线性代数的操作来移除对法向量错误缩放的影响。【后面需要时再学习这个】


4.2 光源发射的光线

  ● 经过 4.1 后,每个顶点都有了法向量。现在我们需要光源的位置和片段的位置,由它们俩来计算 “光源发射的光线”。

"该代码在被着色物体的片段着色器 shader_f.txt 中"
...
uniform vec3 LightPos;		// 光源位置
...
void main()
{
	...				"为突出重点内容, 已省去部分内容"
	vec3 SendLight= normalize(LightPos - FragPos);
	...
}

  ◆ 代码说明:当计算光照时,我们通常不关心一个向量的模长或它的位置,我们只关心它们的方向。所以,几乎所有的计算都使用单位向量完成,因为这简化了大部分的计算(比如点乘)。所以当进行光照计算时,确保你总是对相关向量进行标准化normalize()

  ● 最后,我们还需要片元的位置,因为是片元发生的 “明暗变化” 。我们可以通过把顶点位置属性乘以转换矩阵来把它变换到世界空间坐标。这个在顶点着色器中完成,如下代码所示。

  ● 被照物体的顶点着色器 shader_v.txt 【理解版】:

#version 330 core
...												"为突出重点内容, 已省去部分内容"	
layout (location = 0) in vec3 position;			// 将片元的坐标位置传入
...

...
out vec3 FragPos;		// 将计算后的片元坐标传给片段着色器
..

void main()
{
	...
	FragPos = vec3(转换矩阵 * vec4(position, 1.0f));
	...
}


4.3 法向量与光线的夹角

  ● 我们对 法向量 和 发射光线 向量进行点乘,即可计算光源对当前片段实际的漫发射影响。结果值再乘以光的颜色,得到漫反射分量。两个向量之间的角度越大,漫反射分量就会越小。

"该代码也在被着色物体的片段着色器 shader_f.txt 中"
...
in vec3 FragNormal;
...

void main()
{
	...													"为突出重点内容, 已省去部分内容"	
	float diff = 0.6 * max(dot(FragNormal, SendLight), 0.0);	// 乘以 0.6 是为了不让漫反射太亮 
	vec3 diffuse = diff * 光源颜色;
	...

	...
	vec3 result = (环境光照 + diffuse + 镜面反射光照) * 被照物体的颜色;
	FragColor = vec4(result, 1.0);
	...
}

  ◆ 代码说明:如果两个向量之间的角度大于 90 度,点乘的结果就会变成负数,这样会导致漫反射分量变为负数。为此,我们使用max()函数返回两个参数之间较大的参数,从而保证漫反射分量不会变成负数。(负数颜色的光照是没有定义的,所以最好避免它)



五、镜面光照

  ● 镜面光照是基于光的反射特性。如果我们想象物体表面像一面镜子一样。和漫反射光照一样,镜面光照也是依据 光的方向向量物体的法向量 来决定的,同时它也依赖于(摄像机的)观察方向。

【OpenGL学习笔记⑧】——键盘控制正方体+光源【冯氏光照模型 光照原理 环境光照+漫反射光照+镜面光照】_第5张图片

  ● 我们首先计算 反射向量(即图中深橙黄色的光线)。然后我们计算反射向量和视线方向的 角度差 γ γ γ ,如果夹角越小,那么镜面光的影响就会越大。

  ● 和漫反射一样,我们也需要计算出 光源反射的光线(图中的虚黑线)。此时我们就需要 片元位置(反射点)和摄像机位置 ,两个相减就能得到:

"这段代码在被照物体的片元着色器 shader_f.txt 中:"
...							
uniform vec3 CameraPos;
...							

void main()
{
	...					"为突出重点内容, 已省去部分内容"	
	vec3 CameraDir = normalize(CameraPos - FragPos);		// 片元指向摄像机的方向(图中的虚黑线)
	vec3 ReflectLight = reflect(-SendLight, FragNormal);	// 反射光线
	...
	
	float specularStrength = 0.8;
	vec3 specular = specularStrength * pow(max(dot(CameraDir, ReflectLight), 0.0), 32) * 光源颜色;	// 计算图中的蓝虚线
	
	...
	vec3 result = (环境光照 + 漫反射光照 + specular) * 被照物体颜色;
	FragColor = vec4(result, 1.0);
	...
}

  ◆ 代码说明
    ① reflect()函数要求第一个向量是从光源指向片元位置的向量,但是我们这里的 SendLight 当前正好相反,是从片元指向光源(由先前我们计算 SendLight 向量时,减法的顺序决定)。为了保证我们得到正确的 ReflectLight 向量,我们通过对 SendLight 向量取反来获得相反的方向。第二个参数要求是一个以标准化后的法向量。
    ② specularStrength:镜面强度变量,给镜面高光一个中等亮度颜色,让它不要产生过度的影响。
    ③ 计算镜面分量(即计算图中的蓝虚线):我们先计算视线方向与反射方向的点乘(并确保它不是负值),然后取它的 32 次幂。这个 32 是镜面反射光的反光度(Shininess)。一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。在下面的图片里,你会看到不同反光度的视觉效果影响:

【OpenGL学习笔记⑧】——键盘控制正方体+光源【冯氏光照模型 光照原理 环境光照+漫反射光照+镜面光照】_第6张图片



六、主函数中光源的配置

  ● 主函数的代码和 第⑦篇 文章类似。我们只是新增了一个 “投光物”,然后原来的 “被照物体” 的顶点/片元着色器的通道上进行了相应的配置。

/* 引入相应的库 */
...												"为了突出重点内容, 已省去部分内容"	
#include"Shader.h"
#include"Camera.h"
#include "Point_Light.h"
...

/* 编写各顶点位置 */
float vertices_1[] = {
	...
};

...
glm::vec3 lightPos = glm::vec3(0.0f, 0.0f, 1.0f);	// 光源位置初始化
...

int main()
{
	...
	
	/* 初始化 glew */
	glewInit();
	Point_Light My_light = Point_Light();		// 新建一个光源(必须在 glew 初始化后才行)
	...

	...
	/* 将我们自己设置的着色器文本传进来 */
	Shader ourShader = Shader("shader_v.txt", "shader_f.txt");		// 相对路径
	Shader lightShader = Shader("light_v.txt", "light_f.txt");		// 相对路径
	...
	
	/* draw loop 画图循环 */
	while (!glfwWindowShouldClose(window_1))
	{
		...
		Square_Movement(..., ..., ...);		// 正方体移动
		...
		
		/* 绘制光照 */
		...
		lightShader.Use();
		glm::mat4 transform_1 = glm::mat4(1.0f);
		lightPos = glm::rotate(lightPos, glm::radians(0.1f)/2, glm::vec3(0.0f, 1.0f, 0.0f));
		transform_1 = glm::translate(transform_1, lightPos);
		transform_1 = glm::scale(transform_1, glm::vec3(0.1f, 0.1f, 0.1f));
		glm::mat4 projection_1 = glm::perspective(glm::radians(45.0f), (float)screenWidth_1/(float)screenHeight_1, 0.1f, 100.0f);
		glm::mat4 view_1 = camera.GetViewMatrix();	// 求得观察矩阵
		int transform_1_Location = glGetUniformLocation(lightShader.Program, "transform_2");
		glUniformMatrix4fv(transform_1_Location, 1, GL_FALSE, glm::value_ptr(transform_1));
		int projection_1_Location = glGetUniformLocation(lightShader.Program, "projection_2");
		glUniformMatrix4fv(projection_1_Location, 1, GL_FALSE, glm::value_ptr(projection_1));
		int view_1_Location = glGetUniformLocation(lightShader.Program, "view_2");
		glUniformMatrix4fv(view_1_Location, 1, GL_FALSE, glm::value_ptr(view_1));
		My_light.Draw(lightShader);


		transform_1 = glm::mat4(1.0f);		// 这里需要重新初始化
		...
		/* 绘制正方体 */
		ourShader.Use();					// 调用着色器程序
		...
	}

	...
	return 0;
}
...



七、键盘控制正方体

  ● 这和上一篇文章第⑦篇——键盘控制移动摄像机的原理差不多。我们只需要在上面的主函数中加如下按键响应代码即可:

"这段代码在主函数中:"
...							"为突出重点内容, 已省去部分内容"	
void Square_Movement(GLfloat&, GLfloat&, GLfloat&);			// 正方体移动
...	

int main()
{
	...
	GLfloat up_down_move = 0.0f;		// 上下移动的变量
	GLfloat left_right_move = 0.0f;		// 左右移动的变量
	GLfloat front_back_move = 0.0f;		// 前后移动的变量
	...
	Square_Movement(up_down_move, left_right_move, front_back_move);		// 正方体移动
	...

	/* draw loop 画图循环 */
	while (!glfwWindowShouldClose(window_1))
	{
		...
		transform_1 = glm::translate(transform_1, glm::vec3(left_right_move, up_down_move, front_back_move));	// 转换矩阵
		...
	}
	...
	return 0;
}

...
void Square_Movement(GLfloat& shang_and_xia_move,  GLfloat& left_and_right_move, GLfloat &up_and_down_move)			// 正方体移动
{
	if (keys[GLFW_KEY_UP])		// 向上
	{
		shang_and_xia_move += 0.0005f;
	}

	if (keys[GLFW_KEY_DOWN])	// 向下
	{
		shang_and_xia_move -= 0.0005f;
	}

	if (keys[GLFW_KEY_LEFT])	// 向左
	{
		left_and_right_move += 0.0005f;
	}

	if (keys[GLFW_KEY_RIGHT])	// 向右
	{
		left_and_right_move -= 0.0005f;
	}

	if (keys[GLFW_KEY_F])		// 向前(按 F 键)
	{
		up_and_down_move += 0.0005f;
	}
	
	if (keys[GLFW_KEY_B])		// 向后(按 B 键)
	{
		up_and_down_move -= 0.0005f;
	}
}


八、完整代码(主函数)

  ● 头文件 Shader.h 依旧沿用第③篇中的代码【OpenGL学习笔记③】——⭐着色器【GLSL Uniform 彩色三角形 变色正方形】⭐

  ● 头文件 Camera.h 依旧沿用第⑦篇中的代码【OpenGL学习笔记⑦】——键盘控制镜头的平移【3D正方体 透视投影 观察矩阵】

  ● shader_v.txt的完整代码如下:

#version 330 core
layout (location = 0) in vec3 position;
layout(location = 1) in vec3 color;			
layout(location = 2) in vec3 normal;

out vec3 ourColor;
out vec3 FragNormal;
out vec3 FragPos;

uniform mat4 transform_1;						
uniform mat4 projection_1;
uniform mat4 view_1;

void main()
{
	ourColor = color;
	gl_Position = projection_1 * view_1 * transform_1 * vec4(position, 1.0f);
	FragPos = vec3(transform_1 * vec4(position, 1.0f));
	FragNormal = normal;	
}

  ● shader_f.txt的完整代码如下:

#version 330 core
in vec3 ourColor;
in vec3 FragNormal;
in vec3 FragPos;

out vec4 FragColor;

uniform vec3 LightPos;
uniform vec3 CameraPos;

void main()
{
	vec3 std_norm = normalize(FragNormal);

	// 环境光照
	float ambientStrength = 0.1;					// 常量(光照)颜色因子
	vec3 ambient = ambientStrength  * ourColor;		// 环境光照

	// 漫反射光照
	vec3 SendLight = normalize(LightPos - FragPos);
	float diff = 0.6 * max(dot(FragNormal, SendLight), 0.0f);
	vec3 diffuse = diff * ourColor;

	// 镜面反射光照
	vec3 ReflectLight = reflect(-SendLight, FragNormal);
	float specularStrength = 0.8;
	vec3 CameraDir = normalize(CameraPos - FragPos);
	vec3 specular = specularStrength * pow(max(dot(CameraDir, ReflectLight), 0.0), 32) * ourColor;	
	
	FragColor = vec4(ambient + diffuse + specular, 1.0f);
}

  ● 投光物的顶点着色器 light_v.txt 和投光物的片元着色器 light_f.txt 在本文开头已经写了,关于投光物 的封装类 Point_Light.h 也在文章前面配置好了,其所有环境配置如下图所示:

【OpenGL学习笔记⑧】——键盘控制正方体+光源【冯氏光照模型 光照原理 环境光照+漫反射光照+镜面光照】_第7张图片


  ● 最后主函数代码如下

/* 引入相应的库 */
#include 
using namespace std;
#define GLEW_STATIC	
#include"Shader.h"
#include"Camera.h"
#include "Point_Light.h"
#include							// 注:这一部分要根据个人情况进行设定	
#include
#include"glm\glm.hpp"
#include"glm\gtc\matrix_transform.hpp"
#include"glm\gtc\type_ptr.hpp"
#include"glm/gtx/rotate_vector.hpp"

/* 编写各顶点位置 */
float vertices_1[] = {
	...      "前文已写"		// 前文已写
};

const GLint WIDTH = 600, HEIGHT = 600;

bool keys[1024];				// 专门存储按过的键
Camera camera(glm::vec3(1.0f, 1.0f, -5.0f), glm::vec3(-1.0f, -1.0f, 5.0f), glm::vec3(0.0f, 1.0f, 0.0f));
void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mode);
void Key_Movement();
void square_Movement(GLfloat&, GLfloat&, GLfloat&);
GLfloat deltaTime = 0.0f;
GLfloat lastTime = 0.0f;

glm::vec3 lightPos = glm::vec3(0.0f, 0.0f, 1.0f);
const double Shift_pix = 0.0005;					// 正方体移动速度
int main()
{
	/* 初始化 glfw */
	glfwInit();
	glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);		// 缩放关闭

	/* 窗口捕获与处理 */
	GLFWwindow* window_1 = glfwCreateWindow(WIDTH, HEIGHT, "Learn OpenGL Light Test", nullptr, nullptr);
	int screenWidth_1, screenHeight_1;
	glfwGetFramebufferSize(window_1, &screenWidth_1, &screenHeight_1);
	glfwMakeContextCurrent(window_1);
	glfwSetKeyCallback(window_1, KeyCallback);

	/* 初始化 glew + 光照生成 */
	glewInit();
	Point_Light my_light = Point_Light();
	
	/* 深度测试开启 */
	glEnable(GL_DEPTH_TEST);

	/* 将我们自己设置的着色器文本传进来 */
	Shader ourShader = Shader("shader_v.txt", "shader_f.txt");		// 相对路径
	Shader lightShader = Shader("light_v.txt", "light_f.txt");		// 相对路径

	/* 设置顶点缓冲对象(VBO) + 设置顶点数组对象(VAO) */
	GLuint VAO, VBO;
	glGenVertexArrays(1, &VAO);
	glGenBuffers(1, &VBO);
	glBindVertexArray(VAO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_1), vertices_1, GL_STATIC_DRAW);

	/* 设置链接顶点属性 */
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(GLfloat), (GLvoid*)0);
	glEnableVertexAttribArray(0);		// 通道 0 打开
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
	glEnableVertexAttribArray(1);		// 通道 1 打开
	glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
	glEnableVertexAttribArray(2);		// 通道 2 打开
		
	GLfloat up_down_move = 0.0f;		// 上下移动的变量
	GLfloat left_right_move = 0.0f;		// 左右移动的变量
	GLfloat front_back_move  = 0.0f;	// 前后移动的变量

	/* draw loop 画图循环 */
	while (!glfwWindowShouldClose(window_1))
	{
		/* 时间获取 */
		GLfloat currentTime = glfwGetTime();
		deltaTime = currentTime - lastTime;
		lastTime = (float)currentTime;

		/* 视口 +  键鼠捕获 */
		glViewport(0, 0, screenWidth_1, screenHeight_1);
		glfwPollEvents();		// 获取键盘鼠标
		Key_Movement();			// 获取键盘
		square_Movement(up_down_move, left_right_move, front_back_move );		// 正方体移动
		
		/* 渲染 + 清除颜色缓冲 */
		glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		
		/* 光照绘制 */
		lightShader.Use();
		glm::mat4 my_transform = glm::mat4(1.0f);
		lightPos = glm::rotate(lightPos, glm::radians(0.1f), glm::vec3(0.0f, 1.0f, 0.0f));		// 旋转
		my_transform = glm::translate(my_transform, lightPos);									// 平移
		my_transform = glm::scale(my_transform, glm::vec3(0.1f, 0.1f, 0.1f));					// 缩放
		glm::mat4 my_projection = glm::perspective(glm::radians(45.0f), (float)screenWidth_1 / (float)screenHeight_1, 0.1f, 100.0f);		
		glm::mat4 my_view = camera.GetViewMatrix();	// 求得观察矩阵
		int my_transform_Location = glGetUniformLocation(lightShader.Program, "transform_2");
		glUniformMatrix4fv(my_transform_Location, 1, GL_FALSE, glm::value_ptr(my_transform));
		int my_projection_Location = glGetUniformLocation(lightShader.Program, "projection_2");
		glUniformMatrix4fv(my_projection_Location, 1, GL_FALSE, glm::value_ptr(my_projection));
		int my_view_Location = glGetUniformLocation(lightShader.Program, "view_2");
		glUniformMatrix4fv(my_view_Location, 1, GL_FALSE, glm::value_ptr(my_view));
		my_light.Draw(lightShader);

		/* 正方体绘制 */
		my_transform = glm::mat4(1.0f);		// 初始化是必要的
		ourShader.Use();					// 调用着色器程序
		glBindVertexArray(VAO);				// 绑定 VAO
		my_transform = glm::translate(my_transform, glm::vec3(left_right_move, up_down_move, front_back_move ));
		my_transform = glm::scale(my_transform, glm::vec3(0.5, 0.5, 0.5));
		my_projection = glm::perspective(glm::radians(45.0f), (float)screenWidth_1 / (float)screenHeight_1, 0.1f, 100.0f);
		my_view = camera.GetViewMatrix();
		my_transform_Location = glGetUniformLocation(ourShader.Program, "transform_1");
		glUniformMatrix4fv(my_transform_Location, 1, GL_FALSE, glm::value_ptr(my_transform));
		my_projection_Location = glGetUniformLocation(ourShader.Program, "projection_1");
		glUniformMatrix4fv(my_projection_Location, 1, GL_FALSE, glm::value_ptr(my_projection));
		my_view_Location = glGetUniformLocation(ourShader.Program, "view_1");
		glUniformMatrix4fv(my_view_Location, 1, GL_FALSE, glm::value_ptr(my_view));
		int LightPos_Location = glGetUniformLocation(ourShader.Program, "LightPos");
		glUniform3f(LightPos_Location, lightPos.x, lightPos.y, lightPos.z);
		int CameraPos_Location = glGetUniformLocation(ourShader.Program, "CameraPos");
		glUniform3f(CameraPos_Location, camera.GetPosition().x, camera.GetPosition().y, camera.GetPosition().z);

		glDrawArrays(GL_TRIANGLES, 0, 36);	// 绘制 36 个点(正方体)
		glBindVertexArray(0);				// 解绑定 VAO
		glfwSwapBuffers(window_1);
	}
	glDeleteVertexArrays(1, &VAO);
	glDeleteBuffers(1, &VBO);
	glfwTerminate();
	return 0;
}

void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mode)	// 按键捕获(固定函数格式)
{
	if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
	{
		glfwSetWindowShouldClose(window, GL_TRUE);
	}
	if (key >= 0 && key <= 1024)
	{
		if (action == GLFW_PRESS)
			keys[key] = true;
		else if (action == GLFW_RELEASE)
			keys[key] = false;
	}
}

void Key_Movement()		// Camera 
{
	if (keys[GLFW_KEY_Q])		// 向前
		camera.ProcessKeyboard(FORWARD, deltaTime);

	if (keys[GLFW_KEY_E])		// 向后
		camera.ProcessKeyboard(BACKWARD, deltaTime);

	if (keys[GLFW_KEY_A])		// 向左
		camera.ProcessKeyboard(LEFT, deltaTime);

	if (keys[GLFW_KEY_D])		// 向右
		camera.ProcessKeyboard(RIGHT, deltaTime);

	if (keys[GLFW_KEY_W])		// 向上
		camera.ProcessKeyboard(UPWARD, deltaTime);

	if (keys[GLFW_KEY_S])		// 向下
		camera.ProcessKeyboard(DOWNWARD, deltaTime);
}

void square_Movement(GLfloat& up_down_move, GLfloat& left_right_move, GLfloat& front_back_move)	 // Square
{

	if (keys[GLFW_KEY_UP])		// 向上
	{
		up_down_move += Shift_pix;
	}

	if (keys[GLFW_KEY_DOWN])	// 向下
	{
		up_down_move -= Shift_pix;
	}

	if (keys[GLFW_KEY_LEFT])	// 向左
	{
		left_right_move += Shift_pix;
	}

	if (keys[GLFW_KEY_RIGHT])	// 向右
	{
		left_right_move -= Shift_pix;
	}

	if (keys[GLFW_KEY_F])		// 向前(按 F 键)
	{
		front_back_move += Shift_pix;
	}

	if (keys[GLFW_KEY_B])		// 向后(按 B 键)
	{
		front_back_move -= Shift_pix;
	}
}


九、运行结果

  ● 如果你认真按照上诉内容一步一步地进行配置,即可得到如下结果:

【OpenGL学习笔记⑧】——键盘控制正方体+光源【冯氏光照模型 光照原理 环境光照+漫反射光照+镜面光照】_第8张图片



十、参考附录:

[1] 《Learn OpenGL——颜色》
链接: https://learnopengl-cn.github.io/02%20Lighting/01%20Colors/.

[2] 《Learn OpenGL——基础光照》
链接: https://learnopengl-cn.github.io/02%20Lighting/02%20Basic%20Lighting/#_1.

上一篇文章链接: 【OpenGL学习笔记⑦】——键盘控制镜头的平移【3D正方体 透视投影 观察矩阵 对LookAt的理解】.

下一篇文章链接: 【OpenGL学习笔记⑨】——鼠标控制镜头 + 滚轮控制镜头缩放.

OpenGL总学习目录: https://blog.csdn.net/Wang_Dou_Dou_/article/details/121240714.


⭐️ ⭐️

你可能感兴趣的:(OpenGL学习笔记,opengl,计算机图形学,着色器)