使用Camera和GLSurfaceView创建自定义相机预览画面并在预览画面上加入自定义图形

注:本文档参考https://blog.csdn.net/lb377463323/article/details/77071054大家可以看看他的讲解,我用的代码是在他的基础上更改的,并在代码里合适的地方加入了注解,由于我还不会gitHub所以直接在这里贴代码了。(现已改为百度链接)

注意!注意!注意!

一定要去看他的讲解,我能力有限说的没他好,不过我还是决定把他全搬过来了,望谅解!!!

 

首先讲一下,本文不使用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预览就会显示为黑白滤镜,如下图所示。(我加了我绘制的三角形) 

 

 

使用Camera和GLSurfaceView创建自定义相机预览画面并在预览画面上加入自定义图形_第1张图片

原创部分:

先说说自定义相机的原理:以下全是个人的理解,如有错误,希望能指出,谢!

我们实现预览其实是将外部数据传入安卓设备,通过安卓系统处理数据再显示在屏幕上

 

首先外部数据可以通过外部设备摄像头获取,(这步是由摄像头给我们做的)然后摄像头把数据发送给安卓(在camera2中,是直接在两者之间建立一个会话层,即管道pipeline)最后是安卓处理之后再显示在屏幕上。这其实应该就是手机相机的工作原理吧。

 

好吧,现在来说说程序:

1、

首先要将摄像头的数据传送到安卓中,那么我们得需要一个容器来存储数据,这个容器就是我们创建的surfaceTexture。

 (这里我们了解下surfaceTexture的作用:它对图像流的处理并不直接显示,而是转为GL外部纹理,因此可用于图像流数据的二次处理(如Camera滤镜,桌面特效等)比如Camera的预览数据,变成纹理后可以交给GLSurfaceView直接显示)

 

2、

这个surfaceTexture是怎么生成的呢?

mSurfaceTexture = new SurfaceTexture(mOESTextureId);

 

就是这步,是通过一个纹理ID创建的,这里就将surfaceTexture与我们的安卓内部的Renderer建立联系

 

 3、

然后数据怎么传给mSurfaceTexture的呢?

mSurfaceTexture.updateTexImage();

updateTexImage();

官方描述:Update the texture image to the most recent frame from the image stream.

这个方法更新摄像头的数据到mSurfaceTexture里面

 

4、

再看看这个监听器注册setOnFrameAvailableListener

官方描述:Register a callback to be invoked when a new image frame becomes available to the SurfaceTexture.
这两个合在一起也就是,外部数据有更新时,触发监听器,然后要求重绘,然后在渲染器里面更新数据到surfaceTexture上

 

5、

然后initSurfaceTexture函数里面还有这两个函数调用:

1)、

mCamera.setPreviewTexture(mSurfaceTexture);

这是一个自定义的函数,但它里面又通过我们的摄像头调用了一个函数mCamera.setPreviewTexture(surfaceTexture);

(看着他们是一样的,但其实不一样:第一个函数是我们自定义相机的自定义函数,而第二个函数是我们Android API的相机的函数,
         * 看看原型:public native final void setPreviewTexture(SurfaceTexture surfaceTexture),
         * Sets the SurfaceTexture to be used for live preview.)

2)、

然后是startPreview()

         *官方描述:Starts capturing and drawing preview frames to the screen.
         *这两个函数没怎么搞明白(英语不好),但我觉得它们的作用必须是在camera和SurfaceTexture之间建立联系
         * 因为我们之前建立了surfaceTexture与OpenGL的联系,但没有建立surfaceTexture与摄像头的联系。
         *

         * 最后就是我们OpenGL操作从SurfaceTexture转化来的纹理了,

定义一个屏幕一样大小的矩形,将纹理贴到矩形上,其间可以对纹理的颜色做处理。

好了,预览结束

 

         * 这里我想着在预览画面上画一个三角形,然后自定义一个三角形的着色器,然后之间就画上了。
         * 然后我想着给三角形加一些交互,但加入矩阵变换后,它就不见了。
         * 我觉得问题可能是出在这儿
         * glGenFramebuffers(1, mFBOIds, 0);
         * glBindFramebuffer(GL_FRAMEBUFFER, mFBOIds[0]);

         * 这里是用我不熟悉的帧缓存对象。

以后做出来的时候再来更新一篇讲解。

代码:

https://pan.baidu.com/s/1CixmWtP0uN4k7DPYkqwt6Q

密码:8bv2

 

 

 

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