14、OpenGL ES视频渲染:YUV转RGB着色器、多纹理渲染、视频滤镜实现
视频渲染这块,说白了就是让手机屏幕把视频数据正确显示出来。但摄像头采集到的原始数据通常是YUV格式,而屏幕显示需要RGB。这个转换过程,就是咱们今天要啃的第一块硬骨头。
我记得刚入行那会儿,第一次用OpenGL ES渲染视频,画面全是绿的。排查了半天,发现是YUV到RGB的转换矩阵用错了。嗯,这种坑踩过一次,你就再也不会忘了。
14.1 YUV与RGB:视频渲染的基石
YUV和RGB是两种不同的色彩空间。RGB大家很熟悉,红绿蓝三通道。YUV呢,Y是亮度,UV是色度。为什么视频要用YUV?因为人眼对亮度敏感,对色度不敏感。压缩时可以多丢点UV信息,画质损失不大。
常见的YUV格式有:
| 格式 | 说明 | 常见场景 |
|---|---|---|
| YUV420P (I420) | 三个平面,Y、U、V分开存储 | 软件编码、FFmpeg解码输出 |
| NV12 | 两个平面,Y平面 + UV交错平面 | Android Camera、硬件编码器 |
| NV21 | 两个平面,Y平面 + VU交错平面 | 旧版Android Camera |
我个人习惯用NV12,因为Android硬件编码器默认输出就是它,省去一次格式转换。
14.2 YUV转RGB着色器:核心公式
YUV转RGB不是简单的加减法,它有一套标准转换公式。以BT.601标准为例:
// 顶点着色器(固定不变)
attribute vec4 aPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;
void main() {
gl_Position = aPosition;
vTexCoord = aTexCoord;
}
// 片元着色器(YUV420P转RGB)
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D yTexture;
uniform sampler2D uTexture;
uniform sampler2D vTexture;
void main() {
float y = texture2D(yTexture, vTexCoord).r;
float u = texture2D(uTexture, vTexCoord).r - 0.5;
float v = texture2D(vTexture, vTexCoord).r - 0.5;
vec3 rgb;
rgb.r = y + 1.402 * v;
rgb.g = y - 0.344 * u - 0.714 * v;
rgb.b = y + 1.772 * u;
gl_FragColor = vec4(rgb, 1.0);
}
这里有个细节:UV值要减去0.5,因为纹理采样返回的是[0,1]范围,而UV的合法范围是[-0.5, 0.5]。不做这步,颜色就全偏了。
14.3 多纹理渲染:三纹理 vs 双纹理
YUV420P需要三个纹理(Y、U、V各一个),NV12只需要两个纹理(Y一个,UV一个)。
多纹理渲染的关键点:
- 纹理单元绑定:每个纹理要绑定到不同的纹理单元(GL_TEXTURE0、GL_TEXTURE1等)
- 采样器对应:着色器中的uniform sampler2D要和纹理单元一一对应
- 纹理参数:YUV纹理建议用GL_LINEAR过滤,不要用GL_NEAREST,否则会有锯齿
// 绑定纹理的代码片段
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, yTexId);
glUniform1i(yTextureLoc, 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, uTexId);
glUniform1i(uTextureLoc, 1);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, vTexId);
glUniform1i(vTextureLoc, 2);
你想想看,如果纹理单元绑定错了,画面会怎样?要么全黑,要么颜色错乱。我调试时经常用glGetError()来排查这类问题。
14.4 视频滤镜实现:从简单到复杂
滤镜的本质,就是在片元着色器里对RGB值做数学变换。咱们从最简单的开始:
14.4.1 灰度滤镜
void main() {
vec4 color = texture2D(inputTexture, vTexCoord);
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
gl_FragColor = vec4(vec3(gray), 1.0);
}
灰度滤镜的权重系数是有讲究的。人眼对绿色最敏感,所以绿色权重最高(0.587)。直接用平均值(0.333)的话,灰度图会偏暗。
14.4.2 冷暖色调滤镜
void main() {
vec4 color = texture2D(inputTexture, vTexCoord);
// 暖色调:增强红色,减弱蓝色
color.r *= 1.2;
color.b *= 0.8;
gl_FragColor = color;
}
冷色调反过来,增强蓝色,减弱红色。这种滤镜实现简单,但效果明显。
14.4.3 查找表(LUT)滤镜
这是商业级App最常用的方案。把颜色映射关系存到一张小图里,渲染时查表替换。
// LUT滤镜着色器(简化版)
uniform sampler2D lutTexture; // 64x64的LUT图
void main() {
vec4 color = texture2D(inputTexture, vTexCoord);
// 根据RGB值在LUT图中查找映射后的颜色
float blue = color.b * 63.0;
vec2 quad1 = vec2(floor(blue) / 64.0, 0.0);
vec2 quad2 = vec2(ceil(blue) / 64.0, 0.0);
// ... 插值计算
gl_FragColor = mappedColor;
}
14.5 知识体系总览
下面这张图把本章的核心逻辑串起来了:
14.6 性能优化:别让渲染成为瓶颈
视频渲染对性能要求很高,尤其是高分辨率(4K)和高帧率(60fps)场景。我总结了几条优化经验:
- 减少纹理上传次数:用PBO(Pixel Buffer Object)异步上传,避免阻塞渲染线程
- 复用EGL上下文:不要每帧都创建销毁,用EGLSurface来回切换
- 着色器预编译:滤镜切换时,提前编译好所有着色器,运行时直接切换
- 降低纹理精度:如果不需要高精度,用GL_RGB565代替GL_RGBA8888,带宽减半
核心要点回顾:
- YUV转RGB是视频渲染的第一步,矩阵选对是关键
- 多纹理渲染要管理好纹理单元和采样器的对应关系
- 滤镜本质是片元着色器里的数学变换,LUT是工业级方案
- 性能优化从减少纹理上传和复用资源入手
好了,这一章的内容就到这儿。YUV转RGB的着色器代码我已经给了,建议你动手跑一遍。遇到颜色不对的情况,先检查矩阵,再检查纹理绑定——这两个地方出问题的概率最高。