1、概述
Android框架提供了大量標(biāo)準(zhǔn)工具,用于創(chuàng)建有吸引力的功能性圖形用戶界面。如果想要更多地控制應(yīng)用程序在屏幕上繪制的內(nèi)容,或者想要繪制三維圖形,則需要使用不同的工具。Android框架提供的OpenGL ES提供了一組工具,用于顯示高端動畫圖形,并且還可以受益于許多Android設(shè)備上提供的圖形處理單元(GPU)的加速。這邊主要初步使用OpenGL ES來顯示圖形并與之交互。這邊采用的OpenGL ES 2.0,使用這個(gè)版本的原因是OpenGL ES 1.x的版本和2.0基本是兩套框架,許多東西不兼容而且過時(shí),而現(xiàn)在比較新的OpenGL ES 3.x的版本是兼容和復(fù)用2.0的接口的,所以這邊以2.0版本作為切入點(diǎn)來討論。我之前有一篇ARCore的文章——ARCore 相關(guān),里面就有提到OpenGL ES 3.x來實(shí)現(xiàn)相關(guān)AR功能。這篇文章主要討論最最基礎(chǔ)的一些OpenGL ES 2.0操作,來實(shí)現(xiàn)用OpenGL ES在Android系統(tǒng)上顯示圖形,并與之交互。
2、構(gòu)建環(huán)境
要在Android應(yīng)用程序中使用OpenGL ES繪制圖形,必須為它們創(chuàng)建一個(gè)視圖容器。其中一種比較直接的方法是實(shí)現(xiàn)一個(gè) GLSurfaceView和一個(gè)GLSurfaceView.Renderer。
① GLSurfaceView:一個(gè)用OpenGL ES來繪制圖片的視圖容器。這個(gè)類是一個(gè)View可以使用OpenGL API調(diào)用繪制和操作對象的類,在功能上類似于SurfaceView。
② GLSurfaceView.Renderer:用于控制將什么內(nèi)容顯示在GLSurfaceView上。這是一個(gè)接口類,此接口定義了在一個(gè)GLSurfaceView中繪制圖形所需的方法。必須將此接口的實(shí)現(xiàn)作為單獨(dú)的類提供,并使用GLSurfaceView.setRenderer()將其附加到GLSurfaceView實(shí)例中 。GLSurfaceView.Renderer接口要求實(shí)現(xiàn)以下方法:
onSurfaceCreated():創(chuàng)建GLSurfaceView的時(shí)候會調(diào)用一次該方法。使用此方法執(zhí)行僅需要發(fā)生一次的操作,例如設(shè)置OpenGL環(huán)境參數(shù)或初始化OpenGL圖形對象。
onDrawFrame():GLSurfaceView在每次重繪時(shí)調(diào)用此方法。使用此方法作為繪制(和重新繪制)圖形對象的主要執(zhí)行點(diǎn)。
onSurfaceChanged():GLSurfaceView在幾何圖形更改時(shí)調(diào)用此方法,包括更改GLSurfaceView設(shè)備屏幕的大小或方向。例如,當(dāng)設(shè)備從縱向更改為橫向時(shí),系統(tǒng)會調(diào)用此方法。使用此方法響應(yīng)GLSurfaceView容器更改時(shí)需要進(jìn)行的操作。
GLSurfaceView只是將OpenGL ES圖形合并到應(yīng)用程序中的一種方法。對于全屏或近全屏圖形視圖,這是一個(gè)合理的選擇。想要在布局的一小部分中加入OpenGL ES圖形可以使用TextureView。也可以使用構(gòu)建OpenGL ES視圖SurfaceView,這樣更靈活但這需要編寫相當(dāng)多的額外代碼。
2.1 添加聲明
為了使應(yīng)用程序能夠使用OpenGL ES 2.0 API,必須在manifest中添加以下聲明:
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
如果應(yīng)用程序使用紋理壓縮,則還必須聲明應(yīng)用程序支持哪種壓縮格式,以便它僅安裝在兼容設(shè)備上。
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
<supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />
這邊說到紋理壓縮指可以通過減少內(nèi)存需求和更有效地利用內(nèi)存帶寬來顯著提高OpenGL應(yīng)用程序的性能。Android框架提供對ETC1壓縮格式的支持,作為標(biāo)準(zhǔn)功能。但是ETC1紋理壓縮格式不支持具有透明度(alpha通道)的紋理。如果應(yīng)用程序需要具有透明度的紋理,則應(yīng)調(diào)查目標(biāo)設(shè)備上可用的其他紋理壓縮格式。支持使用OpenGL ES 3.0 API時(shí),要保證可以使用ETC2 / EAC紋理壓縮格式。這種紋理格式提供出色的壓縮比和高視覺質(zhì)量,格式還支持透明度(alpha通道)。而paletted是指通用的調(diào)色板紋理壓縮。還有ATITC(ATC)、PVRTC、S3TC(DXT n / DXTC)、3DC等壓縮策略。
對于Google Play上的應(yīng)用,如果你添加了這些聲明,會自動檢測手機(jī)是否支持相關(guān)功能,如果不支持就不能下載該應(yīng)用。對于國內(nèi)的應(yīng)用商店,不是很清楚是否會有改過濾。
2.2 創(chuàng)建用于OpenGL ES圖形的Activity
使用OpenGL ES的Android應(yīng)用程序就像任何其他具有用戶界面的應(yīng)用程序一樣具有Activity。與其他應(yīng)用程序的主要區(qū)別在于在Activity的布局中添加的內(nèi)容。在使用OpenGL ES的應(yīng)用程序,可以添加一個(gè)GLSurfaceView。
以下代碼示例顯示了使用一個(gè)GLSurfaceView作為其主視圖的Activity,當(dāng)然GLSurfaceView也可以像一般View一樣用在XML中:
public class OpenGLES20Activity extends Activity {
public static final String TAG = "OpenGLES20";
private GLSurfaceView mGLView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 創(chuàng)建一個(gè)GLSurfaceView實(shí)例并將其設(shè)置為此Activity的ContentView。
mGLView = new MyGLSurfaceView(this);
setContentView(mGLView);
}
}
2.3 構(gòu)建GLSurfaceView
一個(gè)GLSurfaceView是一個(gè)專門的視圖,可以在其中繪制OpenGL ES圖形。它本身并沒有太大作用。對象的實(shí)際繪制在GLSurfaceView.Renderer中。這邊暫時(shí)可以直接使用GLSurfaceView,但下面會講到的用于捕獲觸摸事件來進(jìn)行交互時(shí)候就需要擴(kuò)展這個(gè)類了。
class MyGLSurfaceView extends GLSurfaceView {
private final MyGLRenderer mRenderer;
public MyGLSurfaceView(Context context){
super(context);
// 創(chuàng)建一個(gè)OpenGL ES 2.0 的context
setEGLContextClientVersion(2);
mRenderer = new MyGLRenderer();
// 設(shè)置渲染器(Renderer)以在GLSurfaceView上繪制
setRenderer(mRenderer);
// 僅在繪圖數(shù)據(jù)發(fā)生更改時(shí)才渲染視圖
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
}
上面這邊先是設(shè)置了使用OpenGL ES 的版本,然后創(chuàng)建了一個(gè)GLSurfaceView.Renderer并將其設(shè)置為該GLSurfaceView的渲染者,最后設(shè)置了一下渲染模式,將其設(shè)置為RENDERMODE_WHEN_DIRTY,在該模式下當(dāng)渲染內(nèi)容變化時(shí)不會主動刷新效果,需要手動調(diào)用requestRender() 才行。
2.4 構(gòu)建渲染器(GLSurfaceView.Renderer)
Renderer這個(gè)類前面已經(jīng)提到,需要重寫onSurfaceCreated() 、onDrawFrame() 、onSurfaceChanged() 這三個(gè)方法,下面實(shí)現(xiàn)一個(gè)最基本的渲染器,之后會再增加內(nèi)容。
public class MyGLRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
// 設(shè)置重繪背景框架顏色
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}
public void onDrawFrame(GL10 unused) {
// 重繪背景顏色
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
// 設(shè)置渲染的位置和大小
GLES20.glViewport(0, 0, width, height);
}
}
上面的代碼示例創(chuàng)建了一個(gè)簡單的OpenGL ES應(yīng)用程序,它使用OpenGL顯示黑屏。
3、定義形狀
OpenGL ES 中主要能定義點(diǎn)線和三角形,而其他多邊形都是由三角形組合而成的,這邊舉一個(gè)三角形和正方形的例子。
3.1 三角形
OpenGL ES允許使用三維空間中的坐標(biāo)定義繪制對象。因此,在繪制三角形之前,必須定義其坐標(biāo)。在OpenGL中,執(zhí)行此操作的典型方法是為坐標(biāo)定義浮點(diǎn)數(shù)的頂點(diǎn)數(shù)組。為了獲得最大效率,可以將這些坐標(biāo)寫入一個(gè)ByteBuffer中,然后傳入OpenGL ES圖形管道進(jìn)行處理。
public class Triangle {
private FloatBuffer vertexBuffer;
// 此數(shù)組中每個(gè)頂點(diǎn)的維度
static final int COORDS_PER_VERTEX = 3;
static float triangleCoords[] = { // 按逆時(shí)針順序
0.0f, 0.622008459f, 0.0f, // 上
-0.5f, -0.311004243f, 0.0f, // 左下
0.5f, -0.311004243f, 0.0f // 右下
};
// 設(shè)置顏色的R(紅),G(綠),B(藍(lán)),A(透明度) 值
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
public Triangle() {
// 為形狀坐標(biāo)數(shù)組初始化頂點(diǎn)的字節(jié)緩沖區(qū)
ByteBuffer bb = ByteBuffer.allocateDirect(
// (# squareCoords 數(shù)組長度 * 每個(gè)float占4字節(jié))
triangleCoords.length * 4);
// 緩沖區(qū)讀取順序使用設(shè)備硬件的本地字節(jié)讀取順序
bb.order(ByteOrder.nativeOrder());
// 從ByteBuffer創(chuàng)建一個(gè)浮點(diǎn)緩沖區(qū)
vertexBuffer = bb.asFloatBuffer();
// 將坐標(biāo)點(diǎn)加到FloatBuffer中
vertexBuffer.put(triangleCoords);
// 設(shè)置緩沖區(qū)開始讀取位置,這邊設(shè)置為從頭開始讀取
vertexBuffer.position(0);
}
}
默認(rèn)情況下,OpenGL ES會有一個(gè)坐標(biāo)系,其中[0,0,0](X,Y,Z)指GLSurfaceView框架的中心,[1,1,0]是框架的右上角,并且[-1 ,-1,0]是框架的左下角。此形狀的坐標(biāo)以逆時(shí)針順序定義。繪圖順序很重要,因?yàn)樗x了哪一面是想要繪制的形狀的正面,以及背面,在繪制時(shí)候可以根據(jù)需求控制只繪制正面或者背面或者都繪制。
3.2 正方形
在OpenGL中定義三角形如上所示,但是如果想定義一個(gè)多邊形,例如正方形。在OpenGL ES中繪制這樣一個(gè)形狀的典型途徑是使用兩個(gè)繪制在一起的三角形:

同樣,應(yīng)該以逆時(shí)針順序?yàn)楸硎敬诵螤畹膬蓚€(gè)三角形定義頂點(diǎn),并將值放在一個(gè)ByteBuffer中。為了避免定義每個(gè)三角形共享的兩個(gè)坐標(biāo)點(diǎn),使用繪圖列表告訴OpenGL ES圖形管道如何繪制這些頂點(diǎn)。
public class Square {
private FloatBuffer vertexBuffer;
private ShortBuffer drawListBuffer;
// 此數(shù)組中每個(gè)頂點(diǎn)的坐標(biāo)數(shù)
static final int COORDS_PER_VERTEX = 3;
static float squareCoords[] = {
-0.5f, 0.5f, 0.0f, // 左上
-0.5f, -0.5f, 0.0f, // 左下
0.5f, -0.5f, 0.0f, // 右下
0.5f, 0.5f, 0.0f }; // 右上
private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // 頂點(diǎn)繪制順序
public Square() {
// 為形狀坐標(biāo)數(shù)組初始化頂點(diǎn)的字節(jié)緩沖區(qū)
ByteBuffer bb = ByteBuffer.allocateDirect(
// (# squareCoords 數(shù)組長度 * 每個(gè)float占4字節(jié))
squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);
// 為繪制順序數(shù)組 初始化字節(jié)緩沖區(qū)
ByteBuffer dlb = ByteBuffer.allocateDirect(
// (# drawOrder 數(shù)組長度 * 每個(gè) short 占2字節(jié))
drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
}
}
4 繪制形狀
在前面一節(jié)定義要使用OpenGL繪制的形狀后,這一節(jié)介紹如何繪制。使用OpenGL ES 2.0繪制形狀需要的代碼比較多,因?yàn)锳PI提供了對圖形渲染管道的大量控制。這也是為什么說OpenGL ES對開發(fā)者不友好的的原因了。
4.1 初始化形狀
在進(jìn)行任何繪圖之前,必須初始化并加載計(jì)劃繪制的形狀。除非在程序中使用的形狀的結(jié)構(gòu)(原始坐標(biāo))在執(zhí)行過程中發(fā)生更改,否則應(yīng)該在onSurfaceCreated()渲染器的方法中初始化它們以避免反復(fù)初始化。
public class MyGLRenderer implements GLSurfaceView.Renderer {
...
private Triangle mTriangle;
private Square mSquare;
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
...
// 初始化三角形
mTriangle = new Triangle();
// 初始化正方形
mSquare = new Square();
}
...
}
4.2 繪制形狀
使用OpenGL ES 2.0繪制定義的形狀需要大量代碼,因?yàn)楸仨毾驁D形渲染管道提供大量細(xì)節(jié)。具體而言,必須定義以下內(nèi)容:
① 頂點(diǎn)著色器(Vertex Shader):用于渲染形狀頂點(diǎn)的OpenGL ES圖形代碼。
② 片段著色器(Fragment Shader):OpenGL ES代碼,用于渲染具有顏色或紋理的形狀的面。
③ 程序(Program):一個(gè)OpenGL ES對象,包含要用于繪制一個(gè)或多個(gè)形狀的著色器。
需要至少一個(gè)頂點(diǎn)著色器來繪制形狀,并使用一個(gè)片段著色器為該形狀著色。必須編譯這些著色器,然后將其添加到OpenGL ES程序中,然后使用該程序繪制形狀。以下是如何定義可用于在Triangle類中繪制形狀的基本著色器的示例:
public class Triangle {
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"void main() {" +
" gl_Position = vPosition;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}";
...
}
著色器包含OpenGL著色語言(GLSL)代碼,必須在OpenGL ES環(huán)境中使用它之前進(jìn)行編譯。要編譯此代碼,需在渲染器類中創(chuàng)建實(shí)用程序方法:
public static int loadShader(int type, String shaderCode){
//創(chuàng)建頂點(diǎn)著色器類型(GLES20.GL_VERTEX_SHADER)
//或片段著色器類型(GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
// 將源代碼添加到著色器并進(jìn)行編譯
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
為了繪制形狀,必須編譯著色器代碼,將它們添加到OpenGL ES程序?qū)ο螅缓箧溄釉摮绦?。在繪制對象的構(gòu)造函數(shù)中執(zhí)行此操作,也就是說只執(zhí)行一次就好了。因?yàn)榫幾gOpenGL ES著色器和鏈接程序在CPU周期和處理時(shí)間方面的消耗比較大,因此應(yīng)該避免多次執(zhí)行此操作。如果在運(yùn)行時(shí)不需要修改著色器代碼的內(nèi)容,則應(yīng)在構(gòu)造器中構(gòu)建代碼,使其僅創(chuàng)建一次,然后緩存以供以后使用。
public class Triangle() {
...
private final int mProgram;
public Triangle() {
...
int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// 創(chuàng)建一個(gè)空的OpenGL ES 程序
mProgram = GLES20.glCreateProgram();
// 將頂點(diǎn)著色器添加到程序中
GLES20.glAttachShader(mProgram, vertexShader);
// 將片段著色器添加到程序中
GLES20.glAttachShader(mProgram, fragmentShader);
// 編譯鏈接OpenGL ES程序
GLES20.glLinkProgram(mProgram);
}
}
此時(shí),已準(zhǔn)備好添加繪制形狀的實(shí)際調(diào)用。使用OpenGL ES繪制形狀需要指定幾個(gè)參數(shù)來告訴渲染管道想要繪制什么以及如何繪制它。由于繪圖選項(xiàng)可能因形狀而異,因此最好讓形狀類包含自己的繪圖邏輯。創(chuàng)建draw()繪制形狀的方法。此代碼將位置和顏色值設(shè)置為形狀的頂點(diǎn)著色器和片段著色器,然后執(zhí)行繪圖功能。
// Triangle.class
private int mPositionHandle;
private int mColorHandle;
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; //一個(gè)頂點(diǎn)占用空間,其中每個(gè)頂點(diǎn)單維值占4字節(jié)
public void draw(float[] mvpMatrix) {
// 將程序添加到OpenGL ES環(huán)境
GLES20.glUseProgram(mProgram);
// 獲取頂點(diǎn)著色器vPosition屬性(位置)的句柄
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// 啟用三角形頂點(diǎn)的句柄
GLES20.glEnableVertexAttribArray(mPositionHandle);
// 準(zhǔn)備三角坐標(biāo)數(shù)據(jù)
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// 獲取片段著色器vColor成員(顏色)的句柄
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
//設(shè)置繪制三角形的顏色
GLES20.glUniform4fv(mColorHandle, 1, color, 0);
// 獲取形狀變換矩陣的具柄
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
// Pass the projection and view transformation to the shader
// 將模型視圖投影矩陣傳遞給著色器
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
// 繪制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// 禁用頂點(diǎn)數(shù)組
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
一旦準(zhǔn)備好所有這些代碼,繪制此對象只需要在渲染器的onDrawFrame()方法中調(diào)用draw()方法。
// MyGLRenderer.class
public void onDrawFrame(GL10 unused) {
...
mTriangle.draw();
}
之后運(yùn)行程序會得到如下效果:


上面已經(jīng)初步將三角形顯示在屏幕上了。但明顯可以看到存在問題,首先如果按照前面三角形的坐標(biāo),按理說應(yīng)該是一個(gè)等邊三角形。其次,這個(gè)三角形豎屏和橫屏拉伸方向也明顯不同。關(guān)于這個(gè)問題的原因和解決辦法,會在下一篇文章OpenGL ES 顯示圖形(下)中進(jìn)行討論。