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

2,742 阅读8分钟

前言

前面一篇文章, 我们深入学习了如何选取预览尺寸和旋转角度来获取合适的预览帧, 这篇就要结合上 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(外部纹理) 输出, 也就是说我们只需要将这个 SurfaceTexture 中的数据绘制到屏幕上, 最基础的预览功能就完成了, 那么我们面临的第一个问题便是如何将 SurfaceTexture 绘制到屏幕上呢?

一) 方案一

第一种方案便是使用系统提供的 GLSurfaceView, 其内部已经搭建好了 EGL 的环境, 我们可以在 Renderer 的 onDrawFrame 中将这个 SurfaceTexture 绘制到 EGLSurface 上, 最终 swap 到 EGLDisplay 上即可

不过 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 中, 如有不解, 请点击查看