Android平臺下OpenGL初步
轉自網上,網上沒找到出處,只看到一些論壇中有這篇文章,組織的有點混亂,這篇文章感覺講的挺好的。
本文只關注於如何一步步實現在Android平臺下運用OpenGl。
1、GLSurfaceView
GLSurfaceView是Android應用程式中實現OpenGl畫圖的重要組成部分。GLSurfaceView中封裝了一個Surface。而android平臺下關於影象的現實,差不多都是由Surface來實現的。
2、Renderer
有了GLSurfaceView之後,就相當於我們有了畫圖的紙。現在我們所需要做的就是如何在這張紙上畫圖。所以我們需要一支筆。
Renderer是GLSurfaceView的內部靜態介面,它就相當於在此GLSurfaceView上作畫的筆。我們通過實現這個介面來“作畫”。最後通過GLSurfaceView的setRenderer(GLSurfaceView.Renderer renderer)方法,就可以將紙筆關聯起來了。
實現Renderer需要實現它的三個介面:onSurfaceCreated(GL10 gl, EGLConfig config)、 onSurfaceChanged(GL10 gl, int width, int height)、onDrawFrame(GL10 gl)。下面就這三個介面的具體意義做個簡單的介紹。
2.1、onSurfaceCreated
此方法看名字就知道它是在Surface建立的時候被呼叫的。因此我們可以在這個函式的實現中做一些初始化的工作。例如取出文房四寶、鋪好畫布、調好顏料之類的。它的函式原
型如下:
public abstract void onSurfaceCreated (GL10 gl, EGLConfig config)
第二個引數在文件中沒有關於它的任何public方法和域。因此我們可以不用管它。
第一個引數非常重要。如果說Renderer是畫筆的話,那麼這個gl引數,就可以說是我們的手了。如何操作這支畫筆,都是它說了算!所以我們絕大部分時候都是通過這個叫做gl的手來指揮Renderer畫圖的。
2.2 onSurfaceChanged
當GLSurfaceView大小改變時,對應的Surface大小也會改變。值得注意的是,在Surface剛建立的時候,它的size其實是0,也就是說在畫第一次圖之前它也會被呼叫一次的。(而且對於很多時候,Surface的大小是不會改變的,那麼此函式就只在建立之初被呼叫一次而已)
原型如下:
public abstract void onSurfaceChanged (GL10 gl, int width, int height)
同樣的,畫圖的手是必需的。
另外值得注意的是,它告訴了我們這張紙有多高多寬。這點很重要。因為在onSurfaceCreated的時候我們是不知道紙的寬高的,所以有一些和長寬相關的初始化工作還得在此函式中來做。
2.3 onDrawFrame
好了,我們的初始化工作做得差不多了,那麼現在就是該真刀真槍畫畫的時候了!此函式就是真正給你畫畫用的。每呼叫一次就畫一幅圖。可能的疑問是這個函式什麼時候會被調
用呢?最開始的時候肯定是會被呼叫的。以後會有兩種模式供你選擇:
- RENDERMODE_CONTINUOUSLY
- RENDERMODE_WHEN_DIRTY
第一種模式(RENDERMODE_CONTINUOUSLY):
連續不斷的刷,畫完一幅圖馬上又畫下一幅。這種模式很明顯是用來畫動畫的;
第二種模式(RENDERMODE_WHEN_DIRTY):
只有在需要重畫的時候才畫下一幅。這種模式就比較節約CPU和GPU一些,適合用來畫不經常需要重新整理的情況。多說一句,系統如何知道需要重畫了呢?當然是你要告訴它……
呼叫GLSurfaceView的requestRender ()方法,告訴它,你髒了。
這兩種模式在什麼地方設定呢? GLSurfaceView的setRenderMode(int renderMode)方法。可以供你設定你需要的重新整理模式。
還是來看看這個函式的原型吧: public abstract void onDrawFrame (GL10 gl) 很簡單,只有手。
3、 Android下OpenGL繪圖基本流程:
我們從畫一個三角形開始說起:
3.1 MyRender
經過前面的介紹,我們應該知道現在需要做的事,就是寫好Renderer的三個介面方法。
我們需要重新寫一個類實現它,然後重寫這三個方法。
class MyRender implements GLSurfaceView.Renderer
OK,筆已經拿好了。“鋪好紙”是非常關鍵的一步。雖然我們說GLSurfaceView就是我們作圖的紙,但是“鋪”好這張紙,卻也非常的重要。
下面我們重點講下,這紙該怎麼鋪。OpenGL這張紙可不是一般的紙啊。最起碼,它是三維的。然而實際的顯示螢幕卻是一個平面。所以這紙還不好“鋪”。
首先,不一定整張紙都用來作畫吧,Surface不一定全部都用上(當然,一般情況下我們是全部用上的)。那麼我們需要計劃好,哪部分割槽域用來作畫。
gl.glViewport(0, 0, width, height);
根據這裡的引數width和height,我們可以知道這個寬高需要在onSurfaceChanged裡得知。
然後,這一步很關鍵。如何在平面上畫三維座標的點或圖形呢?OpenGL有一個座標系,如下圖:
我們需要將這個座標系和我們的GLSurfaceView裡的Surface做一個對映關係。
glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
gl.glFrustumf(-400, 400, -240, 240, 0.3f, 100);
glMatrixMode(GL10.GL_PROJECTION); 是說我們現在改變的是座標系與Surface的對映關係(投影矩陣)。
下一句 gl.glLoadIdentity(); 是將以前的改變都清掉(之前對投影矩陣的任何改變)。
glFrustumf (float left, float right, float bottom, float top, float zNear, float zFar) 這個函式非常Powerful。它實現了Surface和座標系之間的對映關係。它是以透視投影的方式來進行對映的。
透視投影的意思見下圖:
對映說明:
1、 前面一個矩形表示的是我們平面作圖的範圍。即Surface的作圖範圍。它有四條邊,
我們將它們暫時命名edgeLeft、edgeRight、edgeTop、edgeBottom。
2、 glFrustumf 引數中的left、right、bottom、top指的是作圖範圍的四條edge在OpenGL x = -400的位置。同理top、right、bottom的值表示的是edgeRight、edgeTop、edgeBottom這幾條邊在座標系中的位置。
3、上面第二點定出了作圖範圍的x和y的範圍。那麼對於3D的OpenGL這張紙來說,我們還需要定出z的範圍。首先,要想象一下,相機或者眼睛在座標系的哪個位置?
預設的眼睛的位置在OpenGL座標的原點處(0,0,0)。視線方向是平行於Z軸往裡看。
near表示的是眼睛到作圖平面的距離(絕對值哦!),far表示眼睛到最遠可見處的平面範圍。於是,預設情況下z的作圖範圍就是在-near到-far的位置。
4、好了,我們已經確定好x、y、z方向的作圖範圍了。回過頭來,就可以發現,這張“立體”的紙,是一個方椎體切去頭部的平截頭體。我們所畫的物體座標落在這個區域範圍內的部分將可以被我們看到(即在螢幕裡畫出來)。OK,至此,我們把紙終於鋪好了。
glMatrixMode函式的選項(引數)有後面三種:GL_PROJECTION,GL_MODELVIEW和GL_TEXTURE;
- GL_PROJECTION,是投影的意思,就是要對投影相關進行操作,也就是把物體投影到一個平面上,就像我們照相一樣,把3維物體投到2維的平面上。這樣,接下來的語句可以是跟透視相關的函式,比如glFrustum()或gluPerspective();
- GL_MODELVIEW,是對模型視景的操作,接下來的語句描繪一個以模型為基礎的適應,這樣來設定引數,接下來用到的就是像gluLookAt()這樣的函式;
- GL_TEXTURE,就是對紋理相關進行操作;
順便說下,OpenGL裡面的操作,很多是基於對矩陣的操作的,比如位移,旋轉,縮放,所以,
這裡其實說的規範一點就是glMatrixMode是用來指定哪一個矩陣是當前矩陣,而它的引數代表要操作的目標:
- GL_PROJECTION是對投影矩陣操作;
- GL_MODELVIEW是對模型視景矩陣操作;
- GL_TEXTURE是對紋理矩陣進行隨後的操作;
3.2 畫圖之前先構圖
Android 的 OpenGL 作圖,不同於一般的作圖,這點我們不得不感慨。它將資料和畫完全分開來。例如,我們要畫一個三角形。很顯然,三角形有三個點。我們在畫圖之前首先要構圖,比如每個點在哪個地方。我們將這些資料放在一個一個數組緩衝區中,放好這些資料之後,再統一一起畫出來。
下面,主要講下,如何將頂點資料和顏色資料放入符合 Android OpenGL 的陣列緩衝區中。
首先我們要明白的是,OpenGL 是一個非常底層的畫圖介面,它所使用的緩衝區儲存結構是和我們的 java 程式中不相同的。Java 是大端位元組序(BigEdian),而 OpenGL 所需要的資料是小端位元組序(LittleEdian)。所以,我們在將 Java 的緩衝區轉化為 OpenGL 可用的緩衝區時需要作一些工作。
byte 資料緩衝區
不管我們的資料是整型的還是浮點型的,為了完成 BigEdian 到 LittleEdian 的轉換,我們都首先需要一個 ByteBuffer。我們通過它來得到一個可以改變位元組序的緩衝區。
ByteBuffer mBuffer = ByteBuffer.allocateDirect(pointCount*dimension*4);
mBuffer.order(ByteOrder.nativeOrder());
注意,我們應該用“allocateDirect”來分配記憶體空間,因為這樣才可以 order 排序。最後我們就可以通過:
resultBuffer = mBuffer.asFloatBuffer();
resultBuffer = mBuffer.asIntBuffer();
將位元組緩衝區轉化為整型或者浮點型緩衝區了。
3.3 終於畫圖了!
有了前面所有的準備工作之後,有一個好訊息可以告訴你,我們終於可以畫圖了!而且,有了前面這麼多工作,真正畫圖的工作其實比較簡單。
3.3.1、清理好你的紙
前面我們說過了,在畫圖之前,一定要把紙給清理乾淨嘍:
gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
另外,之前我們在對映座標系的時候,用了glMatrixMode(GL10.GL_PROJECTION);來指定改變的是投影矩陣。那麼現在要畫圖了,所以我們需要指定改變的是“檢視矩陣”:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
3.3.2、啟用陣列
我們的前面說過,畫圖的資料都放在陣列緩衝區裡,最後再一起傳過來作畫。那麼我們首先要告訴 OpenGL,我們需要用到哪些陣列。例如我們需要頂點陣列和顏色陣列:
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
3.3.3、指定陣列資料
我們前面已經構造好了我們的資料緩衝區,floatBuffer(或 IntBuffer)。現在我們只需要將這個資料緩衝區和對應的功能繫結起來就好了:
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, VertexBuffer);
gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorBuffer);
這兩句話分別綁定了頂點資料陣列和顏色資料陣列。其中第一個引數表示的是每個點有幾個座標。例如頂點,有 x、y、z值,所以是 3;而顏色是 r、g、b、a 值,所以是 4。
3.3.4、畫圖!
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);
第一個引數指明瞭畫圖的型別——三角形(android 似乎只支援畫三角形、點、線,不支援畫多邊形)。後面兩個引數指明,從哪個頂點開始畫,畫多少個頂點。
OK!至此,我們的第一個三角形就畫出來了,來看看效果吧。