3D應用程序開發(fā)中,一項很重要的工作就是對場景中的物體進行各種投影與變換。相比于OpenGL ES 1.x的封閉模式,OpenGL ES 2.0在變換方面采取了開放模式,其API中不再提供完成各種變換的方法,變換所有的矩陣都由開發(fā)人員直接提供給渲染管線。進行投影與變換時,需要一些線性代數(shù)的知識。
OpenGL映屏坐標系
在Android系統(tǒng)中,普通View的坐標系是把屏幕的左上角當作坐標原點,向右是x的正方向,向下是y的正方向。OpenGL映屏坐標系是標準的數(shù)學坐標系(笛卡爾坐標系),坐標原點是屏幕的左下方。
向右是x的正方向,向上是y的正方向。如下圖:

OpenGL統(tǒng)一視圖
OpenGL使用一個規(guī)格化正方形來表示統(tǒng)一視圖,其實就是一個(-1.0,-1.0,1.0,1.0)的Rect.

所有圖像都是首先映射到這個統(tǒng)一視圖中,然后等比例投影到手機的屏幕上。當然也可以理解,所有屏的大小就是一個規(guī)格化的(-1.0,-1.0,1.0,1.0)的Rect.
攝像機的設置
在真實的世界,我們用眼睛看物體,眼睛的位置、姿態(tài)的不同,就算是對同一場景進行觀察,我們所看到景像也是不一樣的。在OpenGL虛擬世界中,會虛擬一個攝像機,來表示人眼觀察世界。攝像機的設置來模擬人眼的位置、姿態(tài)等信息。
攝像機的設置主要需要給出3方面的信息,包括攝像機的位置、觀察的方向以及up方向。

攝像機的位置很容易理解,可以用3D空間的坐標表示。
攝像機的觀察的方向可以理解為攝像機鏡頭的指向,可以用攝像機的位置和觀察目標點確定的向量表示。
攝像機的up方向可以理解攝像機為了確定視角的向量。
人眼觀察現(xiàn)實世界,也是這樣的,如下圖:

從上圖可以看出,OpenGL虛擬世界模擬現(xiàn)實世界時,攝像機的位置、朝向、up方向可以有很多不同的組合。同樣的位置可以有不同的朝向、不同的up方向;不同的位置也可以具有相同的朝向,相同的up方向等。
OpenGL可以通過調(diào)用Matrix類的setLookAtM方法來完成對攝像機的設置,代碼如下:
Matrix.setLookAtM (
mVMatrix, // 攝像機矩陣
0, // 起始偏移量
cx,cy,cz, // 攝像機的位置 x、y、z的坐標
tx,ty,tz, // 觀察目標點 x、y、z的坐標
upx,upy,upz // up向量在x、y、z軸上的分量
);
投影方式
在《OpenGL ES基礎》的介紹中,我們知道在圖元裝配之后光柵化階段前,首先需要將虛擬3D世界中的物體投影到二維平面上。OpenGL ES 2.0中常用的投影模式有兩種,分別為正交投影與透視投影。
投影概念
OpenGL中,根據(jù)應用程序提供的投影矩陣,管線會確定一個可視空間區(qū)域,稱之為視景體。視景體是由6個平面確定的,這6個平面分別為:上平面(up)、下平面(down)、左平面(left) 、右平面(right)、遠平面(far)、近平面(near)。

場景中處于視景體內(nèi)的的物體會被投影到近平面上,視景體外面的物體將被裁剪掉。然后再將近平面的上投影出內(nèi)容映射到屏幕上的視口中。

正交投影
正交投影是平行投影的一種,其投影線(物體的頂點與近平面上投影點的連線)是平行的,因此它的視景體為長方體,投影到近平面上的圖形就是物體的平面鏡像。
正交投影的視景體如下圖:

正交投影的圖形效果如下圖:

設置正交投影的代碼:
Matrix.orthoM (
mProjectMatrix, // 投影矩陣
0, // 起始偏移量
left,right, // near平面的left、right
bottom,top, // near平面的bottom、top
near,far // near平面、far平面與視點的距離
);
透視投影
現(xiàn)實世界中人眼觀察物體會有“近大遠小”的效果,因此要想開發(fā)出更加真實的場景,僅使用正交投影是遠遠不夠的,這時可以采用透視投影。透視投影的投影線是不平行的,他們相交于視點。通過透視投影,可以產(chǎn)生現(xiàn)實世界中“近大遠小”的效果,大部分3D圖形采用的都是透視投影。
透視投影的視景體是錐形體。上面介紹投影概念時,使用的就是透視投影。視景體不再復畫,投影效果如下:

透視投影的設置代碼:
Matrix.frustumM (
mProjectionMatrix, // 投影矩陣
0 , // 起始偏移量
left,right, // near平面的left、right
bottom,top, // near平面的bottom、?top
near,far // near平面、far平面與視點的矩離
) ;
Matrix變換
在《OpenGL ES基礎》中說過,數(shù)字圖像的本質(zhì)就是一個像素矩陣。因此對數(shù)字圖像的處理,可以理解為是對矩陣的各種變換。Matrix變換是非常重要的。
Matrix變換的數(shù)學知識
Matrix變換都是通過將表示點坐標的向量與特定的變換矩陣相乘完成的,進行變換時,三維空間中點的位置需要表示成齊次坐標形式。所謂齊次坐標形式也就是在x、y、z 3個坐標值后面增加第四個量w,未變換時w值一般為1。這樣設計是為了計算方便。
例如:點P(Px,Py,Pz,1)與一個矩陣相乘即可完成一次變換,得到變換后的點Q(Qx,Qy,Qz,1)。

當矩陣M中的元素取適當?shù)闹禃r,等式Q=MP就會有其特殊的幾何意義,例如,可以將三維空間中的點P平移、旋轉(zhuǎn)、縮放到點Q。這些變換的具體信息就存放在矩陣M中,因此通常矩陣M為變換矩陣。當對一個圖像進行變換時,讓圖像的所有像素點乘以一個變換矩陣,就是可以得到變換后的另一個圖像。
平移變換

縮放變換

放轉(zhuǎn)變換
介紹旋轉(zhuǎn)變換的矩陣格式前,首先需要知道繞坐標軸或任意軸旋轉(zhuǎn)的一些規(guī)則。OpenGL中,旋轉(zhuǎn)角度的正負可以用右手螺旋定則來確定。螺旋定則:右手握住旋轉(zhuǎn)軸,使大姆指指向旋轉(zhuǎn)軸的正方向,4指環(huán)繞的方向即為旋轉(zhuǎn)的正方向,也就是旋轉(zhuǎn)角度為正值。下圖為繞任意軸r的旋轉(zhuǎn)Φ的矩陣:

具體推導過程可以參考:
http://www.cppblog.com/lovedday/archive/2008/01/12/41031.html
一般情況下,對繞任意軸的旋轉(zhuǎn)變化,可以拆分為繞x、y、z軸的旋轉(zhuǎn)變化的組合。
操作代碼
/**
* Translates matrix m by x, y, and z in place.
*
* @param m matrix
* @param mOffset index into m where the matrix starts
* @param x translation factor x
* @param y translation factor y
* @param z translation factor z
*/
public static void translateM(
float[] m, int mOffset,
float x, float y, float z) {
for (int i=0 ; i<4 ; i++) {
int mi = mOffset + i;
m[12 + mi] += m[mi] * x + m[4 + mi] * y + m[8 + mi] * z;
}
}
/**
* Scales matrix m in place by sx, sy, and sz.
*
* @param m matrix to scale
* @param mOffset index into m where the matrix starts
* @param x scale factor x
* @param y scale factor y
* @param z scale factor z
*/
public static void scaleM(float[] m, int mOffset,
float x, float y, float z) {
for (int i=0 ; i<4 ; i++) {
int mi = mOffset + i;
m[ mi] *= x;
m[ 4 + mi] *= y;
m[ 8 + mi] *= z;
}
}
/**
* Rotates matrix m in place by angle a (in degrees)
* around the axis (x, y, z).
*
* @param m source matrix
* @param mOffset index into m where the matrix starts
* @param a angle to rotate in degrees
* @param x X axis component
* @param y Y axis component
* @param z Z axis component
*/
public static void rotateM(float[] m, int mOffset,
float a, float x, float y, float z) {
synchronized(sTemp) {
setRotateM(sTemp, 0, a, x, y, z);
multiplyMM(sTemp, 16, m, mOffset, sTemp, 0);
System.arraycopy(sTemp, 16, m, mOffset, 16);
}
}