26. Camera与OpenGL ES:GLSurfaceView配置、Camera帧数据传入OpenGL、实时渲染与特效

说实话,Camera + OpenGL ES 这套组合,是 Android 视频采集进阶路上绕不开的一道坎。很多朋友做摄像头预览,用系统自带的 SurfaceView 或 TextureView 就够了,但一旦你想加滤镜、做美颜、搞特效,那就必须请出 OpenGL ES 了。

我最早接触这个组合,是在一个直播项目里。产品经理说:「我们要加个瘦脸大眼特效。」我当时心想,这不就是调个参数吗?结果一查,发现 Camera 输出的数据是 YUV 格式,OpenGL 要的是纹理,中间还得做格式转换……嗯,那段时间没少加班。

今天我们就来拆解一下,Camera 和 OpenGL ES 到底怎么配合工作。我会从 GLSurfaceView 的配置讲起,再到 Camera 帧数据怎么喂给 OpenGL,最后聊聊实时渲染和特效的实现思路。

GLSurfaceView:OpenGL 的专属舞台

GLSurfaceView 是 Android 官方提供的一个 View,它内部封装了 EGL 环境管理和渲染线程。说白了,你不需要自己折腾 EGL 的创建和销毁,它全帮你搞定了。

使用 GLSurfaceView 有三个核心步骤:

  1. 创建 GLSurfaceView 实例,并设置 OpenGL ES 版本
  2. 实现 Renderer 接口,重写 onSurfaceCreated、onSurfaceChanged、onDrawFrame 三个方法
  3. 设置渲染模式,RENDERMODE_CONTINUOUSLY 或 RENDERMODE_WHEN_DIRTY

我个人习惯用 RENDERMODE_WHEN_DIRTY,因为 Camera 帧数据是按固定频率来的,没必要让 OpenGL 空转。你想想看,如果每秒钟渲染 60 帧,但 Camera 只输出 30 帧,那另外 30 帧就是白费力气。

核心要点:GLSurfaceView 的渲染线程和 UI 线程是分开的。所有 OpenGL 操作必须在渲染线程中执行,不要在主线程里调用 gl 相关方法。

配置 GLSurfaceView 的代码骨架

先看一段基础配置代码。这段代码我几乎每个项目都会复用,改改参数就能用:

// 在 Activity 或 Fragment 中
GLSurfaceView glSurfaceView = new GLSurfaceView(this);
glSurfaceView.setEGLContextClientVersion(2); // 使用 OpenGL ES 2.0

glSurfaceView.setRenderer(new CameraRenderer());
glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

// 记得在 onPause 和 onResume 中调用对应方法
@Override
protected void onPause() {
    super.onPause();
    glSurfaceView.onPause();
}

@Override
protected void onResume() {
    super.onResume();
    glSurfaceView.onResume();
}

Renderer 接口的实现是重头戏。onSurfaceCreated 里做初始化,比如编译着色器、创建纹理;onSurfaceChanged 里设置视口和投影矩阵;onDrawFrame 里做实际的渲染工作。

我曾经犯过一个低级错误:在 onSurfaceCreated 里调用了 Camera.open()。结果每次屏幕旋转都会重新创建 Surface,Camera 就被重复打开了。后来我学乖了,Camera 的生命周期和 GLSurfaceView 分开管理。

Camera 帧数据如何传入 OpenGL

这是整个流程中最关键的一环。Camera 输出的数据是 YUV 格式(通常是 NV21 或 NV12),但 OpenGL 只认 RGB 纹理。所以我们需要做一次格式转换。

有两种主流做法:

  • CPU 转换:在 Java 层把 YUV 转成 RGB 位图,然后上传到纹理。简单但慢,不适合实时场景。
  • GPU 转换:把 YUV 数据分别上传到三个纹理(Y、U、V),在片段着色器里用矩阵运算转成 RGB。效率高,是业界标准做法。

我强烈推荐第二种。虽然写着色器代码有点麻烦,但性能差距太大了。在 1080p 分辨率下,CPU 转换可能要花 20-30ms,而 GPU 转换几乎不占时间。

小技巧:如果你用的是 Camera2 API,可以通过 ImageReader 获取 YUV_420_888 格式的 Image 对象。这个格式的 planes 数组里,Y、U、V 数据是分开的,正好对应 OpenGL 的三个纹理。

实时渲染与特效:从纹理到屏幕

当 Camera 帧数据变成 OpenGL 纹理后,剩下的就是纯图形学的工作了。渲染流程大致如下:

  1. 在 onDrawFrame 中,绑定 Camera 纹理
  2. 绘制一个全屏四边形(两个三角形组成)
  3. 片段着色器对每个像素做处理
  4. 输出到屏幕

特效的实现,本质上就是修改片段着色器。比如:

  • 黑白滤镜:把 RGB 转成灰度值
  • 美颜:用高斯模糊做磨皮,再用边缘检测做锐化
  • LUT 滤镜:通过查找表映射颜色
  • 形变特效:修改纹理坐标,实现瘦脸、大眼

我做过一个比较有意思的特效——「哈哈镜」。原理很简单,在片段着色器里根据像素位置计算一个偏移量,让中心区域放大,边缘区域压缩。代码就几行,但效果很欢乐。

注意:实时特效对性能要求很高。在低端机上,不要同时叠加太多特效。我曾经在一个项目里同时开了美颜、滤镜、贴纸,结果帧率掉到 15fps 以下。后来做了个性能检测,动态降低特效等级,才解决问题。

整体流程梳理

为了让你更直观地理解 Camera 和 OpenGL ES 的协作关系,我画了一张流程图:

Camera + OpenGL ES 实时渲染流程 Camera 硬件 输出 YUV 帧数据 YUV → RGB 纹理转换 GPU 着色器转换 / CPU 转换 片段着色器特效处理 滤镜 / 美颜 / 形变 / LUT GLSurfaceView 显示 onDrawFrame 渲染 关键组件 • Camera / Camera2 • GLSurfaceView • Renderer 接口 • EGL 环境 • 着色器程序 • 纹理对象 • FBO(可选) 渲染模式: RENDERMODE_WHEN_DIRTY 或 CONTINUOUSLY

从图上可以看得很清楚:Camera 输出 YUV 数据,经过格式转换变成纹理,然后由片段着色器做特效处理,最后通过 GLSurfaceView 显示到屏幕上。整个流程是流水线式的,每一环都紧密相连。

避坑指南:我踩过的几个坑

做 Camera + OpenGL 开发,坑是真的多。我挑几个典型的说说:

  • 纹理 ID 为 0:在 onSurfaceCreated 里创建的纹理,不能在 onDrawFrame 里直接用。因为 GLSurfaceView 的渲染线程可能还没初始化好。解决办法是用一个 volatile 变量做同步。
  • Camera 方向不对:Camera 输出的数据方向是固定的,但手机屏幕方向会变。需要在 onSurfaceChanged 里根据当前屏幕方向做旋转。我一般用 Surface.ROTATION_ 系列常量来判断。
  • 内存泄漏:Camera 的预览回调里如果持有 Activity 的引用,会导致 Activity 无法释放。建议用弱引用或者 Application Context。
  • 着色器编译失败:不同厂商的 GPU 对着色器的支持程度不一样。我遇到过某款手机上,precision mediump float 声明导致编译失败的情况。后来统一改成 highp 才解决。

调试技巧:如果画面黑屏,先检查 glGetError() 的返回值。如果返回 GL_NO_ERROR,那多半是纹理数据没传进来。可以在 onDrawFrame 里加个 glClearColor 看看背景色能不能正常显示。

好了,这一章的内容就到这里。Camera 和 OpenGL ES 的配合,说复杂也复杂,说简单也简单。核心就是理解数据流的方向:Camera → 纹理 → 着色器 → 屏幕。只要把这个链条理清楚,剩下的就是对着色器编程的熟练度了。

下一章我们会深入具体的着色器代码,看看怎么实现一个完整的滤镜效果。到时候我会把顶点着色器和片段着色器的代码一行行拆开讲,保证你看完就能自己写。


公众号:蓝海资料掘金营,微信deep3321