15. 特效与滤镜:视频滤镜框架、GPU加速渲染、OpenGL ES集成
说到视频特效和滤镜,这其实是多媒体框架里最“炫”的一块。用户看到的那些美颜、风格化、转场动画,底层都是靠滤镜框架在撑。我早期做播放器的时候,总觉得滤镜就是个锦上添花的东西,直到有一次客户要求在低端机上跑实时美颜……嗯,那才真正领教了GPU加速的重要性。
15.1 视频滤镜框架的整体设计
Stagefright里的滤镜框架,说白了就是一条“处理链”。每一帧数据流经一个个滤镜节点,每个节点干自己的活——有的调色,有的模糊,有的做变形。我个人习惯把这种架构叫做“流水线模型”。
框架的核心接口是 sp 和 MediaBuffer。滤镜之间通过这两个东西传递数据和控制信息。你想想看,如果每个滤镜都要自己管理内存和同步,那代码得多乱?
核心设计原则:
- 每个滤镜只做一件事,做好一件事
- 滤镜之间通过Buffer传递,不共享状态
- 支持动态插拔,运行时增减滤镜
我在项目中遇到过一个问题:多个滤镜串联时,中间Buffer的格式转换开销巨大。后来我们加了一个“格式协商”阶段,让相邻滤镜直接使用相同的颜色格式,省掉了不必要的拷贝。
15.2 GPU加速渲染:为什么非用不可?
CPU做图像处理,不是不行,但效率差太远。举个例子,一个1080p的帧,每个像素做一次颜色矩阵运算,CPU可能要花5ms,而GPU只需要0.5ms。实时视频要求每帧16ms内完成,CPU根本扛不住。
Stagefright里集成了GPU加速的路径,主要通过两种方式:
- SurfaceFlinger合成层:利用硬件合成器做叠加和变换
- OpenGL ES渲染管线:自定义Shader做像素级操作
我个人更倾向于后者,因为灵活性高。你可以写自己的Fragment Shader,实现任意效果。我曾经用20行Shader代码就实现了一个复古胶片滤镜,效果比CPU版本快10倍。
避坑指南:GPU加速不是万能的。我曾经在某个项目里,把所有滤镜都扔到GPU上跑,结果发现小分辨率下CPU反而更快。原因是GPU启动上下文有固定开销,帧太小的话,这个开销占比就太大了。建议根据分辨率动态选择渲染路径。
15.3 OpenGL ES集成:从Buffer到纹理
要把视频帧送到GPU里处理,关键一步是把 MediaBuffer 转换成OpenGL纹理。这个过程我拆成三步:
- 从
MediaBuffer里取出原始数据(通常是YUV格式) - 创建OpenGL纹理,上传像素数据
- 绑定纹理到渲染管线,执行Shader
代码大概长这样:
// 创建纹理
GLuint textureId;
glGenTextures(1, &textureId);
glBindTexture(GL_TEXTURE_2D, textureId);
// 上传YUV数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE,
width, height, 0, GL_LUMINANCE,
GL_UNSIGNED_BYTE, yuvData);
// 设置纹理参数
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
嗯,这里要注意:YUV数据不能直接当RGB用。你需要写一个Shader来做颜色空间转换。我习惯在Fragment Shader里做这个事,省掉CPU转换的开销。
15.4 滤镜链与渲染流程
实际项目中,滤镜很少单独使用。通常是一个链:先做颜色校正,再做美颜,最后加一个风格化效果。Stagefright里用 FilterChain 来管理这个流程。
我画了一张图,帮你理解整个数据流:
从图里你能看到,每一帧从 MediaBuffer 出发,经过纹理上传,进入滤镜链,最后交给 SurfaceFlinger 合成显示。每个滤镜对应一个Shader Pass,多个Pass串联就是完整的滤镜效果。
15.5 实战:写一个简单的黑白滤镜
理论说完了,咱们动手写个例子。一个黑白滤镜,其实就是把RGB转成灰度。Fragment Shader 代码:
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
gl_FragColor = vec4(gray, gray, gray, color.a);
}
这段代码里,dot 函数做的是加权求和。为什么权重是0.299、0.587、0.114?因为人眼对绿色最敏感,对蓝色最不敏感。这个权重是ITU-R BT.601标准里定义的。
注意:如果你在低端GPU上跑这个Shader,可能会发现性能不如预期。原因是 dot 指令在某些硬件上不是原生支持的。我曾经在某个芯片上被迫改成三个乘法加两个加法,性能才上去。所以,写Shader时最好查一下目标平台的指令集。
15.6 滤镜框架的扩展性
Stagefright的滤镜框架设计得挺巧妙。它允许你注册自定义滤镜,就像插件一样。你只需要实现 Filter 接口,重写 process() 方法,然后通过 FilterFactory 注册进去。
我个人的经验是,做滤镜框架时一定要预留“参数通道”。比如美颜滤镜的磨皮强度、瘦脸程度,这些参数需要能从上层动态传入。我见过一个项目,滤镜写死了参数,结果产品经理每次改需求都要重新编译……那叫一个痛苦。
所以,建议你在设计滤镜接口时,加一个 setParameter() 方法,接收一个 KeyValueMap。这样上层可以随时调整参数,底层不用改代码。
好了,关于特效与滤镜,核心就是这些。记住:框架设计要灵活,渲染路径要选对,Shader要写精。这三样做好了,视频滤镜这块基本就稳了。