Learn OpenGL (五):向量
叉乘
叉乘只在3D空間中有定義,它需要兩個不平行向量作為輸入,生成一個正交於兩個輸入向量的第三個向量。如果輸入的兩個向量也是正交的,那麼叉乘之後將會產生3個互相正交的向量。接下來的教程中這會非常有用。下面的圖片展示了3D空間中叉乘的樣子:
不同於其他運算,如果你沒有鑽研過線性代數,可能會覺得叉乘很反直覺,所以只記住公式就沒問題啦(記不住也沒問題)。下面你會看到兩個正交向量A和B叉積:
是不是看起來毫無頭緒?不過只要你按照步驟來了,你就能得到一個正交於兩個輸入向量的第三個向量。
縮放
位移
齊次座標(Homogeneous Coordinates)
向量的w分量也叫齊次座標。想要從齊次向量得到3D向量,我們可以把x、y和z座標分別除以w座標。我們通常不會注意這個問題,因為w分量通常是1.0。使用齊次座標有幾點好處:它允許我們在3D向量上進行位移(如果沒有w分量我們是不能位移向量的),而且下一章我們會用w值建立3D視覺效果。
如果一個向量的齊次座標是0,這個座標就是方向向量(Direction Vector),因為w座標是0,這個向量就不能位移(譯註:這也就是我們說的不能位移一個方向)。
旋轉
大多數旋轉函式需要用弧度制的角,但幸運的是角度制的角也可以很容易地轉化為弧度制的:
- 弧度轉角度:
角度 = 弧度 * (180.0f / PI)
- 角度轉弧度:
弧度 = 角度 * (PI / 180.0f)
GLM
GLM是OpenGL Mathematics的縮寫,它是一個只有標頭檔案的庫,也就是說我們只需包含對應的標頭檔案就行了,不用連結和編譯。GLM可以在它們的網站上下載。把標頭檔案的根目錄複製到你的includes資料夾,然後你就可以使用這個庫了。
我們來看看是否可以利用我們剛學的變換知識把一個向量(1, 0, 0)位移(1, 1, 0)個單位(注意,我們把它定義為一個glm::vec4
型別的值,齊次座標設定為1.0):
glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f); // 譯註:下面就是矩陣初始化的一個例子,如果使用的是0.9.9及以上版本 // 下面這行程式碼就需要改為: // glm::mat4 trans = glm::mat4(1.0f) glm::mat4 trans; trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f)); vec = trans * vec; std::cout << vec.x << vec.y << vec.z << std::endl;
我們先用GLM內建的向量類定義一個叫做vec
的向量。接下來定義一個mat4
型別的trans
,預設是一個4×4單位矩陣。下一步是建立一個變換矩陣,我們是把單位矩陣和一個位移向量傳遞給glm::translate
函式來完成這個工作的(然後用給定的矩陣乘以位移矩陣就能獲得最後需要的矩陣)。
之後我們把向量乘以位移矩陣並且輸出最後的結果。如果你仍記得位移矩陣是如何工作的話,得到的向量應該是(1 + 1, 0 + 1, 0 + 0),也就是(2, 1, 0)。這個程式碼片段將會輸出210
,所以這個位移矩陣是正確的。
我們來做些更有意思的事情,讓我們來旋轉和縮放之前教程中的那個箱子。首先我們把箱子逆時針旋轉90度。然後縮放0.5倍,使它變成原來的一半大。我們先來建立變換矩陣:
glm::mat4 trans;
trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0));
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
首先,我們把箱子在每個軸都縮放到0.5倍,然後沿z軸旋轉90度。GLM希望它的角度是弧度制的(Radian),所以我們使用glm::radians
將角度轉化為弧度。注意有紋理的那面矩形是在XY平面上的,所以我們需要把它繞著z軸旋轉。因為我們把這個矩陣傳遞給了GLM的每個函式,GLM會自動將矩陣相乘,返回的結果是一個包括了多個變換的變換矩陣。
如何把矩陣傳遞給著色器?我們在前面簡單提到過GLSL裡也有一個mat4
型別。所以我們將修改頂點著色器讓其接收一個mat4
的uniform變數,然後再用矩陣uniform乘以位置向量:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 transform;
void main()
{
gl_Position = transform * vec4(aPos, 1.0f);
TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
}
GLSL也有mat2
和mat3
型別從而允許了像向量一樣的混合運算。前面提到的所有數學運算(像是標量-矩陣相乘,矩陣-向量相乘和矩陣-矩陣相乘)在矩陣型別裡都可以使用。當出現特殊的矩陣運算的時候我們會特別說明。
在把位置向量傳給gl_Position之前,我們先新增一個uniform,並且將其與變換矩陣相乘。我們的箱子現在應該是原來的二分之一大小並(向左)旋轉了90度。當然,我們仍需要把變換矩陣傳遞給著色器:
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
我們首先查詢uniform變數的地址,然後用有Matrix4fv
字尾的glUniform函式把矩陣資料傳送給著色器。第一個引數你現在應該很熟悉了,它是uniform的位置值。第二個引數告訴OpenGL我們將要傳送多少個矩陣,這裡是1。第三個引數詢問我們我們是否希望對我們的矩陣進行置換(Transpose),也就是說交換我們矩陣的行和列。OpenGL開發者通常使用一種內部矩陣佈局,叫做列主序(Column-major Ordering)佈局。GLM的預設佈局就是列主序,所以並不需要置換矩陣,我們填GL_FALSE
。最後一個引數是真正的矩陣資料,但是GLM並不是把它們的矩陣儲存為OpenGL所希望接受的那種,因此我們要先用GLM的自帶的函式value_ptr來變換這些資料。
我們建立了一個變換矩陣,在頂點著色器中聲明瞭一個uniform,並把矩陣傳送給了著色器,著色器會變換我們的頂點座標。最後的結果應該看起來像這樣:
完美!我們的箱子向左側旋轉,並是原來的一半大小,所以變換成功了。我們現在做些更有意思的,看看我們是否可以讓箱子隨著時間旋轉,我們還會重新把箱子放在視窗的右下角。要讓箱子隨著時間推移旋轉,我們必須在遊戲迴圈中更新變換矩陣,因為它在每一次渲染迭代中都要更新。我們使用GLFW的時間函式來獲取不同時間的角度:
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f));
trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));
要記住的是前面的例子中我們可以在任何地方宣告變換矩陣,但是現在我們必須在每一次迭代中建立它,從而保證我們能夠不斷更新旋轉角度。這也就意味著我們不得不在每次遊戲迴圈的迭代中重新建立變換矩陣。通常在渲染場景的時候,我們也會有多個需要在每次渲染迭代中都用新值重新建立的變換矩陣。
練習:
嘗試再次呼叫glDrawElements畫出第二個箱子,只使用變換將其擺放在不同的位置。讓這個箱子被擺放在視窗的左上角,並且會不斷的縮放(而不是旋轉)
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <stb_image.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <learnopengl/filesystem.h>
#include <learnopengl/shader_s.h>
#include <iostream>
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()
{
// glfw: initialize and configure
// ------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif
// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// build and compile our shader zprogram
// ------------------------------------
Shader ourShader("5.2.transform.vs", "5.2.transform.fs");
// set up vertex data (and buffer(s)) and configure vertex attributes
// ------------------------------------------------------------------
float vertices[] = {
// positions // texture coords
0.5f, 0.5f, 0.0f, 1.0f, 1.0f, // top right
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f // top left
};
unsigned int indices[] = {
0, 1, 3, // first triangle
1, 2, 3 // second triangle
};
unsigned int VBO, VAO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// texture coord attribute
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// load and create a texture
// -------------------------
unsigned int texture1, texture2;
// texture 1
// ---------
glGenTextures(1, &texture1);
glBindTexture(GL_TEXTURE_2D, texture1);
// set the texture wrapping parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// set texture filtering parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// load image, create texture and generate mipmaps
int width, height, nrChannels;
stbi_set_flip_vertically_on_load(true); // tell stb_image.h to flip loaded texture's on the y-axis.
unsigned char *data = stbi_load(FileSystem::getPath("resources/textures/container.jpg").c_str(), &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);
// texture 2
// ---------
glGenTextures(1, &texture2);
glBindTexture(GL_TEXTURE_2D, texture2);
// set the texture wrapping parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// set texture filtering parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// load image, create texture and generate mipmaps
data = stbi_load(FileSystem::getPath("resources/textures/awesomeface.png").c_str(), &width, &height, &nrChannels, 0);
if (data)
{
// note that the awesomeface.png has transparency and thus an alpha channel, so make sure to tell OpenGL the data type is of GL_RGBA
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
// tell opengl for each sampler to which texture unit it belongs to (only has to be done once)
// -------------------------------------------------------------------------------------------
ourShader.use();
ourShader.setInt("texture1", 0);
ourShader.setInt("texture2", 1);
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// bind textures on corresponding texture units
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glm::mat4 transform;
// first container
// ---------------
transform = glm::translate(transform, glm::vec3(0.5f, -0.5f, 0.0f));
transform = glm::rotate(transform, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));
// get their uniform location and set matrix (using glm::value_ptr)
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(transform));
// with the uniform matrix set, draw the first container
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// second transformation
// ---------------------
transform = glm::mat4(); // reset it to an identity matrix
transform = glm::translate(transform, glm::vec3(-0.5f, 0.5f, 0.0f));
float scaleAmount = sin(glfwGetTime());
transform = glm::scale(transform, glm::vec3(scaleAmount, scaleAmount, scaleAmount));
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, &transform[0][0]); // this time take the matrix value array's first element as its memory pointer value
// now with the uniform matrix being replaced with new transformations, draw it again.
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// optional: de-allocate all resources once they've outlived their purpose:
// ------------------------------------------------------------------------
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}