Camera 开发实战(二) —— OpenGL ES 渲染预览帧

 

前言

前面一篇文章, 我们深入学习了如何选取预览尺寸和旋转角度来获取合适的预览帧, 这篇就要结合上 OpenGL ES 的知识, 对相机输出的数据进行渲染操作了, 主要流程如下

  • 获取预览数据
  • 渲染方式的选取
  • 渲染环境的搭建
  • 渲染器的实现

接下来我们从第一部分开始分析, 看看如何获取预览数据

一. 获取预览数据

笔者对 Camera2 不熟悉, 这里主要看看 Camera1 和 CameraX 预览数据的获取方式

一) Camera1

private static final int INVALID_CAMERA_ID = -1;
private final SurfaceTexture mBufferTexture;

// 1. 使用默认的纹理 ID 创建一个 SurfaceTexture
mBufferTexture = new SurfaceTexture(MAGIC_TEXTURE_ID);

// 2. 将这个 SurfaceTexture 传入相机
private Camera mCamera;
mCamera.setPreviewTexture(mBufferTexture);

好的, 可以看到 Camera1 的获取相机数据的方式, 只需要自己创建一个 SurfaceTexture, 然后将其作为传出参数调用 Camera.setPreviewTexture, 便可以获取到 Camera 输出的预览数据了

二) CameraX

private mPreview;
mPreview = new Preview(config);
mPreview.setOnPreviewOutputUpdateListener(new Preview.OnPreviewOutputUpdateListener() {
    @Override
    public void onUpdated(Preview.PreviewOutput output) {
        SurfaceTexture texture = output.getSurfaceTexture();
    }
});

CameraX 中只需要给 Preview 添加一个 OnPreviewOutputUpdateListener, 在相机预览数据变更时, 便可以通过回调中的 PreviewOutput 获取到 SurfaceTexture 相关信息了

预览数据的获取还是非常简单的, 接下来进入第二部分, 看看通过怎样的方式将预览数据呈现到屏幕上

二. 渲染方式的选取

无论是 Camera1 还是 CameraX, 其预览的数据都可以通过 SurfaceTexture(外部纹理) 输出, 与 Surface 仅作为生产者不同, SurfaceTexture 它是一个自成一体的生产者消费者模型

  • 相机可以将构建好的数据投入 SurfaceTexture, 同时会回调 onFrameAvailable 通知外界有了一个新的 GraphicBuffer
  • 通过 updateTexImage 可以从其队列中获取一个新的 GraphicBuffer 交由 OpenGL 进行加工

也就是说我们只需要将这个 SurfaceTexture 中的数据绘制到屏幕上, 最基础的预览功能就完成了, 那么我们面临的第一个问题便是如何将 SurfaceTexture 绘制到屏幕上呢?

一) 方案一

第一种方案便是使用系统提供的 GLSurfaceView

其内部已经搭建好了 EGL 的环境, 当 SurfaceTexture 有新的数据到来时, 我们可以在 Renderer 的 onDrawFrame 中从这个 SurfaceTexture 中获取最新的纹理数据绘制到 GLSurfaceView 中自带的 Surface 中即可

不过 GLSurfaceView 并不是一个普通的 View, 它在 WMS 中有自己的 WindowState, 在 SurfaceFlinger 中也有自己的 Layer, 它的显示不受 view 属性的控制, 因此它也无法实现普通 view 的平移, 缩放变换等操作

GLSurfaceView 虽能实现功能, 但使用起来有诸多的限制, 因此并非首选

二) 方案二

第二种方案是使用 TextureView 来渲染 SurfaceTexture, TextureView 是 14 中引入的组件, 它是一个普通的 View, 没有独立的 WindowState 和 Layer, 因此它支持普通 View 的所有操作, 包括平移旋转等变化

public class TextureView extends View {
    
    public void setSurfaceTexture(SurfaceTexture surfaceTexture) {
        ......
    }
    
}

只需要调用 TextureView.setSurfaceTexture 这个方法, 将 Camera 输出的 SurfaceTexture 传入, 便可以将预览的图像输出到手机屏幕上了

不过若是将相机输出的 SurfaceTexture 直接输出到屏幕上, 那我们想要拓展的滤镜效果, 水印效果….就没有任何可操作的空间了, 因此我们需要改造 TextureView, 让其支持我们对数据源加工操作 TextureView 渲染流程图

SurfaceTexture 是 OpenGL ES 外部纹理的 Java 描述, 我们可以为 SurfaceTexture 绑定一个 TextureID, 然后就如同绘制普通纹理一般, 通过 OpenGL ES 渲染管线对其进行加工处理

  • 如此一来纹理的旋转缩放, 便可以简单的转化成为 OpenGL 坐标系的旋转与缩放了

确定了使用 TextureView 渲染的方式, 接下来看看如何在 TextureView 中搭建渲染环境

三. 渲染环境的搭建

若想在 TextureView 中使用 EGL 的 API, 我们只需要仿照 GLSurfaceView 搭建一个 GLTextureView 便好, 把加工的操作定义成一个 Renderer 接口, 交由用户去实现具体的加工操作

接下来我们便逐个分析

一) Renderer 接口的定义

public interface ITextureRenderer {

    @WorkerThread
    void onEglContextCreated(EGLContext eglContext);

    @WorkerThread
    void onSurfaceSizeChanged(int width, int height);

    @WorkerThread
    void drawTexture(int textureId, float[] textureMatrix);

}

接口的定义如上所示, 主要流程与 GLSurfaceView.Renderer 类似, 与之不同的是第三个方法, 因为是 TextureView, 其主要职责是渲染外来的 SurfaceTexture, 因此, drawTexture 的必要参数中有 textureId 和对应的变化矩阵

接下来便是搭建 EGL, 然后调用 ITextureRenderer 的相关接口了

二) 独立线程执行渲染管线

public class GLTextureView extends TextureView {

    private static final String TAG = GLTextureView.class.getSimpleName();
    
    protected ITextureRenderer mRenderer;
    private SurfaceTexture mBufferTexture;
    private RendererThread mRendererThread;

    ......
    
    private static class RendererThread extends HandlerThread
            implements SurfaceTexture.OnFrameAvailableListener, Handler.Callback {

        private static final int MSG_CREATE_EGL_CONTEXT = 0;
        private static final int MSG_RENDERER_CHANGED = 1;
        private static final int MSG_SURFACE_SIZE_CHANGED = 2;
        private static final int MSG_TEXTURE_CHANGED = 3;
        private static final int MSG_DRAW_FRAME = 4;

        private final WeakReference<GLTextureView> mWkRef;
        private final float[] mTextureMatrix = new float[16];
        private final EglCore mEglCore = new EglCore();
        private int mOESTextureId;
        private Handler mRendererHandler;

        private RendererThread(String name, WeakReference<GLTextureView> view) {
            super(name);
            mWkRef = view;
        }

        @Override
        public synchronized void start() {
            super.start();
            mRendererHandler = new Handler(getLooper(), this);
            mRendererHandler.sendEmptyMessage(MSG_CREATE_EGL_CONTEXT);
        }

        @Override
        public boolean quitSafely() {
            release();
            return super.quitSafely();
        }

        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                // 创建 EGL 上下文
                case MSG_CREATE_EGL_CONTEXT:
                    preformCreateEGL();
                    break;
                // 渲染器变更
                case MSG_RENDERER_CHANGED:
                    performRenderChanged();
                    break;
                // 画布尺寸变更
                case MSG_SURFACE_SIZE_CHANGED:
                    performSurfaceSizeChanged();
                    break;
                // 纹理变更
                case MSG_TEXTURE_CHANGED:
                    performTextureChanged();
                    break;
                // 绘制数据帧
                case MSG_DRAW_FRAME:
                    performDrawTexture();
                    break;
                default:
                    break;
            }
            return false;
        }

        @Override
        public void onFrameAvailable(SurfaceTexture surfaceTexture) {
            if (mRendererHandler != null) {
                mRendererHandler.sendEmptyMessage(MSG_DRAW_FRAME);
            }
        }

        void handleRenderChanged() {
            if (mRendererHandler != null) {
                mRendererHandler.sendEmptyMessage(MSG_RENDERER_CHANGED);
            }
        }

        void handleSizeChanged() {
            if (mRendererHandler != null) {
                mRendererHandler.sendEmptyMessage(MSG_SURFACE_SIZE_CHANGED);
            }
        }

        void handleTextureChanged() {
            if (mRendererHandler != null) {
                mRendererHandler.sendEmptyMessage(MSG_TEXTURE_CHANGED);
            }
        }

        private void preformCreateEGL() {
            GLTextureView view = mWkRef.get();
            if (view == null) {
                return;
            }
            // Create egl context
            mEglCore.initialize(view.getSurfaceTexture(), null);
        }

        private void performRenderChanged() {
            GLTextureView view = mWkRef.get();
            if (view == null) {
                return;
            }
            view.mRenderer.onEglContextCreated(mEglCore.getContext());
        }

        private void performSurfaceSizeChanged() {
            GLTextureView view = mWkRef.get();
            if (view == null) {
                return;
            }
            ITextureRenderer renderer = view.mRenderer;
            renderer.onSurfaceSizeChanged(view.getWidth(), view.getHeight());
        }

        private void performTextureChanged() {
            // 为这个纹理绑定 textureId
            GLTextureView view = mWkRef.get();
            if (view == null) {
                return;
            }
            // 更新纹理数据
            SurfaceTexture bufferTexture = view.mBufferTexture;
            try {
                // 确保这个 Texture 没有绑定其他的纹理 id
                bufferTexture.detachFromGLContext();
            } catch (Throwable e) {
                // ignore.
            } finally {
                /*
                 CameraX 切换摄像头返回新的 SurfaceTexture 时, 会导致 SurfaceTexture 的 transform matrix 旋转角度改变, 从而引发跳闪
                 这里通过创建新的 textureId 解决
                */
                // 创建纹理
                mOESTextureId = createOESTextureId();
                // 绑定纹理
                bufferTexture.attachToGLContext(mOESTextureId);
                // 设置监听器
                bufferTexture.setOnFrameAvailableListener(this);
            }
        }

        private void performDrawTexture() {
            GLTextureView view = mWkRef.get();
            if (view == null) {
                return;
            }
            // 设置当前的环境
            mEglCore.makeCurrent();
            // 更新纹理数据
            SurfaceTexture bufferTexture = view.mBufferTexture;
            ITextureRenderer renderer = view.mRenderer;
            if (bufferTexture != null) {
                bufferTexture.updateTexImage();
                bufferTexture.getTransformMatrix(mTextureMatrix);
            }
            // 执行渲染器的绘制
            if (renderer != null) {
                renderer.drawTexture(mOESTextureId, mTextureMatrix);
            }
            // 将 EGL 绘制的数据, 输出到 View 的 preview 中
            mEglCore.swapBuffers();
        }
        
        ......

    }

}

部分代码如上所示, 可以看到线程使用的 HandleThread, EGL 初始化操作在 start 时开启, 对外界提供了如下方法来控制渲染流程

  • handleRenderChanged: 处理 Renderer 的实现方式变化了
  • handleSizeChanged: 处理 View 的尺寸变化了
  • handleTextureChanged: 处理外界的 SurfaceTexture 对象变化了

可以看到整个流程还是非常清晰的, 其交互方式与 ActivityThread 中的类似, 相信应该很容易看懂

好的, 支持 EGL 环境的 GLTextureView 搭建好了, 接下来就该实现具体的 Renderer 了

三. 渲染器的实现

一) 顶点的定义

public class PreviewRenderer implements ITextureRenderer {

    private static final String TAG = PreviewRenderer.class.getSimpleName();

    private final float[] mVertexCoordinate = new float[]{
            -1f, 1f,  // 左上
            -1f, -1f, // 左下
            1f, 1f,   // 右上
            1f, -1f   // 右下
    };
    private final float[] mTextureCoordinate = new float[]{
            0f, 1f,   // 左上
            0f, 0f,   // 左下
            1f, 1f,   // 右上
            1f, 0f    // 右下
    };

}

可以看到, 这里定义了两个顶点坐标, 分别是矩形的顶点, 还有纹理的顶点

  • 矩形顶点
    • 定义在世界坐标系上, 忽略了 Z 轴
    • 最终会通过 GL_TRIANGLE_STRIP 绘制, 即将两个三角形, 拼接成一个矩形
  • 纹理顶点
    • 忽略了 R 轴, 与矩形顶点一一对应

接下来看看着色器的编写

二) 着色器的编写

顶点着色器

attribute vec4 aVertexCoordinate;  // 传入参数: 顶点坐标, Java 传入
attribute vec4 aTextureCoordinate; // 传入参数: 纹理坐标, Java 传入
uniform mat4 uVertexMatrix;        // 全局参数: 4x4 顶点的裁剪矩阵, Java 传入
uniform mat4 uTextureMatrix;       // 全局参数: 4x4 矩阵纹理变化矩阵, Java 传入
varying vec2 vTextureCoordinate;   // 传出参数: 计算纹理坐标传递给 片元着色器
void main() {
    // 计算纹理坐标, 传出给片元着色器
    vTextureCoordinate = (uTextureMatrix * aTextureCoordinate).xy;
    // 计算顶点坐标, 输出给内建输出变量
    gl_Position = uVertexMatrix * aVertexCoordinate;
}

片元着色器

#extension GL_OES_EGL_image_external : require
// 设置精度,中等精度
precision mediump float;
// 由顶点着色器输出, 经过栅格化转换之后的纹理坐标
varying vec2 vTextureCoordinate;
// 2D 纹理, uniform 用于 application 向 gl 传值 (扩展纹理)
uniform samplerExternalOES uTexture;
void main(){
    // 取相应坐标点的范围转成 texture2D
    gl_FragColor = texture2D(uTexture, vTextureCoordinate);
}

其注释都比较详细, 对 Shading language 不了解的, 可以查看这篇文章

三) 坐标系统的转换

public class PreviewRenderer implements ITextureRenderer {

    private final float[] mProjectionMatrix = new float[16];      // 投影矩阵
    
    @Override
    public void onSurfaceSizeChanged(int width, int height) {
        GLES20.glViewport(0, 0, width, height);
         Matrix.orthoM(
                mProjectionMatrix, 0,
                -1, 1, -1, 1,
                1, -1
        );
    }
    
}

我们的顶点坐标定义在世界坐标系, 想让其顶点可以被着色语言使用, 应该转为裁剪坐标系的顶点

但这里的矩阵仅仅定义了一个矩阵, 便是基础的投影矩阵, 这种定义方式观察点默认就在坐标系原点, 可以减少视图矩阵的定义所带来的内存消耗(对坐标转换不熟悉的请点击查看)

四) 执行绘制

上面的初始工作准备好了, 绘制就变得轻而易举了, 相关代码如下

public class PreviewRenderer implements ITextureRenderer {
    
    @Override
    public void drawTexture(int OESTextureId, float[] textureMatrix) {
        // 清屏
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glClearColor(0f, 0f, 0f, 0f);
        // 激活着色器
        GLES20.glUseProgram(mProgram);
        // 绑定纹理
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, OESTextureId);

        /*
         顶点着色器
         */
        // 顶点坐标赋值
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);
        GLES20.glEnableVertexAttribArray(aVertexCoordinate);
        GLES20.glVertexAttribPointer(aVertexCoordinate, 2, GL_FLOAT, false,
                8, 0);
        // 纹理坐标赋值
        GLES20.glEnableVertexAttribArray(aTextureCoordinate);
        GLES20.glVertexAttribPointer(aTextureCoordinate, 2, GL_FLOAT, false,
                8, mVertexCoordinate.length * 4);
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
        // 顶点变换矩阵赋值
        GLES20.glUniformMatrix4fv(uVertexMatrix, 1, false, mFinalMatrix, 0);
        // 纹理变换矩阵赋值
        GLES20.glUniformMatrix4fv(uTextureMatrix, 1, false, textureMatrix, 0);

        /*
         片元着色器, 为 uTexture 赋值
         */
        GLES20.glUniform1i(uTexture, 0);

        // 执行渲染管线
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

        // 解绑纹理
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
    }
    
}

好的, 到这里基础的 Renderer 便完成了, 接下来只需要将 Camera 输出的 SurfaceTexture 和这里编写的 Renderer 传入 GLTextureView 便可以实现基础的显示了, 效果如下

五) 效果展示

4:3 的预览效果

4:3 的预览

16:9 的预览效果

16:9 的预览效果

总结

本次开发实战到这里就结束了, 通过 Camera 开发的实战, 我们便将 OpenGL ES 的知识点全部串联了起来, 并应用到了实践中

这次实现的是最基础的版本, 不过框架已经搭建好了, 感兴趣的小伙伴可以自可以在此基础上进行拓展实现以下的功能

  • 滤镜效果
  • 添加水印
  • 使用纹理中学到的 CenterCrop 算法, 实现全屏预览
  • 添加 FBO, 配合 MediaCodec 进行 H.264 的硬编
  • ……

文中所有的代码均在 CameraSample 中, 如有不解, 请点击查看