OpenGL ES系列 一 :GLSurfaceView基本使用配合理解OpenGL常用概念

GLSurfaceView,SurfaceView,TextureView跟OpenGL,OpenGL ES关系,前面有整理过一篇:Android UI OpenGL初识,SurfaceView,GLSurfaceView 和 Renderer

本篇主要通过实现:GLSurfaceView预览摄像头数据,并且对摄像头数据进行简单再处理来熟悉GLSurfaceView使用

1. GLSurfaceView基本用法

1.1 GLSurfaceView创建

        // 1. 创建GLSurfaceView
        glSurfaceView = new GLSurfaceView(this);
        // 2. GLContext设置OpenGLES2.0
        glSurfaceView.setEGLContextClientVersion(2);
        // 3. 指定自定义渲染器
        glSurfaceView.setRenderer(new MyRender());
        // 4. 渲染方式:提供连续渲染或按需渲染能力
        // 1. RENDERMODE_WHEN_DIRTY表示被动渲染 只有在调用 requestRender 或者 onResume 等方法时才会进行渲染
        // 2. RENDERMODE_CONTINUOUSLY表示持续渲染
        glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

GLSurfaceView主要包括以下能力:

  1. 提供一个OpenGL的渲染线程,以防止渲染阻塞主线程。
  2. 提供连续渲染或按需渲染能力。
  3. 封装EGL相关资源和创建和释放,极大地简化了OpenGL与窗口系统接口的使用方式。

1.2 获取摄像头数据

获取摄像头数据有一般有两种方式

  1. 相机设置预览的SurfaceTexture,通过回调获得当前可用的摄像头纹理
  2. 相机设置Camera.PreviewCallback回调,通过回调拿到YUV数据。YUV数据格式默认为NV21,也可以通过parameter.setPreviewFormat(ImageFormat format)来指定YUV数据格式。一般来说,NV21和YV12两种格式是所有Android机型都支持的,其他格式可能在不同机型上有兼容性问题

要对摄像头数据做再处理,首先要拿到摄像头数据(这里我们采用第一种方式获取)

1.2.1 开启摄像头

private void startCamera() {
        if (mCamera != null) {
            return;
        }
        Camera.CameraInfo info = new Camera.CameraInfo();
        // Try to find a front-facing camera
        int numCameras = Camera.getNumberOfCameras();
//        Log.i(TAG, "startCamera numCameras " + numCameras);
        int i;
        for (i = 0; i < numCameras; i++) {
            Camera.getCameraInfo(i, info);
			// 遍历摄像头,默认设置为前置摄像头ID
            int cameraFacing = Camera.CameraInfo.CAMERA_FACING_FRONT;
            if (info.facing == cameraFacing) {
                mCamera = Camera.open(i);
                break;
            }
        }
        if (mCamera == null) {
            throw new RuntimeException("Unable to open camera");
        }
        Camera.Parameters params = mCamera.getParameters();
        // 设置预览尺寸
        // 获取摄像头支持的PreviewSize列表
        List<Camera.Size> previewSizeList = params.getSupportedPreviewSizes();
        for (Camera.Size size : previewSizeList) {
            Log.i(TAG, "previewSizeList width: " + size.width + " height=" + size.height);
        }
        Camera.Size preSize = getCloselyPreSize(glSurfaceView.getWidth(), glSurfaceView.getHeight(), previewSizeList);
        if (null != preSize) {
            params.setPreviewSize(preSize.width, preSize.height);
        }
        mCamera.setParameters(params);
    }

因为采用GLSurfaceView预览,为避免画面变形,需要筛选最合适的相机预览尺寸,通过上述1中getCloselyPreSize方法,具体代码查看 Android Camera 从0到1,文章中有现成代码段实现了通过预览GLSurfaceView宽高比筛选合适的预览宽高。

1.2.2 相机设置预览的SurfaceTexture

这个只需要创建SurfaceTexture设置给Camera预览就好了

设置之后,相机会源源不断地把摄像头帧数据更新到SurfaceTexture上,即更新到对应的OpenGL纹理上。但是此时我们并不知道相机数据帧何时会更新到SurfaceTexture,也没有在GLSurfaceView的OnDrawFrame方法中将更新后的纹理渲染到屏幕,所以并不能在屏幕上看到预览画面

@Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            // 清除画布
            GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
            mSurfaceTexture = new SurfaceTexture(createOESTextureObject());
            createProgram();
            startCamera();
//            mCamera = Camera.open(camera_status);
            try {
                mCamera.setPreviewTexture(mSurfaceTexture);
                mCamera.startPreview();
            } catch (IOException e) {
                e.printStackTrace();
            }
            activeProgram();
        }

1.3 设置SurfaceTexture回调,通知摄像头预览数据已更新

SurfaceTexture有一个很重要的回调:OnFrameAvailableListener。通过名字也可以看出该回调的调用时机,当相机有新的预览帧数据时,此回调会被调用。所以我们为前面的SurfaceTexture设置一个回调,来通知我们相机预览数据已更新

SurfaceTexture的updateTexImage方法会更新接收到的预览数据到其绑定的OpenGL纹理中,接下来利用OpenGL对相机流数据进行处理(顶点着色器(Vertex Shader)和片段着色器(Fragment Shader)),以相机纹理变换矩阵作为输入,把相机流数据渲染在GSurfaceView

1.4 编写及初始化OpenGL着色器程序
代码段如下,主要是实现GLSurfaceView.Renderer接口,实现OpenGL ES程序对象创建,然后通过顶点着色器片段着色器来对步骤2中获取的纹理进行绘制,处理操作

class MyRender implements GLSurfaceView.Renderer {

        // -------------------------------------------------------------------------

        // 顶点着色器代码
        private final String vertexShaderCode = "uniform mat4 textureTransform;\n" +
                "attribute vec2 inputTextureCoordinate;\n" +
                "attribute vec4 position;            \n" +//NDK坐标点
                "varying   vec2 textureCoordinate; \n" +//纹理坐标点变换后输出
                "\n" +
                " void main() {\n" +
                "     gl_Position = position;\n" +
                "     textureCoordinate = inputTextureCoordinate;\n" +
                " }";

        // 片段着色器
        private final String fragmentShaderCode = "#extension GL_OES_EGL_image_external : require\n" +
                "precision mediump float;\n" +
                "uniform samplerExternalOES videoTex;\n" +
                "varying vec2 textureCoordinate;\n" +
                "\n" +
                "void main() {\n" +
                "    vec4 tc = texture2D(videoTex, textureCoordinate);\n" +
                "    float color = tc.r * 0.3 + tc.g * 0.59 + tc.b * 0.11;\n" +  //所有视图修改成黑白
                "    gl_FragColor = vec4(color,color,color,1.0);\n" +
//                "    gl_FragColor = vec4(tc.r,tc.g,tc.b,1.0);\n" +
                "}\n";

        // OpenGL ES程序
        public int mProgram;

        private float[] mProjectMatrix = new float[16];

        // 视见转换矩阵
        private float[] mCameraMatrix = new float[16];

        private float[] mTempMatrix = new float[16];

        public MyRender() {
            Matrix.setIdentityM(mProjectMatrix, 0);
            Matrix.setIdentityM(mCameraMatrix, 0);
            Matrix.setIdentityM(mMVPMatrix, 0);
            Matrix.setIdentityM(mTempMatrix, 0);
        }

        // -------------------------------------------------------------------------

        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            // 清除画布
            GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
            mSurfaceTexture = new SurfaceTexture(createOESTextureObject());
            createProgram();
            startCamera();
//            mCamera = Camera.open(camera_status);
            try {
                mCamera.setPreviewTexture(mSurfaceTexture);
                mCamera.startPreview();
            } catch (IOException e) {
                e.printStackTrace();
            }
            activeProgram();
        }

        // 投影矩阵:主要进行 3D 及 NDC 坐标变换
        private float[] mMVPMatrix = new float[16];

        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            Log.i(TAG, "onSurfaceChanged width:" + width + " | height:" + height);
            GLES20.glViewport(0, 0, width, height);
//            Matrix.scaleM(mMVPMatrix, 0, 1, -1, 1);
//            float ratio = (float) width / height;

//            Log.i(TAG, "onSurfaceChanged ratio:" + ratio);

            // 正交投影方法 (3 和 7 代表远近视点与眼睛的距离,非坐标点)
//            Matrix.orthoM(mProjectMatrix, 0, -1, 1, -ratio, ratio, 1, 7);

            // Android 使用 OpenGL ES2.0 绘制3D图像或者加载3D模型时,为了达到立体效果往往需要设置视见转换矩阵
            // 3代表眼睛的坐标点 (图片旋转等需要调节此方法相关参数)
//            Matrix.setLookAtM(mCameraMatrix, 0, 0, 0, 3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

//            Matrix.multiplyMM(mMVPMatrix, 0, mProjectMatrix, 0, mCameraMatrix, 0);
        }

        public boolean mBoolean = false;

        @Override
        public void onDrawFrame(GL10 gl) {
            if (mBoolean) {
                activeProgram();
                mBoolean = false;
            }
            if (mSurfaceTexture != null) {
                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
                mSurfaceTexture.updateTexImage();
//                GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix, 0);

                // 以 GL_TRIANGLE_STRIP 方式渲染传入的 (mPosCoordinate.length / 2) 个顶点数据
                GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, mPosCoordinate.length / 2);
            }
        }

        // -------------------------------------------------------------------------

        public 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];
        }

        // -------------------------------------------------------------------------

        /**
         * 有了顶点着色器和片段着色器程序, 把它们加在OpenGL渲染管线中运行起来
         * 

* OpenGL着色器程序和普通程序的运行准备过程差不多,也需要通过编译和链接后才可使用 */ private void createProgram() { //通常做法 // String vertexSource = AssetsUtils.read(CameraGlSurfaceShowActivity.this, "vertex_texture.glsl"); // int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); // String fragmentSource = AssetsUtils.read(CameraGlSurfaceShowActivity.this, "fragment_texture.glsl"); // int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); // 1. 创建,加载并编译顶点着色器, int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode); // 2. 创建,加载并编译片段着色器, int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode); // 3. 创建一个 空的OpenGL ES程序 mProgram = GLES20.glCreateProgram(); // 4. 添加顶点着色器到程序中 GLES20.glAttachShader(mProgram, vertexShader); // 5. 添加片段着色器到程序中 GLES20.glAttachShader(mProgram, fragmentShader); // 6. 链接OpenGL ES程序(创建OpenGL ES程序可执行文件) GLES20.glLinkProgram(mProgram); // 释放shader资源 GLES20.glDeleteShader(vertexShader); GLES20.glDeleteShader(fragmentShader); } /** * 着色器中有三种类型的参数:uniform、attribute和varying。 * varying参数是顶点着色器和片段着色器之前传递参数用的,对外部程序不可见, * 所以外部程序能传入着色器的参数只有 uniform 和 attribute 类型 * * @param type * @param shaderCode * @return 创建并加载着色器代码 */ private int loadShader(int type, String shaderCode) { // 创建指定类型的着色器 int shader = GLES20.glCreateShader(type); // 加载着色器代码(添加上面编写的着色器代码并编译它) GLES20.glShaderSource(shader, shaderCode); // 编译着色器 GLES20.glCompileShader(shader); return shader; } private float[] mPosCoordinate = {-1, -1, -1, 1, 1, -1, 1, 1}; private FloatBuffer mPosBuffer; // 顺时针转90并沿Y轴翻转 后摄像头正确,前摄像头上下颠倒 private float[] mTexCoordinateBackRight = {1, 1, 0, 1, 1, 0, 0, 0}; // 顺时针旋转90 后摄像头上下颠倒了,前摄像头正确 private float[] mTexCoordinateForntRight = {0, 1, 1, 1, 0, 0, 1, 0}; private FloatBuffer mTexBuffer; private int uPosHandle; private int aTexHandle; private int mMVPMatrixHandle; /** * 添加程序到ES环境中 */ private void activeProgram() { // 将程序添加到OpenGL ES环境,激活程序对象 // 在 glUseProgram 函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了 GLES20.glUseProgram(mProgram); mSurfaceTexture.setOnFrameAvailableListener(SurfaceAct.this); // attribute 类型参数都需要用glGetAttribLocation获取句柄 // 获取顶点着色器的位置的句柄 uPosHandle = GLES20.glGetAttribLocation(mProgram, "position"); aTexHandle = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate"); // uniform 参数则是用 glGetUniformLocation 获取句柄 mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "textureTransform"); mPosBuffer = convertToFloatBuffer(mPosCoordinate); if (camera_status == 0) { mTexBuffer = convertToFloatBuffer(mTexCoordinateBackRight); } else { mTexBuffer = convertToFloatBuffer(mTexCoordinateForntRight); } // glVertexAttribPointer 或 VBO 只是建立CPU和GPU之间的逻辑连接,从而实现了CPU数据上传至GPU // 1. 第一个参数指定句柄 // 2. 第二个参数指定顶点属性的大小,每个坐标点包含x和y两个float值 // 3. 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec都是由浮点数值组成的) // 4. 第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间, // 这里我们把它设置为GL_FALSE // 5. 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔,由于下个组位置数据在2个GLfloat之后,我们把步长设置为2 sizeof(GLfloat) GLES20.glVertexAttribPointer(uPosHandle, 2, GLES20.GL_FLOAT, false, 0, mPosBuffer); GLES20.glVertexAttribPointer(aTexHandle, 2, GLES20.GL_FLOAT, false, 0, mTexBuffer); // 启用顶点位置的句柄 // 着色器能否读取到数据 由下面的 glEnableVertexAttribArray 调用来决定 // glEnableVertexAttribArray 调用后允许顶点着色器读取句柄对应的GPU数据 GLES20.glEnableVertexAttribArray(uPosHandle); GLES20.glEnableVertexAttribArray(aTexHandle); } private FloatBuffer convertToFloatBuffer(float[] buffer) { FloatBuffer fb = ByteBuffer.allocateDirect(buffer.length * 4) .order(ByteOrder.nativeOrder()) .asFloatBuffer(); fb.put(buffer); fb.position(0); return fb; } // ------------------------------------------------------------------------- }

简单走完上面4步,总体GLSurfaceView用法应该是完整的(步骤4中代码很长,注释也写了一些,可以拷贝到项目中运行看一下效果)

2. SurfaceView使用常见问题

2.1 Fragment显示SurfaceView黑屏

// 给窗口Window设置
getWindow().setFormat(PixelFormat.TRANSLUCENT);
// SurfaceHolder设置
getHolder().setFormat(PixelFormat.TRANSPARENT);

2.2 解决SurfaceView调用setZOrderOnTop(true)遮挡其他控件的问题

// 
...
setZOrderOnTop(true)
//
...
setZOrderMediaOverlay(true)

2.3 SurfaceView绘图表面的类型

// 设置类型(设置SurfaceView显示类型,已经标记为过时)
// 当一个SurfaceView的绘图表面的类型等于SURFACE_TYPE_NORMAL的时候,就表示该SurfaceView的绘图表面所使用的内存是一块普通的内存
// 通常情况下,当一个SurfaceView是用来显示摄像头预览或者视频播放的时候,我们就会将它的绘图表面的类型设置为SURFACE_TYPE_PUSH_BUFFERS
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

实话说 我也还有很多代码不懂(OpenGL不熟悉),后面我会继续完善,文末的链接大家真心想学好OpenGL或者想熟悉它的,可以好好花半小时看两遍

最后还是简单总结一下:

  1. GLSurfaceView让我们自定义录制视频源有了可能(直播中美颜,滤镜等)
  2. SurfaceTexture很好的解决了视频源只获取不展示的功能,有点类似Bitmap是否加载到内存的那个isJustBu…的方法
  3. GLSurfaceView渲染方式:提供连续渲染或按需渲染能力

6. 参考链接

  1. Android OpenGL开发实践 - GLSurfaceView对摄像头数据的再处理
  2. Android OpenGL 的基本使用
  3. Android SurfaceView的基本使用
  4. Android视图SurfaceView的实现原理分析(老罗的文章,看完一遍需要45分钟,就看你能不能静下心来看)

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