26、Camera 与 OpenGL ES:GLSurfaceView集成、纹理绑定与更新、GPU滤镜实现、实时特效渲染
说实话,Camera 和 OpenGL ES 的结合,是 Android 相机开发里最硬核、也最有趣的一块。你想想看,Camera 给我们的原始数据,说白了就是一帧一帧的 YUV 或 JPEG 字节流。但我们要做美颜、做滤镜、做实时特效,总不能拿 CPU 去一像素一像素地算吧?那性能根本扛不住。
这时候 GPU 就派上用场了。OpenGL ES 就是操作 GPU 的接口。我当年第一次把 Camera 预览画面渲染到 GLSurfaceView 上时,那种「哇,画面动起来了」的感觉,至今还记得。嗯,咱们今天就把这条路走一遍。
GLSurfaceView 集成:把 Camera 画面搬上 GPU 画布
GLSurfaceView 是 Android 官方提供的一个 View,它内部管理了一个 EGL 上下文和一个渲染线程。我们不需要自己创建 GL 线程,只需要实现 Renderer 接口就行。
基本流程是这样的:
- 在布局里放一个 GLSurfaceView
- 设置
setEGLContextClientVersion(2)—— 我建议用 2.0 或 3.0 - 实现
Renderer的三个回调:onSurfaceCreated、onSurfaceChanged、onDrawFrame - 在
onSurfaceCreated里初始化纹理、编译着色器 - 在
onDrawFrame里绑定 Camera 纹理并绘制
核心要点:GLSurfaceView 的渲染线程和 Camera 的回调线程不是同一个。Camera 的 onPreviewFrame 或 onImageAvailable 跑在 Camera 线程,而 onDrawFrame 跑在 GL 线程。跨线程传递纹理数据时,一定要做同步。
我个人习惯用 SurfaceTexture 来桥接 Camera 和 OpenGL。Camera 把数据输出到 SurfaceTexture,OpenGL 从 SurfaceTexture 里取纹理。这样避免了手动拷贝 YUV 数据的麻烦。
// 初始化 SurfaceTexture
SurfaceTexture surfaceTexture = new SurfaceTexture(textureId);
surfaceTexture.setOnFrameAvailableListener(listener);
// 设置 Camera 预览输出
Camera camera = Camera.open();
camera.setPreviewTexture(surfaceTexture);
camera.startPreview();
// 在 onDrawFrame 中更新纹理
surfaceTexture.updateTexImage();
surfaceTexture.getTransformMatrix(matrix);
纹理绑定与更新:从 SurfaceTexture 到 GPU 纹理
这里有个关键点:SurfaceTexture 内部维护了一个 GLConsumer,它会把 Camera 的帧数据绑定到一个外部纹理(External Texture)上。这种纹理类型是 GLES11Ext.GL_TEXTURE_EXTERNAL_OES,不是普通的 GL_TEXTURE_2D。
为什么用外部纹理?因为 Camera 数据是流式的、不断更新的。外部纹理允许 GPU 直接读取最新的帧,而不需要每次都重新上传纹理数据。性能好很多。
注意:外部纹理的采样器在着色器里要声明为 samplerExternalOES,并且需要启用扩展 #extension GL_OES_EGL_image_external : require。我刚开始做的时候忘了加这个扩展,画面一片黑,查了半天才发现。
纹理更新的时机也很讲究。你不能在 onDrawFrame 里无脑调用 updateTexImage()。正确的做法是:
- 在
onFrameAvailable回调里设置一个标志位 - 在
onDrawFrame里检查标志位,如果为 true 才更新纹理 - 更新完后清除标志位
我曾经遇到过一个问题:如果 onDrawFrame 的调用频率比 Camera 帧率快,连续调用 updateTexImage() 会导致纹理被重复更新,画面出现撕裂。后来加了一个 AtomicBoolean 做同步,问题就解决了。
GPU 滤镜实现:着色器才是灵魂
滤镜的本质,就是对着色器里的像素颜色做数学变换。OpenGL ES 2.0 之后,我们用 GLSL 写片段着色器(Fragment Shader)来实现各种效果。
一个最简单的滤镜——灰度滤镜,它的片段着色器长这样:
#extension GL_OES_EGL_image_external : require
precision mediump float;
varying vec2 vTexCoord;
uniform samplerExternalOES sTexture;
void main() {
vec4 color = texture2D(sTexture, vTexCoord);
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
gl_FragColor = vec4(gray, gray, gray, color.a);
}
你看,核心就一行:用亮度公式把 RGB 转成灰度值。但实际项目中,滤镜要复杂得多。我整理了几种常见滤镜的实现思路:
| 滤镜类型 | 核心算法 | 性能开销 |
|---|---|---|
| 灰度 | 点乘亮度系数 | 极低 |
| 怀旧/暖色 | 颜色查找表(LUT) | 低 |
| 美颜磨皮 | 双边滤波 + 高斯模糊 | 中高 |
| 边缘检测 | Sobel 算子卷积 | 中 |
| 马赛克 | 像素块取均值 | 低 |
我个人最推荐用 LUT(颜色查找表)来实现滤镜。为什么?因为 LUT 本质上是一个 64x64 或 512x512 的图片,你把原始颜色映射到目标颜色。换滤镜只需要换一张 LUT 图片,不需要重新编译着色器。我在项目里用这个方案,一套代码支持了 20 多种滤镜,上线后效果很稳定。
实时特效渲染:性能是王道
实时特效,说白了就是每一帧都要在 16ms 内完成渲染(60fps)。如果做不到,画面就会卡顿。这里有几个性能优化的关键点:
- 减少纹理上传次数:Camera 数据不要每次都上传到 GPU,用 SurfaceTexture 的零拷贝机制
- 避免在 GL 线程做 I/O:文件读写、网络请求统统放到子线程
- 使用 VBO 和 VAO:顶点数据不要每帧都传,一次绑定,多次绘制
- 合并绘制调用:能用一次 draw call 搞定的,别拆成多次
小技巧:如果你要做多级滤镜(比如先磨皮再调色再加边框),不要用多个 Pass 渲染。把多个效果写到一个片段着色器里,一次绘制完成。我试过,性能能提升 30% 以上。
还有一个容易被忽略的点:EGL 上下文切换。如果你在多个 GLSurfaceView 或后台线程里操作 OpenGL,每次切换上下文都有不小的开销。我建议所有 GL 操作都在同一个线程里完成,用 Handler 或消息队列来调度。
嗯,说到实时特效,我记得有一次做 AR 贴纸项目。用户脸上要实时叠加一个猫耳朵,需要人脸检测 + 特效渲染。人脸检测跑在 CPU 上,渲染跑在 GPU 上。两个线程之间的数据同步如果没做好,猫耳朵就会在脸上乱跳。后来我用了一个 ConcurrentLinkedQueue 来传递人脸关键点坐标,渲染线程每帧从队列里取最新的数据,问题就解决了。
SVG 流程图:Camera + OpenGL ES 渲染管线
下面这张图展示了 Camera 数据从采集到最终显示在屏幕上的完整流程。我建议你仔细看看纹理绑定和着色器处理这两个环节,它们是整个管线的核心。
从这张图你能看到,整个管线其实不复杂。Camera 采集 → SurfaceTexture 桥接 → 外部纹理绑定 → 着色器处理 → 屏幕显示。每一步都有它的坑,但只要你理解了数据流的方向,调试起来就有思路了。
最后说一句:OpenGL ES 的学习曲线确实陡,但一旦你掌握了纹理和着色器这两个核心概念,剩下的就是数学和经验的积累了。我做了这么多年,每次遇到新的特效需求,第一反应还是「这个效果能不能用着色器实现?」—— 答案通常是「能」。
公众号:蓝海资料掘金营,微信deep3321