Android OpenGL 編寫簡單濾鏡

KingsLanding發表於2015-05-24

  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);
   }

}

  這裡就不提供整個工程了,結合上面的程式碼,自己在資原始檔中提供一個圖片載入就可以看到效果了。

相關文章