1. 程式人生 > >ANDROID 高效能圖形處理 之 二. OPENGL ES

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的

文件介紹。 同時,在Java的世界中,Android提供了兩個可以執行OpenGL ES的類:GLSurfaceView和TextureView。由於真正的OpenGL ES仍然執行在native在層,因此在performance上,使用SDK並不比NDK差。而避免了JNI,客觀上對於APP開發者來說使用SDK要比NDK容易。

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 處理的實時預覽。
Screenshot_2013-09-23-11-17-42

使用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的濾鏡效果
Screenshot_2013-09-23-15-27-27