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

Leave a Reply

Your email address will not be published. Required fields are marked *