LearnOpenGL 光照—基础光照

写在前面

原文链接。原文应该是github上的一个项目,本文主要用来记录一些知识点和自己遇到的问题。

基础光照

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

  • 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
  • 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
  • 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。

为了创建有趣的视觉场景,我们希望模拟至少这三种光照分量。我们将以最简单的一个开始:环境光照。

环境光照

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

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

把环境光照添加到场景里非常简单。我们用光的颜色乘以一个很小的常量环境因子,再乘以物体的颜色,然后将最终结果作为片段的颜色:
在这里插入图片描述
如果你现在运行你的程序,你会注意到冯氏光照的第一个阶段已经应用到你的物体上了。这个物体非常暗,但由于应用了环境光照(注意光源立方体没受影响是因为我们对它使用了另一个着色器),也不是完全黑的。它看起来应该像这样:
在这里插入图片描述

漫反射光照

环境光照本身不能提供最有趣的结果,但是漫反射光照就能开始对物体产生显著的视觉影响了。漫反射光照使物体上与光线方向越接近的片段能从光源处获得更多的亮度。为了能够更好的理解漫反射光照,请看下图:
在这里插入图片描述
图左上方有一个光源,它所发出的光线落在物体的一个片段上。我们需要测量这个光线是以什么角度接触到这个片段的。如果光线垂直于物体表面,这束光对物体的影响会最大化(译注:更亮)。为了测量光线和片段的角度,我们使用一个叫做法向量(Normal Vector)的东西,它是垂直于片段表面的一个向量(这里以黄色箭头表示),我们在后面再讲这个东西。这两个向量之间的角度很容易就能够通过点乘计算出来。

你可能记得在变换那一节教程里,我们知道两个单位向量的夹角越小,它们点乘的结果越倾向于1。当两个向量的夹角为90度的时候,点乘会变为0。这同样适用于θ,θ越大,光对片段颜色的影响就应该越小。
在这里插入图片描述
点乘返回一个标量,我们可以用它计算光线对片段颜色的影响。不同片段朝向光源的方向的不同,这些片段被照亮的情况也不同。

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

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

法向量

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

由于我们向顶点数组添加了额外的数据,所以我们应该更新光照的顶点着色器:
在这里插入图片描述

现在我们已经向每个顶点添加了一个法向量并更新了顶点着色器,我们还要更新顶点属性指针。注意,灯使用同样的顶点数组作为它的顶点数据,然而灯的着色器并没有使用新添加的法向量。我们不需要更新灯的着色器或者是属性的配置,但是我们必须至少修改一下顶点属性指针来适应新的顶点数组的大小:
在这里插入图片描述
我们只想使用每个顶点的前三个float,并且忽略后三个float,所以我们只需要把步长参数改成float大小的6倍就行了。
在这里插入图片描述
所有光照的计算都是在片段着色器里进行,所以我们需要将法向量由顶点着色器传递到片段着色器。我们这么做:
在这里插入图片描述
接下来,在片段着色器中定义相应的输入变量:
在这里插入图片描述

计算漫反射光照

我们现在对每个顶点都有了法向量,但是我们仍然需要光源的位置向量和片段的位置向量。由于光源的位置是一个静态变量,我们可以简单地在片段着色器中把它声明为uniform:
在这里插入图片描述
然后在渲染循环中(渲染循环的外面也可以,因为它不会改变)更新uniform。我们使用在前面声明的lightPos向量作为光源位置:
在这里插入图片描述
最后,我们还需要片段的位置。我们会在世界空间中进行所有的光照计算,因此我们需要一个在世界空间中的顶点位置。我们可以通过把顶点位置属性乘以模型矩阵(不是观察和投影矩阵)来把它变换到世界空间坐标。这个在顶点着色器中很容易完成,所以我们声明一个输出变量,并计算它的世界空间坐标:
在这里插入图片描述
最后,在片段着色器中添加相应的输入变量。
在这里插入图片描述
现在,所有需要的变量都设置好了,我们可以在片段着色器中添加光照计算了。

我们需要做的第一件事是计算光源和片段位置之间的方向向量。前面提到,光的方向向量是光源位置向量与片段位置向量之间的向量差。你可能记得在变换教程中,我们能够简单地通过让两个向量相减的方式计算向量差。我们同样希望确保所有相关向量最后都转换为单位向量,所以我们把法线和最终的方向向量都进行标准化:
在这里插入图片描述
在这里插入图片描述
下一步,我们对norm和lightDir向量进行点乘,计算光源对当前片段实际的漫发射影响。结果值再乘以光的颜色,得到漫反射分量。两个向量之间的角度越大,漫反射分量就会越小:
在这里插入图片描述
如果两个向量之间的角度大于90度,点乘的结果就会变成负数,这样会导致漫反射分量变为负数。为此,我们使用max函数返回两个参数之间较大的参数,从而保证漫反射分量不会变成负数。负数颜色的光照是没有定义的,所以最好避免它,除非你是那种古怪的艺术家。

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

如果你的应用(和着色器)编译成功了,你可能看到类似的输出:
在这里插入图片描述
你可以看到使用了漫反射光照,立方体看起来就真的像个立方体了。尝试在你的脑中想象一下法向量,并在立方体周围移动,注意观察法向量和光的方向向量之间的夹角越大,片段就会越暗。
o b j e c t   s h a d e r : object\ shader: object shader:

#version 330 core
layout (location = 0) in vec3 aPos;   // 位置变量的属性位置值为 0 
layout (location = 1) in vec3 aNormal;

uniform mat4 model;	//模型
uniform mat4 view;	//观察
uniform mat4 projection;	//投影

out vec3 Normal;
out vec3 FragPos;

void main()
{
    
    
	// 注意乘法要从右向左读
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    Normal = aNormal;
    FragPos = vec3(model * vec4(aPos, 1.0));
}
#version 330 core

in vec3 Normal;
in vec3 FragPos;

out vec4 FragColor;  

uniform vec3 objectColor;
uniform vec3 lightColor;
uniform vec3 lightPos;

void main()
{
    
    
	//环境光
	float ambientStrength=0.1;
	vec3 ambient=ambientStrength*lightColor;
	//漫反射
	vec3 norm=normalize(Normal);
	vec3 lightDir=normalize(lightPos-FragPos);
	float diff=max(dot(norm,lightDir),0.0);
	vec3 diffuse=diff*lightColor;
	//最终结果
	vec3 result=(ambient+diffuse)*objectColor;
	FragColor=vec4(result,1.0);
}

c p p : cpp: cpp:

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>
#include "shader.h"
#include "stb_image.h"
#include "camera.h"
using std::cout;

//窗口回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    
    
	//绘图视口 3D坐标到2D坐标的转换(映射)和这些参数(宽高)有关
	glViewport(0, 0, width, height);
}

//键盘回调
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);

//鼠标回调
void mouse_callback(GLFWwindow* window, double xpos, double ypos);

//滚轮回调
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);

//窗口初始大小
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

//物体着色器
const char* vShaderPath = "ShaderFiles/shader.vs";
const char* fShaderPath = "ShaderFiles/shader.fs";
//光源着色器
const char* lightvShaderPath = "ShaderFiles/shader.vs";
const char* lightfShaderPath = "ShaderFiles/light_shader.fs";

//混合颜色的插值
float mixValue = 0.2f;
//记录鼠标坐标
float lastX, lastY;
bool firstMouse = true;

//摄像机
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));

//光源位置
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);

int main()
{
    
    
	//glfw初始化
	glfwInit();
	//告诉glfw我们所使用的opengl版本 此处为3.3
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

	//创建窗口
	GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
	if (window == NULL)
	{
    
    
		cout << "Failed to create GLFW window\n";
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
	//设置窗口回调函数
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
	//键盘回调函数
	glfwSetKeyCallback(window, key_callback);
	//鼠标回调
	glfwSetCursorPosCallback(window, mouse_callback);
	//滚轮回调
	glfwSetScrollCallback(window, scroll_callback);

	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
    
    
		cout << "Failed to initialize GLAD\n";
		return -1;
	}

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

	//着色器对象
	Shader shaderProgram = Shader(vShaderPath, fShaderPath);
	Shader lightShaderProgram = Shader(lightvShaderPath, lightfShaderPath);

	float vertices[] = {
    
    
		-0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
		 0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
		 0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
		 0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
		-0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
		-0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,

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

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

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

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

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

	//顶点缓冲对象 VBO
	//顶点数组对象 VAO
	unsigned int VBO, VAO;
	//渲染物体
	glGenVertexArrays(1, &VAO);
	glGenBuffers(1, &VBO);

	glBindVertexArray(VAO);

	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	//设置顶点属性
	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);

	//光源
	unsigned int lightVAO;
	glGenVertexArrays(1, &lightVAO);
	glBindVertexArray(lightVAO);

	glBindBuffer(GL_ARRAY_BUFFER, VBO);

	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);

	//线框模式
	//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

	//这些uniform不会更新 可以放到循环外面
	shaderProgram.use();
	shaderProgram.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
	shaderProgram.setVec3("lightColor", 1.0f, 1.0f, 1.0f);
	shaderProgram.setVec3("lightPos", lightPos);

	while (!glfwWindowShouldClose(window))
	{
    
    
		glClearColor(0.1f, 0.1f, 0.1f, 0.1f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		//矩阵运算
		glm::mat4 lightModel(1.0f);
		glm::mat4 view = camera.GetViewMatrix();
		glm::mat4 projection = glm::perspective(glm::radians(camera.Fov), SCR_WIDTH * 1.0f / SCR_HEIGHT, 0.1f, 100.0f);
		//激活着色器
		shaderProgram.use();
		shaderProgram.setMat4("model", lightModel);
		shaderProgram.setMat4("view", view);
		shaderProgram.setMat4("projection", projection);
		glBindVertexArray(VAO);
		glDrawArrays(GL_TRIANGLES, 0, 36);
		//光源着色器
		lightShaderProgram.use();
		lightModel = glm::translate(lightModel, lightPos);
		lightModel = glm::scale(lightModel, glm::vec3(0.2f));
		lightShaderProgram.setMat4("model", lightModel);
		lightShaderProgram.setMat4("view", view);
		lightShaderProgram.setMat4("projection", projection);
		glBindVertexArray(lightVAO);
		glDrawArrays(GL_TRIANGLES, 0, 36);

		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	//这一步是可选的
	glDeleteVertexArrays(1, &VAO);
	glDeleteBuffers(1, &VBO);
	//glDeleteBuffers(1, &EBO);

	//释放资源
	glfwTerminate();
	return 0;
}

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
    
    
	if (action == GLFW_REPEAT || action == GLFW_PRESS)
	{
    
    
		if (key == GLFW_KEY_ESCAPE)
		{
    
    
			glfwSetWindowShouldClose(window, GL_TRUE);
			return;
		}
		switch (key)
		{
    
    
		case GLFW_KEY_UP:
			mixValue += 0.1f;
			if (mixValue >= 1.0f)
				mixValue = 1.0f;
			break;
		case GLFW_KEY_DOWN:
			mixValue -= 0.1f;
			if (mixValue <= 0.0f)
				mixValue = 0.0f;
			break;
		case GLFW_KEY_W:
			camera.ProcessKeyboard(FORWARD);
			break;
		case GLFW_KEY_S:
			camera.ProcessKeyboard(BACKWARD);
			break;
		case GLFW_KEY_A:
			camera.ProcessKeyboard(LEFT);
			break;
		case GLFW_KEY_D:
			camera.ProcessKeyboard(RIGHT);
			break;
		default:
			break;
		}
	}
}

void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    
    
	if (firstMouse)
	{
    
    
		firstMouse = false;
		lastX = xpos, lastY = ypos;
	}
	camera.ProcessMouseMovement(xpos - lastX, lastY - ypos);
	lastX = xpos;
	lastY = ypos;
}

void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    
    
	camera.ProcessMouseScroll(yoffset);
}

最后一件事

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

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

其次,如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了。因此,我们不能用这样的模型矩阵来变换法向量。下面的图展示了应用了不等比缩放的模型矩阵对法向量的影响:
在这里插入图片描述
每当我们应用一个不等比缩放时(注意:等比缩放不会破坏法线,因为法线的方向没被改变,仅仅改变了法线的长度,而这很容易通过标准化来修复),法向量就不会再垂直于对应的表面了,这样光照就会被破坏。

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

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

顶点着色器中,我们可以使用inversetranspose函数自己生成这个法线矩阵,这两个函数对所有类型矩阵都有效。注意我们还要把被处理过的矩阵强制转换为3×3矩阵,来保证它失去了位移属性以及能够乘以vec3的法向量。
在这里插入图片描述
在漫反射光照部分,光照表现并没有问题,这是因为我们没有对物体本身执行任何缩放操作,所以并不是必须要使用一个法线矩阵,仅仅让模型矩阵乘以法线也可以。可是,如果你进行了不等比缩放,使用法线矩阵去乘以法向量就是必不可少的了。
在这里插入图片描述

镜面光照

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

和漫反射光照一样,镜面光照也是依据光的方向向量和物体的法向量来决定的,但是它也依赖于观察方向,例如玩家是从什么方向看着这个片段的。镜面光照是基于光的反射特性。如果我们想象物体表面像一面镜子一样,那么,无论我们从哪里去看那个表面所反射的光,镜面光照都会达到最大化。你可以从下面的图片看到效果:
在这里插入图片描述
我们通过反射法向量周围光的方向来计算反射向量。然后我们计算反射向量和视线方向的角度差,如果夹角越小,那么镜面光的影响就会越大。它的作用效果就是,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光

观察向量是镜面光照附加的一个变量,我们可以使用观察者世界空间位置片段的位置来计算它。之后,我们计算镜面光强度,用它乘以光源的颜色,再将它加上环境光和漫反射分量。
在这里插入图片描述
为了得到观察者的世界空间坐标,我们简单地使用摄像机对象的位置坐标代替(它当然就是观察者)。所以我们把另一个uniform添加到片段着色器,把相应的摄像机位置坐标传给片段着色器:
在这里插入图片描述
现在我们已经获得所有需要的变量,可以计算高光强度了。首先,我们定义一个镜面强度(Specular Intensity)变量,给镜面高光一个中等亮度颜色,让它不要产生过度的影响。
在这里插入图片描述
如果我们把它设置为1.0f,我们会得到一个非常亮的镜面光分量,这对于一个珊瑚色的立方体来说有点太多了。下一节教程中我们会讨论如何合理设置这些光照强度,以及它们是如何影响物体的。下一步,我们计算视线方向向量,和对应的沿着法线轴的反射向量:
在这里插入图片描述
需要注意的是我们对lightDir向量进行了取反reflect函数要求第一个向量是从光源指向片段位置的向量,但是lightDir当前正好相反,是从片段指向光源(由先前我们计算lightDir向量时,减法的顺序决定)。为了保证我们得到正确的reflect向量,我们通过对lightDir向量取反来获得相反的方向。第二个参数要求是一个法向量,所以我们提供的是已标准化的norm向量。

剩下要做的是计算镜面分量。下面的代码完成了这件事:
在这里插入图片描述
我们先计算视线方向与反射方向的点乘(并确保它不是负值),然后取它的32次幂。这个32是高光的反光度(Shininess)。一个物体的反光度越高反射光的能力越强散射得越少高光点就会越小。在下面的图片里,你会看到不同反光度的视觉效果影响:
在这里插入图片描述
我们不希望镜面成分过于显眼,所以我们把指数保持为32。剩下的最后一件事情是把它加到环境光分量和漫反射分量里,再用结果乘以物体的颜色:
在这里插入图片描述
我们现在为冯氏光照计算了全部的光照分量。根据你的视角,你可以看到类似下面的画面:
在这里插入图片描述
注:在这个位置左右移动可以看到高光的移动。
o b j e c t   s h a d e r : object\ shader: object shader:

#version 330 core

in vec3 Normal;
in vec3 FragPos;

out vec4 FragColor;  

uniform vec3 objectColor;
uniform vec3 lightColor;
uniform vec3 lightPos;
uniform vec3 viewPos;

void main()
{
    
    
	//环境光
	float ambientStrength=0.1;
	vec3 ambient=ambientStrength*lightColor;
	//漫反射
	vec3 norm=normalize(Normal);
	vec3 lightDir=normalize(lightPos-FragPos);
	float diff=max(dot(norm,lightDir),0.0);
	vec3 diffuse=diff*lightColor;
	//镜面反射
	float specularStrength=0.5;
	vec3 viewDir=normalize(viewPos-FragPos);
	vec3 reflectDir=reflect(-lightDir,norm);
	float spec=pow(max(dot(reflectDir,viewDir),0.0),32);
	vec3 specular=specularStrength*spec*lightColor;
	//最终结果
	vec3 result=(ambient+diffuse+specular)*objectColor;
	FragColor=vec4(result,1.0);
}

c p p : cpp: cpp:

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>
#include "shader.h"
#include "stb_image.h"
#include "camera.h"
using std::cout;

//窗口回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    
    
	//绘图视口 3D坐标到2D坐标的转换(映射)和这些参数(宽高)有关
	glViewport(0, 0, width, height);
}

//键盘回调
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);

//鼠标回调
void mouse_callback(GLFWwindow* window, double xpos, double ypos);

//滚轮回调
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);

//窗口初始大小
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

//物体着色器
const char* vShaderPath = "ShaderFiles/shader.vs";
const char* fShaderPath = "ShaderFiles/shader.fs";
//光源着色器
const char* lightvShaderPath = "ShaderFiles/shader.vs";
const char* lightfShaderPath = "ShaderFiles/light_shader.fs";

//混合颜色的插值
float mixValue = 0.2f;
//记录鼠标坐标
float lastX, lastY;
bool firstMouse = true;

//摄像机
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));

//光源位置
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);

int main()
{
    
    
	//glfw初始化
	glfwInit();
	//告诉glfw我们所使用的opengl版本 此处为3.3
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

	//创建窗口
	GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
	if (window == NULL)
	{
    
    
		cout << "Failed to create GLFW window\n";
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
	//设置窗口回调函数
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
	//键盘回调函数
	glfwSetKeyCallback(window, key_callback);
	//鼠标回调
	glfwSetCursorPosCallback(window, mouse_callback);
	//滚轮回调
	glfwSetScrollCallback(window, scroll_callback);

	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
    
    
		cout << "Failed to initialize GLAD\n";
		return -1;
	}

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

	//着色器对象
	Shader shaderProgram = Shader(vShaderPath, fShaderPath);
	Shader lightShaderProgram = Shader(lightvShaderPath, lightfShaderPath);

	float vertices[] = {
    
    
		-0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
		 0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
		 0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
		 0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
		-0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,
		-0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,

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

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

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

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

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

	//顶点缓冲对象 VBO
	//顶点数组对象 VAO
	unsigned int VBO, VAO;
	//渲染物体
	glGenVertexArrays(1, &VAO);
	glGenBuffers(1, &VBO);

	glBindVertexArray(VAO);

	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	//设置顶点属性
	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);

	//光源
	unsigned int lightVAO;
	glGenVertexArrays(1, &lightVAO);
	glBindVertexArray(lightVAO);

	glBindBuffer(GL_ARRAY_BUFFER, VBO);

	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);

	//线框模式
	//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

	//这些uniform不会更新 可以放到循环外面
	shaderProgram.use();
	shaderProgram.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
	shaderProgram.setVec3("lightColor", 1.0f, 1.0f, 1.0f);
	shaderProgram.setVec3("lightPos", lightPos);

	while (!glfwWindowShouldClose(window))
	{
    
    
		glClearColor(0.1f, 0.1f, 0.1f, 0.1f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		//矩阵运算
		glm::mat4 lightModel(1.0f);
		glm::mat4 view = camera.GetViewMatrix();
		glm::mat4 projection = glm::perspective(glm::radians(camera.Fov), SCR_WIDTH * 1.0f / SCR_HEIGHT, 0.1f, 100.0f);
		//激活着色器
		shaderProgram.use();
		shaderProgram.setVec3("viewPos", camera.Position);
		shaderProgram.setMat4("model", lightModel);
		shaderProgram.setMat4("view", view);
		shaderProgram.setMat4("projection", projection);
		glBindVertexArray(VAO);
		glDrawArrays(GL_TRIANGLES, 0, 36);
		//光源着色器
		lightShaderProgram.use();
		lightModel = glm::translate(lightModel, lightPos);
		lightModel = glm::scale(lightModel, glm::vec3(0.2f));
		lightShaderProgram.setMat4("model", lightModel);
		lightShaderProgram.setMat4("view", view);
		lightShaderProgram.setMat4("projection", projection);
		glBindVertexArray(lightVAO);
		glDrawArrays(GL_TRIANGLES, 0, 36);

		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	//这一步是可选的
	glDeleteVertexArrays(1, &VAO);
	glDeleteBuffers(1, &VBO);
	//glDeleteBuffers(1, &EBO);

	//释放资源
	glfwTerminate();
	return 0;
}

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
    
    
	if (action == GLFW_REPEAT || action == GLFW_PRESS)
	{
    
    
		if (key == GLFW_KEY_ESCAPE)
		{
    
    
			glfwSetWindowShouldClose(window, GL_TRUE);
			return;
		}
		switch (key)
		{
    
    
		case GLFW_KEY_UP:
			mixValue += 0.1f;
			if (mixValue >= 1.0f)
				mixValue = 1.0f;
			break;
		case GLFW_KEY_DOWN:
			mixValue -= 0.1f;
			if (mixValue <= 0.0f)
				mixValue = 0.0f;
			break;
		case GLFW_KEY_W:
			camera.ProcessKeyboard(FORWARD);
			break;
		case GLFW_KEY_S:
			camera.ProcessKeyboard(BACKWARD);
			break;
		case GLFW_KEY_A:
			camera.ProcessKeyboard(LEFT);
			break;
		case GLFW_KEY_D:
			camera.ProcessKeyboard(RIGHT);
			break;
		default:
			break;
		}
	}
}

void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    
    
	if (firstMouse)
	{
    
    
		firstMouse = false;
		lastX = xpos, lastY = ypos;
	}
	camera.ProcessMouseMovement(xpos - lastX, lastY - ypos);
	lastX = xpos;
	lastY = ypos;
}

void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    
    
	camera.ProcessMouseScroll(yoffset);
}

在这里插入图片描述
现在你应该能够看到着色器的强大之处了。只用很少的信息,着色器就能计算出光照如何影响到所有物体的片段颜色。下一节中,我们会更深入的研究光照模型,看看我们还能做些什么。

总结

冯氏光照模型的结构主要由三个分量组成:环境光、漫反射、镜面反射。

  • 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
  • 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
  • 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。

它们三者的计算方式如下(GLSL):
在这里插入图片描述
上述片段着色器代码是在世界坐标下进行计算的。我们知道物体的世界坐标等于模型矩阵*物体的本地坐标,但是法向量不能直接使用该变换矩阵,而要使用法线矩阵进行变换。

最后简单提到了 G o u r a u d Gouraud Gouraud着色,它是在顶点着色器中实现的冯氏光照模型,由于顶点比片段少得多,所以效率会很高,但是结果很不真实。

猜你喜欢

转载自blog.csdn.net/xiji333/article/details/115048318