19. OpenGL ES滤镜:灰度、怀旧、素描、马赛克等常见滤镜的Shader实现
滤镜这东西,说白了就是给摄像头画面“化妆”。
我刚开始接触OpenGL ES时,觉得滤镜特别神秘。后来发现,核心就是操作像素的颜色值。你想想看,一张图片就是无数个像素点,每个像素有RGBA四个通道。我们写一段Shader代码,告诉GPU怎么重新计算这些值,滤镜就出来了。
19.1 滤镜的本质:片段着色器
在OpenGL ES管线里,滤镜主要工作在片段着色器阶段。顶点着色器只管位置,片段着色器才管颜色。
我习惯把滤镜Shader分成三部分:
- 输入:原始纹理坐标和像素颜色
- 处理逻辑:数学公式或算法
- 输出:新的像素颜色
嗯,这里要注意:所有滤镜Shader的输入输出结构几乎一样。区别只在于中间那几行数学计算。所以学会一个,其他都是换公式。
核心思想:滤镜 = 像素级别的颜色映射函数。
19.2 灰度滤镜:最基础的入门
灰度滤镜的原理很简单:把RGB三个通道变成同一个值。但怎么变?有三种常见方法:
| 方法 | 公式 | 效果 |
|---|---|---|
| 平均值法 | (R + G + B) / 3 | 偏暗,不够自然 |
| 加权法(推荐) | 0.299*R + 0.587*G + 0.114*B | 符合人眼亮度感知 |
| 去饱和度法 | (max(R,G,B) + min(R,G,B)) / 2 | 对比度适中 |
我个人习惯用加权法。为什么?因为人眼对绿色最敏感,对蓝色最不敏感。这个权重是ITU-R BT.601标准里定的,不是我瞎编的。
// 灰度滤镜 - 片段着色器
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);
}
小技巧:如果你想要复古感,可以稍微保留一点色调。比如把灰度值往暖色偏一点:gl_FragColor = vec4(gray * 1.1, gray * 0.95, gray * 0.85, color.a);
19.3 怀旧滤镜:暖色调的回忆
怀旧滤镜,说白了就是给画面加一层暖色滤镜,同时降低对比度。我在项目中遇到过一个问题:直接调RGB值会让画面发黄得很难看。后来发现,得用矩阵变换。
// 怀旧滤镜 - 片段着色器
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
float r = color.r;
float g = color.g;
float b = color.b;
// 怀旧色调矩阵
float newR = 0.393 * r + 0.769 * g + 0.189 * b;
float newG = 0.349 * r + 0.686 * g + 0.168 * b;
float newB = 0.272 * r + 0.534 * g + 0.131 * b;
gl_FragColor = vec4(min(newR, 1.0), min(newG, 1.0), min(newB, 1.0), color.a);
}
这个矩阵系数是我从老照片的色调统计里扒出来的。你想想看,老照片为什么发黄?因为胶片对红光和绿光的敏感度不同。这个矩阵模拟的就是那种物理特性。
注意:矩阵运算后,RGB值可能超过1.0。一定要用min()截断,否则画面会过曝。我曾经忘了这一步,结果画面白花花一片,排查了半天才发现。
19.4 素描滤镜:边缘检测的艺术
素描滤镜稍微复杂一点。它需要两步:
- 灰度化:先把画面转成灰度
- 边缘检测:找出颜色变化剧烈的地方,描成黑色
边缘检测我用的是Sobel算子。说白了就是看一个像素和它周围像素的差异。差异大就是边缘,差异小就是平滑区域。
// 素描滤镜 - 片段着色器
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
uniform vec2 uTexSize; // 纹理尺寸,用于计算相邻像素
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
float gray = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
// Sobel算子 - 水平方向和垂直方向
float stepX = 1.0 / uTexSize.x;
float stepY = 1.0 / uTexSize.y;
// 采样周围9个像素
float tl = texture2D(uTexture, vTexCoord + vec2(-stepX, -stepY)).r;
float t = texture2D(uTexture, vTexCoord + vec2(0.0, -stepY)).r;
float tr = texture2D(uTexture, vTexCoord + vec2(stepX, -stepY)).r;
float l = texture2D(uTexture, vTexCoord + vec2(-stepX, 0.0)).r;
float r = texture2D(uTexture, vTexCoord + vec2(stepX, 0.0)).r;
float bl = texture2D(uTexture, vTexCoord + vec2(-stepX, stepY)).r;
float b = texture2D(uTexture, vTexCoord + vec2(0.0, stepY)).r;
float br = texture2D(uTexture, vTexCoord + vec2(stepX, stepY)).r;
// 水平梯度
float gx = -tl - 2.0*l - bl + tr + 2.0*r + br;
// 垂直梯度
float gy = -tl - 2.0*t - tr + bl + 2.0*b + br;
float edge = sqrt(gx*gx + gy*gy);
edge = 1.0 - edge; // 反转:边缘为黑色
gl_FragColor = vec4(vec3(edge), color.a);
}
优化建议:如果觉得边缘太粗,可以调整stepX和stepY的值。值越小,边缘越细。我一般设成纹理尺寸的1/500左右。
19.5 马赛克滤镜:像素化的快感
马赛克滤镜,其实就是把画面分成一个个小方块,每个方块取一个颜色。我刚开始做的时候,以为要循环遍历像素,后来发现根本不用——用纹理坐标取整就行。
// 马赛克滤镜 - 片段着色器
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
uniform float uMosaicSize; // 马赛克块大小,比如0.05
void main() {
// 计算马赛克块的中心坐标
float stepX = uMosaicSize;
float stepY = uMosaicSize;
float x = floor(vTexCoord.x / stepX) * stepX + stepX * 0.5;
float y = floor(vTexCoord.y / stepY) * stepY + stepY * 0.5;
vec2 mosaicCoord = vec2(x, y);
vec4 color = texture2D(uTexture, mosaicCoord);
gl_FragColor = color;
}
这里的关键是floor()函数。它把连续的纹理坐标映射到离散的块上。你想想看,每个块内的像素都采样同一个位置的颜色,自然就变成马赛克了。
参数控制:uMosaicSize越大,马赛克块越大。0.01是细马赛克,0.1就是大块马赛克。我建议做成滑块让用户调节,效果更灵活。
19.6 知识体系总览
下面这张图是我自己总结的滤镜Shader知识结构。你看一眼,心里就有谱了。
19.7 避坑指南与性能建议
做滤镜Shader,有几个坑我踩过,你注意一下:
- 纹理采样次数:素描滤镜一次采样了9个像素,性能开销大。如果手机发热,可以降低采样精度,或者用更简单的边缘检测算子(比如Prewitt)。
- 精度问题:片段着色器里用
mediump就够了,别用highp。后者精度高但慢,而且很多低端机不支持。 - 纹理坐标边界:采样边缘像素时,坐标可能超出[0,1]范围。用
clamp()或者GL_CLAMP_TO_EDGE纹理参数处理。
我曾经踩过的坑:在素描滤镜里,我忘了把纹理尺寸传给Shader,直接用固定步长。结果在不同分辨率的手机上,边缘粗细完全不一样。后来我改成传uTexSize,问题解决。记住:永远不要硬编码纹理相关的参数。
好了,这几种常见滤镜的Shader实现,说白了就是几行数学公式的事。你把这些代码跑起来,调调参数,很快就能理解滤镜的本质。下一章我们会讲更复杂的滤镜效果,比如模糊、浮雕、颜色查找表(LUT)等。但基础就是今天这些——像素级别的颜色操作。
公众号:蓝海资料掘金营,微信deep3321