OpenGL學習筆記:法線矩陣和鏡面光照

法線矩陣

爲什麼需要法線矩陣

之前我們計算的座標都是世界座標,但法線向量卻是局部空間的,應該把法線向量也轉換到世界空間纔對。但把法線向量轉換到世界空間,不能簡單的乘以模型矩陣,模型矩陣中可能會有位移、縮放、旋轉等變換。
首先,法線向量只有方向意義,沒有位置意義,因此不能對法線進行位移操作。
其次,法線向量雖然不能位移,但可以跟隨平面做旋轉和縮放操作。如果是旋轉,法線向量直接跟隨旋轉即可,但如果是縮放,等比例縮放還算簡單,再標準化一次就行,非等比例縮放就麻煩了,因爲非等比例縮放會導致向量的各個分量縮放比例不同,從而影響向量的方向,導致法向量不再垂直於平面。
在這裏插入圖片描述
爲了解決這個問題,將法向量從局部空間向世界空間轉換時需要專門的轉換矩陣,我們稱之爲法線矩陣。

法線矩陣推導

根據法線性質可知,平面上任一向量都與法線向量垂直, 即它們的數量積爲0。
設平面上任一向量爲v\vec{v},法線向量爲n\vec{n},轉換矩陣爲MM,法線矩陣爲SS,轉換後的平面向量爲v\vec{v'},轉換後的法線向量爲n\vec{n'},則有
v=Mvn=Snnv=(nxnynz)(vxvyvz)=nxvx+nyvy+nzvz=(nxnynz)(vxvyvz)=(n)Tv=0 \vec{v'}=M\vec{v}\\ \vec{n'}=S\vec{n}\\ \vec{n}\cdot \vec{v}=\left( \begin{array}{ccc}n_x \\ n_y \\ n_z \end{array} \right)\cdot \left( \begin{array}{ccc}v_x \\ v_y \\ v_z \end{array} \right)=n_xv_x+n_yv_y+n_zv_z= \left( \begin{array}{ccc}n_x & n_y & n_z \end{array} \right)\cdot \left( \begin{array}{ccc}v_x \\ v_y \\ v_z \end{array} \right)=(\vec{n})^T\cdot \vec{v}=0\\
若要使轉換後的n\vec{n'}依然是v\vec{v'}的法向量,則有
nv=(n)Tv=(Sn)TMv=(n)TSTMv=(n)Tv=0 \vec{n'}\cdot\vec{v'}=(\vec{n'})^T\cdot \vec{v'}=(S\vec{n})^T\cdot M\vec{v} =(\vec{n})^TS^TM\vec{v}=(\vec{n})^T\cdot \vec{v}=0
若要等式成立,STMS^T\cdot M的結果必須是單位矩陣,根據逆矩陣的性質,STS^TMM必爲互逆矩陣,則
S=(ST)T=(M1)T S=(S^T)^T=(M^{-1})^T

使用法線矩陣

在頂點着色器中,我們可以使用inverse和transpose函數自己生成這個法線矩陣,這兩個函數對所有類型矩陣都有效。注意我們還要把被處理過的矩陣強制轉換爲3×3矩陣,來保證它失去了位移屬性以及能夠乘以vec3的法向量。

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

在漫反射光照部分,光照表現並沒有問題,這是因爲我們沒有對物體本身執行任何縮放操作,所以並不是必須要使用一個法線矩陣,僅僅讓模型矩陣乘以法線也可以。可是,如果你進行了不等比縮放,使用法線矩陣去乘以法向量就是必不可少的了。
即使是對於着色器來說,逆矩陣也是一個開銷比較大的運算,因此,只要可能就應該避免在着色器中進行逆矩陣運算,它們必須爲你場景中的每個頂點都進行這樣的處理。用作學習目這樣做是可以的,但是對於一個對效率有要求的應用來說,在繪製之前你最好用CPU計算出法線矩陣,然後通過uniform把值傳遞給着色器(像模型矩陣一樣)。

鏡面光照

前面的章節中已經把光照強度和光照角度對顏色的影響計算進去了,但對比現實世界還少點東西。在現實世界中,如果我們用一面鏡子去觀察一盞燈,隨着我們的移動,燈在鏡子中的位置也會跟隨移動,接下來我們就要把現實世界中的這個現象模擬到OpenGL中。由於這個現象和觀察者有關係,所以需要計算觀察向量和反射向量的角度差。
我們通過反射法向量周圍光的方向來計算反射向量。然後我們計算反射向量和視線方向的角度差,如果夾角越小,那麼鏡面光的影響就會越大。它的作用效果就是,當我們去看光被物體所反射的那個方向的時候,我們會看到一個高光。
在這裏插入圖片描述
觀察向量是鏡面光照附加的一個變量,可以用觀察者(攝像機)的世界空間位置與被觀察者的世界空間位置做差求得。

完整例子

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>
#include <windows.h>

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

// 定義攝像機的初始信息
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 lastFrame = 0.0f; 

// 上一次鼠標的位置,默認是屏幕中心
float lastX = 400;
float lastY = 300;

float yaw = -90.0f;
float pitch = 0.0f;
float fov = 45.0f;

bool firstMouse = true;

// 燈在世界座標的位置
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);

// 本例需要兩個着色器
const char *lightingVertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"layout(location = 1) in vec3 aNormal;\n"  // 添加法線向量
"out vec3 Normal;\n"
"out vec3 FragPos;\n"
"uniform mat4 model;\n"
"uniform mat4 view;\n"
"uniform mat4 projection; \n"
"void main()\n"
"{\n"
"   gl_Position = projection * view * model * vec4(aPos, 1.0);\n"
// 我們會在世界空間中進行所有的光照計算,因此我們需要一個在世界空間中的頂點位置。
// 我們可以通過把頂點位置屬性乘以模型矩陣(不是觀察和投影矩陣)來把它變換到世界空間座標。
"	FragPos = vec3(model * vec4(aPos, 1.0));\n"
// 生成法線矩陣,防止不等比縮放導致法線向量方向錯誤
// 注意我們還要把被處理過的矩陣強制轉換爲3×3矩陣,來保證它失去了位移屬性以及能夠乘以vec3的法向量。
// 即使是對於着色器來說,逆矩陣也是一個開銷比較大的運算,
// 因此,只要可能就應該避免在着色器中進行逆矩陣運算,它們必須爲你場景中的每個頂點都進行這樣的處理。
// 用作學習目這樣做是可以的,但是對於一個對效率有要求的應用來說,
// 在繪製之前你最好用CPU計算出法線矩陣,然後通過uniform把值傳遞給着色器(像模型矩陣一樣)
"	Normal = mat3(transpose(inverse(model))) * aNormal;\n"
"}\0";

// 這個是被光照射的物體的片段着色器,從uniform變量中接受物體的顏色和光源的顏色。
// 將光照的顏色和物體自身的顏色作分量相乘,結果就是最終要顯示出來的顏色向量
const char *lightingFragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"in vec3 Normal;\n"
"in vec3 FragPos;\n"
"uniform vec3 viewPos;\n"
"uniform vec3 objectColor;\n"
"uniform vec3 lightColor;\n"
"uniform vec3 lightPos;\n"
"void main()\n"
"{\n"
"	float ambientStrength = 0.1;\n"
"	vec3 ambient = ambientStrength * lightColor;\n"
// 標準化法線向量,向量標準化就是不關心長度只關心方向的時候使向量的模爲1
"	vec3 norm = normalize(Normal);\n"
// 標準化光照方向向量,光源位置減去片段位置就是光源指向片段的方向向量
// 這裏應該是FragPos-lightPos纔是光源指向片段的方向
// 雖然這裏只是計算夾角餘弦,不會對結果產生影響
// 但是後面在計算鏡面反射的時候還要對這個結果進行取反修正,不知道原教程爲毛這麼搞
"	vec3 lightDir = normalize(lightPos - FragPos);\n"
// 對norm和lightDir向量進行點乘,計算光源對當前片段實際的漫發射影響。
// 結果值再乘以光的顏色,得到漫反射分量。兩個向量之間的角度越大,漫反射分量就會越小
// 如果兩個向量之間的角度大於90度,點乘的結果就會變成負數,這樣會導致漫反射分量變爲負數。
// 爲此,我們使用max函數返回兩個參數之間較大的參數,從而保證漫反射分量不會變成負數。
// 負數顏色的光照是沒有定義的,所以最好避免它,除非你是那種古怪的藝術家。
"	float diff = max(dot(norm, lightDir), 0.0);\n"
"	vec3 diffuse = diff * lightColor;\n"

// 鏡面強度(Specular Intensity)變量,給鏡面高光一箇中等亮度顏色,讓它不要產生過度的影響
"	float specularStrength = 0.5;\n"
// 計算視線方向向量
// 需要注意的是我們對lightDir向量進行了取反。
// reflect函數要求第一個向量是從光源指向片段位置的向量,
// 但是lightDir當前正好相反,是從片段指向光源(由先前我們計算lightDir向量時,減法的順序決定)。
// 爲了保證我們得到正確的reflect向量,我們通過對lightDir向量取反來獲得相反的方向。
// 第二個參數要求是一個法向量,所以我們提供的是已標準化的norm向量。
"	vec3 viewDir = normalize(viewPos - FragPos);\n"
"	vec3 reflectDir = reflect(-lightDir, norm); \n"
// 計算鏡面分量
// 我們先計算視線方向與反射方向的點乘(並確保它不是負值),然後取它的32次冪。
// 這個32是高光的反光度(Shininess)。
// 一個物體的反光度越高,反射光的能力越強,散射得越少,高光點就會越小
"	float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);\n"
"	vec3 specular = specularStrength * spec * lightColor;\n"
// 它鏡面分量加到環境光分量和漫反射分量裏,再用結果乘以物體的顏色:
"	vec3 result = (ambient + diffuse + specular) * objectColor;\n"
"	FragColor = vec4(result, 1.0);\n"
"}\n\0";


// 當我們修改頂點或者片段着色器後,燈的位置或顏色也會隨之改變,這並不是我們想要的效果。
// 我們不希望燈的顏色在接下來的教程中因光照計算的結果而受到影響,而是希望它能夠與其它的計算分離。
// 我們希望燈一直保持明亮,不受其它顏色變化的影響(這樣它才更像是一個真實的光源)。
// 爲了實現這個目標,我們需要爲燈的繪製創建另外的一套着色器,
// 從而能保證它能夠在其它光照着色器發生改變的時候不受影響。
// 頂點着色器與我們當前的頂點着色器是一樣的,所以你可以直接把現在的頂點着色器用在燈上。
// 燈的片段着色器給燈定義了一個不變的常量白色,保證了燈的顏色一直是亮的:
const char *lampVertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"uniform mat4 model;\n"
"uniform mat4 view;\n"
"uniform mat4 projection; \n"
"void main()\n"
"{\n"
"   gl_Position = projection * view * model * vec4(aPos, 1.0);\n"
"}\0";

const char *lampFragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
"	FragColor = vec4(1.0);\n"
"}\n\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_SPACE) == GLFW_PRESS)
		glfwSetWindowShouldClose(window, true);

	// 由於前後平移方向上並沒有改變,直接在觀察方向上進行移動,所以直接加減就可以了
	// 但是左右平移需要在左右向量上進行加減,因此需要利用叉乘計算出右向量
	// glm::normalize是對右向量的標準化
	// 如果我們沒對這個向量進行標準化,最後的叉乘結果會根據cameraFront變量返回大小不同的向量。
	// 如果我們不對向量進行標準化,我們就得根據攝像機的朝向不同加速或減速移動了,
	// 但如果進行了標準化移動就是勻速的。
	float cameraSpeed = 2.5f * deltaTime; // 相應調整
	if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
		cameraPos += cameraSpeed * cameraFront;
	if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
		cameraPos -= cameraSpeed * cameraFront;
	if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
		cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
	if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
		cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}

void cursor_position_callback(GLFWwindow* window, double x, double y)
{
	// 防止第一次進入時圖像跳動
	if (firstMouse)
	{
		lastX = x;
		lastY = y;
		firstMouse = false;
	}

	float xoffset = x - lastX;
	float yoffset = lastY - y; // 注意這裏是相反的,因爲y座標是從底部往頂部依次增大的
	lastX = x;
	lastY = y;

	// 判斷右鍵是否按下,如果不判斷右鍵按下,每次移動鼠標都會轉動視角
	// 但計算偏移量必須在if外面,否則右鍵沒有按下時鼠標移動不會更新last座標導致下次右鍵圖像跳動
	if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS)
	{
		float sensitivity = 0.5f;
		xoffset *= sensitivity;
		yoffset *= sensitivity;

		yaw += xoffset;
		pitch += yoffset;

		if (pitch > 89.0f)
			pitch = 89.0f;
		if (pitch < -89.0f)
			pitch = -89.0f;

		// 數學太渣,這裏是真心看不懂
		glm::vec3 front;
		front.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
		front.y = sin(glm::radians(pitch));
		front.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
		cameraFront = glm::normalize(front);
	}
}

void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
	if (fov >= 1.0f && fov <= 45.0f)
		fov -= yoffset;
	if (fov <= 1.0f)
		fov = 1.0f;
	if (fov >= 45.0f)
		fov = 45.0f;
}


int main(int argc, char **argv)
{
	// 初始化,配置版本號,配置核心模式
	glfwInit();
	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, "mytest", NULL, NULL);
	if (!window)
	{
		std::cout << "Create Window Error!\n";
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	// 註冊窗口大小變化的回調函數
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
	glfwSetCursorPosCallback(window, cursor_position_callback);
	glfwSetScrollCallback(window, scroll_callback);
	// 讓鼠標消失
	//	glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

	// 初始化glad
	// 我們給GLAD傳入了用來加載系統相關的OpenGL函數指針地址的函數。
	// GLFW給我們的是glfwGetProcAddress,它根據我們編譯的系統定義了正確的函數。
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		glfwTerminate();
		glfwDestroyWindow(window);
		return -1;
	}

	// 分別創建物體着色器和光源着色器
	int success;
	char infoLog[512] = { 0 };

	unsigned int lightingVertexShader;
	lightingVertexShader = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(lightingVertexShader, 1, &lightingVertexShaderSource, NULL);
	glCompileShader(lightingVertexShader);

	glGetShaderiv(lightingVertexShader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(lightingVertexShader, sizeof(infoLog), NULL, infoLog);
		std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
	}

	int lightingFragmentShader;
	lightingFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(lightingFragmentShader, 1, &lightingFragmentShaderSource, NULL);
	glCompileShader(lightingFragmentShader);

	glGetShaderiv(lightingFragmentShader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		memset(infoLog, 0, sizeof(infoLog));
		glGetShaderInfoLog(lightingFragmentShader, sizeof(infoLog), NULL, infoLog);
		std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
	}

	unsigned int lightingShader;
	lightingShader = glCreateProgram();

	glAttachShader(lightingShader, lightingVertexShader);
	glAttachShader(lightingShader, lightingFragmentShader);

	glLinkProgram(lightingShader);

	glGetProgramiv(lightingShader, GL_LINK_STATUS, &success);
	if (!success)
	{
		memset(infoLog, 0, sizeof(infoLog));
		glGetProgramInfoLog(lightingShader, sizeof(infoLog), NULL, infoLog);
		std::cout << "ERROR::SHADER::PROGRAM::LINK_FAILED\n" << infoLog << std::endl;
	}

	glUseProgram(lightingShader);
	glDeleteShader(lightingVertexShader);
	glDeleteShader(lightingFragmentShader);

	unsigned int lampVertexShader;
	lampVertexShader = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(lampVertexShader, 1, &lampVertexShaderSource, NULL);
	glCompileShader(lampVertexShader);

	glGetShaderiv(lampVertexShader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		memset(infoLog, 0, sizeof(infoLog));
		glGetShaderInfoLog(lampVertexShader, sizeof(infoLog), NULL, infoLog);
		std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
	}

	int lampFragmentShader;
	lampFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(lampFragmentShader, 1, &lampFragmentShaderSource, NULL);
	glCompileShader(lampFragmentShader);

	glGetShaderiv(lampFragmentShader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		memset(infoLog, 0, sizeof(infoLog));
		glGetShaderInfoLog(lampFragmentShader, sizeof(infoLog), NULL, infoLog);
		std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
	}

	unsigned int lampShader;
	lampShader = glCreateProgram();

	glAttachShader(lampShader, lampVertexShader);
	glAttachShader(lampShader, lampFragmentShader);
	glLinkProgram(lampShader);

	glGetProgramiv(lampShader, GL_LINK_STATUS, &success);
	if (!success)
	{
		memset(infoLog, 0, sizeof(infoLog));
		glGetProgramInfoLog(lampShader, sizeof(infoLog), NULL, infoLog);
		std::cout << "ERROR::SHADER::PROGRAM::LINK_FAILED\n" << infoLog << std::endl;
	}

	glUseProgram(lampShader);
	glDeleteShader(lampVertexShader);
	glDeleteShader(lampFragmentShader);

	// 啓用深度測試
	glEnable(GL_DEPTH_TEST);


	// 創建立方體
	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
	};

	unsigned int VBO;
	unsigned int cubeVAO, lightVAO;

	glGenBuffers(1, &VBO);

	// 創建物體立方體的頂點數據
	glGenVertexArrays(1, &cubeVAO);
	glBindVertexArray(cubeVAO);
	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);

	// 創建光源的頂點數據
	glGenVertexArrays(1, &lightVAO);
	glBindVertexArray(lightVAO);
	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的調用將VBO註冊爲頂點屬性的綁定頂點緩衝區對象,
	// 因此我們可以安全地解除綁定
	glBindBuffer(GL_ARRAY_BUFFER, 0);

	// 您可以在之後取消綁定VAO,以便其他VAO調用不會意外地修改此VAO,但這種情況很少發生。
	// 修改其他VAO需要調用glBindVertexArray,因此我們通常不會在不直接需要時取消綁定VAO(也不是VBO)。
	glBindVertexArray(0);

	// 創建渲染循環
	while (!glfwWindowShouldClose(window))
	{
		float currentFrame = glfwGetTime();
		deltaTime = currentFrame - lastFrame;
		lastFrame = currentFrame;

		// 處理輸入事件
		processInput(window);

		// 清空背景顏色,這次設置爲黑色背景
		glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
		// 由於我們使用了深度測試,所以需要再與上一個GL_DEPTH_BUFFER_BIT清楚深度緩衝
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		// 繪製物體
		glUseProgram(lightingShader);
		// 我們把物體的顏色設置爲珊瑚紅色,並把光源設置爲白色。
		glUniform3fv(glGetUniformLocation(lightingShader, "objectColor"), 1, 
			glm::value_ptr(glm::vec3(1.0f, 0.5f, 0.31f)));
		glUniform3fv(glGetUniformLocation(lightingShader, "lightColor"), 1, 
			glm::value_ptr(glm::vec3(1.0f, 1.0f, 1.0f)));
		glUniform3fv(glGetUniformLocation(lightingShader, "lightPos"), 1,
			glm::value_ptr(lightPos));
		glUniform3fv(glGetUniformLocation(lightingShader, "viewPos"), 1,
			glm::value_ptr(cameraPos));

		glm::mat4 projection(1.0f);
		projection = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);
			
		glm::mat4 view(1.0f);
		view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);

		glUniformMatrix4fv(glGetUniformLocation(lightingShader, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
		glUniformMatrix4fv(glGetUniformLocation(lightingShader, "view"), 1, GL_FALSE, glm::value_ptr(view));

		glm::mat4 model = glm::mat4(1.0f);
		glUniformMatrix4fv(glGetUniformLocation(lightingShader, "model"), 1, GL_FALSE, glm::value_ptr(model));

		glBindVertexArray(cubeVAO);
		glDrawArrays(GL_TRIANGLES, 0, 36);



		// 繪製光源
		// 當我們想要繪製我們的物體的時候,我們需要使用剛剛定義的光照着色器來繪製箱子(或者可能是其它的物體)。
		// 當我們想要繪製燈的時候,我們會使用燈的着色器。
		// 在之後的教程裏我們會逐步更新這個光照着色器,從而能夠慢慢地實現更真實的效果。
		// 使用這個燈立方體的主要目的是爲了讓我們知道光源在場景中的具體位置。
		// 我們通常在場景中定義一個光源的位置,但這只是一個位置,它並沒有視覺意義。
		// 爲了顯示真正的燈,我們將表示光源的立方體繪製在與光源相同的位置。
		// 我們將使用我們爲它新建的片段着色器來繪製它,讓它一直處於白色的狀態,不受場景中的光照影響。
		glUseProgram(lampShader);
		glUniformMatrix4fv(glGetUniformLocation(lampShader, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
		glUniformMatrix4fv(glGetUniformLocation(lampShader, "view"), 1, GL_FALSE, glm::value_ptr(view));

		// 然後我們把燈位移到這裏,然後將它縮小一點,讓它不那麼明顯
		model = glm::mat4(1.0f);
		model = glm::translate(model, lightPos);
		model = glm::scale(model, glm::vec3(0.2f)); // a smaller cube
		glUniformMatrix4fv(glGetUniformLocation(lampShader, "model"), 1, GL_FALSE, glm::value_ptr(model));

		glBindVertexArray(lightVAO);
		glDrawArrays(GL_TRIANGLES, 0, 36);


		glfwPollEvents();
		glfwSwapBuffers(window);
		Sleep(1);
	}

	glDeleteVertexArrays(1, &cubeVAO);
	glDeleteVertexArrays(1, &lightVAO);
	glDeleteBuffers(1, &VBO);

	glfwTerminate();
	glfwDestroyWindow(window);

	return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章