12、Camera2与OpenGL结合:SurfaceTexture创建、纹理更新、预览帧处理、滤镜渲染
说实话,Camera2 的 API 设计得挺绕的。我刚接触那会儿,光是把预览画面显示出来就折腾了两天。但当你把 Camera2 和 OpenGL 结合起来,你会发现一个全新的世界——每一帧画面你都能掌控,想加滤镜就加滤镜,想做特效就做特效。
这节课,我们就来拆解这个组合的核心:SurfaceTexture。它就像一座桥,一头连着 Camera2 的实时数据流,另一头连着 OpenGL 的纹理管线。
12.1 SurfaceTexture 是什么?
说白了,SurfaceTexture 就是一个“生产者-消费者”模型里的中间人。Camera2 作为生产者,把每一帧图像数据塞进一个 Surface 里。OpenGL 作为消费者,从对应的纹理 ID 里把数据读出来渲染。
我习惯把 SurfaceTexture 理解成一个“会说话的纹理”。普通的纹理你得自己加载图片,而 SurfaceTexture 会自动从相机流里拿数据填充纹理。你只需要在每一帧到来时,调用 updateTexImage() 就行了。
核心流程:
- 创建 SurfaceTexture,绑定到一个 OpenGL 纹理 ID 上
- 把这个 SurfaceTexture 的 Surface 传给 Camera2 的 CaptureRequest
- 每帧回调时,调用 updateTexImage() 更新纹理内容
- 用这个纹理去做渲染、滤镜、编码等操作
12.2 创建 SurfaceTexture 并绑定纹理
这一步很关键。你得先创建一个 OpenGL 纹理,然后把纹理 ID 传给 SurfaceTexture 的构造函数。
// 1. 生成一个 OpenGL 纹理
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
int textureId = textures[0];
// 2. 绑定并设置纹理参数
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
// 3. 创建 SurfaceTexture,传入纹理 ID
SurfaceTexture surfaceTexture = new SurfaceTexture(textureId);
我的经验:纹理参数一定要设置 GL_CLAMP_TO_EDGE,否则在某些机型上会出现边缘黑边或花屏。我曾经在小米某款机型上踩过这个坑,排查了半天才发现是纹理环绕方式的问题。
12.3 把 Surface 交给 Camera2
SurfaceTexture 内部维护了一个 Surface 对象。你通过 surfaceTexture.getSurface() 拿到它,然后传给 Camera2 的预览请求。
// 获取 Surface
Surface surface = new Surface(surfaceTexture);
// 创建 CaptureRequest.Builder
CaptureRequest.Builder previewBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
previewBuilder.addTarget(surface);
// 创建会话
cameraDevice.createCaptureSession(Arrays.asList(surface),
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
session.setRepeatingRequest(previewBuilder.build(),
new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(CameraCaptureSession session,
CaptureRequest request,
TotalCaptureResult result) {
// 每帧回调
}
}, null);
}
}, null);
嗯,这里要注意:onCaptureCompleted 回调并不代表纹理已经更新了。真正的纹理更新,需要你在 OpenGL 线程里主动调用 surfaceTexture.updateTexImage()。
12.4 纹理更新:updateTexImage() 的正确姿势
为什么说“正确姿势”?因为很多人把 updateTexImage() 放在了 Camera2 的回调里,结果发现画面卡顿或者纹理不更新。
原因很简单:updateTexImage() 必须在 OpenGL 线程(也就是持有 EGL 上下文的线程)中调用。Camera2 的回调跑在 Camera 线程,跟 OpenGL 线程不是同一个。
我曾经犯过的错:直接在 Camera2 的 onCaptureCompleted 里调 updateTexImage(),结果纹理一直没更新,画面黑屏。后来才意识到,OpenGL 的操作必须在线程绑定的 EGL 上下文中执行。
正确的做法是:在 Camera2 回调里发一个消息到 OpenGL 线程的 Handler,然后在 OpenGL 线程里调用 updateTexImage()。
// Camera2 回调中
@Override
public void onCaptureCompleted(CameraCaptureSession session,
CaptureRequest request,
TotalCaptureResult result) {
// 发送消息到 GL 线程
glHandler.post(() -> {
surfaceTexture.updateTexImage();
// 获取变换矩阵(如果需要)
float[] mtx = new float[16];
surfaceTexture.getTransformMatrix(mtx);
// 开始渲染
renderFrame(mtx);
});
}
12.5 预览帧处理:拿到数据做点什么
纹理更新之后,你就可以为所欲为了。常见的预览帧处理包括:
- 截图:从纹理中读取像素数据,保存为 Bitmap
- 人脸检测:把纹理数据传给 ML Kit 或 OpenCV
- 录制视频:把纹理渲染到 MediaCodec 的 Surface 上
- 推流:结合 RTMP 库,把纹理数据编码后推送
我个人最常用的是截图功能。实现起来也不复杂:
// 创建一个帧缓冲对象
int[] fbo = new int[1];
GLES20.glGenFramebuffers(1, fbo, 0);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fbo[0]);
// 绑定纹理到帧缓冲
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, textureId, 0);
// 读取像素
IntBuffer buffer = IntBuffer.allocate(width * height);
GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer);
// 别忘了解绑
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
避坑指南:glReadPixels 读取的像素是上下颠倒的。你需要做一次垂直翻转,或者用 Bitmap 的 Matrix 处理一下。我曾经因为这个原因,截出来的图全是倒的,被测试同学吐槽了好久。
12.6 滤镜渲染:从简单到复杂
有了纹理,滤镜就是写 Fragment Shader 的事了。我习惯把滤镜分为三类:
| 类型 | 原理 | 示例 |
|---|---|---|
| 颜色滤镜 | 修改像素的 RGB 值 | 灰度、怀旧、冷色调 |
| 卷积滤镜 | 用卷积核处理像素邻域 | 模糊、锐化、边缘检测 |
| 混合滤镜 | 多纹理叠加或混合 | 美颜、贴纸、双重曝光 |
最简单的灰度滤镜,Fragment Shader 长这样:
#version 300 es
precision mediump float;
uniform sampler2D uTexture;
in vec2 vTexCoord;
out vec4 fragColor;
void main() {
vec4 color = texture(uTexture, vTexCoord);
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
fragColor = vec4(vec3(gray), color.a);
}
你看,核心就是一行 dot 运算。你想想看,从 Camera2 拿到实时画面,到 OpenGL 纹理,再到 Shader 里做颜色变换,整个过程行云流水。
12.7 滤镜链:多个滤镜叠加
实际项目中,很少只用一个滤镜。用户可能想先调亮度,再加个美颜,最后加个边框。这就涉及到滤镜链的概念。
我习惯的做法是:用多个 FBO(帧缓冲对象)串联起来。每个 FBO 对应一个滤镜,上一个的输出纹理作为下一个的输入。
代码实现上,每个滤镜就是一个渲染 Pass:
// 滤镜链渲染
int currentTexture = originalTextureId;
for (Filter filter : filterChain) {
// 绑定当前滤镜的 FBO
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, filter.getFboId());
// 用当前纹理作为输入,渲染到 FBO
filter.render(currentTexture);
// 下一个滤镜的输入就是当前 FBO 的纹理
currentTexture = filter.getOutputTextureId();
}
// 最后把结果渲染到屏幕
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
renderToScreen(currentTexture);
注意:滤镜链越长,GPU 开销越大。在低端机型上,3 个滤镜以上就可能掉帧。我建议在性能敏感的场景下,把多个滤镜合并到一个 Shader 里,减少 Pass 数量。
12.8 实战中的几个坑
最后分享几个我实际项目中踩过的坑,希望能帮你少走弯路:
- 纹理大小不匹配:Camera2 输出的分辨率可能跟 OpenGL 视口不一致。记得用
getTransformMatrix()获取变换矩阵,应用到顶点坐标上。 - EGL 上下文丢失:App 切到后台再回来,EGL 上下文可能失效。需要重建纹理、SurfaceTexture 和 FBO。
- 内存泄漏:SurfaceTexture 用完后记得
release(),否则 Camera2 会话无法正常关闭。 - 线程安全:SurfaceTexture 的方法不是线程安全的。所有操作都放在 OpenGL 线程里做。
Camera2 与 OpenGL 的结合,说难不难,说简单也不简单。核心就是理解 SurfaceTexture 这个桥梁的作用,以及纹理更新的时机。一旦你掌握了这套流程,实时滤镜、美颜、特效、视频录制,统统不在话下。
公众号:蓝海资料掘金营,微信deep3321