1. OpenGL ES 簡介
OpenGL 是一個跨平臺的圖形 API,為 3D 圖形處理硬件制定了一個標準軟件接口。OpenGL ES 是為嵌入式設備設計的 OpenGL 規范,Android 提供了對 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 里,我們可以通過兩個基礎類調用 OpenGL ES API 從而創建和操作圖形,它們是 GLSurfaceView 和 GLSurfaceView.Renderer。如果想在應用中使用 OpenGL,那么應該首先理解這兩個類的實現。
GLSurfaceView 是一個視圖類,可以使用 OpenGL ES API 繪制和處理圖形對象,就和 SurfaceView 的功能一樣。創建 GLSurfaceView 的實例,并設置 Renderer,就可以使用了。
不用于一般的視圖,GLSurfaceView 自己創建了一個窗口,并在視圖層次(view hierarchy)上穿了個「洞」,讓底層的 OpenGL Surface 顯示出來。它與常規視圖(view)不同,沒有動畫或者變形特效,因為它是窗口(window)的一部分。
GLSurfaceView.Renderer 是一個接口,定義了 GLSurfaceView 繪制圖形所需的接口,實現該接口并附加到 GLSurfaceView 就可以了。有三個接口:
- onSurfaceCreated():創建 GLSurfaceView 時,系統調用一次該方法。使用此方法執行只需要執行一次的操作,例如設置 OpenGL 環境參數或初始化 OpenGL 圖形對象。
- onDrawFrame():系統在每次重繪 GLSurfaceView 時調用該方法。使用此方法作為繪制(和重新繪制)圖形對象的主要執行方法。
- onSurfaceChanged():當 GLSurfaceView 的幾何發生變化時,系統調用此方法,這些變化包括 GLSurfaceView 的大小或設備屏幕方向的變化。例如:設備從縱向變為橫向時,系統調用此方法。我們應該使用此方法來響應 GLSurfaceView 容器的改變。
2. OpenGL ES 繪制流程
在 OpenGL ES 里,只能繪制點、直線和三角形。如果想要構建更復雜的圖形,例如拱形,那就需要足夠的點擬合這樣的曲線。點和直線可以用于某些效果,但是只有三角形才能用來構建擁有復雜對象和紋理的場景。
圖形數據在 OpenGL 管道(pipeline)中傳輸,需要使用著色器(shader)的子例程,著色器告訴 GPU 如何繪制數據。一旦生成了最終顏色,OpenGL 就會把它們寫到幀緩沖區(frame buffer),然后 Android 會把這個幀緩沖區顯示到屏幕上。
OpenGL 管道執行流程
讀取頂點數據 -> 執行頂點著色器 -> 組裝圖元 -> 光柵化圖元 -> 執行片段著色器 -> 寫入幀緩沖區 -> 顯示在屏幕上
頂點著色器(vertex shader)
一個頂點就是一個代表幾何對象的拐角的點,這個點有很多附加屬性;最重要的屬性就是位置,它代表了這個頂點在空間中的定位。頂點著色器生成每個頂點的最終位置,針對每個頂點,它都會執行一次;一旦最終位置確定了,OpenGL 就可以把這些可見頂點的集合組裝成點、直線和三角形。
片段著色器(fragment shader)
組成點、線或三角形的每個片段生成最終的顏色,針對每個片段,它都會執行一次;一個片段是一個小的、單一顏色的長方形區域,類似于計算機屏幕上的一個像素。片段著色器的目的就是告訴 GPU 每個片段的最終顏色是什么。
光柵化技術(rasterization)
OpenGL 通過光柵化把每個點、直線以及三角形分解成大量的小片段,它們可以映射到移動設備顯示屏的像素上,從而生成一副圖像。這些片段類似于顯示屏上的像素,每個都包含單一的純色。
3. 使用 OpenGL ES 繪制三角形
在 AndroidManifest.xml 聲明應用需要 OpenGL ES 2.0:
<!-- Tell the system this app requires OpenGL ES 2.0. -->
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
檢查設備是否支持 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,設置版本和 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 在單獨的線程中進行繪制,所以在 Activity 的生命周期方法中,需要暫停和恢復運行渲染線程。如果需要在主線程和繪制線程通信,可以使用 GLSurfaceView 的 queueEvent 方法。
@Override
protected void onStart() {
super.onStart();
mGlSurfaceView.onResume();
}
@Override
protected void onStop() {
super.onStop();
mGlSurfaceView.onPause();
}
實現 GLSurfaceView.Renderer 接口,主要通過 OpenGL 來清空屏幕、設置視口。
glClearColor 設置清空屏幕用到的顏色,參數是 Red、Green、Blue、Alpha,范圍 [0, 1]。這里我們使用白色背景。
glViewPort 設置視口的尺寸,告訴 OpenGL 可以用來渲染的 surface 大小。
glClear 清空屏幕,擦除屏幕上的所有顏色,并用之前 glClearColor 定義的顏色填充整個屏幕。
最新的 GPU 使用特殊的渲染技術,清空屏幕可以節省幀拷貝浪費的時間,還可以幫助避免很多問題。
@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 ...
}
定義三角形,包括它的坐標和顏色,把數據傳遞給 OpenGL 管道。
無論是 x 還是 y 坐標,OpenGL 都會把屏幕映射到 [-1, 1] 的范圍內。不管屏幕時什么形狀和大小,這個坐標范圍都是一樣的。如果想在屏幕上顯示任何東西,就需要在這個范圍內進行繪制。
OpenGL 作為本地系統庫直接運行在硬件上。所以需要把數據從 Java 堆復制到本地堆,我們使用 ByteBuffer 類。本地內存被本地環境存取,不受 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 的著色語言,語法結構和 C 語言相似。頂點著色器決定每個頂點的最終位置,片段著色器決定每個片段最后的顏色。頂點和片段著色器一起合作生成屏幕上最終的圖像。
簡單說,一個 OpenGL 程序就是把一個頂點著色器和一個片段著色器鏈接在一起變成單個對象。頂點著色器和片段著色器總是一起工作的。
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 語言的指針)。這樣在繪制的時候就可以給 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 矩陣,用來調整圖像的位置,一般放在 onSurfaceChanged 方法中。
- Projection — 這個變換是基于 GLSurfaceView 的寬高來調整繪制對象的坐標。如果沒有這個計算變換,繪制的形狀會在不同顯示窗口變形。這個投影變化通常只會在 GLSurfaceView 的比例被確定或者在渲染器的 onSurfaceChanged 方法中被計算。
- Camera View — 這個變換是基于虛擬的相機的位置來調整繪制對象坐標的。OpenGL ES 并沒有定義一個真實的相機對象,而是提供一個實用方法,通過變換繪制對象的顯示來模擬一個相機。相機視圖變換可能只會在 GLSurfaceView 被確定時計算,或者基于用戶操作或應用的功能來動態改變。
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 繪制每幀時,設置頂點數據和顏色數據,就能繪制出三角形了。
// 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);
運行看一下效果,一個中規中矩的三角形。上面的源碼在 GitHub
OpenGL ES 的知識面比較多,下面給出一些學習資料: