11、OpenGL ES滤镜实现:灰度滤镜、高斯模糊、颜色矩阵变换、LUT滤镜

滤镜这东西,说白了就是给视频画面“化妆”。

我记得刚做音视频那会儿,老板丢过来一句:“给直播加个美颜滤镜。”我当时心想,不就是调个颜色嘛,能有多难?结果一上手才发现,从像素点操作到GPU渲染,中间隔着一整座山。

今天咱们就把这座山翻一翻。我会带你手撸四种经典滤镜:灰度、高斯模糊、颜色矩阵变换、LUT滤镜。每一种我都会讲清楚原理,贴出核心代码,再附上我踩过的坑。

本章核心知识点:

  • 灰度滤镜:加权平均法,去掉色彩信息
  • 高斯模糊:卷积核计算,实现平滑效果
  • 颜色矩阵变换:4x5矩阵,灵活控制RGBA
  • LUT滤镜:查表映射,实现复杂风格
OpenGL ES 滤镜实现体系 片元着色器 灰度滤镜 加权平均法 高斯模糊 卷积核计算 颜色矩阵 4x5矩阵变换 LUT滤镜 查表映射 核心原理:对每个像素的RGBA值进行数学变换 实现方式:编写GLSL片元着色器,GPU并行计算 性能关键:减少纹理采样次数,避免分支判断

11.1 灰度滤镜:去掉颜色,保留亮度

灰度滤镜是所有滤镜里最基础的。原理很简单:把RGB三个通道合并成一个亮度值。

但这里有个坑——不能简单取平均值。人眼对绿色最敏感,对蓝色最不敏感。所以业界通用的加权公式是:

// 灰度加权公式
float gray = 0.299 * R + 0.587 * G + 0.114 * B;

我在项目中遇到过有人直接用 (R+G+B)/3,结果画面灰蒙蒙的,暗部细节全丢了。嗯,这就是没考虑人眼感知曲线。

来看完整的GLSL片元着色器代码:

// 灰度滤镜 - 片元着色器
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;

void main() {
    vec4 color = texture2D(uTexture, vTexCoord);
    float gray = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
    gl_FragColor = vec4(gray, gray, gray, color.a);
}

小技巧:如果你想要复古风格,可以把加权系数稍微调一调。比如增加红色权重,画面会偏暖黄。我做过一个“老电影”滤镜,就是把红色权重提到0.4,绿色降到0.4,蓝色0.2,效果很有年代感。

11.2 高斯模糊:让画面变“柔”

高斯模糊是美颜滤镜的基石。原理是用一个高斯核(卷积核)对每个像素及其周围像素做加权平均。

说白了,就是让每个像素“看”一眼邻居,然后取个平均值。离得近的邻居权重高,离得远的权重低。

一个3x3的高斯核长这样:

// 3x3高斯核(归一化后)
float kernel[9] = float[](
    1.0/16, 2.0/16, 1.0/16,
    2.0/16, 4.0/16, 2.0/16,
    1.0/16, 2.0/16, 1.0/16
);

我曾经犯过一个错误:直接用5x5的核在手机上跑,结果帧率直接掉到15fps。为什么?因为每个像素要采样25次纹理!

避坑指南:移动端GPU的纹理采样是性能瓶颈。建议用“两步分离法”——先横向模糊,再纵向模糊。这样5x5的核只需要5+5=10次采样,性能翻倍。

来看分离高斯模糊的实现:

// 第一步:横向模糊
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
uniform float uOffset; // 像素偏移量,通常为1.0/width

void main() {
    vec4 color = vec4(0.0);
    float weights[5] = float[](0.0545, 0.2442, 0.4026, 0.2442, 0.0545);
    
    for (int i = -2; i <= 2; i++) {
        vec2 offset = vec2(float(i) * uOffset, 0.0);
        color += texture2D(uTexture, vTexCoord + offset) * weights[i + 2];
    }
    gl_FragColor = color;
}

// 第二步:纵向模糊(把offset改成vec2(0.0, float(i) * uOffset)即可)

性能数据:在骁龙865上,3x3单步模糊耗时约0.3ms,5x5分离模糊约0.5ms,而5x5单步模糊要1.2ms。差距很明显吧?

11.3 颜色矩阵变换:万能调色板

颜色矩阵变换是我个人最喜欢的方式。它用一个4x5的矩阵,对每个像素的RGBA做线性变换。

矩阵长这样:

// 颜色矩阵结构
// [ R' ]   [ a, b, c, d, e ]   [ R ]
// [ G' ] = [ f, g, h, i, j ] * [ G ]
// [ B' ]   [ k, l, m, n, o ]   [ B ]
// [ A' ]   [ p, q, r, s, t ]   [ A ]
//                               [ 1 ]

举个例子,要实现“怀旧棕色调”,矩阵是这样的:

// 怀旧棕色调矩阵
mat4 colorMatrix = mat4(
    0.393, 0.769, 0.189, 0.0,
    0.349, 0.686, 0.168, 0.0,
    0.272, 0.534, 0.131, 0.0,
    0.0,   0.0,   0.0,   1.0
);

我在项目中用颜色矩阵做过一个“一键日系”滤镜。说白了就是把蓝色通道压低一点,绿色通道提亮一点,再整体加一点曝光。矩阵调了大概20分钟,效果就出来了。

片元着色器实现:

precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
uniform mat4 uColorMatrix;

void main() {
    vec4 color = texture2D(uTexture, vTexCoord);
    gl_FragColor = uColorMatrix * color;
}

注意:矩阵乘法是列优先的。在GLSL里,vec4乘以mat4,vec4会被当作列向量处理。如果你发现颜色不对,检查一下矩阵的转置。

11.4 LUT滤镜:专业级的色彩魔法

LUT(Look Up Table)滤镜是工业级方案。它用一张图片(通常是512x512或64x64)来存储颜色映射关系。

原理很简单:把输入颜色的RGB值作为坐标,去LUT图上查对应的输出颜色。

LUT图长这样:

  • 横向:蓝色通道从0到255
  • 纵向:红色通道从0到255
  • 每张子图:绿色通道从0到255

听起来有点绕?我刚开始也懵。后来想明白了:LUT图就是把一个三维的颜色空间(RGB)展开到二维平面上。

核心着色器代码:

precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
uniform sampler2D uLUT;

void main() {
    vec4 color = texture2D(uTexture, vTexCoord);
    
    // 把RGB映射到LUT坐标
    float blue = color.b * 63.0; // 64x64的LUT
    float quad1 = floor(blue / 8.0);
    float quad2 = floor(blue / 8.0) + 1.0;
    
    vec2 lutPos1 = vec2(
        (color.r * 7.0 + quad1 * 8.0) / 64.0,
        (color.g * 7.0 + 0.5) / 64.0
    );
    vec2 lutPos2 = vec2(
        (color.r * 7.0 + quad2 * 8.0) / 64.0,
        (color.g * 7.0 + 0.5) / 64.0
    );
    
    vec4 lutColor1 = texture2D(uLUT, lutPos1);
    vec4 lutColor2 = texture2D(uLUT, lutPos2);
    
    // 双线性插值
    float mixFactor = fract(blue / 8.0);
    gl_FragColor = mix(lutColor1, lutColor2, mixFactor);
}

我曾经踩过的坑:LUT图的精度很关键。用8x8x8的LUT(512色)和64x64x64的LUT(262144色),效果天差地别。但64x64的LUT图有512x512像素,纹理采样开销不小。建议在性能和效果之间取平衡,一般用32x32x32就够了。

11.5 四种滤镜对比总结

滤镜类型 原理 性能消耗 适用场景 实现难度
灰度滤镜 加权平均 极低(1次采样) 黑白风格、去色 ★☆☆☆☆
高斯模糊 卷积核 中等(多次采样) 美颜、背景虚化 ★★★☆☆
颜色矩阵 线性变换 极低(1次采样+矩阵乘) 快速调色、风格化 ★★☆☆☆
LUT滤镜 查表映射 中等(2-3次采样) 专业调色、电影风格 ★★★★☆

我个人建议:如果只是简单调色,用颜色矩阵就够了。如果要实现复杂风格(比如电影色调、赛博朋克),LUT是首选。灰度滤镜和模糊滤镜,属于基础工具,必须掌握。

最后说一句:滤镜的实现核心都在片元着色器里。你写的每一行GLSL代码,GPU都会对每个像素并行执行。所以代码要精简,避免if-else分支,减少纹理采样次数。记住:移动端GPU最怕的就是“纹理采样风暴”。


公众号:蓝海资料掘金营,微信deep3321