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); } }
這裡就不提供整個工程了,結合上面的程式碼,自己在資原始檔中提供一個圖片載入就可以看到效果了。