1、概述
在上一篇文章OpenGL ES 顯示圖形(上)中已經實現了顯示三角形,但是遇到了一些問題,也就是說這個三角形和實際需求的不一樣,存在變形拉伸。關于這個問題出現的原因是因為在Android設備上顯示圖形它們的屏幕尺寸和形狀可能不同。OpenGL采用方形,均勻的坐標系統,默認情況下,可以將這些坐標快速繪制到典型的非方形屏幕上,就好像它是完全正方形一樣。
上圖左側顯示了OpenGL框架的統一坐標系統,以及這些坐標如何實際映射到右側橫向的典型設備屏幕。要解決此問題,可以應用OpenGL投影模式和攝像機視圖來轉換坐標,以便圖形對象在任何顯示上都具有正確的比例。為了應用投影和攝像機視圖,需要創建投影矩陣和攝像機視圖矩陣,并將它們應用于OpenGL渲染管道。投影矩陣重新計算圖形的坐標,以便它們正確映射到Android設備屏幕。攝像機視圖矩陣創建一個變換,從特定的眼睛位置渲染對象。這邊就涉及到一個視見體的概念,其實OpenGL圖形所處與于一個三維的世界,可以想象一下在屏幕上顯示的其實是一個圖像的投影,而屏幕可以理解為一塊投影幕布,這樣就可以通過改變相機的坐標和旋轉移動投影來達到顯示在投影上的物體的形狀的拉伸與縮放。
如上圖所示前面的那個點就代表一個假想的攝像機點,或者可以理解為假想人眼所處位置,中間的那個屏幕是要展示的渲染的對象。最后一塊屏幕是手機屏幕。就可以通過移動假象攝像機點和手機屏幕來得到不同縮放和拉伸程度的圖像。當然,這些都是假象的,這邊的攝像機點不是指真的手機攝像頭,也不會真的需要通過移動手機屏幕來實現效果。這邊都是通過對圖像進行相關的矩陣變換來實現實際的想要的效果。移動假想的攝像機就相當于對原圖像其做一個攝像機矩陣變換,旋轉移動手機屏幕相當于對原圖做一個投影矩陣變換。下面還是繼續以之前一篇文章的Demo為例子,討論下用OpenGL ES 對這個問題的解決方法,以及圖像的動畫和交互。
2、投影和相機矩陣變換
如前面所說在OpenGL ES環境中,投影和攝像機矩陣允許假想以更接近用眼睛看物理對象的方式顯示繪制對象。這種物理觀察模擬是通過繪制對象坐標的數學變換完成的:
① 投影變換:此變換根據繪制對象的顯示位置的寬度和高度調整繪制對象的坐標GLSurfaceView。如果沒有這個變換,OpenGL ES繪制的對象會因視圖窗口的不等比例而拉伸。通常只需要在onSurfaceChanged()渲染器的方法中建立或更改OpenGL視圖的比例計算投影變換。
② 攝像機變換:此變換根據假想攝像機位置調整繪制對象的坐標。注意OpenGL ES不定義實際的相機對象,而是提供通過轉換繪制對象的顯示來模擬相機的效果。建立攝像機轉換時,可能只計算一次 GLSurfaceView,或者也可能會根據用戶操作或應用程序的功能動態更改。
2.1 定義投影變換
投影變換的數據在GLSurfaceView.Renderer類中onSurfaceChanged() 進行計算。以下示例代碼通過獲取GLSurfaceView的高度和寬度,通過使用其寬高來計算填充投影變換矩陣Matrix。
// mMVPMatrix是“模型視圖投影矩陣”(Model View Projection Matrix)的縮寫
private final float[] mMVPMatrix = new float[16];
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
Log.d(TAG,"onSurfaceChanged:width = "+width+",height = "+height);
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
// 此投影矩陣應用于onDrawFrame()方法中的對象坐標
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
投影矩陣的核心方法是frustumM(float[] m, int offset, float left, float right, float bottom, float top, float near, float far)
第一個參數是輸出的投影矩陣,第二個參數是從輸出的數組的什么位置開始寫入,第三到第六之后分別是投影對應于OpenGL ES默認坐標的對應上限的映射關系順序分別為左右下上。最后兩個參數有點抽象,用于顯示圖像的前面和背面,near和far需要結合假想攝相機即觀察者眼睛的位置來設置,例如setLookAtM()(下面會討論這個函數)中設置cx = 0, cy = 0, cz = 10,near設置的范圍需要是小于10才可以看得到繪制的圖像,如果大于10,圖像就會處于了觀察這眼睛的后面,這樣繪制的圖像就會消失在鏡頭前,far參數影響的是立體圖形的背面,far一定比near大,一般會設置得比較大,如果設置的比較小,一旦3D圖形尺寸很大,這時候由于far太小,這個投影矩陣就沒法容納圖形全部的背面,這樣3D圖形的背面會有部分無法顯示。
2.2 定義攝像機變換
通過在渲染器中添加攝像機視圖變換作為繪圖過程的一部分,完成變換繪制對象的過程。在以下示例代碼中,使用Matrix.setLookAtM() 方法計算攝像機視圖變換,然后將其與先前計算的投影矩陣組合。然后將組合的變換矩陣傳遞到繪制的形狀。
// MyGLRenderer.class
@Override
public void onDrawFrame(GL10 unused) {
...
// 設置相機的位置 (View matrix)
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
// 計算投影和視圖轉換
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
// 繪制三角形
mTriangle.draw(mMVPMatrix);
}
相機位置設置核心函數是setLookAtM(float[] rm, int rmOffset, float eyeX, float eyeY, float eyeZ, float centerX, float centerY, float centerZ, float upX, float upY, float upZ) 這個函數的參數非常多,這也側面印證了OpenGL ES 相當靈活而復雜。
第一個參數還是輸出的矩陣,第二個還是輸出的矩陣從那個下標開始寫入。之后幾個參數如下:eyeX、eyeY、eyeZ 假想相機或者說觀察者眼睛位置,centerX、centerY、centerZ 目標視圖中心位置,upX、upY、upZ相機角度。也就是說隨著upX、upY、upZ的變化可以理解為相機在全方位的旋轉。
結下來的multiplyMM(float[] result, int resultOffset, float[] lhs, int lhsOffset, float[] rhs, int rhsOffset); 這個方法是兩個矩陣相乘,如果學過線性代數肯定知道矩陣相乘的話AB 和 BA 得到的是不同的矩陣,所以這里面的順序不能對換。
2.3 應用投影和相機變換
為了使用上面的投影和攝像機視圖變換矩陣,首先將矩陣變量添加到先前在Triangle類中定義的頂點著色器中:
public class Triangle {
private final StringvertexShaderCode =
//此矩陣成員變量提供了一個鉤子來操縱使用此頂點著色器的對象的坐標
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"void main() {" +
// uMVPMatrix因子必須在*前面才能使矩陣乘法乘積正確。
" gl_Position = uMVPMatrix * vPosition;" +
"}";
// 用于訪問和設置模型視圖投影矩陣(mMVPMatrix)
private int mMVPMatrixHandle;
...
}
接下來,修改圖形對象的draw()方法以接受組合變換矩陣并將其應用于形狀:
public void draw(float[] mvpMatrix) { // 將投影矩陣傳入
...
// 獲取形狀變換矩陣的具柄
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
// 將模型視圖投影矩陣傳遞給著色器
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
// 繪制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// 禁用頂點數組
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
一旦正確計算并應用了投影和攝像機視圖轉換,圖形對象將以正確的比例繪制,并且應如下所示:
3、添加動畫
在屏幕上繪制的對象是OpenGL的一個非常基本的功能。OpenGL ES還提供了三維或以其他獨特方式移動和轉換繪制對象的附加功能,以創建非常酷炫的用戶體驗。其實所有這些動畫都和上面所述的圖像修正的核心原理是一樣的,都是通過對圖像進行各種矩陣變換來實現。
使用OpenGL ES 2.0旋轉繪圖對象,需要在渲染器中,創建另一個變換矩陣(旋轉矩陣),然后將其與投影和攝像機視圖變換矩陣組合:
// MyGLRenderer.class
private float[] mRotationMatrix = new float[16];
Override
public void onDrawFrame(GL10 gl) {
float[] scratch = new float[16];
...
// 創建旋轉矩陣
long time = SystemClock.uptimeMillis() % 4000L;
float angle = 0.090f * ((int) time);
Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f);
//將旋轉矩陣與投影和攝像機視圖結合時,mMVPMatrix因子必須在*前面才能使矩陣乘法乘積正確。
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// 繪制三角形
mTriangle.draw(scratch);
}
如果在進行這些更改后三角形不旋轉,請確保已注釋掉 GLSurfaceView.RENDERMODE_WHEN_DIRTY 設置。否則OpenGL只旋轉一個增量的形狀然后等待調用requestRender()來進行圖像重繪。
public MyGLSurfaceView(Context context) extends GLSurfaceView {
...
//setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
這邊要說明下,除非在確定沒有任何用戶交互的情況對圖像進行動畫,否則平時建議打開這個標志。下個小節會取消注釋此代碼。
4、觸摸事件交互
對象能運動之后,肯定會進一步思考如何使其實現和用戶的交互。 OpenGL ES圖形交互和普通的視圖于用戶比較類似。這里舉一個通過擴展GLSurfaceView以覆蓋 onTouchEvent()偵聽觸摸事件來進行交互的例子。
4.1 設置觸摸監聽器
為了使的OpenGL ES應用程序響應觸摸事件,必須在GLSurfaceView類中實onTouchEvent()方法 。下面的示例實現通過監聽 MotionEvent.ACTION_MOVE事件并將其轉換為形狀的旋轉角度的方法來實現觸摸交互。
// MyGLSurfaceView.class
private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
private float mPreviousX;
private float mPreviousY;
@Override
public boolean onTouchEvent(MotionEvent e) {
// MotionEvent報告觸摸屏和其他輸入控件的輸入詳細信息。
// 在這種情況下,這里只對觸摸位置發生變化的事件感興趣。
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = x - mPreviousX;
float dy = y - mPreviousY;
// reverse direction of rotation above the mid-line
if (y > getHeight() / 2) {
dx = dx * -1 ;
}
// reverse direction of rotation to left of the mid-line
if (x < getWidth() / 2) {
dy = dy * -1 ;
}
mRenderer.setAngle(
mRenderer.getAngle() +
((dx + dy) * TOUCH_SCALE_FACTOR));
requestRender();
}
mPreviousX = x;
mPreviousY = y;
return true;
}
這邊要注意,在計算旋轉角度后,需要調用 requestRender()告訴渲染器是時候進行重新渲染幀。在這個例子中,這樣做是最有效的,因為除非旋轉發生變化,否則不需要重繪幀。要實現這樣的效率,需要把上一節的setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);注釋取消掉。
4.2 暴露旋轉角度屬性
上面的示例代碼已經通過觸摸事件獲取到來要旋轉的角度,現在要將這個旋轉角度傳給渲染器進行渲染,所以要求通過添加公共成員來公開渲染器旋轉角度。由于渲染器代碼在與應用程序的主用戶界面線程在不同的線程上運行,因此必須將此公共變量聲明為volatile。以下是聲明變量并公開getter和setter對的代碼:
public class MyGLRenderer implements GLSurfaceView.Renderer {
...
public volatile float mAngle;
public float getAngle() {
return mAngle;
}
public void setAngle(float angle) {
mAngle = angle;
}
}
4.3、應用旋轉交互
要應用觸摸輸入生成的旋轉,請注釋掉前面的角度和添加的代碼mAngle,其中包含觸摸輸入生成的角度。
public void onDrawFrame(GL10 gl) {
...
float[] scratch = new float[16];
// 給三角形創建一個旋轉變換
// long time = SystemClock.uptimeMillis() % 4000L;
// float angle = 0.090f * ((int) time);
// Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f);
Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);
//將旋轉矩陣與投影和攝像機視圖結合時,mMVPMatrix因子必須在*前面才能使矩陣乘法乘積正確。
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// 繪制三角形
mTriangle.draw(scratch);
}
這樣就可以通過拖動屏幕實現三角形的動畫。