ANDROID 高效能圖形處理 之 二. OPENGL ES
在之前的介紹中我們說到在Android 4.2上使用RenderScript有諸多限制,我們於是嘗試改用OpenGL ES 2.0來實現濾鏡。本文不詳細介紹OpenGL ES的規範以及組成部分,感興趣的同學可以閱讀 《OpenGL -ES Programming Guide》。這本書是OpenGL ES的權威參考,內容深入淺出,只可惜沒有中文版引進。
根據Intel的介紹,在Android平臺上使用OpenGL
ES主要有兩種方式:NDK和SDK。通過NativeActivity,應用在native(c/c++)中管理整個activity的生命週期,以及繪製過程。由於在native程式碼中,可以訪問OpenGL ES 1.1/2.0的程式碼,因此,可以認為NativeActivity提供了一個OpenGL ES的執行環境,關於NativeActivity的詳細用法,可以參考Google的
GLSurfaceView在Android 1.5 Cupcake就被引入,是一個非常方便的類。使用GLSurfaceView, Android會自動為你建立執行OpenGL ES所需要的環境,包括E2GL Surface和GL context。開發者只需要專注於如何使用OpenGL的commands繪製螢幕。在Android的網上教程和API Demo中也都採用了GLSurfaceView來演示Android的OpenGL ES能力。
考慮到示例程式碼的簡潔,我們移除了錯誤檢查,以及異常的處理。可以在Github查詢完整的實現。
GLSurfaceView
建立並初始化GLSurfaceView
建立一個新的類,繼承自GLSurfaceView,在建構函式中指定 OpenGL ES的版本,這裡我們使用OpenGL ES 2.0。在Android 4.3之後,Google開始支援ES 3.0。指定Render方式,GLSurfaceView支援兩種render方式,”CONTINUOUSLY“是指連續繪製,“WHEN_DIRTY”是由使用者呼叫requestRenderer()繪製。值得注意的是,GLSurfaceView的繪製(renderer)是在單獨的執行緒裡執行的,因此即使選擇連續繪製,並不會阻塞應用的主執行緒。最後,還必須設定GLSurfaceView的renderer。程式在renderer中處理GLSurfaceView的回撥,包括GLSurfaceView建立成功,尺寸變化,以及最最重要的繪製(onDrawFrame())
class PreviewGLSurfaceView extends GLSurfaceView { public PreviewGLSurfaceView(Context context){ super(context); setEGLContextClientVersion(2); setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); setRenderer(new PreviewGLRenderer()); } } public class PreviewGLRenderer implements GLSurfaceView.Renderer{ private GLCameraPreview mView; @Override public void onDrawFrame(GL10 gl) { // TODO Auto-generated method stub GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLPreviewActivity app = GLPreviewActivity.getAppInstance(); app.updateCamPreview(); mView.draw(); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { GLES20.glViewport(0,0,width,height); } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { GLES20.glClearColor(1.0f, 0, 0, 1.0f); mView = new GLCameraPreview(0); } }
然後將我們自己的GLSurfaceView插入View hierachy中。為了簡便,我在練習中直接將它設定為Activity的congtent
protected void onCreate(Bundle savedInstanceState) { ...... mGLSurfaceView = new PreviewGLSurfaceView(this); setContentView(mGLSurfaceView); }
建立,載入和編譯(連結)著色器
著色器是OpenGL ES 2.0的核心。自從2.0開始,OpenGL ES轉向可程式設計管線,並不再支援固定管線。一次OpenGL的繪製動作必須包含一個定點著色器(Vertex Shader)和一個片段著色器()。
對於Live filter的實現來說,Vertex Shader比較簡單,就是畫一個矩形(2個三角)
attribute vec4 aPosition; attribute vec2 aTextureCoord; varying vec2 vTextureCoord; void main() { gl_Position = aPosition; vTextureCoord = aTextureCoord; }
Fragment Shader取決於具體實現的濾鏡效果,這裡只選取最簡單的灰階濾鏡作為例子
#extension GL_OES_EGL_image_external : require precision mediump float; varying vec2 vTextureCoord; uniform samplerExternalOES sTexture; const vec3 monoMultiplier = vec3(0.299, 0.587, 0.114); void main() { vec4 color = texture2D(sTexture, vTextureCoord); float monoColor = dot(color.rgb,monoMultiplier); gl_FragColor = vec4(monoColor, monoColor, monoColor, 1.0); }
值得注意的是,在Android中Camera產生的preview texture是以一種特殊的格式傳送的,因此shader裡的紋理型別並不是普通的sampler2D,而是samplerExternalOES, 在shader的頭部也必須宣告OES 的擴充套件。除此之外,external OES的紋理和Sampler2D在使用時沒有差別。
為了方便頻繁修改,以及增加新的著色器,將著色器的指令碼放在應用資源中是一個不錯的選擇,同時提供一個靜態函式,讀取資源中的內容,以字串形式返回。由於編譯和連結著色器是一項費時的工作,一般在應用中只編譯/連結一次,將結果儲存在program物件中。然後在每次繪製螢幕時使用program物件。效能要求更高的程式也可以用GPU廠商提供的SDK將shader提前編譯好,放到應用資源中。
Load Shader 資源
private static String readRawTextFile(Context context, int resId){ InputStream inputStream = context.getResources().openRawResource(resId); InputStreamReader inputreader = new InputStreamReader(inputStream); BufferedReader buffreader = new BufferedReader(inputreader); String line; StringBuilder text = new StringBuilder(); try { while (( line = buffreader.readLine()) != null) { text.append(line); text.append('\n'); } } catch (Exception e) { e.printStackTrace(); } return text.toString(); }
編譯,連結 Shader
private int compileShader(final int filterType){ int program; GLPreviewActivity app = GLPreviewActivity.getAppInstance(); //1. Create Shader Object int vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER); int fragmentShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER); //2. Load Shader source code (in string) GLES20.glShaderSource(vertexShader, readRawTextFile(app, R.raw.vertex)); GLES20.glShaderSource(fragmentShader, readRawTextFile(app, R.raw.fragment_fish_eye)); //3. Compile Shader GLES20.glCompileShader(vertexShader);; GLES20.glCompileShader(fragmentShader); //4. Link Shader program = GLES20.glCreateProgram(); GLES20.glAttachShader(program, vertexShader); GLES20.glAttachShader(program, fragmentShader); GLES20.glLinkProgram(program); return program; }
繪製螢幕
做完這些準備工作之後,就可以開始著手處理繪製函數了。繪製函式的內容在GLSurfaceView.Renderer::onDrawFrame()中。根據使用者設定的render型別(持續繪製/按需要繪製),onDrawFrame()在獨立的GL執行緒中被呼叫。一般地,onDrawFrame()需要處理 背景清楚=>選擇Program物件=>設定Vertex Attribute/Uniform=>呼叫glDrawArrays()或者glDrawElements()進行繪製。
背景擦除,由於在我們的應用中沒有使用depth buffer 和 stencil buffer (主要用於3D繪圖),因此只需要擦除color buffer
GLES20.glClearColor(0, 0, 0, 1.0f); //Set clear color as pure black GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
設定當前的Program物件。Program中包含了已經編譯,連結的vertex shader和fragment shader。如果程式執行過程中只有一個program的話,也可以之設定一次。
GLES20.glUseProgram(mProgram);
在SDK中,所有的GLESXX.glXXX函式都只接受java.nio.Buffer的物件作為Buffer handler,而不直接接受java陣列物件。因此,在設定vertex attribute時,我們需要先將陣列轉為java.nio.Buffer,然後將其對映到vertex shader中相應的attribute變數。
//Original array private static float shapeCoords[] = { -1.0f, 1.0f, 0.0f, // top left -1.0f, -1.0f, 0.0f, // bottom left 1.0f, -1.0f, 0.0f, // bottom right 1.0f, 1.0f, 0.0f }; // top right ...... //Convert to java.nio.Buffer ByteBuffer bb = ByteBuffer.allocateDirect(4*shapeCoords.length); bb.order(ByteOrder.nativeOrder()); mVertexBuffer = bb.asFloatBuffer(); mVertexBuffer.put(shapeCoords); mVertexBuffer.position(0); ...... //Set Vertex Attributes int positionHandler = GLES20.glGetAttribLocation(mProgram, "aPosition"); GLES20.glEnableVertexAttribArray(positionHandler); GLES20.glVertexAttribPointer(positionHandler, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, COORDS_PER_VERTEX*4, mVertexBuffer);
接下來是將通過照相機得到的紋理傳入。不考慮如何從Camera的到紋理,首先我們在GL的上下文(Java執行緒)中建立紋理。值得注意的是,GLSurfaceView.Renderer在同一個執行緒中(GL THREAD)中執行所有的回撥(onSurfaceCreated, onSurfaceChanged, onDrawFrame),因此我們需要在onSurfaceCreated()中完成所有的gl初始化工作,而不能在應用的主執行緒中執行這些操作,比如,activity的onCreate,onResume回撥函式。
紋理
建立一個紋理物件
int textures[] = new int[1]; GLES20.glGenTextures(1, textures, 0); mTexName = textures[0];
繫結紋理,值得注意的是,紋理幫定的目標(target)並不是通常的GL_TEXTURE_2D,而是GL_TEXTURE_EXTERNAL_OES,這是因為Camera使用的輸出texture是一種特殊的格式。同樣的,在shader中我們也必須使用SamperExternalOES 的變數型別來訪問該紋理。
GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTexName);
繫結之後,我們還需要設定紋理的插值方式和wrap方式,雖然我們的應用中不會使用0-1。0以外的紋理座標,按照慣例,還是會設定wrap的引數。
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
然後,由於我們將紋理繫結到了TEXTURE_0單元,需要將shader中的uniform變數也設定成0(其實不設定,預設也是0)。在Android上,OpenGL最多可以支援到16個紋理單元(TEXTURE_0 ~ TEXTURE_15)
int textureHandler = GLES20.glGetUniformLocation(mProgram, "sTexture"); GLES20.glUniform1i(textureHandler, 0);
獲取照相機預覽
最後,我們需要將Camera的預覽繫結到我們建立的紋理上。Android SDK提供了SurfaceTexture類,來處理從Camera或者Video得到的資料,並繫結到OpenGL的紋理上。首先,我們先建立一個Camera物件
mCamera = Camera.open()
建立SurfaceTexture物件
mSurfaceTexture = new SurfaceTexture(texture);
將SurfaceTexture設定成camera預覽的紋理,並開始preview
mCamera.setPreviewTexture(mSurfaceTexture); mCamera.startPreview();
為SurfaceTexture註冊frame available的回撥,並且在回撥函式中請求重繪(requestRenderer)。
... @Override public void onFrameAvailable(SurfaceTexture surfaceTexture) { mGLSurfaceView.requestRender(); } ... //在start preview之前設定callback ++mSurfaceTexture.setOnFrameAvailableListener(this); mCamera.setPreviewTexture(mSurfaceTexture); mCamera.startPreview();
在GLSurfaceView.Renderer::onDrawFrame()中(被請求重繪),用updateTexImage將Camera中新的預覽寫入紋理。
mSurfaceTexture.updateTexImage();
有人可能會覺得在onFerameAvailable()中更新texture會比較直接,但是這裡有一個陷阱。必須在GL thread中執行updateTexImage(),而onFrameAvailable()會在設定回撥的執行緒中被執行。
這樣,大功告成。執行應用,可以在螢幕上看到一個通過GL 處理的實時預覽。
使用TextureView
TextureView在Android ICS被引入。通過TextureView,可以將一個內容流(視訊或者是照相機預覽)直接投射到一個View中,或者在這個View中通過OpenGL 進行繪製。和GLSurfaceView不同,Window manager不會為TextureView建立單獨的視窗,而把它作為一個普通的View,插入view hierachy,這樣,就可以對TextureView進行移動,旋轉和縮放(甚至設定成半透明)。
和GLSurfaceView不同,TextureView並沒有自動為我們建立GL 上下文,render surface和L thread.因此,如果我們需要在TextureView中用OpenGL進行繪製,必須手動地做這些事。
實現自己的GL執行緒
由於每個OpenGL的上下文和單獨的執行緒繫結,因此,如果我們需要在螢幕上繪製多個TextureView的話,必須要為每個View建立單獨的執行緒。。
實現GL renderer 執行緒。
public class GLCameraRenderThread extends Thread{ ...... @Override public void run(){ ...... } ...... }
建立egl context
在GL執行緒中,首先需要建立gl context, render surface,並將它們設定為當前(啟用的)上下文。具體的步驟比較繁瑣,可以參考<> Chapter 3. An Introduction to EGL
private void initGL() { /*Get EGL handle*/ mEgl = (EGL10)EGLContext.getEGL(); /*Get EGL display*/ mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); /*Initialize & Version*/ int versions[] = new int[2]; mEgl.eglInitialize(mEglDisplay, versions)); /*Configuration*/ int configsCount[] = new int[1]; EGLConfig configs[] = new EGLConfig[1]; int configSpec[] = new int[]{ EGL10.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, EGL10.EGL_RED_SIZE, 8, EGL10.EGL_GREEN_SIZE, 8, EGL10.EGL_BLUE_SIZE, 8, EGL10.EGL_ALPHA_SIZE, 8, EGL10.EGL_DEPTH_SIZE, 0, EGL10.EGL_STENCIL_SIZE, 0, EGL10.EGL_NONE }; mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, configsCount); mEglConfig = configs[0]; /*Create Context*/ int contextSpec[] = new int[]{ EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE }; mEglContext = mEgl.eglCreateContext(mEglDisplay, mEglConfig, EGL10.EGL_NO_CONTEXT, contextSpec); /*Create window surface*/ mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig, mSurface, null); /*Make current*/ mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext); } public void run(){ initGL(); ...... }
要注意的是,在eglCreateWindowSurface()中的第三個引數,mSurface代表實際繪製的視窗handle。在這裡代表TextureView的繪製表面。可以通過TextureView::getSxurfaceTexture()獲取,或者從TextureVisiew.SurfaceTextureListener::OnSurfaceTextureAvailable()中返回。
在GL 執行緒中,完成初始化之後,我們就可以開始進行繪製。繪製被放在一個無限迴圈中,以保證繪製內容被不斷更新,但是為了節約不必要的重繪,我們在迴圈中加入了 wait()/notify() 執行緒同步。GL執行緒在畫完一幀之後等待,直到camera預覽有資料更新之後繪製下一幀。
class XXXMyGLThread extends Thread{ ...... public void run(){ initGL(); ... while(true){ ... drawFrame(); ... wait(); //Wait for next frame available } } ...... } zzz implements SurfacaTexture.onFrameAvailableListener { ...... public void onFrameAvailable(SurfaceTexture surfaceTexture) { for (int i=0; i < mActiveRender; i++){ synchronized(mRenderThread[i]){G mRenderThread[i].notify(); //Notify a new frame comes } } } ......
從Camera中獲取紋理的過程和GLSurfaceView基本類似。SurfaceTexture很好地解決了多個執行緒(多個你EGL上下文)共同使用一個輸入源(video, camera preview)的問題。通過SurfaceTexture.attachToGLContext(int texName)和SurfaceTexture.detachFromGLContext(),可以將SurfaceTexture繫結到當前EGL上下文的指定紋理物件上。因此,在GL thread中的繪製迴圈看起來是:
synchronized(app){ public void run(){ ... while(true){ synchronized(app){ mSurfaceTexture.attachToGLContext(mTexName); mSurfaceTexture.updateTexImage(); ... drawFrame(); ... mSurfaceTexture.detachFromGLContext(); } eglSwapBuffers(mEglDisplay, mEglSurface); wait(); }
為了避免多個執行緒同時嘗試繫結一個SurfaceTexture,我們還在這這段繪製程式碼之外增加了同步互斥。以保證每個GL執行緒都可以不被打斷地執行“繫結=》繪圖=》解除”的動作。
最後,在每次繪製完成之後,我們還要手動呼叫eglSwapBuffers()將front buffer替換成當前buffer,從而使繪製內容可見。
全部完成之後,我們可以在一屏上顯示多個camera preview的濾鏡效果