1、概述
前面幾篇關于OpenGLES的文章:
有討論到關于圖像的變換和移動,前面這些變換是圖像基于每一幀改變物體的頂點并且重配置緩沖區從而使它們移動,但這太繁瑣了,而且會消耗很多的處理時間。現在有一個更好的解決方案,使用(多個)矩陣(Matrix)對象可以更好的變換(Transform)一個物體。
矩陣是一種非常有用的數學工具,如果上過線性代數這門課的話應該都會有所了解。這篇主要討論一下在圖像處理中一些最簡單的矩陣變換,并且在OpenGL ES中進行實踐。
2、向量的乘
要討論矩陣,一定繞不開向量。向量可以說是最簡單的矩陣,可以把它理解為1n的矩陣或者是n1的矩陣,關于向量的加減運算和長度計算等實在太過基礎這邊不再討論。這邊關于向量的運算主要討論其乘法相關的運算。
2.1 點乘
向量的點乘一般用 v · k 這樣表示,兩個向量的點乘結果等于它們的數乘結果再乘兩個向量之間夾角的余弦值。公式如下:
等式右邊||v|| 和 ||k|| 分別代表兩個向量的長度。通過上面的公式也可以反推出兩個向量之間的夾角。而對于具體的兩個向量,其點乘就是對應位置上的數字相乘再進行累加。
2.2 叉乘
叉乘只在3D空間中有定義,它需要兩個不平行向量作為輸入,生成一個正交于兩個輸入向量的第三個向量。如果輸入的兩個向量也是正交的,那么叉乘之后將會產生3個互相正交的向量。下圖展示了3D空間中叉乘的樣子:
叉乘公式如下:
3、矩陣
上面討論的向量其實也是矩陣的一種,矩陣就是數字、符號或表達式數組。矩陣中每一項叫做矩陣的元素(Element)。下面是一個2×3矩陣的例子:
矩陣可以通過(i, j)進行索引,i是行,j是列,這就是上面的矩陣叫做2×3矩陣的原因。獲取上面4的索引是(2, 1)(第二行,第一列)(注:如果是圖像索引應該是(1, 2),先算列,再算行)。
下面討論一下一些矩陣的基本運算。
3.1 矩陣加減
矩陣的加減是矩陣最簡單的操作,矩陣和標量進行加減其標量值要加減到矩陣每一個值當中。矩陣與矩陣之間的加減就是兩個矩陣對應元素的加減運算,不過在相同索引下的元素才能進行運算。這也就是說加法和減法只對同維度的矩陣才是有定義的。
3.2 矩陣數乘
矩陣與標量相乘和矩陣與標量的加減一樣,會對每個數據進行乘。所以一般也用矩陣的數乘來縮放矩陣。
3.3 矩陣之間相乘
矩陣之間相乘會復雜許多,并且會有一些限制:
**① **只有當左側矩陣的列數與右側矩陣的行數相等,兩個矩陣才能相乘。
**② **矩陣相乘不遵守交換律,也就是說A ·B 和 B·A 并不相等。
矩陣相乘時是將左邊矩陣的i行和右邊矩陣的j列分別對應相乘并相加得到新矩陣i行j列位置上的值。結果矩陣的維度是(n, m),n等于左側矩陣的行數,m等于右側矩陣的列數。
4、矩陣變換
前面已經討論了矩陣和向量的一些基本操作,接下來討論通過這些操作來對矩陣進行一些常見的變換。
4.1 縮放
對一個向量進行縮放(Scaling)就是對向量的長度進行縮放,而保持它的方向不變。由于進行的是2維或3維操作,可以分別定義一個有2或3個縮放變量的向量,每個變量縮放一個軸(x、y或z)。
先來嘗試縮放向量v = (3,2)。可以把向量沿著x軸縮放0.5,使它的寬度縮小為原來的二分之一;將沿著y軸把向量的高度縮放為原來的兩倍。最終可得到如下向量s = (1.5,4):
OpenGL ES通常是在3D空間進行操作的,對于2D的情況可以把z軸縮放1倍,就相當于不縮放z軸。上圖的縮放操作是不均勻縮放,因為每個軸的縮放因子都不一樣。如果每個軸的縮放因子都一樣那么就叫均勻縮放。
下面構造一個變換矩陣來提供縮放功能。從單位矩陣(矩陣正斜對角線為1,其余為0的矩陣),每個對角線元素會分別與向量的對應元素相乘,而不會干擾其他維度上的向量值。對于需要將任意向量(x,y,x)分別縮放(S1,S2,S3)倍時,可以用這樣的特性來構造縮放矩陣。如下:
第四個縮放向量仍然是1,因為在3D空間中縮放w分量是無意義的。w分量另有其他用途,會在后面討論。
4.2 位移
位移是在原始向量的基礎上加上另一個向量從而獲得一個在不同位置的新向量的過程,從而在位移向量基礎上移動了原始向量。和縮放矩陣一樣,在4×4矩陣上有幾個特別的位置用來執行特定的操作,對于位移來說它們是第四列最上面的3個值。如果我們把位移向量表示為(Tx,Ty,Tz),就能把位移矩陣定義為:
因為所有的位移值都要乘以向量的w行,所以位移值會加到向量的原始值上。而如果用3x3矩陣位移值就沒地方放也沒地方乘了,所以是不行的。這也是為什么3維的向量要用四維變換矩陣進行。
這個向量的w分量也叫齊次坐標分量。想要從齊次向量得到3維向量,可以把x、y和z坐標分別除以w坐標。通常不會注意這個問題,因為w分量通常是1.0。使用齊次坐標的好處在于它允許在3D向量上進行位移(如果沒有w分量是不能位移向量的)。如果一個向量的齊次坐標分量值是0,這個坐標就是方向向量,因為w坐標是0,這個向量就不能位移。有了位移矩陣就可以在3個方向(x、y、z)上移動物體。
4.3 旋轉
上面幾個的變換內容相對容易理解,在二維或三維空間中也容易表示出來,但旋轉稍復雜些。
首先來定義一個向量的旋轉到底是什么。二維或三維空間中的旋轉用角來表示。角可以是角度制或弧度制的,周角是360角度或2 PI弧度。下圖中展示的二維向量v是由k向右旋轉72度所得的:
在三維空間中旋轉需要定義一個角和一個旋轉軸。物體會沿著給定的旋轉軸旋轉特定角度。當二維向量在三維空間中旋轉時,一般把旋轉軸設為z軸。
給定一個角度,可以把一個向量變換為一個經過旋轉的新向量。這通常是使用一系列正弦和余弦函數的各種巧妙的組合得到的。旋轉矩陣在三維空間中每個單位軸都有不同定義,旋轉角度用θ表示:
沿x軸旋轉:
沿y軸旋轉:
沿z軸旋轉:
利用旋轉矩陣可以把任意位置向量沿一個單位旋轉軸進行旋轉。也可以將多個矩陣復合,比如先沿著x軸旋轉再沿著y軸旋轉。但是這會很快導致一個問題——萬向節死鎖。對于三維空間中的旋轉,一個更好的模型是沿著任意的一個軸旋轉。這樣的一個旋轉矩陣是存在的,但其非常復雜這邊不進行展開討論。
4.4 組合變換
使用矩陣進行變換的真正厲害之處在于,根據矩陣之間的乘法,可以把多個變換組合到一個矩陣中。假設有一個頂點(x, y, z),希望將其縮放2倍,然后位移(1, 2, 3)個單位。需要一個位移和縮放矩陣來完成這些變換。其最終的組合變換矩陣如下:
當矩陣相乘時先寫位移再寫縮放變換。矩陣乘法是不遵守交換律的,這意味著它們的順序很重要。當矩陣相乘時,在最右邊的矩陣是第一個與向量相乘的,所以應該從右向左讀這個乘法。建議在設計組合矩陣時,先進行縮放操作,然后是旋轉,最后才是位移,否則它們會互相影響。比如,如果先位移再縮放,位移的向量也會同樣被縮放。
用最終的變換矩陣左乘我們的向量會得到以下結果:
如上所示,其目標向量確實放大來兩倍并進行了位移。
5、OpenGLES中的矩陣
前面已經討論完了矩陣的基本知識及操作,接下來討論下關于OpenGLES 中矩陣的應用。OpenGLES中有一個類是專門用來進行矩陣處理的android.opengl.Matrix。這個類里面包括了對于矩陣的各種前面提到的處理變換操作。
**① **Matrix.setIdentityM() :用來創建一個單位矩陣。其中第一個參數是創建出來的單位矩陣存儲的地方,是一個float類型的一維數組。第二個參數是存儲的數據位置的偏移量,也就是說從哪里開始存儲。生成的結果先按照列優先存儲的,也就是說先存放第一列的數據,再存放第二列的數據,以此類推。前面理論部分已經提到,所有變換都是基于單位矩陣的基礎上進行的,所以第一步創建單位矩陣是必須的。
**② **Matrix.rotateM() :用來進行旋轉變換的。第一個參數是需要變換的矩陣;第二參數是偏移量;第三個參數是旋轉角度,這邊是以角度制,也就是說是0-360這個范圍;第四、五、六個參數分別代表旋轉軸向量的x,y,z值。如果x=0,y=0,z = 1 就相當于以z軸為旋轉軸進行旋轉,其他類似。
**③ **Matrix.translateM() :用來進行圖像的位移,第一個參數是需要變換的矩陣;第二個參數是偏移量;第三、四、五個參數分別對應x,y,z 方向的位移量。其以圖像自身x,y,z方向為單位,也就是說當x方向位移量為0.5時,相當于向右移動0.5個身位,其他類似。
**④ **Matrix.scaleM():用來進行圖像的縮放,第一個參數是需要變換的矩陣;第三、四、五個參數分別對應x,y,z 方向的縮放比例,當x方向縮放為0.5時,相當于向x方向縮放為原來的0.5倍,其他類似。
前面說過這些變換是可以進行組合運算的,其組合代碼如下:
Triangle.kt
init{
...
Matrix.setIdentityM(mTransMatrix, 0)
Matrix.scaleM(mTransMatrix,0,2f,0.5f,1f)
Matrix.rotateM(mTransMatrix, 0, mAngle, 0f, 0f, 1.0f)
Matrix.translateM(mTransMatrix, 0, 0.5f, 0.5f, 0f)
...
}
之間用上面的操作相當于生成了一個組合矩陣,其效果如下圖:
光生成了變換矩陣還不能完成上述操作,還需要將變換矩陣數據傳入到頂點著色器上:
// Triangle.kt
private val vertexShaderCode =
...
"uniform mat4 transform;" +
"void main() {" +
" gl_Position = transform * vec4(aPos, 1.0);" +
...
"}"
fun draw() {
...
val transformLoc = GLES30.glGetUniformLocation(mProgram, "transform")
GLES30.glUniformMatrix4fv(transformLoc, 1, false, mTransMatrix, 0)
...
}