ANDROID 高性能图形处理 之 二. OPENGL ES
OCTOBER 8, 2013 TANGZHIMING LEAVE A COMMENT
在之前的介绍中我们说到在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 处理的实时预览。
使用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的滤镜效果
Post navigation
PREVIOUS POSTAndroid 高性能图形处理 之 一. RenderScriptNEXT POSTGoogle IO Video Collections
LEAVE A REPLY
Your email address will not be published. Required fields are marked *
Comment