1、概述
前面幾篇關(guān)于OpenGLES的文章:
有討論到關(guān)于圖像的變換和移動懒豹,前面這些變換是圖像基于每一幀改變物體的頂點(diǎn)并且重配置緩沖區(qū)從而使它們移動拉岁,但這太繁瑣了,而且會消耗很多的處理時間【辛欤現(xiàn)在有一個更好的解決方案膝晾,使用(多個)矩陣(Matrix)對象可以更好的變換(Transform)一個物體亩冬。
矩陣是一種非常有用的數(shù)學(xué)工具,如果上過線性代數(shù)這門課的話應(yīng)該都會有所了解公条。這篇主要討論一下在圖像處理中一些最簡單的矩陣變換拇囊,并且在OpenGL ES中進(jìn)行實(shí)踐。
2靶橱、向量的乘
要討論矩陣寥袭,一定繞不開向量。向量可以說是最簡單的矩陣关霸,可以把它理解為1n的矩陣或者是n1的矩陣传黄,關(guān)于向量的加減運(yùn)算和長度計(jì)算等實(shí)在太過基礎(chǔ)這邊不再討論。這邊關(guān)于向量的運(yùn)算主要討論其乘法相關(guān)的運(yùn)算队寇。
2.1 點(diǎn)乘
向量的點(diǎn)乘一般用 v · k 這樣表示膘掰,兩個向量的點(diǎn)乘結(jié)果等于它們的數(shù)乘結(jié)果再乘兩個向量之間夾角的余弦值。公式如下:
等式右邊||v|| 和 ||k|| 分別代表兩個向量的長度佳遣。通過上面的公式也可以反推出兩個向量之間的夾角识埋。而對于具體的兩個向量,其點(diǎn)乘就是對應(yīng)位置上的數(shù)字相乘再進(jìn)行累加零渐。
2.2 叉乘
叉乘只在3D空間中有定義窒舟,它需要兩個不平行向量作為輸入,生成一個正交于兩個輸入向量的第三個向量相恃。如果輸入的兩個向量也是正交的辜纲,那么叉乘之后將會產(chǎn)生3個互相正交的向量。下圖展示了3D空間中叉乘的樣子:
叉乘公式如下:
3拦耐、矩陣
上面討論的向量其實(shí)也是矩陣的一種耕腾,矩陣就是數(shù)字、符號或表達(dá)式數(shù)組杀糯。矩陣中每一項(xiàng)叫做矩陣的元素(Element)扫俺。下面是一個2×3矩陣的例子:
矩陣可以通過(i, j)進(jìn)行索引,i是行固翰,j是列狼纬,這就是上面的矩陣叫做2×3矩陣的原因羹呵。獲取上面4的索引是(2, 1)(第二行,第一列)(注:如果是圖像索引應(yīng)該是(1, 2)疗琉,先算列冈欢,再算行)。
下面討論一下一些矩陣的基本運(yùn)算盈简。
3.1 矩陣加減
矩陣的加減是矩陣最簡單的操作凑耻,矩陣和標(biāo)量進(jìn)行加減其標(biāo)量值要加減到矩陣每一個值當(dāng)中。矩陣與矩陣之間的加減就是兩個矩陣對應(yīng)元素的加減運(yùn)算柠贤,不過在相同索引下的元素才能進(jìn)行運(yùn)算香浩。這也就是說加法和減法只對同維度的矩陣才是有定義的。
3.2 矩陣數(shù)乘
矩陣與標(biāo)量相乘和矩陣與標(biāo)量的加減一樣臼勉,會對每個數(shù)據(jù)進(jìn)行乘邻吭。所以一般也用矩陣的數(shù)乘來縮放矩陣。
3.3 矩陣之間相乘
矩陣之間相乘會復(fù)雜許多宴霸,并且會有一些限制:
**① **只有當(dāng)左側(cè)矩陣的列數(shù)與右側(cè)矩陣的行數(shù)相等囱晴,兩個矩陣才能相乘。
**② **矩陣相乘不遵守交換律猖败,也就是說A ·B 和 B·A 并不相等速缆。
矩陣相乘時是將左邊矩陣的i行和右邊矩陣的j列分別對應(yīng)相乘并相加得到新矩陣i行j列位置上的值降允。結(jié)果矩陣的維度是(n, m)恩闻,n等于左側(cè)矩陣的行數(shù),m等于右側(cè)矩陣的列數(shù)剧董。
4幢尚、矩陣變換
前面已經(jīng)討論了矩陣和向量的一些基本操作,接下來討論通過這些操作來對矩陣進(jìn)行一些常見的變換翅楼。
4.1 縮放
對一個向量進(jìn)行縮放(Scaling)就是對向量的長度進(jìn)行縮放尉剩,而保持它的方向不變。由于進(jìn)行的是2維或3維操作毅臊,可以分別定義一個有2或3個縮放變量的向量理茎,每個變量縮放一個軸(x、y或z)管嬉。
先來嘗試縮放向量v = (3,2)皂林。可以把向量沿著x軸縮放0.5蚯撩,使它的寬度縮小為原來的二分之一础倍;將沿著y軸把向量的高度縮放為原來的兩倍。最終可得到如下向量s = (1.5,4):
OpenGL ES通常是在3D空間進(jìn)行操作的胎挎,對于2D的情況可以把z軸縮放1倍沟启,就相當(dāng)于不縮放z軸忆家。上圖的縮放操作是不均勻縮放,因?yàn)槊總€軸的縮放因子都不一樣德迹。如果每個軸的縮放因子都一樣那么就叫均勻縮放芽卿。
下面構(gòu)造一個變換矩陣來提供縮放功能。從單位矩陣(矩陣正斜對角線為1胳搞,其余為0的矩陣)蹬竖,每個對角線元素會分別與向量的對應(yīng)元素相乘,而不會干擾其他維度上的向量值流酬。對于需要將任意向量(x,y,x)分別縮放(S1,S2,S3)倍時币厕,可以用這樣的特性來構(gòu)造縮放矩陣。如下:
第四個縮放向量仍然是1芽腾,因?yàn)樵?D空間中縮放w分量是無意義的旦装。w分量另有其他用途,會在后面討論摊滔。
4.2 位移
位移是在原始向量的基礎(chǔ)上加上另一個向量從而獲得一個在不同位置的新向量的過程阴绢,從而在位移向量基礎(chǔ)上移動了原始向量。和縮放矩陣一樣艰躺,在4×4矩陣上有幾個特別的位置用來執(zhí)行特定的操作呻袭,對于位移來說它們是第四列最上面的3個值。如果我們把位移向量表示為(Tx,Ty,Tz)腺兴,就能把位移矩陣定義為:
因?yàn)樗械奈灰浦刀家艘韵蛄康膚行左电,所以位移值會加到向量的原始值上。而如果用3x3矩陣位移值就沒地方放也沒地方乘了页响,所以是不行的篓足。這也是為什么3維的向量要用四維變換矩陣進(jìn)行。
這個向量的w分量也叫齊次坐標(biāo)分量闰蚕。想要從齊次向量得到3維向量栈拖,可以把x、y和z坐標(biāo)分別除以w坐標(biāo)没陡。通常不會注意這個問題涩哟,因?yàn)閣分量通常是1.0。使用齊次坐標(biāo)的好處在于它允許在3D向量上進(jìn)行位移(如果沒有w分量是不能位移向量的)盼玄。如果一個向量的齊次坐標(biāo)分量值是0贴彼,這個坐標(biāo)就是方向向量,因?yàn)閣坐標(biāo)是0强岸,這個向量就不能位移锻弓。有了位移矩陣就可以在3個方向(x、y蝌箍、z)上移動物體青灼。
4.3 旋轉(zhuǎn)
上面幾個的變換內(nèi)容相對容易理解暴心,在二維或三維空間中也容易表示出來,但旋轉(zhuǎn)稍復(fù)雜些杂拨。
首先來定義一個向量的旋轉(zhuǎn)到底是什么专普。二維或三維空間中的旋轉(zhuǎn)用角來表示。角可以是角度制或弧度制的弹沽,周角是360角度或2 PI弧度檀夹。下圖中展示的二維向量v是由k向右旋轉(zhuǎn)72度所得的:
在三維空間中旋轉(zhuǎn)需要定義一個角和一個旋轉(zhuǎn)軸。物體會沿著給定的旋轉(zhuǎn)軸旋轉(zhuǎn)特定角度策橘。當(dāng)二維向量在三維空間中旋轉(zhuǎn)時炸渡,一般把旋轉(zhuǎn)軸設(shè)為z軸。
給定一個角度丽已,可以把一個向量變換為一個經(jīng)過旋轉(zhuǎn)的新向量蚌堵。這通常是使用一系列正弦和余弦函數(shù)的各種巧妙的組合得到的。旋轉(zhuǎn)矩陣在三維空間中每個單位軸都有不同定義沛婴,旋轉(zhuǎn)角度用θ表示:
沿x軸旋轉(zhuǎn):
沿y軸旋轉(zhuǎn):
沿z軸旋轉(zhuǎn):
利用旋轉(zhuǎn)矩陣可以把任意位置向量沿一個單位旋轉(zhuǎn)軸進(jìn)行旋轉(zhuǎn)吼畏。也可以將多個矩陣復(fù)合,比如先沿著x軸旋轉(zhuǎn)再沿著y軸旋轉(zhuǎn)嘁灯。但是這會很快導(dǎo)致一個問題——萬向節(jié)死鎖泻蚊。對于三維空間中的旋轉(zhuǎn),一個更好的模型是沿著任意的一個軸旋轉(zhuǎn)丑婿。這樣的一個旋轉(zhuǎn)矩陣是存在的性雄,但其非常復(fù)雜這邊不進(jìn)行展開討論。
4.4 組合變換
使用矩陣進(jìn)行變換的真正厲害之處在于枯冈,根據(jù)矩陣之間的乘法毅贮,可以把多個變換組合到一個矩陣中办悟。假設(shè)有一個頂點(diǎn)(x, y, z)尘奏,希望將其縮放2倍,然后位移(1, 2, 3)個單位病蛉。需要一個位移和縮放矩陣來完成這些變換炫加。其最終的組合變換矩陣如下:
當(dāng)矩陣相乘時先寫位移再寫縮放變換。矩陣乘法是不遵守交換律的铺然,這意味著它們的順序很重要俗孝。當(dāng)矩陣相乘時,在最右邊的矩陣是第一個與向量相乘的魄健,所以應(yīng)該從右向左讀這個乘法赋铝。建議在設(shè)計(jì)組合矩陣時,先進(jìn)行縮放操作沽瘦,然后是旋轉(zhuǎn)革骨,最后才是位移农尖,否則它們會互相影響。比如良哲,如果先位移再縮放盛卡,位移的向量也會同樣被縮放。
用最終的變換矩陣左乘我們的向量會得到以下結(jié)果:
如上所示筑凫,其目標(biāo)向量確實(shí)放大來兩倍并進(jìn)行了位移滑沧。
5、OpenGLES中的矩陣
前面已經(jīng)討論完了矩陣的基本知識及操作巍实,接下來討論下關(guān)于OpenGLES 中矩陣的應(yīng)用滓技。OpenGLES中有一個類是專門用來進(jìn)行矩陣處理的android.opengl.Matrix。這個類里面包括了對于矩陣的各種前面提到的處理變換操作棚潦。
**① **Matrix.setIdentityM() :用來創(chuàng)建一個單位矩陣殖属。其中第一個參數(shù)是創(chuàng)建出來的單位矩陣存儲的地方,是一個float類型的一維數(shù)組瓦盛。第二個參數(shù)是存儲的數(shù)據(jù)位置的偏移量洗显,也就是說從哪里開始存儲。生成的結(jié)果先按照列優(yōu)先存儲的原环,也就是說先存放第一列的數(shù)據(jù)挠唆,再存放第二列的數(shù)據(jù),以此類推嘱吗。前面理論部分已經(jīng)提到玄组,所有變換都是基于單位矩陣的基礎(chǔ)上進(jìn)行的,所以第一步創(chuàng)建單位矩陣是必須的谒麦。
**② **Matrix.rotateM() :用來進(jìn)行旋轉(zhuǎn)變換的俄讹。第一個參數(shù)是需要變換的矩陣;第二參數(shù)是偏移量绕德;第三個參數(shù)是旋轉(zhuǎn)角度患膛,這邊是以角度制,也就是說是0-360這個范圍耻蛇;第四踪蹬、五、六個參數(shù)分別代表旋轉(zhuǎn)軸向量的x臣咖,y跃捣,z值。如果x=0夺蛇,y=0,z = 1 就相當(dāng)于以z軸為旋轉(zhuǎn)軸進(jìn)行旋轉(zhuǎn)疚漆,其他類似。
**③ **Matrix.translateM() :用來進(jìn)行圖像的位移,第一個參數(shù)是需要變換的矩陣娶聘;第二個參數(shù)是偏移量灵临;第三、四趴荸、五個參數(shù)分別對應(yīng)x,y,z 方向的位移量儒溉。其以圖像自身x,y发钝,z方向?yàn)閱挝欢倩粒簿褪钦f當(dāng)x方向位移量為0.5時,相當(dāng)于向右移動0.5個身位酝豪,其他類似涛碑。
**④ **Matrix.scaleM():用來進(jìn)行圖像的縮放,第一個參數(shù)是需要變換的矩陣孵淘;第三蒲障、四、五個參數(shù)分別對應(yīng)x,y,z 方向的縮放比例瘫证,當(dāng)x方向縮放為0.5時揉阎,相當(dāng)于向x方向縮放為原來的0.5倍,其他類似背捌。
前面說過這些變換是可以進(jìn)行組合運(yùn)算的毙籽,其組合代碼如下:
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)
...
}
之間用上面的操作相當(dāng)于生成了一個組合矩陣,其效果如下圖:
光生成了變換矩陣還不能完成上述操作毡庆,還需要將變換矩陣數(shù)據(jù)傳入到頂點(diǎn)著色器上:
// 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)
...
}