23、OpenGL ES后处理:YUV转RGB着色器、色彩增强、滤镜效果、GPU加速
各位同学,今天我们来聊聊视频硬解码后的重头戏——OpenGL ES后处理。
解码器吐出来的数据,通常是YUV格式。但屏幕要的是RGB。这个转换,如果放在CPU上做,性能会很难看。我早期做播放器时,就踩过这个坑——解码帧率明明有60fps,一进渲染管线直接掉到30fps。后来才发现,瓶颈就在YUV转RGB这一步。
所以,用GPU做后处理,是Android视频开发的必修课。今天我会把YUV转RGB的着色器写法、色彩增强、滤镜效果,以及GPU加速的实战技巧,一次性讲透。
23.1 YUV转RGB着色器:从数学到代码
YUV转RGB,说白了就是一个矩阵乘法。不同的颜色标准(BT.601、BT.709、BT.2020),系数不一样。我习惯在着色器里写死BT.709的系数,因为大部分高清视频都用这个标准。
先看顶点着色器,它负责传递纹理坐标:
// 顶点着色器
attribute vec4 aPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;
void main() {
gl_Position = aPosition;
vTexCoord = aTexCoord;
}
片元着色器才是核心。这里要注意:YUV数据通常分三个纹理传入(Y、U、V各一个)。采样后,用矩阵运算转成RGB:
// 片元着色器 - YUV转RGB (BT.709)
precision mediump float;
uniform sampler2D uTextureY;
uniform sampler2D uTextureU;
uniform sampler2D uTextureV;
varying vec2 vTexCoord;
void main() {
float y = texture2D(uTextureY, vTexCoord).r;
float u = texture2D(uTextureU, vTexCoord).r - 0.5;
float v = texture2D(uTextureV, vTexCoord).r - 0.5;
vec3 rgb;
rgb.r = y + 1.5748 * v;
rgb.g = y - 0.1873 * u - 0.4681 * v;
rgb.b = y + 1.8556 * u;
gl_FragColor = vec4(rgb, 1.0);
}
23.2 色彩增强:让画面更通透
YUV转RGB只是基础。很多时候,解码出来的画面偏灰、偏暗。这时候就需要色彩增强。
我个人常用的手法是对比度+饱和度联合调整。在RGB空间里,先转成HSL或HSV,调整后再转回来。但这样计算量太大。我推荐一个轻量级方案——直接在RGB空间做线性变换:
// 色彩增强着色器
uniform float uContrast; // 对比度,1.0为原始
uniform float uSaturation; // 饱和度,1.0为原始
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
// 对比度调整
color.rgb = (color.rgb - 0.5) * uContrast + 0.5;
// 饱和度调整
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
color.rgb = mix(vec3(gray), color.rgb, uSaturation);
gl_FragColor = color;
}
你想想看,uContrast设为1.2,画面立刻精神很多。uSaturation设为1.3,色彩就鲜艳了。这两个参数配合使用,能覆盖大部分调色需求。
23.3 滤镜效果:从LUT到实时算法
滤镜,说白了就是给画面套一层“风格”。实现方式有两种:LUT(颜色查找表)和实时算法。
LUT方案:预先生成一个3D颜色映射表(通常是64x64x64),在着色器里根据输入颜色查表输出。优点是效果丰富,缺点是LUT文件大、加载慢。
实时算法方案:直接在着色器里写数学公式。比如复古滤镜,就是降低饱和度、增加暖色调:
// 复古滤镜
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
// 降低饱和度
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
color.rgb = mix(color.rgb, vec3(gray), 0.4);
// 增加暖色调
color.r *= 1.1;
color.g *= 0.95;
gl_FragColor = color;
}
我项目中更倾向于混合方案:基础滤镜用实时算法,复杂滤镜用LUT。这样既保证了性能,又保留了扩展性。
23.4 GPU加速:为什么比CPU快?
GPU加速的核心,是并行计算。CPU处理一个像素,可能需要几十条指令。而GPU一次可以处理成百上千个像素。你想想看,一个1080p的画面有200多万个像素,GPU的优势是碾压级的。
具体到Android平台,GPU加速的流程是这样的:
- 创建OpenGL上下文:与SurfaceView或TextureView绑定。
- 上传纹理:把YUV数据上传到GPU显存。
- 执行着色器:GPU并行处理每个像素。
- 渲染到屏幕:或者渲染到FBO(帧缓冲对象)做后续处理。
这里有个关键点:纹理上传的带宽。YUV数据量很大(1080p一帧约3MB),如果每帧都从CPU拷贝到GPU,带宽会成为瓶颈。我建议用PBO(像素缓冲对象)来异步上传,或者直接用SurfaceTexture让解码器直接输出到GPU纹理。
23.5 知识体系总览
下面这张图,是我整理的本章节知识体系。你可以把它当作一个“思维导图”,快速回顾核心内容:
23.6 避坑指南:我踩过的三个坑
最后,分享几个我实际项目中遇到的坑,希望能帮你少走弯路。
- 纹理坐标翻转:Android的纹理坐标系和OpenGL标准坐标系是上下颠倒的。如果不做翻转,画面会倒立。我习惯在顶点着色器里直接处理:
vTexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y); - YUV数据对齐:有些解码器输出的YUV数据,每行像素数不是4的倍数。如果直接当纹理上传,会出现“条纹”现象。解决办法是按行对齐,或者用
glPixelStorei设置对齐方式。 - 多线程渲染:OpenGL上下文不是线程安全的。如果解码线程和渲染线程共用同一个上下文,会崩溃。我的做法是每个线程创建独立的上下文,或者用
eglMakeCurrent切换。
gl_FragColor = vec4(vTexCoord, 0.0, 1.0);来输出纹理坐标。如果画面出现渐变,说明纹理坐标传递正确。如果画面是纯色,说明坐标有问题。
嗯,以上就是本章的全部内容。YUV转RGB、色彩增强、滤镜效果、GPU加速,这四个点环环相扣。掌握了它们,你就能在Android上做出流畅、漂亮的视频渲染效果。
公众号:蓝海资料掘金营,微信deep3321