OpenGL(七) 變換

儘管我們現在已經知道了如何創建一個物體、着色、加入紋理,給它們一些細節的表現,但因爲它們都還是靜態的物體,仍是不夠有趣。我們可以嘗試着在每一幀改變物體的頂點並且重配置緩衝區從而使它們移動,但這太繁瑣了,而且會消耗很多的處理時間。我們現在有一個更好的解決方案,使用(多個)矩陣(Matrix)對象可以更好的變換(Transform)一個物體。當然,這並不是說我們會去討論武術和數字虛擬世界(譯註:Matrix同樣也是電影「黑客帝國」的英文名,電影中人類生活在數字虛擬世界,主角會武術)。

矩陣是一種非常有用的數學工具,儘管聽起來可能有些嚇人,不過一旦你理解了它們後,它們會變得非常有用。在討論矩陣的過程中,我們需要使用到一些數學知識。對於一些願意多瞭解這些知識的讀者,我會附加一些資源給你們閱讀。

爲了深入瞭解變換,我們首先要在討論矩陣之前進一步瞭解一下向量。這一節的目標是讓你擁有將來需要的最基礎的數學背景知識。如果你發現這節十分困難,儘量嘗試去理解它們,當你以後需要它們的時候回過頭來複習這些概念。

向量

向量最基本的定義就是一個方向。或者更正式的說,向量有一個方向(Direction)和大小(Magnitude,也叫做強度或長度)。你可以把向量想像成一個藏寶圖上的指示:“向左走10步,向北走3步,然後向右走5步”;“左”就是方向,“10步”就是向量的長度。那麼這個藏寶圖的指示一共有3個向量。向量可以在任意維度(Dimension)上,但是我們通常只使用2至4維。如果一個向量有2個維度,它表示一個平面的方向(想象一下2D的圖像),當它有3個維度的時候它可以表達一個3D世界的方向。

下面你會看到3個向量,每個向量在2D圖像中都用一個箭頭(x, y)表示。我們在2D圖片中展示這些向量,因爲這樣子會更直觀一點。你可以把這些2D向量當做z座標爲0的3D向量。由於向量表示的是方向,起始於何處並不會改變它的值。下圖我們可以看到向量v¯和w¯是相等的,儘管他們的起始點不同
在這裏插入圖片描述
數學家喜歡在字母上面加一橫表示向量,比如說v¯。當用在公式中時它們通常是這樣的:
在這裏插入圖片描述
由於向量是一個方向,所以有些時候會很難形象地將它們用位置(Position)表示出來。爲了讓其更爲直觀,我們通常設定這個方向的原點爲(0, 0, 0),然後指向一個方向,對應一個點,使其變爲位置向量(Position Vector)(你也可以把起點設置爲其他的點,然後說:這個向量從這個點起始指向另一個點)。比如說位置向量(3, 5)在圖像中的起點會是(0, 0),並會指向(3, 5)。我們可以使用向量在2D或3D空間中表示方向與位置.

向量與標量運算

標量(Scalar)只是一個數字(或者說是僅有一個分量的向量)。當把一個向量加/減/乘/除一個標量,我們可以簡單的把向量的每個分量分別進行該運算。對於加法來說會像這樣:
在這裏插入圖片描述
其中的+可以是+,-,·或÷,其中·是乘號。注意-和÷運算時不能顛倒(標量-/÷向量),因爲顛倒的運算是沒有定義的

向量取反

對一個向量取反(Negate)會將其方向逆轉。一個指向東北的向量取反後就指向西南方向了。我們在一個向量的每個分量前加負號就可以實現取反了(或者說用-1數乘該向量):
在這裏插入圖片描述

向量加減

向量的加法可以被定義爲是分量的(Component-wise)相加,即將一個向量中的每一個分量加上另一個向量的對應分量:
在這裏插入圖片描述
向量v = (4, 2)和k = (1, 2)可以直觀地表示爲:
在這裏插入圖片描述
就像普通數字的加減一樣,向量的減法等於加上第二個向量的相反向量:
在這裏插入圖片描述
兩個向量的相減會得到這兩個向量指向位置的差。這在我們想要獲取兩點的差會非常有用。
在這裏插入圖片描述

長度

我們使用勾股定理(Pythagoras Theorem)來獲取向量的長度(Length)/大小(Magnitude)。如果你把向量的x與y分量畫出來,該向量會和x與y分量爲邊形成一個三角形:
在這裏插入圖片描述
因爲兩條邊(x和y)是已知的,如果希望知道斜邊v¯的長度,我們可以直接通過勾股定理來計算:
在這裏插入圖片描述
例子中向量(4, 2)的長度等於:
在這裏插入圖片描述
有一個特殊類型的向量叫做單位向量(Unit Vector)。單位向量有一個特別的性質——它的長度是1。我們可以用任意向量的每個分量除以向量的長度得到它的單位向量n^:
在這裏插入圖片描述
我們把這種方法叫做一個向量的標準化(Normalizing)。單位向量頭上有一個^樣子的記號。通常單位向量會變得很有用,特別是在我們只關心方向不關心長度的時候(如果改變向量的長度,它的方向並不會改變)。

向量相乘

兩個向量相乘是一種很奇怪的情況。普通的乘法在向量上是沒有定義的,因爲它在視覺上是沒有意義的。但是在相乘的時候我們有兩種特定情況可以選擇:一個是點乘(Dot Product),記作v¯⋅k¯,另一個是叉乘(Cross Product),記作v¯×k¯。

點乘

兩個向量的點乘等於它們的數乘結果乘以兩個向量之間夾角的餘弦值。
在這裏插入圖片描述
它們之間的夾角記作θ。爲什麼這很有用?想象如果v¯和k¯都是單位向量,它們的長度會等於1。這樣公式會有效簡化成:
在這裏插入圖片描述
現在點積只定義了兩個向量的夾角。你也許記得90度的餘弦值是0,0度的餘弦值是1。使用點乘可以很容易測試兩個向量是否正交(Orthogonal)或平行(正交意味着兩個向量互爲直角)。

在這裏插入圖片描述所以,我們該如何計算點乘呢?點乘是通過將對應分量逐個相乘,然後再把所得積相加來計算的。兩個單位向量的(你可以驗證它們的長度都爲1)點乘會像是這樣:
在這裏插入圖片描述
要計算兩個單位向量間的夾角,我們可以使用反餘弦函數cos^{-1} ,可得結果是143.1度。現在我們很快就計算出了這兩個向量的夾角。點乘會在計算光照的時候非常有用

叉乘

叉乘只在3D空間中有定義,它需要兩個不平行向量作爲輸入,生成一個正交於兩個輸入向量的第三個向量。如果輸入的兩個向量也是正交的,那麼叉乘之後將會產生3個互相正交的向量。接下來的教程中這會非常有用。下面的圖片展示了3D空間中叉乘的樣子:
在這裏插入圖片描述
兩個正交向量A和B叉積:
在這裏插入圖片描述

矩陣

簡單來說矩陣就是一個矩形的數字、符號或表達式數組。矩陣中每一項叫做矩陣的元素(Element)。下面是一個2×3矩陣的例子:
在這裏插入圖片描述

矩陣的加減

矩陣與標量之間的加減定義如下:
在這裏插入圖片描述
標量值要加到矩陣的每一個元素上。矩陣與標量的減法也相似:
在這裏插入圖片描述
矩陣與矩陣之間的加減就是兩個矩陣對應元素的加減運算,所以總體的規則和與標量運算是差不多的,只不過在相同索引下的元素才能進行運算。這也就是說加法和減法只對同維度的矩陣纔是有定義的。一個3×2矩陣和一個2×3矩陣(或一個3×3矩陣與4×4矩陣)是不能進行加減的。我們看看兩個2×2矩陣是怎樣相加的:
在這裏插入圖片描述

矩陣的數乘

和矩陣與標量的加減一樣,矩陣與標量之間的乘法也是矩陣的每一個元素分別乘以該標量。下面的例子展示了乘法的過程:
在這裏插入圖片描述
現在我們也就能明白爲什麼這些單獨的數字要叫做標量(Scalar)了。簡單來說,標量就是用它的值縮放(Scale)矩陣的所有元素

矩陣相乘

矩陣之間的乘法不見得有多複雜,但的確很難讓人適應。矩陣乘法基本上意味着遵照規定好的法則進行相乘。當然,相乘還有一些限制:

  • 只有當左側矩陣的列數與右側矩陣的行數相等,兩個矩陣才能相乘。
  • 矩陣相乘不遵守交換律(Commutative),也就是說A⋅B≠B⋅A

先看一個兩個2×2矩陣相乘的例子:
在這裏插入圖片描述

矩陣與向量相乘

目前爲止,通過這些教程我們已經相當瞭解向量了。我們用向量來表示位置,表示顏色,甚至是紋理座標。讓我們更深入瞭解一下向量,它其實就是一個N×1矩陣,N表示向量分量的個數(也叫N維(N-dimensional)向量)。如果你仔細思考一下就會明白。向量和矩陣一樣都是一個數字序列,但它只有1列。那麼,這個新的定義對我們有什麼幫助呢?如果我們有一個M×N矩陣,我們可以用這個矩陣乘以我們的N×1向量,因爲這個矩陣的列數等於向量的行數,所以它們就能相乘。

但是爲什麼我們會關心矩陣能否乘以一個向量?好吧,正巧,很多有趣的2D/3D變換都可以放在一個矩陣中,用這個矩陣乘以我們的向量將變換(Transform)這個向量。

單位矩陣

在OpenGL中,由於某些原因我們通常使用4×4的變換矩陣,而其中最重要的原因就是大部分的向量都是4分量的。我們能想到的最簡單的變換矩陣就是單位矩陣(Identity Matrix)。單位矩陣是一個除了對角線以外都是0的N×N矩陣。在下式中可以看到,這種變換矩陣使一個向量完全不變:
在這裏插入圖片描述

縮放

對一個向量進行縮放(Scaling)就是對向量的長度進行縮放,而保持它的方向不變。由於我們進行的是2維或3維操作,我們可以分別定義一個有2或3個縮放變量的向量,每個變量縮放一個軸(x、y或z)。

我們先來嘗試縮放向量v¯=(3,2)。我們可以把向量沿着x軸縮放0.5,使它的寬度縮小爲原來的二分之一;我們將沿着y軸把向量的高度縮放爲原來的兩倍。我們看看把向量縮放(0.5, 2)倍所獲得的s¯是什麼樣的:
在這裏插入圖片描述
記住,OpenGL通常是在3D空間進行操作的,對於2D的情況我們可以把z軸縮放1倍,這樣z軸的值就不變了。我們剛剛的縮放操作是不均勻(Non-uniform)縮放,因爲每個軸的縮放因子(Scaling Factor)都不一樣。如果每個軸的縮放因子都一樣那麼就叫均勻縮放(Uniform Scale)。

我們下面會構造一個變換矩陣來爲我們提供縮放功能。我們從單位矩陣瞭解到,每個對角線元素會分別與向量的對應元素相乘。如果我們把1變爲3會怎樣?這樣子的話,我們就把向量的每個元素乘以3了,這事實上就把向量縮放3倍。如果我們把縮放變量表示爲(S1,S2,S3)我們可以爲任意向量(x,y,z)定義一個縮放矩陣:
在這裏插入圖片描述
注意,第四個縮放向量仍然是1,因爲在3D空間中縮放w分量是無意義的。w分量另有其他用途

位移

位移(Translation)是在原始向量的基礎上加上另一個向量從而獲得一個在不同位置的新向量的過程,從而在位移向量基礎上移動了原始向量。我們已經討論了向量加法,所以這應該不會太陌生。

和縮放矩陣一樣,在4×4矩陣上有幾個特別的位置用來執行特定的操作,對於位移來說它們是第四列最上面的3個值。如果我們把位移向量表示爲(Tx,Ty,Tz),我們就能把位移矩陣定義爲:
在這裏插入圖片描述
這樣是能工作的,因爲所有的位移值都要乘以向量的w行,所以位移值會加到向量的原始值上(想想矩陣乘法法則)。而如果你用3x3矩陣我們的位移值就沒地方放也沒地方乘了,所以是不行的
在這裏插入圖片描述有了位移矩陣我們就可以在3個方向(x、y、z)上移動物體,它是我們的變換工具箱中非常有用的一個變換矩陣。

旋轉

首先我們來定義一個向量的旋轉到底是什麼。2D或3D空間中的旋轉用角(Angle)來表示。角可以是角度制或弧度制的,周角是360角度或2 PI弧度。
在這裏插入圖片描述轉半圈會旋轉360/2 = 180度,向右旋轉1/5圈表示向右旋轉360/5 = 72度。下圖中展示的2D向量v¯是由k¯向右旋轉72度所得的:
在這裏插入圖片描述
在3D空間中旋轉需要定義一個和一個旋轉軸(Rotation Axis)。物體會沿着給定的旋轉軸旋轉特定角度。如果你想要更形象化的感受,可以試試向下看着一個特定的旋轉軸,同時將你的頭部旋轉一定角度。當2D向量在3D空間中旋轉時,我們把旋轉軸設爲z軸(嘗試想象這種情況)。

使用三角學,給定一個角度,可以把一個向量變換爲一個經過旋轉的新向量。這通常是使用一系列正弦和餘弦函數(一般簡稱sin和cos)各種巧妙的組合得到的。

旋轉矩陣在3D空間中每個單位軸都有不同定義,旋轉角度用θ表示:

沿x軸旋轉:
在這裏插入圖片描述
沿y軸旋轉:
在這裏插入圖片描述
沿z軸旋轉:
在這裏插入圖片描述
利用旋轉矩陣我們可以把任意位置向量沿一個單位旋轉軸進行旋轉。也可以將多個矩陣複合,比如先沿着x軸旋轉再沿着y軸旋轉。但是這會很快導致一個問題——萬向節死鎖(Gimbal Lock,可以看看這個視頻(優酷)來了解)。在這裏我們不會討論它的細節,但是對於3D空間中的旋轉,一個更好的模型是沿着任意的一個軸,比如單位向量(0.662,0.2,0.7222)(0.662, 0.2, 0.7222)旋轉,而不是對一系列旋轉矩陣進行復合。這樣的一個(超級麻煩的)矩陣是存在的,見下面這個公式,其中(Rx,Ry,Rz)代表任意旋轉軸:
在這裏插入圖片描述避免萬向節死鎖的真正解決方案是使用四元數(Quaternion),它不僅更安全,而且計算會更有效率。

矩陣的組合

使用矩陣進行變換的真正力量在於,根據矩陣之間的乘法,我們可以把多個變換組合到一個矩陣中
假設我們有一個頂點(x, y, z),我們希望將其縮放2倍,然後位移(1, 2, 3)個單位。我們需要一個位移和縮放矩陣來完成這些變換。結果的變換矩陣看起來像這樣:
在這裏插入圖片描述
注意,當矩陣相乘時我們先寫位移再寫縮放變換的。矩陣乘法是不遵守交換律的,這意味着它們的順序很重要。當矩陣相乘時,在最右邊的矩陣是第一個與向量相乘的,所以你應該從右向左讀這個乘法。建議您在組合矩陣時,先進行縮放操作,然後是旋轉,最後纔是位移,否則它們會(消極地)互相影響。比如,如果你先位移再縮放,位移的向量也會同樣被縮放(譯註:比如向某方向移動2米,2米也許會被縮放成1米)!

用最終的變換矩陣左乘我們的向量會得到以下結果:
在這裏插入圖片描述
不錯!向量先縮放2倍,然後位移了(1, 2, 3)個單位。

實踐

希望抽象所有的數學細節,使用已經做好了的數學庫。幸運的是,有個易於使用,專門爲OpenGL量身定做的數學庫,那就是GLM

GLM

GLM是OpenGL Mathematics的縮寫,它是一個只有頭文件的庫,也就是說我們只需包含對應的頭文件就行了,不用鏈接和編譯。GLM可以在它們的網站上下載。把頭文件的根目錄複製到你的includes文件夾,然後你就可以使用這個庫了。
在這裏插入圖片描述我們需要的GLM的大多數功能都可以從下面這3個頭文件中找到

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

把一個向量(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 = glm::mat4(1.0f);  // 定義一個4x4單位矩陣
	// 將矩形沿z軸旋轉90度,使用glm::radians將角度轉化成弧度
	trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0));  
	// 把矩形在每個軸都縮放到0.5倍
	trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));

rotate()中的vec3(0.0, 0.0, 1.0),就相當於(Rx,Ry,Rz),帶進去求即可
在這裏插入圖片描述
首先,我們把箱子在每個軸都縮放到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);
}

在這裏插入圖片描述在把位置向量傳給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的時間函數來獲取不同時間的角度:

		// 創建4x4單位矩陣,然後經平移變換,經旋轉變換,經縮放變換,最後與頂點位置向量相乘
		// (將這幾個矩陣和向量寫出來,可以發現實際上是先縮放矩陣與向量相乘,相乘結果再與旋轉矩陣相乘,結果再與平移矩陣相乘)
		// 故先進行縮放操作,然後是旋轉,最後纔是位移
		glm::mat4 trans = glm::mat4(1.0f);  // 定義一個4x4單位矩陣
		// 將矩形平移到屏幕右下角
		trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f));
		// 將矩形沿z軸旋轉
		trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0, 0.0, 1.0));
		// 把矩形在每個軸都縮放到0.5倍
		trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));

要記住的是前面的例子中我們可以在任何地方聲明變換矩陣,但是現在我們必須在每一次迭代中創建它,從而保證我們能夠不斷更新旋轉角度。這也就意味着我們不得不在每次遊戲循環的迭代中重新創建變換矩陣。通常在渲染場景的時候,我們也會有多個需要在每次渲染迭代中都用新值重新創建的變換矩陣

在這裏我們先把箱子圍繞原點(0, 0, 0)旋轉,之後,我們把旋轉過後的箱子位移到屏幕的右下角
記住,實際的變換順序應該與閱讀順序相反:儘管在代碼中我們先位移再旋轉再縮放,實際的變換卻是先應用縮放再是旋轉再是位移的。明白所有這些變換的組合,並且知道它們是如何應用到物體上是一件非常困難的事情。只有不斷地嘗試和實驗這些變換你才能快速地掌握它們。

我們可以定義無限數量的變換,而把它們組合爲僅僅一個矩陣,如果願意的話我們還可以重複使用它。
在着色器中使用矩陣可以省去重新定義頂點數據的功夫,它也能夠節省處理時間,因爲我們沒有一直重新發送我們的數據(這是個非常慢的過程)。

源碼

// main.cpp

// 使用着色器類的代碼
#define STB_IMAGE_IMPLEMENTATION
#include <glad/glad.h>
#include <glfw3.h>
#include <stb_image.h>

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

#include "../include/shader.h" // 從main.cpp所在文件夾開始檢索,自定義類

#include <iostream>


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

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

float mixValue = 0.2f;   // 兩張紋理的混合比例


//int main(int argc, char** argv)
int main()
{
	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, "learnOpenGL", nullptr, nullptr);
	if (window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

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

	// 創建着色器類對象
	Shader ourShader("src/translate.vs", "src/translate.fs");  // 從工程文件目錄的位置開始檢索

	float vertices[] = {
	// 位置                    // 顏色              // 紋理座標
		0.5f,  0.5f,  0.0f,   1.0f, 0.0f, 0.0f,     1.0f, 1.0f, 
		0.5f, -0.5f,  0.0f,   0.0f, 1.0f, 0.0f,     1.0f, 0.0f, 
	   -0.5f, -0.5f,  0.0f,   0.0f, 0.0f, 1.0f,     0.0f, 0.0f,
	   -0.5f,  0.5f,  0.0f,   1.0f, 1.0f, 0.0f,     0.0f, 1.0f, 
	};
	unsigned int indices[] = {
		0, 1, 3,  // 第一個三角形
		1, 2, 3   // 第二個三角形
	};

	unsigned int VBO, VAO, EBO;
	glGenVertexArrays(1, &VAO);
	glGenBuffers(1, &VBO);
	glGenBuffers(1, &EBO);

	glBindVertexArray(VAO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);

	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

	// 位置屬性
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);

	// 顏色屬性
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3*sizeof(float)));
	glEnableVertexAttribArray(1);

	// 紋理座標
	glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6*sizeof(float)));
	glEnableVertexAttribArray(2);


	// 加載並創建第一個紋理(opengl保證至少有16個紋理單元可供使用)
	unsigned int texture1;   // 如果要加載多個紋理,這裏設置爲數組
	glGenTextures(1, &texture1);
	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_WRAP_S, GL_CLAMP_TO_EDGE);
	//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);  // 縮小操作時過濾方式
	//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);  // 放大操作時過濾方式
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	// 加載紋理圖片並附到紋理對象上
	int width, height, nrChannels;
	stbi_set_flip_vertically_on_load(true); // 告訴stb_image.h在y軸上翻轉加載的紋理,後面加載的所有紋理圖片都將翻轉
	unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
	if (data)
	{
		// 當調用glTexImage2D時,當前綁定的紋理對象就會被附加上紋理圖像
		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);


	// 加載並創建第二個紋理
	unsigned int texture2;
	glGenTextures(1, &texture2);
	glBindTexture(GL_TEXTURE_2D, texture2);
	// 爲當前綁定的第二個紋理對象設置環繞、過濾方式
	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_WRAP_S, GL_MIRRORED_REPEAT);
	//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
	//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	// 加載紋理圖片並附到紋理對象上
	data = stbi_load("awesomeface.png", &width, &height, &nrChannels, 0);
	if (data)
	{
		// awesomeface.png具有透明度,因此是alpha通道,所以一定要告訴OpenGL數據類型是GL_RGBA
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 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);


	// 解除綁定
	//glBindBuffer(GL_ARRAY_BUFFER, 0);
	//glBindVertexArray(0);

	// 告訴opengl每個採樣器屬於哪個紋理單元(只需要做一次)
	ourShader.use();  // 在設置uniform之前,不要忘記激活/使用着色器
	//glUniform1i(glGetUniformLocation(ourShader.ID, "ourTexture1"), 0);
	ourShader.setInt("ourTexture1", 0);
	ourShader.setInt("ourTexture2", 1);



	while (!glfwWindowShouldClose(window))
	{
		processInput(window);

		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		// 創建4x4單位矩陣,然後經平移變換,經旋轉變換,經縮放變換,最後與頂點位置向量相乘
		// (將這幾個矩陣和向量寫出來,可以發現實際上是先縮放矩陣與向量相乘,相乘結果再與旋轉矩陣相乘,結果再與平移矩陣相乘)
		// 先進行縮放操作,然後是旋轉,最後纔是位移
		glm::mat4 trans = glm::mat4(1.0f);  // 定義一個4x4單位矩陣
		// 將矩形平移到屏幕右下角
		trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f));
		// 將矩形沿z軸旋轉
		trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0, 0.0, 1.0));
		// 把矩形在每個軸都縮放到0.5倍
		trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));

		unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
		glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));



		glActiveTexture(GL_TEXTURE0);  // 在綁定紋理之前先激活紋理單元,只有一個紋理時,可以不用手動激活,默認激活
		glBindTexture(GL_TEXTURE_2D, texture1);
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, texture2);

		ourShader.setFloat("mixValue", mixValue);  // 循環前調用了use(),所有可以設置uniform

		// 綁定VAO後會綁定紋理,它會自動把紋理賦值給片段着色器的採樣器
		glBindVertexArray(VAO);
		/*glDrawArrays(GL_TRIANGLES, 0, 3);*/
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);   // 矩形由兩個三角形組成,故是6個頂點

		glfwSwapBuffers(window);
		glfwPollEvents();

	}

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

	glfwTerminate();

	return 0;
}


void framebuffer_size_callback(GLFWwindow * window, int width, int height)
{
	glViewport(0, 0, width, height);  // 視口
}


// 處理所有輸入:查詢GLFW是否按下/釋放了此幀的相關鍵,並做出相應的反應
void processInput(GLFWwindow* window)
{
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)  // 鍵盤esc
	{
		glfwSetWindowShouldClose(window, true);
	}
	if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS)     // 鍵盤up
	{
		mixValue += 0.01f;
		if (mixValue >= 1.0f)
			mixValue = 1.0f;
	}
	if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS)  // 鍵盤down
	{
		mixValue -= 0.01f;
		if (mixValue <= 0.0f)
			mixValue = 0.0f;
	}
}

translate.fs

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture1;
uniform sampler2D ourTexture2;
uniform float mixValue;

void main()
{
	//FragColor = vec4(ourColor, 1.0f);
	//FragColor = texture(ourTexture, TexCoord);
	//FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0f);
	FragColor = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, TexCoord), mixValue);
	//FragColor = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, vec2(1.0 - TexCoord.x, TexCoord.y)), 0.2);
}

translate.vs

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec3 ourColor;
out vec2 TexCoord;

uniform float xOffset;
uniform mat4 transform;

void main()
{
	//gl_Position = vec4(aPos, 1.0);
	//gl_Position = vec4(aPos.x + xOffset, aPos.y, aPos.z, 1.0f);
	gl_Position = transform * vec4(aPos, 1.0f);
	ourColor = aColor;
	TexCoord = aTexCoord;
	//TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
}

練習

嘗試再次調用glDrawElements畫出第二個箱子,只使用變換將其擺放在不同的位置。讓這個箱子被擺放在窗口的左上角,並且會不斷的縮放和旋轉(sin函數在這裏會很有用,不過注意使用sin函數時應用負值會導致物體被翻轉):

	while (!glfwWindowShouldClose(window))
	{
		processInput(window);

		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		ourShader.setFloat("mixValue", mixValue);  // 循環前調用了use(),所以可以設置uniform

		glActiveTexture(GL_TEXTURE0);  // 在綁定紋理之前先激活紋理單元,只有一個紋理時,可以不用手動激活,默認激活
		glBindTexture(GL_TEXTURE_2D, texture1);
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, texture2);

		// 創建4x4單位矩陣,然後經平移變換,經旋轉變換,經縮放變換,最後與頂點位置向量相乘
		// (將這幾個矩陣和向量寫出來,可以發現實際上是先縮放矩陣與向量相乘,相乘結果再與旋轉矩陣相乘,結果再與平移矩陣相乘)
		// 先進行縮放操作,然後是旋轉,最後纔是位移
		glm::mat4 trans = glm::mat4(1.0f);  // 定義一個4x4單位矩陣
		// 將矩形平移到屏幕右下角
		trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f));
		// 將矩形沿z軸旋轉
		trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0, 0.0, 1.0));
		/*std::cout << glfwGetTime() << std::endl;*/
		// 把矩形在每個軸都縮放到0.5倍
		trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));

		unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
		glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));

		// 綁定VAO後會綁定紋理,它會自動把紋理賦值給片段着色器的採樣器
		glBindVertexArray(VAO);  // 前面並沒有把VAO解綁定,其實這裏不重新綁定也是可以的,但有多個VAO就需要綁定
		/*glDrawArrays(GL_TRIANGLES, 0, 3);*/
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);   // 矩形由兩個三角形組成,故是6個頂點,畫第一個箱子

		// 第二個箱子
		trans = glm::mat4(1.0f);
		trans = glm::translate(trans, glm::vec3(-0.5f, 0.5f, 0.0f));
		// 將矩形沿z軸旋轉
		trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0, 0.0, 1.0));
		float scaleAmount = sin(glfwGetTime());
		trans = glm::scale(trans, glm::vec3(scaleAmount, scaleAmount, scaleAmount));
		glUniformMatrix4fv(transformLoc, 1, GL_FALSE, &trans[0][0]);

		/*glDrawArrays(GL_TRIANGLES, 0, 3);*/
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);   // 矩形由兩個三角形組成,故是6個頂點,畫第二個箱子

		glfwSwapBuffers(window);
		glfwPollEvents();

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