1. OpenGL ES 簡(jiǎn)介
OpenGL 是一個(gè)跨平臺(tái)的圖形 API,為 3D 圖形處理硬件制定了一個(gè)標(biāo)準(zhǔn)軟件接口。OpenGL ES 是為嵌入式設(shè)備設(shè)計(jì)的 OpenGL 規(guī)范,Android 提供了對(duì) OpenGL ES 的支持。
- OpenGL ES 1.0 和 1.1 能夠被 Android 1.0 及以上版本支持
- OpenGL ES 2.0 能夠被 Android 2.2 及更高版本支持
- OpenGL ES 3.0 能夠被 Android 4.3 及更高版本支持
- OpenGL ES 3.1 能夠被 Android 5.0 及以上版本支持
Android 通過 Framework 接口和 NDK 支持 OpenGL 繪制,這里主要介紹一下 Framework 接口。
在 Android Framework 里,我們可以通過兩個(gè)基礎(chǔ)類調(diào)用 OpenGL ES API 從而創(chuàng)建和操作圖形,它們是 GLSurfaceView 和 GLSurfaceView.Renderer。如果想在應(yīng)用中使用 OpenGL,那么應(yīng)該首先理解這兩個(gè)類的實(shí)現(xiàn)。
GLSurfaceView 是一個(gè)視圖類,可以使用 OpenGL ES API 繪制和處理圖形對(duì)象,就和 SurfaceView 的功能一樣。創(chuàng)建 GLSurfaceView 的實(shí)例,并設(shè)置 Renderer,就可以使用了。
不用于一般的視圖,GLSurfaceView 自己創(chuàng)建了一個(gè)窗口,并在視圖層次(view hierarchy)上穿了個(gè)「洞」,讓底層的 OpenGL Surface 顯示出來(lái)。它與常規(guī)視圖(view)不同,沒有動(dòng)畫或者變形特效,因?yàn)樗谴翱冢╳indow)的一部分。
GLSurfaceView.Renderer 是一個(gè)接口,定義了 GLSurfaceView 繪制圖形所需的接口,實(shí)現(xiàn)該接口并附加到 GLSurfaceView 就可以了。有三個(gè)接口:
- onSurfaceCreated():創(chuàng)建 GLSurfaceView 時(shí),系統(tǒng)調(diào)用一次該方法。使用此方法執(zhí)行只需要執(zhí)行一次的操作,例如設(shè)置 OpenGL 環(huán)境參數(shù)或初始化 OpenGL 圖形對(duì)象。
- onDrawFrame():系統(tǒng)在每次重繪 GLSurfaceView 時(shí)調(diào)用該方法。使用此方法作為繪制(和重新繪制)圖形對(duì)象的主要執(zhí)行方法。
- onSurfaceChanged():當(dāng) GLSurfaceView 的幾何發(fā)生變化時(shí),系統(tǒng)調(diào)用此方法,這些變化包括 GLSurfaceView 的大小或設(shè)備屏幕方向的變化。例如:設(shè)備從縱向變?yōu)闄M向時(shí),系統(tǒng)調(diào)用此方法。我們應(yīng)該使用此方法來(lái)響應(yīng) GLSurfaceView 容器的改變。
2. OpenGL ES 繪制流程
在 OpenGL ES 里,只能繪制點(diǎn)、直線和三角形。如果想要構(gòu)建更復(fù)雜的圖形,例如拱形,那就需要足夠的點(diǎn)擬合這樣的曲線。點(diǎn)和直線可以用于某些效果,但是只有三角形才能用來(lái)構(gòu)建擁有復(fù)雜對(duì)象和紋理的場(chǎng)景。
圖形數(shù)據(jù)在 OpenGL 管道(pipeline)中傳輸,需要使用著色器(shader)的子例程,著色器告訴 GPU 如何繪制數(shù)據(jù)。一旦生成了最終顏色,OpenGL 就會(huì)把它們寫到幀緩沖區(qū)(frame buffer),然后 Android 會(huì)把這個(gè)幀緩沖區(qū)顯示到屏幕上。
OpenGL 管道執(zhí)行流程
讀取頂點(diǎn)數(shù)據(jù) -> 執(zhí)行頂點(diǎn)著色器 -> 組裝圖元 -> 光柵化圖元 -> 執(zhí)行片段著色器 -> 寫入幀緩沖區(qū) -> 顯示在屏幕上
頂點(diǎn)著色器(vertex shader)
一個(gè)頂點(diǎn)就是一個(gè)代表幾何對(duì)象的拐角的點(diǎn),這個(gè)點(diǎn)有很多附加屬性;最重要的屬性就是位置,它代表了這個(gè)頂點(diǎn)在空間中的定位。頂點(diǎn)著色器生成每個(gè)頂點(diǎn)的最終位置,針對(duì)每個(gè)頂點(diǎn),它都會(huì)執(zhí)行一次;一旦最終位置確定了,OpenGL 就可以把這些可見頂點(diǎn)的集合組裝成點(diǎn)、直線和三角形。
片段著色器(fragment shader)
組成點(diǎn)、線或三角形的每個(gè)片段生成最終的顏色,針對(duì)每個(gè)片段,它都會(huì)執(zhí)行一次;一個(gè)片段是一個(gè)小的、單一顏色的長(zhǎng)方形區(qū)域,類似于計(jì)算機(jī)屏幕上的一個(gè)像素。片段著色器的目的就是告訴 GPU 每個(gè)片段的最終顏色是什么。
光柵化技術(shù)(rasterization)
OpenGL 通過光柵化把每個(gè)點(diǎn)、直線以及三角形分解成大量的小片段,它們可以映射到移動(dòng)設(shè)備顯示屏的像素上,從而生成一副圖像。這些片段類似于顯示屏上的像素,每個(gè)都包含單一的純色。
3. 使用 OpenGL ES 繪制三角形
在 AndroidManifest.xml 聲明應(yīng)用需要 OpenGL ES 2.0:
<!-- Tell the system this app requires OpenGL ES 2.0. -->
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
檢查設(shè)備是否支持 OpenGL ES 2.0:
public static boolean isSupportGL20(Context context) {
final ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager == null) {
return false;
}
final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
return configurationInfo.reqGlEsVersion >= 0x20000;
}
初始化 GLSurfaceView,設(shè)置版本和 Renderer。
// Create an OpenGL ES 2.0 context
mGlSurfaceView.setEGLContextClientVersion(2);
GLSurfaceView.Renderer renderer = new TriangleRenderer();
// Set the Renderer for drawing on the GLSurfaceView
mGlSurfaceView.setRenderer(renderer);
// Render the view only when there is a change in the drawing data
mGlSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
另外,GLSurfaceView 在單獨(dú)的線程中進(jìn)行繪制,所以在 Activity 的生命周期方法中,需要暫停和恢復(fù)運(yùn)行渲染線程。如果需要在主線程和繪制線程通信,可以使用 GLSurfaceView 的 queueEvent 方法。
@Override
protected void onStart() {
super.onStart();
mGlSurfaceView.onResume();
}
@Override
protected void onStop() {
super.onStop();
mGlSurfaceView.onPause();
}
實(shí)現(xiàn) GLSurfaceView.Renderer 接口,主要通過 OpenGL 來(lái)清空屏幕、設(shè)置視口。
glClearColor 設(shè)置清空屏幕用到的顏色,參數(shù)是 Red、Green、Blue、Alpha,范圍 [0, 1]。這里我們使用白色背景。
glViewPort 設(shè)置視口的尺寸,告訴 OpenGL 可以用來(lái)渲染的 surface 大小。
glClear 清空屏幕,擦除屏幕上的所有顏色,并用之前 glClearColor 定義的顏色填充整個(gè)屏幕。
最新的 GPU 使用特殊的渲染技術(shù),清空屏幕可以節(jié)省幀拷貝浪費(fèi)的時(shí)間,還可以幫助避免很多問題。
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// set the background frame color
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
// initilize buffer, shader, program, handle...
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
// calculate matrix...
}
@Override
public void onDrawFrame(GL10 gl) {
// redraw background color
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
// draw graphics ...
}
定義三角形,包括它的坐標(biāo)和顏色,把數(shù)據(jù)傳遞給 OpenGL 管道。
無(wú)論是 x 還是 y 坐標(biāo),OpenGL 都會(huì)把屏幕映射到 [-1, 1] 的范圍內(nèi)。不管屏幕時(shí)什么形狀和大小,這個(gè)坐標(biāo)范圍都是一樣的。如果想在屏幕上顯示任何東西,就需要在這個(gè)范圍內(nèi)進(jìn)行繪制。
OpenGL 作為本地系統(tǒng)庫(kù)直接運(yùn)行在硬件上。所以需要把數(shù)據(jù)從 Java 堆復(fù)制到本地堆,我們使用 ByteBuffer 類。本地內(nèi)存被本地環(huán)境存取,不受 Java 垃圾回收的控制。
// Set color with red, green, blue and alpha (opacity) values
private static final float[] COLORS = {0.8f, 0.5f, 0.3f, 1.0f};
// number of coordinates per vertex in this array
private static final int COORDS_PER_VERTEX = 2;
// coordinates in counterclockwise order:
private static final float[] COORDS = {
0, 0.6f, // top
-0.6f, -0.3f, // bottom left
0.6f, -0.3f, // bottom right
};
public static FloatBuffer createFloatBuffer(float[] coords) {
// Allocate a direct ByteBuffer, using 4 bytes per float, and copy coords into it.
ByteBuffer bb = ByteBuffer.allocateDirect(coords.length * SIZEOF_FLOAT);
bb.order(ByteOrder.nativeOrder());
FloatBuffer fb = bb.asFloatBuffer();
fb.put(coords);
fb.position(0);
return fb;
}
定義著色器,編譯著色器,鏈接到程序上。
著色器使用 GLSL 定義,它是 OpenGL 的著色語(yǔ)言,語(yǔ)法結(jié)構(gòu)和 C 語(yǔ)言相似。頂點(diǎn)著色器決定每個(gè)頂點(diǎn)的最終位置,片段著色器決定每個(gè)片段最后的顏色。頂點(diǎn)和片段著色器一起合作生成屏幕上最終的圖像。
簡(jiǎn)單說,一個(gè) OpenGL 程序就是把一個(gè)頂點(diǎn)著色器和一個(gè)片段著色器鏈接在一起變成單個(gè)對(duì)象。頂點(diǎn)著色器和片段著色器總是一起工作的。
private static final String VERTEX_SHADER =
"uniform mat4 uMVPMatrix;" +
"attribute vec4 aPosition;" +
"void main() {" +
" gl_Position = uMVPMatrix * aPosition;" +
"}";
private static final String FRAGMENT_SHADER =
"precision mediump float;" +
"uniform vec4 uColor;" +
"void main() {" +
" gl_FragColor = uColor;" +
"}";
// create a vertex shader type (GLES20.GL_VERTEX_SHADER)
// or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
public static int createShader(int type, String shaderCode) {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, shaderCode);
// add the source code to the shader and compile it
GLES20.glCompileShader(shader);
int[] compileStatus = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
if (compileStatus[0] == 0) {
Log.e(TAG, "compile shader: " + type + ", error: " + GLES20.glGetShaderInfoLog(shader));
GLES20.glDeleteShader(shader);
shader = 0;
}
return shader;
}
public static int createProgram(int vertexShader, int fragmentShader) {
if (vertexShader == 0 || fragmentShader == 0) {
Log.e(TAG, "shader can't be 0!");
}
int program = GLES20.glCreateProgram();
checkGlError("glCreateProgram");
if (program == 0) {
Log.e(TAG, "program can't be 0!");
return 0;
}
GLES20.glAttachShader(program, vertexShader);
checkGlError("glAttachShader");
GLES20.glAttachShader(program, fragmentShader);
checkGlError("glAttachShader");
GLES20.glLinkProgram(program);
int[] linkStatus = new int[1];
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] != GLES20.GL_TRUE) {
Log.e(TAG, "link program error: " + GLES20.glGetProgramInfoLog(program));
GLES20.glDeleteProgram(program);
program = 0;
}
return program;
}
上面這些操作在在 onSurafeceCreated 方法中使用,并且我們要拿到句柄(handle,可以理解為 C 語(yǔ)言的指針)。這樣在繪制的時(shí)候就可以給 OpenGL 傳值了。
mVertexBuffer = GLESUtils.createFloatBuffer(COORDS);
int vertexShader = GLESUtils.createVertexShader(VERTEX_SHADER);
int fragmentShader = GLESUtils.createFragmentShader(FRAGMENT_SHADER);
mProgram = GLESUtils.createProgram(vertexShader, fragmentShader);
// get handle to fragment shader's uColor member
mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
// get handle to vertex shader's aPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");
// get handle to shape's transformation matrix
mMvpMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
下面定義 MVP 矩陣,用來(lái)調(diào)整圖像的位置,一般放在 onSurfaceChanged 方法中。
- Projection — 這個(gè)變換是基于 GLSurfaceView 的寬高來(lái)調(diào)整繪制對(duì)象的坐標(biāo)。如果沒有這個(gè)計(jì)算變換,繪制的形狀會(huì)在不同顯示窗口變形。這個(gè)投影變化通常只會(huì)在 GLSurfaceView 的比例被確定或者在渲染器的 onSurfaceChanged 方法中被計(jì)算。
- Camera View — 這個(gè)變換是基于虛擬的相機(jī)的位置來(lái)調(diào)整繪制對(duì)象坐標(biāo)的。OpenGL ES 并沒有定義一個(gè)真實(shí)的相機(jī)對(duì)象,而是提供一個(gè)實(shí)用方法,通過變換繪制對(duì)象的顯示來(lái)模擬一個(gè)相機(jī)。相機(jī)視圖變換可能只會(huì)在 GLSurfaceView 被確定時(shí)計(jì)算,或者基于用戶操作或應(yīng)用的功能來(lái)動(dòng)態(tài)改變。
float ratio = (float) width / height;
// this projection matrix is applied to object coordinates in the onDrawFrame() method
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 2.5f, 6);
// Set the camera position (View matrix)
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 3, 0, 0, 0, 0, 1, 0);
// Calculate the projection and view transformation
Matrix.multiplyMM(mMvpMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
在 onDrawFrame 繪制每幀時(shí),設(shè)置頂點(diǎn)數(shù)據(jù)和顏色數(shù)據(jù),就能繪制出三角形了。
// Add program to OpenGL ES environment
GLES20.glUseProgram(mProgram);
// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(mPositionHandle);
// Prepare the triangle coordinate data
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, mVertexBuffer);
// Set color for drawing the triangle
GLES20.glUniform4fv(mColorHandle, 1, COLORS, 0);
// Pass the projection and view transformation to the shader
GLES20.glUniformMatrix4fv(mMvpMatrixHandle, 1, false, mMvpMatrix, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, COORDS.length / COORDS_PER_VERTEX);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
GLES20.glUseProgram(0);
運(yùn)行看一下效果,一個(gè)中規(guī)中矩的三角形。上面的源碼在 GitHub

OpenGL ES 的知識(shí)面比較多,下面給出一些學(xué)習(xí)資料: