前言
前面一篇文章, 我们深入学习了如何选取预览尺寸和旋转角度来获取合适的预览帧, 这篇就要结合上 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, 让其支持我们对数据源加工操作
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 的预览效果
16:9 的预览效果
总结
本次开发实战到这里就结束了, 通过 Camera 开发的实战, 我们便将 OpenGL ES 的知识点全部串联了起来, 并应用到了实践中
这次实现的是最基础的版本, 不过框架已经搭建好了, 感兴趣的小伙伴可以自可以在此基础上进行拓展实现以下的功能
- 滤镜效果
- 添加水印
- 使用纹理中学到的 CenterCrop 算法, 实现全屏预览
- 添加 FBO, 配合 MediaCodec 进行 H.264 的硬编
- ……
文中所有的代码均在 CameraSample 中, 如有不解, 请点击查看