OpenGL入門1.7:攝像機


每一個小步驟的源碼都放在了Github

的內容為插入注釋,可以先跳過

前言

我們已經知道了何為觀察矩陣以及如何使用觀察矩陣移動場景(我們向后移動了一點)

OpenGL本身沒有攝像機(Camera)的概念,但我們可以通過把場景中的所有物體往相反方向移動的方式來模擬出攝像機的移動,產生一種 我們在移動,而不是場景在移動 的感覺

我們將會討論如何在OpenGL中配置一個攝像機,並且將會討論FPS風格的攝像機,讓你能夠在3D場景中自由移動。我們也會討論鍵盤和鼠標輸入,最終完成一個自定義的攝像機類

攝像機

攝像機/觀察空間

當我們討論攝像機/觀察空間(Camera/View Space)的時候,是在討論以攝像機的視角作為場景原點時場景中所有的頂點坐標:觀察矩陣把所有的世界坐標變換為相對於攝像機位置與方向的觀察坐標

要定義一個攝像機,我們需要它在世界空間中的位置觀察的方向一個指向它右測的向量以及一個指向它上方的向量,我們實際上創建了一個三個單位軸相互垂直的、以攝像機的位置為原點的坐標系

1. 攝像機位置

我們首先需要獲取攝像機位置

攝像機位置簡單來說就是世界空間中一個指向攝像機位置的向量

glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);

不要忘記正z軸是從屏幕指向你的,如果我們希望攝像機向后移動,我們就沿着z軸的正方向移動

2. 攝像機方向

下一個需要的向量是攝像機的方向,這里指的是攝像機指向哪個方向

現在我們想讓攝像機指向場景原點:(0, 0, 0)

我們知道用 場景原點向量 減去 攝像機位置向量 的結果就是攝像機的指向向量(方向就是攝像機位置指原點)
這時候攝像機指向z軸負方向,但我們希望方向向量(Direction Vector)指向攝像機的z軸正方向
如果我們交換相減的順序(攝像機位置向量 - 場景原點向量)我們就會獲得一個指向攝像機正z軸方向的向量,就如上圖

glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);

PS: 方向向量(Direction Vector)並不是最好的名字,因為它實際上指向從它到目標向量的相反方向(藍色的方向向量大概指向z軸的正方向,與攝像機實際指向的方向是正好相反的)

3. 右軸

我們需要的另一個向量是一個右向量(Right Vector),它代表攝像機空間的x軸的正方向

為獲取右向量我們需要先使用一個小技巧:先定義一個上向量(Up Vector)=(0.0f, 1.0f, 0.0f),接下來把上向量和第二步得到的攝像機方向向量進行叉乘(兩個向量叉乘的結果會同時垂直於兩向量),我們會得到指向x軸正方向的那個向量(如果我們交換兩個向量叉乘的順序就會得到相反的指向x軸負方向的向量)

glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); 
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));

4. 上軸

最后需要一個指向攝像機的正y軸向量

現在我們已經有了x軸向量和z軸向量,我們把右向量和方向向量進行叉乘:

glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);

OK,在叉乘和一些小技巧的幫助下,我們創建了所有構成觀察/攝像機空間的向量,使用這些攝像機向量我們就可以創建一個LookAt矩陣了,它在創建攝像機的時候非常有用。

Look At 矩陣

使用矩陣的好處之一是如果你使用3個相互垂直(或非線性)的軸定義了一個坐標空間,你可以用這3個軸外加一個平移向量來創建一個矩陣,並且你可以用這個矩陣乘以任何向量來將其變換到那個坐標空間。這正是LookAt矩陣所做的,現在我們有了3個相互垂直的軸和一個定義攝像機空間的位置坐標,我們可以創建我們自己的LookAt矩陣了:

\[LookAt=\begin{vmatrix} R_{x}& R_{y} & R_{z} & 0\\ U_{x}& U_{y} & U_{z} & 0\\ D_{x}& D_{y} & D_{z} & 0\\ 0 & 0 & 0 & 1 \end{vmatrix}\cdot \begin{vmatrix} 1 & 0 & 0 & -P_{x}\\ 0 & 1 & 0 & -P_{y}\\ 0 & 0 & 1 & -P_{z}\\ 0 & 0 & 0 & 1 \end{vmatrix} \]

R是右向量,U是上向量,D是方向向量,P是攝像機位置向量(位置向量是相反的,因為我們最終希望把世界平移到與我們自身移動的相反方向)

把這個LookAt矩陣作為觀察矩陣可以很高效地把所有世界坐標變換到剛剛定義的觀察空間,LookAt矩陣就像它的名字表達的那樣:它會創建一個看着(Look at)給定目標的觀察矩陣

幸運的是,GLM已經提供了這些支持,我們要做的只是定義一個攝像機位置一個目標位置一個表示世界空間中的上向量的向量(我們計算右向量使用的那個上向量),GLM就會創建一個LookAt矩陣,我們可以把它當作我們的觀察矩陣:

glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), 
           		   glm::vec3(0.0f, 0.0f, 0.0f), 
          	 	   glm::vec3(0.0f, 1.0f, 0.0f));

這里我們通過上述代碼創建一個和之前效果相同的觀察者矩陣

我們接下來先來嘗試把我們的攝像機在場景中旋轉,同時攝像機的注視點保持在(0, 0, 0)

我們需要用到一點三角學的知識來在每一幀創建一個x和z坐標,它會代表圓上的一點,我們將會使用它作為攝像機的位置,通過重新計算x和y坐標,我們會遍歷圓上的所有點,這樣攝像機就會繞着場景旋轉了

我們預先定義這個圓的半徑radius,在每次渲染迭代中使用GLFW的glfwGetTime函數重新創建觀察矩陣,來擴大這個圓

glm::mat4 view = glm::mat4(1.0f);
float radius = 10.0f;
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
view = glm::lookAt(
    glm::vec3(camX, 0.0f, camZ), 
    glm::vec3(0.0f, 0.0f, 0.0f), 
    glm::vec3(0.0f, 1.0f, 0.0f));
ourShader.setMat4("view", view);

運行:

2JxlHeGIjl

awesome!

參考代碼

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

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

#include "shader.h"

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

// callback
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);

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

int main()
{

	#pragma region 窗口
	// 實例化GLFW窗口
	glfwInit();//glfw初始化
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本號
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本號
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

	GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
	//(寬,高,窗口名)返回一個GLFWwindow類的實例:window
	if (window == NULL)
	{
		// 生成錯誤則輸出錯誤信息
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);

	// 告訴GLFW我們希望每當窗口調整大小的時候調用改變窗口大小的函數
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
	#pragma endregion GLFW

	#pragma region 函數指針
	// glad管理opengl函數指針,初始化glad
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		// 生成錯誤則輸出錯誤信息
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}
	#pragma endregion GLAD

	Shader ourShader("1.7.0vertex.txt", "1.7.0fragment.txt");

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

	#pragma region 頂點數據
	//頂點數據
	float vertices[] = {
	//   ---- 位置 ----		  - 紋理坐標 -
		-0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
		 0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
		 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
		 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
		-0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
		-0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

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

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

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

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

		-0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
		 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
		 0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
		 0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
		-0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
		-0.5f,  0.5f, -0.5f,  0.0f, 1.0f
	};
	glm::vec3 cubePositions[] = {
		glm::vec3(0.0f,  0.0f,  0.0f),
		glm::vec3(2.0f,  5.0f, -15.0f),
		glm::vec3(-1.5f, -2.2f, -2.5f),
		glm::vec3(-3.8f, -2.0f, -12.3f),
		glm::vec3(2.4f, -0.4f, -3.5f),
		glm::vec3(-1.7f,  3.0f, -7.5f),
		glm::vec3(1.3f, -2.0f, -2.5f),
		glm::vec3(1.5f,  2.0f, -2.5f),
		glm::vec3(1.5f,  0.2f, -1.5f),
		glm::vec3(-1.3f,  1.0f, -1.5f)
	};
	#pragma endregion vertices[],indices[]

	#pragma region 緩存對象
	// 初始化緩存對象
	unsigned int VBO;
	glGenBuffers(1, &VBO);
	unsigned int VAO;
	glGenVertexArrays(1, &VAO);
	//unsigned int EBO;
	//glGenBuffers(1, &EBO);
	// 1. 綁定頂點數組對象
	glBindVertexArray(VAO);
	// 2. 把我們的頂點數組復制到一個頂點緩沖中,供OpenGL使用
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	//// 3. 復制我們的索引數組到一個索引緩沖中,供OpenGL使用
	//glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	//glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
	// 4. 設定頂點屬性指針
	// 位置屬性
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	// 紋理坐標屬性
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
	glEnableVertexAttribArray(1);
	#pragma endregion VAO,VBO,EBO

	#pragma region 材質
	// 加載材質
	unsigned int texture1;//紋理也是使用ID引用的
	glGenTextures(1, &texture1);//glGenTextures先輸入要生成紋理的數量,然后把它們儲存在第二個參數的`unsigned int`數組中
	glBindTexture(GL_TEXTURE_2D, texture1);
	// 為當前綁定的紋理對象設置環繞、過濾方式
	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_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	// 加載並生成紋理
	int width, height, nrChannels;
	unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
	if (data)
	{
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
		glGenerateMipmap(GL_TEXTURE_2D);
	}
	else
	{
		std::cout << "Failed to load texture" << std::endl;
	}
	stbi_image_free(data);
	#pragma endregion 加載材質

	#pragma region 渲染

	ourShader.use();
	ourShader.setInt("texture1", 0);

	// 投影矩陣不需要梅幀更新
	glm::mat4 projection = glm::mat4(1.0f);
	projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
	ourShader.setMat4("projection", projection);

	// 渲染循環
	while (!glfwWindowShouldClose(window))
	{
		// 輸入
		processInput(window);

		// 渲染指令
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		// 綁定材質
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, texture1);
		// 創建矩陣
		//glm::mat4 model = glm::mat4(1.0f);
		//model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));
		glm::mat4 view = glm::mat4(1.0f);
		float radius = 10.0f;
		float camX = sin(glfwGetTime()) * radius;
		float camZ = cos(glfwGetTime()) * radius;
		view = glm::lookAt(glm::vec3(camX, 0.0f, camZ), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
		ourShader.setMat4("view", view);

		// 將矩陣傳入着色器
		//ourShader.setMat4("model", model);
		// 渲染箱子
		glBindVertexArray(VAO);
		//glDrawArrays(GL_TRIANGLES, 0, 36);
		for (unsigned int i = 0; i < 10; i++)
		{
			glm::mat4 model = glm::mat4(1.0f);
			model = glm::translate(model, cubePositions[i]);
			float angle = 20.0f * i;
			if (i % 3 == 0)  // every 3rd iteration (including the first) we set the angle using GLFW's time function.
				angle = glfwGetTime() * 25.0f;
			model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
			ourShader.setMat4("model", model);

			glDrawArrays(GL_TRIANGLES, 0, 36);
		}
		// 檢查並調用事件,交換緩沖
		glfwSwapBuffers(window);

		// 檢查觸發什么事件,更新窗口狀態
		glfwPollEvents();
	}

	glDeleteVertexArrays(1, &VAO);
	glDeleteBuffers(1, &VBO);
	//glDeleteBuffers(1, &EBO);

	// 釋放之前的分配的所有資源
	glfwTerminate();
	#pragma endregion Rendering
	
	return 0;
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	// 每當窗口改變大小,GLFW會調用這個函數並填充相應的參數供你處理
	glViewport(0, 0, width, height);
}

void processInput(GLFWwindow* window)
{
	// 返回這個按鍵是否正在被按下
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//是否按下了返回鍵
		glfwSetWindowShouldClose(window, true);
}

鍵盤輸入

我們可以嘗試自己移動攝像了,就像在游戲里一樣,首先我們必須設置一個攝像機系統,所以在我們的程序前面定義一些攝像機相關變量:

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

LookAt函數現在成了:

view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);

我們首先將攝像機位置設置為之前定義的cameraPos,方向是當前的位置加上我們剛剛定義的方向向量。這樣能保證無論我們怎么移動,攝像機都會注視着目標方向

現在讓我們擺弄一下這些向量,在按下某些按鈕時更新cameraPos向量。

我們已經為GLFW的鍵盤輸入定義過一個processInput函數了,我們來新添加幾個需要檢查的按鍵命令:

void processInput(GLFWwindow *window)
{
    ...
    float cameraSpeed = 0.05f; // adjust accordingly
    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;
}

當我們按下WASD鍵的任意一個,攝像機的位置都會相應更新:

  • 如果我們希望向前或向后移動,我們就把位置向量加上或減去方向向量
  • 如果我們希望向左右移動,我們使用叉乘來創建一個右向量(Right Vector),並沿着移動

注意,我們對右向量進行了標准化(normalize),如果我們沒對這個向量進行標准化,最后的叉乘結果會根據cameraFront變量返回大小不同的向量(那么就得根據攝像機的朝向不同加速或減速移動了,但如果進行了標准化移動就是勻速的)

現在你就應該能夠移動攝像機了,但其實這個程序在不同的機器上運行可能鏡頭移動速度都不太一樣,為什么呢?

移動速度

在實際情況下根據處理器的能力不同,有些人可能會比其他人每秒繪制更多幀(以更高的頻率調用processInput函數),結果就是,根據配置的不同,有些人可能移動很快,而有些人會移動很慢,解決方法是用幀時間差來平衡這個量

圖形程序和游戲通常會跟蹤一個時間差(Deltatime)變量,它儲存了渲染上一幀所用的時間,我們把所有速度都去乘以deltaTime值:結果就是,如果我們的deltaTime很大,就意味着上一幀的渲染花費了更多時間,所以這一幀的速度需要變得更高來平衡渲染所花去的時間
使用這種方法時,無論你的電腦快還是慢,攝像機的速度都會相應平衡,這樣每個用戶的體驗就都一樣了

我們跟蹤兩個全局變量來計算出deltaTime值:

float deltaTime = 0.0f; // 當前幀與上一幀的時間差
float lastFrame = 0.0f; // 上一幀的時間

在每一幀中我們計算出新的deltaTime以備后用,那就寫在渲染循環內最上端:

float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;

現在我們有了deltaTime,在計算速度的時候可以將其考慮進去了:

void processInput(GLFWwindow *window)
{
  float cameraSpeed = 2.5f * deltaTime;
  ...
}

與前面的部分結合在一起,現在你可以前后左右平滑移動

現在還不能轉向,移動很受限制,那是時候加入鼠標了

鼠標輸入

為了能夠改變視角,我們需要根據鼠標的輸入改變cameraFront向量來改變攝像機的指向方向,在此之前我們先要了解一些三角學知識

歐拉角

歐拉角(Euler Angle)是可以表示3D空間中任何旋轉的3個值,由萊昂哈德·歐拉(Leonhard Euler)在18世紀提出
一共有3種歐拉角:俯仰角(Pitch)偏航角(Yaw)滾轉角(Roll),下面的圖片展示了它們的含義:

img

每個歐拉角都有一個值來表示,把三個角結合起來我們就能夠計算3D空間中任何的旋轉向量了

對於我們的攝像機系統來說,我們只關心俯仰角和偏航角,所以我們不會討論滾轉角

給定一個俯仰角和偏航角,我們可以把它們轉換為一個代表新的方向向量的3D向量。俯仰角和偏航角轉換為方向向量的處理需要一些三角學知識,我們先從最基本的情況開始:

img

把斜邊邊長定義為1
鄰邊的長度是cos(x/h) = cos(x/1) = cos x
對邊是長度是sin(y/h) = sin(y/1) = sin y

這樣我們獲得了能夠得到x和y方向長度的通用公式,它們取決於所給的角度,我們使用它來計算方向向量的分量:

img

這個三角形看起來和前面的三角形很像,所以如果我們想象自己在xz平面上,看向y軸,我們可以基於第一個三角形計算來計算它的長度/y方向的強度(Strength)(我們往上或往下看多少)。從圖中我們可以看到對於一個給定俯仰角的y值等於sin θ:

direction.y = sin(glm::radians(pitch)); // 注意我們先把角度轉為弧度

這里我們只更新了y值,仔細觀察x和z分量也被影響了,從三角形中我們可以看到它們的值等於:

direction.x = cos(glm::radians(pitch));
direction.z = cos(glm::radians(pitch));

看看我們是否能夠為偏航角找到需要的分量:

img

就像俯仰角的三角形一樣,我們可以看到x分量取決於cos(yaw)的值,z值同樣取決於偏航角的正弦值。把這個加到前面的值中,會得到基於俯仰角和偏航角的方向向量:

// direction代表攝像機的前軸(Front),這個前軸是和本文第二幅圖片的攝像機的方向向量是相反的
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));

這樣我們就有了一個可以把俯仰角和偏航角轉化為用來自由旋轉視角的攝像機的3維方向向量了,你可能會奇怪:我們怎么得到俯仰角和偏航角?

鼠標輸入

偏航角和俯仰角是通過鼠標(或手柄)移動獲得的,水平的移動影響偏航角,豎直的移動影響俯仰角

它的原理就是,儲存上一幀鼠標的位置,在當前幀中我們當前計算鼠標位置與上一幀的位置相差多少。如果水平/豎直差別越大那么俯仰角或偏航角就改變越大,也就是攝像機需要移動更多的距離。

首先我們要告訴GLFW,它應該隱藏光標,並捕捉(Capture)它,捕捉光標表示的是,如果焦點在你的程序上(即表示你正在操作這個程序,Windows中擁有焦點的程序標題欄通常是有顏色的那個,而失去焦點的程序標題欄則是灰色的),光標應該停留在窗口中(除非程序失去焦點或者退出)

我們可以用一個簡單地配置調用來完成:

glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

在調用這個函數之后,無論我們怎么去移動鼠標,光標都不會顯示了,它也不會離開窗口,對於FPS攝像機系統來說非常完美

為了計算俯仰角和偏航角,我們需要讓GLFW監聽鼠標移動事件(和鍵盤輸入相似)

我們會用一個回調函數來完成,函數的原型如下:

void mouse_callback(GLFWwindow* window, double xpos, double ypos);

這里的xpos和ypos代表當前鼠標的位置,當我們用GLFW注冊了回調函數之后,鼠標一移動mouse_callback函數就會被調用:

glfwSetCursorPosCallback(window, mouse_callback);

在處理FPS風格攝像機的鼠標輸入的時候,我們必須在最終獲取方向向量之前做下面這幾步:

  1. 計算鼠標距上一幀的偏移量
  2. 把偏移量添加到攝像機的俯仰角和偏航角中
  3. 對偏航角和俯仰角進行最大和最小值的限制
  4. 計算方向向量

第一步是計算鼠標自上一幀的偏移量,所以我們必須先在程序中儲存上一幀的鼠標位置,我們把它的初始值設置為屏幕的中心(屏幕的尺寸是800x600):

float lastX = 400, lastY = 300;

然后在鼠標的回調函數中我們計算當前幀和上一幀鼠標位置的偏移量:

float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // 注意這里是相反的,因為y坐標是從底部往頂部依次增大的
lastX = xpos;
lastY = ypos;

float sensitivity = 0.05f;
xoffset *= sensitivity;
yoffset *= sensitivity;

注意我們把偏移量乘以了sensitivity(靈敏度)值,如果我們忽略這個值,鼠標移動就會太大了;你可以自己實驗一下,找到適合自己的靈敏度值

接下來我們把偏移量加到全局變量pitch和yaw上:

yaw   += xoffset;
pitch += yoffset;

第三步,我們需要給攝像機添加一些限制,這樣攝像機就不會發生奇怪的移動了(這樣也會避免一些奇怪的問題)

對於俯仰角,要讓用戶不能看向高於89度的地方(在90度時視角會發生逆轉,所以我們把89度作為極限,當然同樣也不允許小於-89度)
這樣能夠保證用戶只能看到天空或腳下,但是不能超越這個限制,我們可以在值超過限制的時候將其改為極限值來實現:

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

計算出來的方向向量就會包含根據鼠標移動計算出來的所有旋轉了。由於cameraFront向量已經包含在GLM的lookAt函數中,我們這就沒什么問題了。


如果你現在運行代碼,你會發現在窗口第一次獲取焦點的時候攝像機會突然跳一下

這個問題產生的原因是,在你的鼠標移動進窗口的那一刻,鼠標回調函數就會被調用,這時候的xpos和ypos會等於鼠標剛剛進入屏幕的那個位置。這通常是一個距離屏幕中心很遠的地方,因而產生一個很大的偏移量,所以就會跳了

我們可以簡單的使用一個bool變量檢驗我們是否是第一次獲取鼠標輸入,如果是,那么我們先把鼠標的初始位置更新為xpos和ypos值,這樣就能解決這個問題;接下來的鼠標移動就會使用剛進入的鼠標位置坐標來計算偏移量了:

if(firstMouse) // 這個bool變量初始時是設定為true的
{
    lastX = xpos;
    lastY = ypos;
    firstMouse = false;
}

最后的代碼應該是這樣的:

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

    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos; 
    lastX = xpos;
    lastY = ypos;

    float sensitivity = 0.05;
    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(yaw)) * cos(glm::radians(pitch));
    front.y = sin(glm::radians(pitch));
    front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
    cameraFront = glm::normalize(front);
}

現在我們就可以自由地在3D場景中移動了!

縮放

作為我們攝像機系統的一個附加內容,我們還會來實現一個縮放(Zoom)接口

在之前的教程中我們說視野(Field of View)或fov定義了我們可以看到場景中多大的范圍,當視野變小時,場景投影出來的空間就會減小,產生放大(Zoom In)了的感覺

我們這里使用鼠標的滾輪來放大,與鼠標移動、鍵盤輸入一樣,我們需要一個鼠標滾輪的回調函數:

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

當滾動鼠標滾輪的時候,yoffset值代表我們豎直滾動的大小。當scroll_callback函數被調用后,我們改變全局變量fov變量的內容。因為45.0f是默認的視野值,我們將會把縮放級別(Zoom Level)限制在1.0f45.0f

我們現在在每一幀都必須把透視投影矩陣上傳到GPU,但現在使用fov變量作為它的視野:

projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);

最后不要忘記注冊鼠標滾輪的回調函數:

glfwSetScrollCallback(window, scroll_callback);

現在,我們就實現了一個簡單的攝像機系統了,它能夠讓我們在3D環境中自由移動

參考源碼:

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

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

#include "shader.h"

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

// callback
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_callback(GLFWwindow* window, double xoffset, double yoffset);

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

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

bool firstMouse = true;
float yaw = -90.0f;
float pitch = 0.0f;
float lastX = 800.0f / 2.0;
float lastY = 600.0 / 2.0;
float fov = 45.0f;

// timing
float deltaTime = 0.0f; // 當前幀與上一幀的時間差
float lastFrame = 0.0f; // 上一幀的時間

int main()
{

	#pragma region 窗口
	// 實例化GLFW窗口
	glfwInit();//glfw初始化
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本號
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本號
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

	GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
	//(寬,高,窗口名)返回一個GLFWwindow類的實例:window
	if (window == NULL)
	{
		// 生成錯誤則輸出錯誤信息
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
	glfwSetCursorPosCallback(window, mouse_callback);
	glfwSetScrollCallback(window, scroll_callback);

	// 告訴GLFW我們希望每當窗口調整大小的時候調用改變窗口大小的函數
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
	#pragma endregion GLFW

	#pragma region 函數指針
	// glad管理opengl函數指針,初始化glad
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		// 生成錯誤則輸出錯誤信息
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}
	#pragma endregion GLAD

	Shader ourShader("1.7.0vertex.txt", "1.7.0fragment.txt");

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

	#pragma region 頂點數據
	//頂點數據
	float vertices[] = {
	//   ---- 位置 ----		  - 紋理坐標 -
		-0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
		 0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
		 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
		 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
		-0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
		-0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

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

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

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

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

		-0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
		 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
		 0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
		 0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
		-0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
		-0.5f,  0.5f, -0.5f,  0.0f, 1.0f
	};
	glm::vec3 cubePositions[] = {
		glm::vec3(0.0f,  0.0f,  0.0f),
		glm::vec3(2.0f,  5.0f, -15.0f),
		glm::vec3(-1.5f, -2.2f, -2.5f),
		glm::vec3(-3.8f, -2.0f, -12.3f),
		glm::vec3(2.4f, -0.4f, -3.5f),
		glm::vec3(-1.7f,  3.0f, -7.5f),
		glm::vec3(1.3f, -2.0f, -2.5f),
		glm::vec3(1.5f,  2.0f, -2.5f),
		glm::vec3(1.5f,  0.2f, -1.5f),
		glm::vec3(-1.3f,  1.0f, -1.5f)
	};
	#pragma endregion vertices[],indices[]

	#pragma region 緩存對象
	// 初始化緩存對象
	unsigned int VBO;
	glGenBuffers(1, &VBO);
	unsigned int VAO;
	glGenVertexArrays(1, &VAO);
	//unsigned int EBO;
	//glGenBuffers(1, &EBO);
	// 1. 綁定頂點數組對象
	glBindVertexArray(VAO);
	// 2. 把我們的頂點數組復制到一個頂點緩沖中,供OpenGL使用
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	//// 3. 復制我們的索引數組到一個索引緩沖中,供OpenGL使用
	//glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	//glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
	// 4. 設定頂點屬性指針
	// 位置屬性
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	// 紋理坐標屬性
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
	glEnableVertexAttribArray(1);
	#pragma endregion VAO,VBO,EBO

	#pragma region 材質
	// 加載材質
	unsigned int texture1;//紋理也是使用ID引用的
	glGenTextures(1, &texture1);//glGenTextures先輸入要生成紋理的數量,然后把它們儲存在第二個參數的`unsigned int`數組中
	glBindTexture(GL_TEXTURE_2D, texture1);
	// 為當前綁定的紋理對象設置環繞、過濾方式
	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_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	// 加載並生成紋理
	int width, height, nrChannels;
	unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
	if (data)
	{
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
		glGenerateMipmap(GL_TEXTURE_2D);
	}
	else
	{
		std::cout << "Failed to load texture" << std::endl;
	}
	stbi_image_free(data);
	#pragma endregion 加載材質

	#pragma region 渲染

	ourShader.use();
	ourShader.setInt("texture1", 0);

	// 渲染循環
	while (!glfwWindowShouldClose(window))
	{
		// 幀時間差
		float currentFrame = glfwGetTime();
		deltaTime = currentFrame - lastFrame;
		lastFrame = currentFrame;

		// 輸入
		processInput(window);

		// 渲染指令
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		// 綁定材質
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, texture1);
		// 創建矩陣
		//glm::mat4 model = glm::mat4(1.0f);
		//model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));
		glm::mat4 view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
		ourShader.setMat4("view", view);
		// 投影矩陣現在需要梅幀更新
		glm::mat4 projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);
		ourShader.setMat4("projection", projection);

		// 將矩陣傳入着色器
		//ourShader.setMat4("model", model);
		// 渲染箱子
		glBindVertexArray(VAO);
		//glDrawArrays(GL_TRIANGLES, 0, 36);
		for (unsigned int i = 0; i < 10; i++)
		{
			glm::mat4 model = glm::mat4(1.0f);
			model = glm::translate(model, cubePositions[i]);
			float angle = 20.0f * i;
			model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
			ourShader.setMat4("model", model);

			glDrawArrays(GL_TRIANGLES, 0, 36);
		}
		// 檢查並調用事件,交換緩沖
		glfwSwapBuffers(window);

		// 檢查觸發什么事件,更新窗口狀態
		glfwPollEvents();
	}

	glDeleteVertexArrays(1, &VAO);
	glDeleteBuffers(1, &VBO);
	//glDeleteBuffers(1, &EBO);

	// 釋放之前的分配的所有資源
	glfwTerminate();

	return 0;
}

#pragma endregion Rendering

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	// 每當窗口改變大小,GLFW會調用這個函數並填充相應的參數供你處理
	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 += 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 mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
	if (firstMouse)
	{
		lastX = xpos;
		lastY = ypos;
		firstMouse = false;
	}

	float xoffset = xpos - lastX;
	float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top
	lastX = xpos;
	lastY = ypos;

	float sensitivity = 0.1f; // change this value to your liking
	xoffset *= sensitivity;
	yoffset *= sensitivity;

	yaw += xoffset;
	pitch += yoffset;

	// make sure that when pitch is out of bounds, screen doesn't get flipped
	if (pitch > 89.0f)
		pitch = 89.0f;
	if (pitch < -89.0f)
		pitch = -89.0f;

	glm::vec3 front;
	front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
	front.y = sin(glm::radians(pitch));
	front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
	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;
}

攝像機類

接下來我們將會一直使用一個攝像機來瀏覽場景,從各個角度觀察結果,然而,由於一個攝像機會占用每篇教程很大的篇幅,我們將會從細節抽象出來,創建我們自己的攝像機對象,它會完成大多數的工作,而且還會提供一些附加的功能

和着色器對象一樣,我們把攝像機類寫在一個單獨的頭文件中

#ifndef CAMERA_H
#define CAMERA_H

#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include <vector>

// 為攝像機的移動定義了幾種的選項
enum Camera_Movement {
	FORWARD,
	BACKWARD,
	LEFT,
	RIGHT
};

// 初始化攝像機變量
const float YAW = -90.0f;
const float PITCH = 0.0f;
const float SPEED = 2.5f;
const float SENSITIVITY = 0.1f;
const float ZOOM = 45.0f;


// 攝像機類,處理輸入並計算相應的歐拉角,矢量和矩陣
class Camera
{
public:
	// 攝像機變量
	glm::vec3 Position;
	glm::vec3 Front;
	glm::vec3 Up;
	glm::vec3 Right;
	glm::vec3 WorldUp;
	// 歐拉角
	float Yaw;
	float Pitch;
	// 可調選項
	float MovementSpeed;
	float MouseSensitivity;
	float Zoom;

	// 向量構造器
	Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), float yaw = YAW, float pitch = PITCH) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM)
	{
		Position = position;
		WorldUp = up;
		Yaw = yaw;
		Pitch = pitch;
		updateCameraVectors();
	}
	// 含標量的構造器
	Camera(float posX, float posY, float posZ, float upX, float upY, float upZ, float yaw, float pitch) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM)
	{
		Position = glm::vec3(posX, posY, posZ);
		WorldUp = glm::vec3(upX, upY, upZ);
		Yaw = yaw;
		Pitch = pitch;
		updateCameraVectors();
	}

	// 返回使用歐拉角和LookAt矩陣計算的view矩陣
	glm::mat4 GetViewMatrix()
	{
		return glm::lookAt(Position, Position + Front, Up);
	}

	// 處理從任何類似鍵盤的輸入系統接收的輸入,以攝像機定義的ENUM形式接受輸入參數(從窗口系統中抽象出來)
	void ProcessKeyboard(Camera_Movement direction, float deltaTime)
	{
		float velocity = MovementSpeed * deltaTime;
		if (direction == FORWARD)
			Position += Front * velocity;
		if (direction == BACKWARD)
			Position -= Front * velocity;
		if (direction == LEFT)
			Position -= Right * velocity;
		if (direction == RIGHT)
			Position += Right * velocity;
	}

	// 處理從鼠標輸入系統接收的輸入,預測x和y方向的偏移值
	void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch = true)
	{
		xoffset *= MouseSensitivity;
		yoffset *= MouseSensitivity;

		Yaw += xoffset;
		Pitch += yoffset;

		// 確保當pitch超出范圍時,屏幕不會翻轉
		if (constrainPitch)
		{
			if (Pitch > 89.0f)
				Pitch = 89.0f;
			if (Pitch < -89.0f)
				Pitch = -89.0f;
		}

		// 使用更新的歐拉角更新3個向量
		updateCameraVectors();
	}

	// 處理從鼠標滾輪事件接收的輸入
	void ProcessMouseScroll(float yoffset)
	{
		if (Zoom >= 1.0f && Zoom <= 45.0f)
			Zoom -= yoffset;
		if (Zoom <= 1.0f)
			Zoom = 1.0f;
		if (Zoom >= 45.0f)
			Zoom = 45.0f;
	}

private:
	// 從更新的CameraEuler的歐拉角計算前向量
	void updateCameraVectors()
	{
		// 計算新的前向量
		glm::vec3 front;
		front.x = cos(glm::radians(Yaw)) * cos(glm::radians(Pitch));
		front.y = sin(glm::radians(Pitch));
		front.z = sin(glm::radians(Yaw)) * cos(glm::radians(Pitch));
		Front = glm::normalize(front);
		// 再計算右向量和上向量
		Right = glm::normalize(glm::cross(Front, WorldUp));  // 標准化
		Up = glm::normalize(glm::cross(Right, Front));
	}
};
#endif

你現在應該能夠理解所有的代碼了,建議至少看一看這個類,看看如何創建一個自己的攝像機類。

我們介紹的攝像機系統是一個FPS風格的攝像機,它能夠滿足大多數情況需要,而且與歐拉角兼容

使用新攝像機對象,我們的源碼就可以改成:

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

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

#include "shader.h"
#include "camera.h"

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

// callback
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_callback(GLFWwindow* window, double xoffset, double yoffset);

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

// camera
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = SCR_WIDTH / 2.0f;
float lastY = SCR_HEIGHT / 2.0f;
bool firstMouse = true;

// timing
float deltaTime = 0.0f; // 當前幀與上一幀的時間差
float lastFrame = 0.0f; // 上一幀的時間

int main()
{

	#pragma region 窗口
	// 實例化GLFW窗口
	glfwInit();//glfw初始化
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本號
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本號
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

	GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
	//(寬,高,窗口名)返回一個GLFWwindow類的實例:window
	if (window == NULL)
	{
		// 生成錯誤則輸出錯誤信息
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
	glfwSetCursorPosCallback(window, mouse_callback);
	glfwSetScrollCallback(window, scroll_callback);

	// 告訴GLFW我們希望每當窗口調整大小的時候調用改變窗口大小的函數
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
	#pragma endregion GLFW

	#pragma region 函數指針
	// glad管理opengl函數指針,初始化glad
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		// 生成錯誤則輸出錯誤信息
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}
	#pragma endregion GLAD

	Shader ourShader("1.7.0vertex.txt", "1.7.0fragment.txt");

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

	#pragma region 頂點數據
	//頂點數據
	float vertices[] = {
	//   ---- 位置 ----		  - 紋理坐標 -
		-0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
		 0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
		 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
		 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
		-0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
		-0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

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

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

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

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

		-0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
		 0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
		 0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
		 0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
		-0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
		-0.5f,  0.5f, -0.5f,  0.0f, 1.0f
	};
	glm::vec3 cubePositions[] = {
		glm::vec3(0.0f,  0.0f,  0.0f),
		glm::vec3(2.0f,  5.0f, -15.0f),
		glm::vec3(-1.5f, -2.2f, -2.5f),
		glm::vec3(-3.8f, -2.0f, -12.3f),
		glm::vec3(2.4f, -0.4f, -3.5f),
		glm::vec3(-1.7f,  3.0f, -7.5f),
		glm::vec3(1.3f, -2.0f, -2.5f),
		glm::vec3(1.5f,  2.0f, -2.5f),
		glm::vec3(1.5f,  0.2f, -1.5f),
		glm::vec3(-1.3f,  1.0f, -1.5f)
	};
	#pragma endregion vertices[],indices[]

	#pragma region 緩存對象
	// 初始化緩存對象
	unsigned int VBO;
	glGenBuffers(1, &VBO);
	unsigned int VAO;
	glGenVertexArrays(1, &VAO);
	//unsigned int EBO;
	//glGenBuffers(1, &EBO);
	// 1. 綁定頂點數組對象
	glBindVertexArray(VAO);
	// 2. 把我們的頂點數組復制到一個頂點緩沖中,供OpenGL使用
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	//// 3. 復制我們的索引數組到一個索引緩沖中,供OpenGL使用
	//glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	//glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
	// 4. 設定頂點屬性指針
	// 位置屬性
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	// 紋理坐標屬性
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
	glEnableVertexAttribArray(1);
	#pragma endregion VAO,VBO,EBO

	#pragma region 材質
	// 加載材質
	unsigned int texture1;//紋理也是使用ID引用的
	glGenTextures(1, &texture1);//glGenTextures先輸入要生成紋理的數量,然后把它們儲存在第二個參數的`unsigned int`數組中
	glBindTexture(GL_TEXTURE_2D, texture1);
	// 為當前綁定的紋理對象設置環繞、過濾方式
	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_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	// 加載並生成紋理
	int width, height, nrChannels;
	unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
	if (data)
	{
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
		glGenerateMipmap(GL_TEXTURE_2D);
	}
	else
	{
		std::cout << "Failed to load texture" << std::endl;
	}
	stbi_image_free(data);
	#pragma endregion 加載材質

	#pragma region 渲染

	ourShader.use();
	ourShader.setInt("texture1", 0);

	// 渲染循環
	while (!glfwWindowShouldClose(window))
	{
		// 幀時間差
		float currentFrame = glfwGetTime();
		deltaTime = currentFrame - lastFrame;
		lastFrame = currentFrame;

		// 輸入
		processInput(window);

		// 渲染指令
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		// 綁定材質
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, texture1);
		// 創建矩陣
		glm::mat4 view = camera.GetViewMatrix();
		ourShader.setMat4("view", view);
		glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
		ourShader.setMat4("projection", projection);

		// 將矩陣傳入着色器
		//ourShader.setMat4("model", model);
		// 渲染箱子
		glBindVertexArray(VAO);
		//glDrawArrays(GL_TRIANGLES, 0, 36);
		for (unsigned int i = 0; i < 10; i++)
		{
			glm::mat4 model = glm::mat4(1.0f);
			model = glm::translate(model, cubePositions[i]);
			float angle = 20.0f * i;
			model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
			ourShader.setMat4("model", model);

			glDrawArrays(GL_TRIANGLES, 0, 36);
		}
		// 檢查並調用事件,交換緩沖
		glfwSwapBuffers(window);

		// 檢查觸發什么事件,更新窗口狀態
		glfwPollEvents();
	}

	glDeleteVertexArrays(1, &VAO);
	glDeleteBuffers(1, &VBO);
	//glDeleteBuffers(1, &EBO);

	// 釋放之前的分配的所有資源
	glfwTerminate();

	return 0;
}

#pragma endregion Rendering

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	// 每當窗口改變大小,GLFW會調用這個函數並填充相應的參數供你處理
	glViewport(0, 0, width, height);
}

void processInput(GLFWwindow* window)
{
	// 返回這個按鍵是否正在被按下
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//是否按下了返回鍵
		glfwSetWindowShouldClose(window, true);
	if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
		camera.ProcessKeyboard(FORWARD, deltaTime);
	if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
		camera.ProcessKeyboard(BACKWARD, deltaTime);
	if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
		camera.ProcessKeyboard(LEFT, deltaTime);
	if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
		camera.ProcessKeyboard(RIGHT, deltaTime);
}

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

	float xoffset = xpos - lastX;
	float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top

	lastX = xpos;
	lastY = ypos;

	camera.ProcessMouseMovement(xoffset, yoffset);
}

// glfw: whenever the mouse scroll wheel scrolls, this callback is called
// ----------------------------------------------------------------------
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
	camera.ProcessMouseScroll(yoffset);
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM