Android OpenGL 编写简单滤镜
Android 上使用Opengl进行滤镜渲染效率较高,比起单纯的使用CPU给用户带来的体验会好很多。滤镜的对象是图片,图片是以Bitmap的形式表示,Opengl不能直接处理Bitmap,在Android上一般是通过GLSurfaceView来进行渲染的,也可以说成Android需要借助GLSurfaceView来完成对图片的渲染。
GlSurfaceView 的图片来源依然是Bitmap,但是Bitmap需要以纹理(Texture)的形式载入到Opengl中。因此我首先来看一下载入纹理的步骤:
1. GLES20.glGenTextures() : 生成纹理资源的句柄
2. GLES20.glBindTexture(): 绑定句柄
3. GLUtils.texImage2D() :将bitmap传递到已经绑定的纹理中
4. GLES20.glTexParameteri() :设置纹理属性,过滤方式,拉伸方式等
这里做滤镜使用Android4.x以后提供的 Effect 类来完成,Effect类实现也是通过Shader的方式来完成的,这些Shader程序内置在Android中,我们只需要按照一定的方式来调用就行了。在Android上使用GLSurfaceView来显示并完成图片的渲染,实现渲染需要实现GLSurfaceView.Render接口,该接口有三个方法:onDrawFrame(GL10 gl) ,该方法按照一定的刷新频率反复执行;onSurfaceChanged(GL10 gl, int width, int height),该方法在窗口重绘的时候执行;onSurfaceCreated(GL10 gl, EGLConfig config) 在创建SurfaceView的时候执行。
使用Effect类会用到EffectFactory 和 EffectContex,在下面的例子中看看具体的使用方式。
首先定义一个Activity:EffectivefilterActivity
package com.example.effectsfilterdemo; import java.nio.IntBuffer; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.opengl.GLSurfaceView; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; public class EffectsFilterActivity extends Activity { private GLSurfaceView mEffectView; private TextureRenderer renderer; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); renderer = new TextureRenderer(); renderer.setImageBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.puppy)); renderer.setCurrentEffect(R.id.none); mEffectView = (GLSurfaceView) findViewById(R.id.effectsview); //mEffectView = new GLSurfaceView(this); mEffectView.setEGLContextClientVersion(2); //mEffectView.setRenderer(this); mEffectView.setRenderer(renderer); mEffectView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); //setContentView(mEffectView); } @Override public boolean onCreateOptionsMenu(Menu menu) { Log.i("info", "menu create"); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { renderer.setCurrentEffect(item.getItemId()); mEffectView.requestRender(); return true; } }
EffectivefilterActivity 中使用了两个布局文件,一个用于Activity的布局,另一个用于菜单的布局。
R.layout.main:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <android.opengl.GLSurfaceView android:id="@+id/effectsview" android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout>
R.menu.main:
<menu xmlns:android="http://schemas.android.com/apk/res/android" > <item android:id="@+id/none" android:showAsAction="never" android:title="none"/> <item android:id="@+id/autofix" android:showAsAction="never" android:title="autofix"/> <item android:id="@+id/bw" android:showAsAction="never" android:title="bw"/> <item android:id="@+id/brightness" android:showAsAction="never" android:title="brightness"/> <item android:id="@+id/contrast" android:showAsAction="never" android:title="contrast"/> <item android:id="@+id/crossprocess" android:showAsAction="never" android:title="crossprocess"/> <item android:id="@+id/documentary" android:showAsAction="never" android:title="documentary"/> <item android:id="@+id/duotone" android:showAsAction="never" android:title="duotone"/> <item android:id="@+id/filllight" android:showAsAction="never" android:title="filllight"/> <item android:id="@+id/fisheye" android:showAsAction="never" android:title="fisheye"/> <item android:id="@+id/flipvert" android:showAsAction="never" android:title="flipvert"/> <item android:id="@+id/fliphor" android:showAsAction="never" android:title="fliphor"/> <item android:id="@+id/grain" android:showAsAction="never" android:title="grain"/> <item android:id="@+id/grayscale" android:showAsAction="never" android:title="grayscale"/> <item android:id="@+id/lomoish" android:showAsAction="never" android:title="lomoish"/> <item android:id="@+id/negative" android:showAsAction="never" android:title="negative"/> <item android:id="@+id/posterize" android:showAsAction="never" android:title="posterize"/> <item android:id="@+id/rotate" android:showAsAction="never" android:title="rotate"/> <item android:id="@+id/saturate" android:showAsAction="never" android:title="saturate"/> <item android:id="@+id/sepia" android:showAsAction="never" android:title="sepia"/> <item android:id="@+id/sharpen" android:showAsAction="never" android:title="sharpen"/> <item android:id="@+id/temperature" android:showAsAction="never" android:title="temperature"/> <item android:id="@+id/tint" android:showAsAction="never" android:title="tint"/> <item android:id="@+id/vignette" android:showAsAction="never" android:title="vignette"/> </menu>
在R.layout.main中只定义了一个GLSurfaceView用于显示图片,R.menu.main用于显示多个菜单项,通过点击菜单来完成调用不同滤镜实现对图片的处理。
接下来看比较关键的Renderer接口的实现。
package com.example.effectsfilterdemo; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Color; import android.media.effect.Effect; import android.media.effect.EffectContext; import android.media.effect.EffectFactory; import android.opengl.GLES20; import android.opengl.GLSurfaceView; import android.opengl.GLUtils; import android.util.Log; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.util.LinkedList; import java.util.Queue; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; public class TextureRenderer implements GLSurfaceView.Renderer{ private int mProgram; private int mTexSamplerHandle; private int mTexCoordHandle; private int mPosCoordHandle; private FloatBuffer mTexVertices; private FloatBuffer mPosVertices; private int mViewWidth; private int mViewHeight; private int mTexWidth; private int mTexHeight; private Context mContext; private final Queue<Runnable> mRunOnDraw; private int[] mTextures = new int[2]; int mCurrentEffect; private EffectContext mEffectContext; private Effect mEffect; private int mImageWidth; private int mImageHeight; private boolean initialized = false; private static final String VERTEX_SHADER = "attribute vec4 a_position;\n" + "attribute vec2 a_texcoord;\n" + "varying vec2 v_texcoord;\n" + "void main() {\n" + " gl_Position = a_position;\n" + " v_texcoord = a_texcoord;\n" + "}\n"; private static final String FRAGMENT_SHADER = "precision mediump float;\n" + "uniform sampler2D tex_sampler;\n" + "varying vec2 v_texcoord;\n" + "void main() {\n" + " gl_FragColor = texture2D(tex_sampler, v_texcoord);\n" + "}\n"; private static final float[] TEX_VERTICES = { 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f }; private static final float[] POS_VERTICES = { -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f }; private static final int FLOAT_SIZE_BYTES = 4; public TextureRenderer() { // TODO Auto-generated constructor stub mRunOnDraw = new LinkedList<>(); } public void init() { // Create program mProgram = GLToolbox.createProgram(VERTEX_SHADER, FRAGMENT_SHADER); // Bind attributes and uniforms mTexSamplerHandle = GLES20.glGetUniformLocation(mProgram, "tex_sampler"); mTexCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_texcoord"); mPosCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_position"); // Setup coordinate buffers mTexVertices = ByteBuffer.allocateDirect( TEX_VERTICES.length * FLOAT_SIZE_BYTES) .order(ByteOrder.nativeOrder()).asFloatBuffer(); mTexVertices.put(TEX_VERTICES).position(0); mPosVertices = ByteBuffer.allocateDirect( POS_VERTICES.length * FLOAT_SIZE_BYTES) .order(ByteOrder.nativeOrder()).asFloatBuffer(); mPosVertices.put(POS_VERTICES).position(0); } public void tearDown() { GLES20.glDeleteProgram(mProgram); } public void updateTextureSize(int texWidth, int texHeight) { mTexWidth = texWidth; mTexHeight = texHeight; computeOutputVertices(); } public void updateViewSize(int viewWidth, int viewHeight) { mViewWidth = viewWidth; mViewHeight = viewHeight; computeOutputVertices(); } public void renderTexture(int texId) { GLES20.glUseProgram(mProgram); GLToolbox.checkGlError("glUseProgram"); GLES20.glViewport(0, 0, mViewWidth, mViewHeight); GLToolbox.checkGlError("glViewport"); GLES20.glDisable(GLES20.GL_BLEND); GLES20.glVertexAttribPointer(mTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0, mTexVertices); GLES20.glEnableVertexAttribArray(mTexCoordHandle); GLES20.glVertexAttribPointer(mPosCoordHandle, 2, GLES20.GL_FLOAT, false, 0, mPosVertices); GLES20.glEnableVertexAttribArray(mPosCoordHandle); GLToolbox.checkGlError("vertex attribute setup"); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLToolbox.checkGlError("glActiveTexture"); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);//把已经处理好的Texture传到GL上面 GLToolbox.checkGlError("glBindTexture"); GLES20.glUniform1i(mTexSamplerHandle, 0); GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); } private void computeOutputVertices() { //调整AspectRatio 保证landscape和portrait的时候显示比例相同,图片不会被拉伸 if (mPosVertices != null) { float imgAspectRatio = mTexWidth / (float)mTexHeight; float viewAspectRatio = mViewWidth / (float)mViewHeight; float relativeAspectRatio = viewAspectRatio / imgAspectRatio; float x0, y0, x1, y1; if (relativeAspectRatio > 1.0f) { x0 = -1.0f / relativeAspectRatio; y0 = -1.0f; x1 = 1.0f / relativeAspectRatio; y1 = 1.0f; } else { x0 = -1.0f; y0 = -relativeAspectRatio; x1 = 1.0f; y1 = relativeAspectRatio; } float[] coords = new float[] { x0, y0, x1, y0, x0, y1, x1, y1 }; mPosVertices.put(coords).position(0); } } private void initEffect() { EffectFactory effectFactory = mEffectContext.getFactory(); if (mEffect != null) { mEffect.release(); } /** * Initialize the correct effect based on the selected menu/action item */ switch (mCurrentEffect) { case R.id.none: break; case R.id.autofix: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_AUTOFIX); mEffect.setParameter("scale", 0.5f); break; case R.id.bw: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_BLACKWHITE); mEffect.setParameter("black", .1f); mEffect.setParameter("white", .7f); break; case R.id.brightness: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_BRIGHTNESS); mEffect.setParameter("brightness", 2.0f); break; case R.id.contrast: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_CONTRAST); mEffect.setParameter("contrast", 1.4f); break; case R.id.crossprocess: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_CROSSPROCESS); break; case R.id.documentary: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_DOCUMENTARY); break; case R.id.duotone: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_DUOTONE); mEffect.setParameter("first_color", Color.YELLOW); mEffect.setParameter("second_color", Color.DKGRAY); break; case R.id.filllight: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_FILLLIGHT); mEffect.setParameter("strength", .8f); break; case R.id.fisheye: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_FISHEYE); mEffect.setParameter("scale", .5f); break; case R.id.flipvert: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_FLIP); mEffect.setParameter("vertical", true); break; case R.id.fliphor: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_FLIP); mEffect.setParameter("horizontal", true); break; case R.id.grain: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_GRAIN); mEffect.setParameter("strength", 1.0f); break; case R.id.grayscale: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_GRAYSCALE); break; case R.id.lomoish: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_LOMOISH); break; case R.id.negative: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_NEGATIVE); break; case R.id.posterize: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_POSTERIZE); break; case R.id.rotate: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_ROTATE); mEffect.setParameter("angle", 180); break; case R.id.saturate: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_SATURATE); mEffect.setParameter("scale", .5f); break; case R.id.sepia: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_SEPIA); break; case R.id.sharpen: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_SHARPEN); break; case R.id.temperature: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_TEMPERATURE); mEffect.setParameter("scale", .9f); break; case R.id.tint: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_TINT); mEffect.setParameter("tint", Color.MAGENTA); break; case R.id.vignette: mEffect = effectFactory.createEffect(EffectFactory.EFFECT_VIGNETTE); mEffect.setParameter("scale", .5f); break; default: break; } } public void setCurrentEffect(int effect) { mCurrentEffect = effect; } public void setImageBitmap(final Bitmap bmp){ runOnDraw(new Runnable() { @Override public void run() { // TODO Auto-generated method stub loadTexture(bmp); } }); } private void loadTexture(Bitmap bmp){ GLES20.glGenTextures(2, mTextures , 0); updateTextureSize(bmp.getWidth(), bmp.getHeight()); mImageWidth = bmp.getWidth(); mImageHeight = bmp.getHeight(); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextures[0]); GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bmp, 0); GLToolbox.initTexParams(); } private void applyEffect() { if(mEffect == null){ Log.i("info","apply Effect null mEffect"); } mEffect.apply(mTextures[0], mImageWidth, mImageHeight, mTextures[1]); } private void renderResult() { if (mCurrentEffect != R.id.none) { renderTexture(mTextures[1]); } else { renderTexture(mTextures[0]); } } @Override public void onDrawFrame(GL10 gl) { // TODO Auto-generated method stub if(!initialized){ init(); mEffectContext = EffectContext.createWithCurrentGlContext(); initialized = true; } GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); synchronized (mRunOnDraw) { while (!mRunOnDraw.isEmpty()) { mRunOnDraw.poll().run(); } } if (mCurrentEffect != R.id.none) { initEffect(); applyEffect(); } renderResult(); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { // TODO Auto-generated method stub updateViewSize(width, height); } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { // TODO Auto-generated method stub } protected void runOnDraw(final Runnable runnable) { synchronized (mRunOnDraw) { mRunOnDraw.add(runnable); } } }
这里有一个地方需要注意,任何使用Opengl接口的方法调用需要在Opengl Context中进行,否则会出现:call to OpenGL ES API with no current context (logged once per thread) 报错信息。所谓的Opengl Context 其实就是需要在onDrawFrame(GL10 gl),onSurfaceChanged(GL10 gl, int width, int height),onSurfaceCreated(GL10 gl, EGLConfig config)中调用,注意到这三个方法都有一个参数GL10。这里还有一个地方就是在载入纹理之前需要载入位图,使用了runOnDraw()方法将loadTexure的步骤放在onDrawFrame() 中来完成,巧妙的为外界提供了一个接口并使得操作在具有Opengl Context的黄金中完成。
最后来看看辅助的工具类(GLToolbox),该类完成Shader程序的创建,应用程序提供Shader 源码给该工具类编译:
package com.example.effectsfilterdemo; import android.opengl.GLES20; public class GLToolbox { public static int loadShader(int shaderType, String source) { int shader = GLES20.glCreateShader(shaderType); if (shader != 0) { GLES20.glShaderSource(shader, source); GLES20.glCompileShader(shader); int[] compiled = new int[1]; GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); if (compiled[0] == 0) { String info = GLES20.glGetShaderInfoLog(shader); GLES20.glDeleteShader(shader); shader = 0; throw new RuntimeException("Could not compile shader " + shaderType + ":" + info); } } return shader; } public static int createProgram(String vertexSource, String fragmentSource) { int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); if (vertexShader == 0) { return 0; } int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); if (pixelShader == 0) { return 0; } int program = GLES20.glCreateProgram(); if (program != 0) { GLES20.glAttachShader(program, vertexShader); checkGlError("glAttachShader"); GLES20.glAttachShader(program, pixelShader); checkGlError("glAttachShader"); GLES20.glLinkProgram(program); int[] linkStatus = new int[1]; GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); if (linkStatus[0] != GLES20.GL_TRUE) { String info = GLES20.glGetProgramInfoLog(program); GLES20.glDeleteProgram(program); program = 0; throw new RuntimeException("Could not link program: " + info); } } return program; } public static void checkGlError(String op) { int error; while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { throw new RuntimeException(op + ": glError " + error); } } public static void initTexParams() { GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); } }
这里就不提供整个工程了,结合上面的代码,自己在资源文件中提供一个图片载入就可以看到效果了。