您的位置:首页 > 移动开发 > Android开发

ANDROID 高性能图形处理 之 二. OPENGL ES

2017-07-17 17:23 337 查看
原文地址: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 : 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的滤镜效果


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: