OpenGL 之 GPUImage 源码分析

GPUImage 是 iOS 上一个基于 OpenGL 进行图像处理的开源框架,后来有人借鉴它的想法实现了一个 Android 版本的 GPUImage ,本文也主要对 Android 版本的 GPUImage 进行分析。

概要

在 GPUImage 中既有对图像进行处理的,也有对相机内容进行处理的,这里主要以相机处理为例进行分析。

大致会分为三个部分:

  • 相机数据的采集
  • OpenGL 对图像的处理与显示
  • 相机的拍摄

相机数据采集

相机数据采集实际上就是把相机的图像数据转换成 OpenGL 中的纹理。

在相机的业务开发中,会给相机设置 PreviewCallback 回调方法,只要相机处于预览阶段,这个回调就会被重复调用,返回当前预览帧的内容。

    camera.setPreviewCallback(GPUImageRenderer.this);
    camera.startPreview();

默认情况下,相机返回的数据是 NV21 格式,也就是 YCbCr_420_SP 格式,而 OpenGL 使用的纹理是 RGB 格式,所以在每一次的回调方法中需要将 YUV 格式的数据转换成 RGB 格式数据。

GPUImageNativeLibrary.YUVtoRBGA(data, previewSize.width, previewSize.height,
                             mGLRgbBuffer.array());

有了图像的 RGB 数据,就可以使用 glGenTextures 生成纹理,并用 glTexImage2D 方法将图像数据作为纹理。

另外,如果纹理已经生成了,当再有图像数据过来时,只需要更新数据就好了,无需重复创建纹理。

    // 根据图像数据加载纹理
    public static int loadTexture(final IntBuffer data, final Size size, final int usedTexId) {
        int textures[] = new int[1];
        if (usedTexId == NO_TEXTURE) {
            GLES20.glGenTextures(1, textures, 0);
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
            // 省略部分代码
            GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, size.width, size.height,
                    0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, data);
        } else {
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, usedTexId);
            // 更新纹理数据就好,无需重复创建纹理
            GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, size.width,
                    size.height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, data);
            textures[0] = usedTexId;
        }
        return textures[0];
    }

通过在 PreviewCallback 回调方法中的操作,就完成了将图像数据转换为 OpenGL 的纹理。

接下来就是如何将纹理数据进行处理,并且显示到屏幕上。

在相机数据采集中,还有一些小的细节问题,比如相机前置与后置摄像头的左右镜像翻转问题。

对于前置摄像头,再把传感器内容作为纹理显示时,前置摄像头要做一个左右的翻转处理,因为我们看到的是一个镜像内容,符合正常的自拍流程。

在 GPUImage 的 TextureRotationUtil 类中有定义了纹理坐标,这些纹理坐标系的原点不是位于左下角进行定义的,而是位于左上角。

如果以左下角为纹理坐标系的坐标原点,那么除了要将纹理坐标向右顺时针旋转 90° 之外,还需要进行上下翻转才行,至于为什么要向右顺时针旋转 90° ,参考这篇文章:

Android 相机开发中的尺寸和方向问题

当我们把纹理坐标以左上角为原点,并相对于顶点坐标顺时针旋转 90 ° 之后,才能够正常的显示图像:

    // 顶点坐标
    static final float CUBE[] = {
            -1.0f, -1.0f,
            1.0f, -1.0f,
            -1.0f, 1.0f,
            1.0f, 1.0f,
    };
    // 以左上角为原点,并相对于顶点坐标顺时针旋转 90° 后的纹理坐标
    public static final float TEXTURE_ROTATED_90[] = {
            1.0f, 1.0f,
            1.0f, 0.0f,
            0.0f, 1.0f,
            0.0f, 0.0f,
    };

图像处理与显示

在有了纹理之后,需要明确的是,这个纹理就是相机采集到的图像内容,我们要将纹理绘制到屏幕上,实际上是绘制一个矩形,然后纹理是贴在这个矩形上的。

所以,这里可以回顾一下 OpenGL 是如何绘制矩形的,并且将纹理贴到矩形上。

OpenGL 学习系列---纹理

在 GPUImage 中,GPUImageFilter 类就完成了上述的操作,它是 OpenGL 中所有滤镜的基类。

解析 GPUImageFilter 的代码实现:

在 GPUImageFilter 的构造方法中会确定好需要使用的顶点着色器和片段着色器脚本内容。

init 方法中会调用 onInit 方法和 onInitialized 方法。

  • onInit 方法会创建 OpenGL 中的 Program,并且会绑定到着色器脚本中声明的 attributeuniform 变量字段。
  • onInitialized 方法会给一些 uniform 字段变量赋值,在 GPUImageFilter 类中还对不同类型的变量赋值进行了对应的方法,比如对 float 变量:
    protected void setFloat(final int location, final float floatValue) {
        runOnDraw(new Runnable() {
            @Override
            public void run() {
                GLES20.glUniform1f(location, floatValue);
            }
        });
    }

在 onDraw 方法中就是执行具体的绘制了,在绘制的时候会执行 runPendingOnDrawTasks 方法,这是因为我们在 init 方法去中给着色器语言中的变量赋值,并没有立即生效,而是添加到了一个链表中,所以需要把链表中的任务执行完了才接着执行绘制。

    public void onDraw(final int textureId, final FloatBuffer cubeBuffer,
                       final FloatBuffer textureBuffer) {
        GLES20.glUseProgram(mGLProgId);
        // 执行赋值的任务
        runPendingOnDrawTasks();
        // 顶点和纹理坐标数据
        GLES20.glVertexAttribPointer(mGLAttribPosition, 2, GLES20.GL_FLOAT, false, 0, cubeBuffer);
        GLES20.glEnableVertexAttribArray(mGLAttribPosition);
        GLES20.glVertexAttribPointer(mGLAttribTextureCoordinate, 2, GLES20.GL_FLOAT, false, 0, textureBuffer);
        GLES20.glEnableVertexAttribArray(mGLAttribTextureCoordinate);
        // 在绘制前的最后一波操作
        onDrawArraysPre();
        // 最终绘制
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    }

在绘制时,还需要给顶点坐标赋值,给纹理坐标赋值,GPUImageFilter 并没有去管理顶点坐标和纹理坐标,而是通过传递参数的形式,这样就不用去处理在前置摄像头与后置前摄像头、手机竖立放置与横屏放置时的关系了。

在执行具体的 glDrawArrays 方法之前,还提供了一个 onDrawArraysPre 方法,在这个方法里面还可以执行绘制前的最后一波操作,在某些滤镜的实现中有用到了。

最后才是 glDrawArrays 方法去完成绘制。

当我们不需要 GPUImageFilter 进行绘制时,需要将它销毁掉,在 destroy 方法去进行销毁,并且提供 onDestory 方法去为某些滤镜提供自定义的销毁。

    public final void destroy() {
        mIsInitialized = false;
        GLES20.glDeleteProgram(mGLProgId);
        onDestroy();
    }

    public void onDestroy() {
    }

在 GPUImageFilter 方法中定义了片段着色器脚本,这个脚本是将图像内容原样贴到了矩形上,并没有做特殊的图像处理操作。

top Created with Sketch.