图像处理(opengl)|ANDROID 高性能图形处理 之 OPENGL ES
原文:http://tangzm.com/blog/?p=20
在之前的介绍中我们说到在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 : requireprecision 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);
【图像处理(opengl)|ANDROID 高性能图形处理 之 OPENGL ES】 将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 classGLCameraRenderThread 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的滤镜效果
推荐阅读
- android第三方框架(五)ButterKnife
- Android中的AES加密-下
- 带有Hilt的Android上的依赖注入
- Java|Java OpenCV图像处理之SIFT角点检测详解
- android|android studio中ndk的使用
- Android事件传递源码分析
- RxJava|RxJava 在Android项目中的使用(一)
- Android7.0|Android7.0 第三方应用无法访问私有库
- 深入理解|深入理解 Android 9.0 Crash 机制(二)
- android防止连续点击的简单实现(kotlin)