Android Camera使用OpenGL ES 2.0和GLSurfaceView对预览进行实时二次处理(黑白滤镜)

本系列教程会有三篇文章讲解Android平台滤镜的实现方式,第三篇以后应该很多人会需要,这三篇写完,看情况是否升级到OpenGL ES 3.0来实现,毕竟OpenGL ES 2.0和3.0虽然有些差别,但差别不算太大。

  • 第一篇 Android Camera使用OpenGL ES 2.0和GLSurfaceView对预览进行实时二次处理(黑白滤镜)
  • 第二篇 Android Camera使用OpenGL ES 2.0和TextureView对预览进行实时二次处理(黑白滤镜)
  • 第三篇 Android Camera2使用OpenGL ES 2.0和GLSurfaceView对预览进行实时二次处理(黑白滤镜)

下面进入正题:第一篇

首先讲一下,本文不使用Camera的PreviewCallback预览回调接口,因为onPreviewFrame()获取的数据格式只能是NV21或NV12,除非修改HAL层代码,一般情况下NV21或NV12需要转成RGB格式然后进行处理,这样太耗时了,所以本文使用SurfaceTexture来获取预览图像。

1. 添加GLSurfaceView作为布局界面

伪代码如下,这个不细讲了,可以参考我之前的博客 Android初始化OpenGL ES,并且分析Renderer子线程原理

    //实例化一个GLSurfaceView
    mGLSurfaceView = new GLSurfaceView(this);
    //配置OpenGL ES,主要是版本设置和设置Renderer,Renderer用于执行OpenGL的绘制
    mGLSurfaceView.setEGLContextClientVersion(2);
    mGLSurfaceView.setRenderer(new GLSurfaceView.Renderer());
    //在屏幕上显示GLSurfaceView
    setContentView(mGLSurfaceView);

2. 在onCreate方法中开启相机并设置参数(本文只使用后置摄像头)

    mCameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
    mCamera = Camera.open(mCameraId)
    Camera.Parameters parameters = mCamera.getParameters();
    parameters.set("orientation", "portrait");
    parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
    parameters.setPreviewSize(1280, 720);
    mCamera.setDisplayOrientation(90)
    setCameraDisplayOrientation(mActivity, mCameraId, mCamera);
    mCamera.setParameters(parameters);

3. GLSurfaceView创建好OpenGL ES的环境后,在Renderer的onSurfaceCreated()中,创建一个外部纹理用于接收预览数据

    public static int createOESTextureObject() {
        int[] tex = new int[1];
        //生成一个纹理
        GLES20.glGenTextures(1, tex, 0);
        //将此纹理绑定到外部纹理上
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, tex[0]);
        //设置纹理过滤参数
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
        //解除纹理绑定
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
        return tex[0];
    }

4. 开启预览

有了外部纹理,现在可以实例化一个SurfaceTexture了,之后即可开启Camera预览

    //在onDrawFrame方法中调用此方法
    public boolean initSurfaceTexture() {
        //根据外部纹理ID创建SurfaceTexture
        mSurfaceTexture = new SurfaceTexture(mOESTextureId);
        mSurfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
            @Override
            public void onFrameAvailable(SurfaceTexture surfaceTexture) {
                //每获取到一帧数据时请求OpenGL ES进行渲染
                mGLSurfaceView.requestRender();
            }
        });
        //讲此SurfaceTexture作为相机预览输出
        mCamera.setPreviewTexture(mSurfaceTexture);
        //开启预览
        mCamera.startPreview();
        return true;
    }

接着在onDrawFrame中更新SurfaceTexture绑定的外部纹理图像,使其获取的是最新的预览数据。

    if (mSurfaceTexture != null) {
        //更新纹理图像
        mSurfaceTexture.updateTexImage();
        //获取外部纹理的矩阵,用来确定纹理的采样位置,没有此矩阵可能导致图像翻转等问题
        mSurfaceTexture.getTransformMatrix(transformMatrix);
    }   

到这里相机已经可以将预览数据发送到SurfaceTexture上,并且此预览数据实际上是填充到了SurfaceTexture绑定的外部纹理中,之后就可以操作此纹理为我们所用了。

5. 现在开始OpenGL ES部分代码编写,首先编写最重要的Shader代码

  • 顶点着色器
    private static final String VERTEX_SHADER = "" +
        //顶点坐标
        "attribute vec4 aPosition;\n" +
        //纹理矩阵
        "uniform mat4 uTextureMatrix;\n" +
        //自己定义的纹理坐标
        "attribute vec4 aTextureCoordinate";\n" +
        //传给片段着色器的纹理坐标
        "varying vec2 vTextureCoord;\n" +
        "void main()\n" +
        "{\n" +
           //根据自己定义的纹理坐标和纹理矩阵求取传给片段着色器的纹理坐标
        "  vTextureCoord = (uTextureMatrix * aTextureCoordinate).xy;\n" +
        "  gl_Position = aPosition;\n" +
        "}\n";
  • 片段着色器
    private static final String FRAGMENT_SHADER = "" +
        //使用外部纹理必须支持此扩展
        "#extension GL_OES_EGL_image_external : require\n" +
        "precision mediump float;\n" +
        //外部纹理采样器
        "uniform samplerExternalOES uTextureSampler;\n" +
        "varying vec2 vTextureCoord;\n" +
        "void main() \n" +
        "{\n" +
           //获取此纹理(预览图像)对应坐标的颜色值
        "  vec4 vCameraColor = texture2D(uTextureSampler, vTextureCoord);\n" +
           //求此颜色的灰度值
        "  float fGrayColor = (0.3*vCameraColor.r + 0.59*vCameraColor.g + 0.11*vCameraColor.b);\n" +
           //将此灰度值作为输出颜色的RGB值,这样就会变成黑白滤镜
        "  gl_FragColor = vec4(fGrayColor, fGrayColor, fGrayColor, 1.0);\n" +
        "}\n";  

6. 定义顶点和纹理坐标

    //每行前两个值为顶点坐标,后两个为纹理坐标
    private static final float[] vertexData = {
         1f,  1f,  1f,  1f,
        -1f,  1f,  0f,  1f,
        -1f, -1f,  0f,  0f,
         1f,  1f,  1f,  1f,
        -1f, -1f,  0f,  0f,
         1f, -1f,  1f,  0f
    };

因为屏幕为四边形,所以需要两个三角形,其坐标系(此时是物体坐标系)如下图所示,左上角为第一个三角形,序号为(1,2,3),右下角为第二个三角形,序号为(4,5,6)

三角形的颜色使用纹理进行填充,所以每个三角形的顶点需要与纹理坐标进行匹配,纹理坐标系如下,顶点在纹理的左下角。

将顶点和纹理坐标数据使用FloatBuffer来存储,防止内存回收

    public FloatBuffer createBuffer(float[] vertexData) {
        FloatBuffer buffer = ByteBuffer.allocateDirect(vertexData.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        buffer.put(vertexData, 0, vertexData.length).position(0);
        return buffer;
    }

7. 编译Shader和链接program

    vertexShader = loadShader(GL_VERTEX_SHADER, VERTEX_SHADER);
    fragmentShader = loadShader(GL_FRAGMENT_SHADER, FRAGMENT_SHADER);
    mShaderProgram = linkProgram(vertexShader, fragmentShader);

    //加载着色器,GL_VERTEX_SHADER代表生成顶点着色器,GL_FRAGMENT_SHADER代表生成片段着色器
    public int loadShader(int type, String shaderSource) {
        //创建Shader
        int shader = glCreateShader(type);
        if (shader == 0) {
            throw new RuntimeException("Create Shader Failed!" + glGetError());
        }
        //加载Shader代码
        glShaderSource(shader, shaderSource);
        //编译Shader
        glCompileShader(shader);
        return shader;
    }

    //将两个Shader链接至program中
    public int linkProgram(int verShader, int fragShader) {
        //创建program
        int program = glCreateProgram();
        if (program == 0) {
            throw new RuntimeException("Create Program Failed!" + glGetError());
        }
        //附着顶点和片段着色器
        glAttachShader(program, verShader);
        glAttachShader(program, fragShader);
        //链接program
        glLinkProgram(program);
        //告诉OpenGL ES使用此program
        glUseProgram(program);
        return program;
    }

8. 关联顶点数据和顶点属性

现在需要将顶点坐标和纹理坐标传输给Shader,在onDrawFrame方法中执行下述代码:

    //获取Shader中定义的变量在program中的位置
    aPositionLocation = glGetAttribLocation(mShaderProgram, "aPosition");
    aTextureCoordLocation = glGetAttribLocation(mShaderProgram, "aTextureCoordinate");
    uTextureMatrixLocation = glGetUniformLocation(mShaderProgram, "uTextureMatrix");
    uTextureSamplerLocation = glGetUniformLocation(mShaderProgram, "uTextureSampler");

    //激活纹理单元0
    glActiveTexture(GLES20.GL_TEXTURE0);
    //绑定外部纹理到纹理单元0
    glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mOESTextureId);
    //将此纹理单元床位片段着色器的uTextureSampler外部纹理采样器
    glUniform1i(uTextureSamplerLocation, 0);

    //将纹理矩阵传给片段着色器
    glUniformMatrix4fv(uTextureMatrixLocation, 1, false, transformMatrix, 0);

    //将顶点和纹理坐标传给顶点着色器
    if (mDataBuffer != null) {
        //顶点坐标从位置0开始读取
        mDataBuffer.position(0);
        //使能顶点属性
        glEnableVertexAttribArray(aPositionLocation);
        //顶点坐标每次读取两个顶点值,之后间隔16(每行4个值 * 4个字节)的字节继续读取两个顶点值
        glVertexAttribPointer(aPositionLocation, 2, GL_FLOAT, false, 16, mDataBuffer);

        //纹理坐标从位置2开始读取
        mDataBuffer.position(2);
        glEnableVertexAttribArray(aTextureCoordLocation);
        /纹理坐标每次读取两个顶点值,之后间隔16(每行4个值 * 4个字节)的字节继续读取两个顶点值
        glVertexAttribPointer(aTextureCoordLocation, 2, GL_FLOAT, false, 16, mDataBuffer);
    }

    //绘制两个三角形(6个顶点)
    glDrawArrays(GL_TRIANGLES, 0, 6);

至此运行此程序Camera预览就会显示为黑白滤镜,如下图所示。

总结一下:首先创建GLSurfaceView和开启相机,之后创建一个外部纹理,根据此纹理ID创建一个SurfaceTexture,Camera将预览数据输出至此SurfaceTexture上,执行SurfaceTexture.updateTexImage()就会将一帧预览数据推送给外部纹理上。之后OpenGL ES就可以操作此纹理,比如加滤镜,滤镜就是对纹理的RGBA通道进行处理,处理后的数据就通过OpenGL ES绘制出来,其实对于OpenGL ES来说,最终就是画两个三角形,三角形的颜色取自纹理对应的位置。

代码地址(顺手给个Star啊):点击查看源码

作者:lb377463323
出处:http://blog.csdn.net/lb377463323
原文链接:http://blog.csdn.net/lb377463323/article/details/77071054
转载请注明出处!

你可能感兴趣的:(Android,OpenGL-ES)