南邮——计算机图像学——光照、冯氏光照模型

颜色可以数字化的由红色(Red)、绿色(Green)和蓝色(Blue)三个分量组成,它们通常被缩写为RGB。这三个不同的分量组合在一起几乎可以表示存在的任何一种颜色。

(一)观察物体

(1)物体的片段着色器

#version 330 core
out vec4 color;

uniform vec3 objectColor;
uniform vec3 lightColor;

void main()
{
    color = vec4(lightColor * objectColor, 1.0f);
}

当我们把光源的颜色与物体的颜色相乘,所得到的就是这个物体所反射该光源的颜色(也就是我们感知到的颜色)。

冯氏光照模型

冯氏光照模型的主要结构由3个元素组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。这些光照元素看起来像下面这样:

 

  • 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上也仍然有一些光亮(月亮、一个来自远处的光),所以物体永远不会是完全黑暗的。我们使用环境光照来模拟这种情况,也就是无论如何永远都给物体一些颜色。
  • 漫反射光照(Diffuse Lighting):模拟一个发光物对物体的方向性影响(Directional Impact)。它是冯氏光照模型最显著的组成部分。面向光源的一面比其他面会更亮。
  • 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色,相比于物体的颜色更倾向于光的颜色。

1、环境光照

把环境光照添加到场景里非常简单。我们用光的颜色乘以一个(数值)很小常量环境因子得到环境光照颜色(物体片段着色器)

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

2、漫反射

我们需要些什么来计算漫反射光照?

  • 法向量:一个垂直于顶点表面的向量。
  • 定向的光线:作为光的位置和片段的位置之间的向量差的方向向量。为了计算这个光线,我们需要光的位置向量和片段的位置向量。

法向量

我们可以简单的把法线数据手工添加到顶点数据中。更新的顶点数据数组可以在这里找到。试着去想象一下,这些法向量真的是垂直于立方体的各个面的表面的(一个立方体由6个面组成)。

因为我们向顶点数组添加了额外的数据,所以我们应该更新物体的顶点着色器:

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

现在我们已经向每个顶点添加了一个法向量,已经更新了顶点着色器,我们还要更新顶点属性指针(Vertex Attibute Pointer)。注意,发光物使用同样的顶点数组作为它的顶点数据,然而发光物的着色器没有使用新添加的法向量。我们不会更新发光物的着色器或者属性配置,但是我们必须至少修改一下顶点属性指针来适应新的顶点数组的大小:

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid * )0);
glEnableVertexAttribArray(0);

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

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

out vec3 Normal;
void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0f);
    Normal = normal;
}

剩下要做的事情是,在片段着色器中定义相应的输入值:

in vec3 Normal;

现在我们已经把法向量从顶点着色器传到了片段着色器。可是,目前片段着色器里,我们都是在世界空间坐标中进行计算的,所以,我们不是应该把法向量转换为世界空间坐标吗?

正规矩阵被定义为“模型矩阵左上角的逆矩阵的转置矩阵”。真拗口,如果你不明白这是什么意思,别担心;我们还没有讨论逆矩阵(Inverse Matrix)和转置矩阵(Transpose Matrix)。注意,定义正规矩阵的大多资源就像应用到模型观察矩阵(Model-view Matrix)上的操作一样,但是由于我们只在世界空间工作(而不是在观察空间),我们只使用模型矩阵。

在顶点着色器中,我们可以使用inversetranspose函数自己生成正规矩阵,inversetranspose函数对所有类型矩阵都有效。注意,我们也要把这个被处理过的矩阵强制转换为3×3矩阵,这是为了保证它失去了平移属性,之后它才能乘以法向量。

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

即可解决这一问题

定向的光线

每个顶点现在都有了法向量,但是我们仍然需要光的位置向量和片段的位置向量。

1、光的位置向量

我们可以简单的在片段着色器中把它声明为uniform:

uniform vec3 lightPos;

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

GLint lightPosLoc = glGetUniformLocation(lightingShader.Program, "lightPos");
glUniform3f(lightPosLoc, lightPos.x, lightPos.y, lightPos.z);

2、片段的位置(Position)

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

out vec3 FragPos;
out vec3 Normal;

void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0f);
    FragPos = vec3(model * vec4(position, 1.0f));
    Normal = normal;
}

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

in vec3 FragPos;

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

3、计算光源和片段位置之间的方向向量

前面提到,光的方向向量是光的位置向量与片段的位置向量之间的向量差。你可能记得,在变换教程中,我们简单的通过两个向量相减的方式计算向量差。我们同样希望确保所有相关向量最后都转换为单位向量,所以我们把法线和方向向量这个结果都进行标准化:

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

4、散射光颜色

我们对normlightDir向量进行点乘,来计算光对当前片段的实际的散射影响。结果值再乘以光的颜色,得到散射因子。两个向量之间的角度越大,散射因子就会越小:

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

如果两个向量之间的角度大于90度,点乘的结果就会变成负数,这样会导致散射因子变为负数。为此,我们使用max函数返回两个参数之间较大的参数,从而保证散射因子不会变成负数。负数的颜色是没有实际定义的,所以最好避免它,除非你是那种古怪的艺术家。

5、最终颜色

既然我们有了一个环境光照颜色和一个散射光颜色,我们把它们相加,然后把结果乘以物体的颜色,来获得片段最后的输出颜色。

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

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

3、镜面光照

和环境光照一样,镜面光照(Specular Lighting)同样依据光的方向向量和物体的法向量,但是这次它也会依据观察方向,例如玩家是从什么方向看着这个片段的。镜面光照根据光的反射特性。如果我们想象物体表面像一面镜子一样,那么,无论我们从哪里去看那个表面所反射的光,镜面光照都会达到最大化。你可以从下面的图片看到效果:

我们通过反射法向量周围光的方向计算反射向量。然后我们计算反射向量和视线方向的角度,如果之间的角度越小,那么镜面光的作用就会越大。它的作用效果就是,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光。

观察向量

是镜面光照的一个附加变量,我们可以使用观察者世界空间位置(Viewer’s World Space Position)和片段的位置来计算。之后,我们计算镜面光亮度,用它乘以光的颜色,在用它加上作为之前计算的光照颜色。

为了得到观察者的世界空间坐标,我们简单地使用摄像机对象的位置坐标代替(它就是观察者)。所以我们把另一个uniform添加到片段着色器,把相应的摄像机位置坐标传给片段着色器:

uniform vec3 viewPos;

GLint viewPosLoc = glGetUniformLocation(lightingShader.Program, "viewPos");
glUniform3f(viewPosLoc, camera.Position.x, camera.Position.y, camera.Position.z);

计算高光亮度

1、我们定义一个镜面强度(Specular Intensity)变量specularStrength

给镜面高光一个中等亮度颜色,这样就不会产生过度的影响了。

float specularStrength = 0.5f;

2、计算视线方向坐标,和沿法线轴的对应的反射坐标:

vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);

需要注意的是我们使用了lightDir向量的相反数。reflect函数要求的第一个是从光源指向片段位置的向量,但是lightDir当前是从片段指向光源的向量(由先前我们计算lightDir向量时,(减数和被减数)减法的顺序决定)。为了保证我们得到正确的reflect坐标,我们通过lightDir向量的相反数获得它的方向的反向。第二个参数要求是一个法向量,所以我们提供的是已标准化的norm向量。

3、计算镜面亮度分量

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

我们先计算视线方向与反射方向的点乘(确保它不是负值),然后得到它的32次幂。这个32是高光的发光值(Shininess)。一个物体的发光值越高,反射光的能力越强,散射得越少,高光点越小。

 

4、计算了全部的光照元素

把它添加到环境光颜色和散射光颜色里,然后再乘以物体颜色:

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

我们现在为冯氏光照计算了全部的光照元素。

(2)uniform变量赋值

1、对于片段着色器

GLint objectColorLoc = glGetUniformLocation(ourShader.Program, "objectColor");
		GLint lightColorLoc = glGetUniformLocation(ourShader.Program, "lightColor");
		GLint lightPosLoc = glGetUniformLocation(ourShader.Program, "lightPos");
		GLint viewPosLoc = glGetUniformLocation(ourShader.Program, "viewPos");

		glUniform3f(objectColorLoc, 1.0f, 0.5f, 0.3f);  //珊瑚红
		glUniform3f(lightColorLoc, 1.0f, 1.0f, 1.0f);  //把光源设置为白色
		glUniform3f(lightPosLoc, lightPos.x, lightPos.y, lightPos.z);
		glUniform3f(viewPosLoc, camera.GetPosition().x, camera.
			GetPosition().y, camera.GetPosition().z);  //使用摄像机对象的位置坐标代替

要注意的是,当我们修改顶点或者片段着色器后,灯的位置或颜色也会随之改变,这并不是我们想要的效果。我们不希望灯对象的颜色在接下来的教程中因光照计算的结果而受到影响,而希望它能够独立。希望表示灯不受其他光照的影响而一直保持明亮(这样它才更像是一个真实的光源)。

2、对于顶点着色器

GLuint modelLoc = glGetUniformLocation(ourShader.Program, "model");  //到 vs 找到那个 model 变量
		GLuint viewLoc = glGetUniformLocation(ourShader.Program, "view");  //到 vs 找到那个 view 变量
		GLuint projectionLoc = glGetUniformLocation(ourShader.Program, "projection");  //到 vs 找到那个 projection 变量
	
		glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
		glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
		glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));

3、绑定

GLuint VAO, VBO;  //VAO:Vertex Array Object   VBO:Vertex Buffer Object传数据
	glGenVertexArrays(1, &VAO);  //创建 VAO
	glGenBuffers(1, &VBO);
	glBindVertexArray(VAO);  //设当前直线
	glBindBuffer(GL_ARRAY_BUFFER, VBO);  //VAO 和 VBO 成对出现
	// transfer the data:传数据
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);  //静态访问,几乎不修改
	// set the attribute
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
		6 * sizeof(GLfloat), (GLvoid *)0);  //0:对应调色器里 location 的值;3:对应 vec3 三个量;GL_FLOAT:浮点型;GL_FALSE:;6*sizeof(GLfloat):对应 Buffer 里传的数据;(GLvoid*)0:从第 0 个位置开始
	glEnableVertexAttribArray(0);
	// 法向量
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE,
		6 * sizeof(GLfloat), (GLvoid *)(3 * sizeof(GLfloat)));  //1:对应调色器里 location 的值;3:对应 vec3 三个量;GL_FLOAT:浮点型;GL_FALSE:;6*sizeof(GLfloat):对应 Buffer 里传的数据;(GLvoid *)(3 * sizeof(GLfloat)):从第 3 个位置开始
	glEnableVertexAttribArray(1);

	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindVertexArray(0);

4、画图

// Draw the triangle
		glBindVertexArray(VAO);  //使用 VAO,直接绑定

		glDrawArrays(GL_TRIANGLES, 0, 36);  //画三角形,总共有 36 个顶点
		//glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
		glBindVertexArray(0);

二、光源

1、灯的顶点数据

同物体的顶点数据

2、为灯创建一个新的VAO

因为我们还要创建一个表示灯(光源)的立方体,所以我们还要为这个灯创建一个特殊的VAO。当然我们也可以让这个灯和其他物体使用同一个VAO然后对他的model(模型)矩阵做一些变换,然而接下来的教程中我们会频繁地对顶点数据做一些改变并且需要改变属性对应指针设置,我们并不想因此影响到灯(我们只在乎灯的位置),因此我们有必要为灯创建一个新的VAO。

GLuint lightVAO;
	glGenVertexArrays(1, &lightVAO);
	// 绑定光源 VAO
	glBindVertexArray(lightVAO);
	// 只需要绑定VBO不用再次设置VBO的数据,因为容器(物体)的VBO数据中已经包含了正确的立方体顶点数据
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	// 设置光源的顶点属性指针(仅设置灯的顶点数据)
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
		6 * sizeof(GLfloat), (GLvoid *)0);  //0:对应调色器里 location 的值;3:对应 vec3 三个量;GL_FLOAT:浮点型;GL_FALSE:;6*sizeof(GLfloat):对应 Buffer 里传的数据;(GLvoid*)0:从第 0 个位置开始
	glEnableVertexAttribArray(0);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindVertexArray(0);

这段代码对你来说应该非常直观。既然我们已经创建了表示灯和被照物体的立方体,我们只需要再定义一个东西就行了了,那就是片段着色器

 

3、为灯创建另外的一套着色器程序

为了实现这个目的,我们需要为灯创建另外的一套着色器程序,从而能保证它能够在其他光照着色器变化的时候保持不变。顶点着色器和我们当前的顶点着色器是一样的,所以你可以直接把灯的顶点着色器复制过来。片段着色器保证了灯的颜色一直是亮的,我们通过给灯定义一个常量的白色来实现:

#version 330 core
out vec4 color;

void main()
{
    color = vec4(1.0f); //设置四维向量的所有元素为 1.0f
}

当我们想要绘制我们的物体的时候,我们需要使用刚刚定义的光照着色器绘制箱子(或者可能是其它的一些物体),让我们想要绘制灯的时候,我们会使用灯的着色器。在之后的教程里我们会逐步升级这个光照着色器从而能够缓慢的实现更真实的效果。

4、赋值Unfiorm

lightShader.Use();
modelLoc = glGetUniformLocation(lightShader.Program, "model");  //到 vs 找到那个 model 变量
		viewLoc = glGetUniformLocation(lightShader.Program, "view");  //到 vs 找到那个 view 变量
		projectionLoc = glGetUniformLocation(lightShader.Program, "projection");  //到 vs 找到那个 projection 变量
		// 对模型进行操作
		model = glm::translate(model, lightPos);  //平移光源
		model = glm::scale(model, glm::vec3(0.2f));  //缩放光源

		// 传入数据
		glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
		glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
		glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));
		lightColorLoc = glGetUniformLocation(lightShader.Program, "lightColor");
		glUniform3f(lightColorLoc, 1.0f, 1.0f, 1.0f);

调用灯的着色器

// 光源调色器
	Shader lightShader = Shader("light.vs", "light.frag");

4、定义一个光源的位置

使用这个灯立方体的主要目的是为了让我们知道光源在场景中的具体位置。我们通常在场景中定义一个光源的位置,但这只是一个位置,它并没有视觉意义。为了显示真正的灯,我们将表示光源的灯立方体绘制在与光源同样的位置。我们将使用我们为它新建的片段着色器让它保持它一直处于白色状态,不受场景中的光照影响。

我们声明一个全局vec3变量来表示光源在场景的世界空间坐标中的位置:

glm::vec3 lightPos(1.2f, 1.0f, 2.0f);

5、灯变换

然后我们把灯平移到这儿,当然我们需要对它进行缩放,让它不那么明显:


model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f));

6、绘制灯立方体

// 绘制光源对象
		glBindVertexArray(lightVAO);  //使用 VAO,直接绑定
		glDrawArrays(GL_TRIANGLES, 0, 36);  //画三角形,总共有 36 个顶点
		
		glBindVertexArray(0);

请把上述的所有代码片段放在你程序中合适的位置,这样我们就能有一个干净的光照实验场地了。

打包灯为一个类

1、生成灯的对象

 Light lightModel = Light();

2、设置灯位置并变换

// 设置光源坐标
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
//旋转角度为 0.01f
		lightPos = glm::rotate(lightPos, 0.01f, glm::vec3(1.0f, 1.0f, 0.0f));

3、调用着色器

// 光源调色器
	Shader lightShader = Shader("light.vs", "light.frag");

4、渲染

// 画光源
		lightShader.Use();
		// 设置模型、视图和投影矩阵 uniform
		modelLoc = glGetUniformLocation(lightShader.Program, "model");  //到 vs 找到那个 model 变量
		viewLoc = glGetUniformLocation(lightShader.Program, "view");  //到 vs 找到那个 view 变量
		projectionLoc = glGetUniformLocation(lightShader.Program, "projection");  //到 vs 找到那个 projection 变量
		// 对模型进行操作
		model = glm::translate(model, lightPos);  //平移光源
		model = glm::scale(model, glm::vec3(0.2f));  //缩放光源

		// 传入数据
		glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
		glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
		glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));
		lightColorLoc = glGetUniformLocation(lightShader.Program, "lightColor");
		glUniform3f(lightColorLoc, 1.0f, 1.0f, 1.0f);
lightModel.Draw(lightShader);

参考链接:

https://blog.csdn.net/Wonz5130/article/details/84098270

https://learnopengl-cn.readthedocs.io/zh/latest/02%20Li

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值