Phase 1 是空手画画,Phase 2 开始拿真实图像开刀——这也是 Android 开发者最常用 Shader 的场景:给照片、视频帧、UI 截图加特效。本章交付三把手术刀:改采样坐标(扭曲)、改采样结果(调色)、多点采样再合成(卷积)。之后三章的所有效果,全是这三刀的组合。
1纹理:终于可以"读"了
0.1 章的规则②说 Fragment Shader 读不到「邻居这一帧的输出」。但有个重要的例外:纹理(Texture)——作为输入提前上传到 GPU 的图像,任何像素都能随便读、读任何位置。读取动作叫采样(sample):
uniform sampler2D uTex; // 纹理采样器,CPU 端绑定
vec2 uv = gl_FragCoord.xy / uResolution;
vec4 color = texture(uTex, uv); // 用 0~1 的 UV 坐标去图里取色
关键认知:texture(uTex, uv) 里的 uv 不必是自己的坐标!原样传入 = 原图;传入被你篡改过的坐标 = 扭曲;在自己坐标周围多取几次 = 模糊。图像特效的全部秘密就在「喂什么坐标、取几次、怎么合」。这解决了 0.1 章练习 3 留下的悬念:模糊读的不是邻居的输出,而是输入纹理里邻居位置的颜色。
本章 Demo 里的 uTex 是一张程序生成的风景图。在 Android 上,它可以是 Bitmap(3.3 章上传)、相机预览帧(SurfaceTexture)、视频帧,甚至你的 Compose UI 本身——AGSL 的 RenderEffect 会把整个 View 树的渲染结果当作纹理喂给你的 Shader(3.4 章),这正是「给 UI 加毛玻璃」的原理。
2UV 手术:扭曲图像
第一把刀:采样前修改 UV。1.3 章所有坐标技巧(平移、旋转、极坐标、噪声扰动)现在全部适用于图像——把「查 SDF」换成「查纹理」而已:
再看一个经典的「漩涡(swirl)」:把坐标转成极坐标,旋转角度随「离中心的距离」变化——中心转得多、边缘转得少:
vec2 center = vec2(0.5);
vec2 d = uv - center;
float angle = smoothstep(0.5, 0.0, length(d)) * 3.0 * sin(uTime * 0.5); // 越近转越多
float s = sin(angle), c = cos(angle);
vec2 swirled = vec2(d.x * c - d.y * s, d.x * s + d.y * c) + center;
fragColor = texture(uTex, swirled);
3逐像素调色
第二把刀:采样后修改颜色。这就是所有「滤镜 App」的底层。几个必备配方:
vec3 c = texture(uTex, uv).rgb;
// 灰度:不是简单平均!人眼对绿最敏感,用亮度权重
float luma = dot(c, vec3(0.299, 0.587, 0.114));
// 反色 / 对比度 / 饱和度,全是 mix 的花样
vec3 invert = 1.0 - c;
vec3 contrast = mix(vec3(0.5), c, 1.4); // >1 增对比,<1 减
vec3 saturate = mix(vec3(luma), c, 1.5); // >1 增饱和,0 = 灰度
// 复古褐(sepia):灰度重新映射到棕色调
vec3 sepia = luma * vec3(1.2, 1.0, 0.8);
// 暗角(vignette):压暗四周,视线聚中心
float vig = smoothstep(0.9, 0.4, length(uv - 0.5));
c *= vig;
注意到没有:对比度、饱和度、灰度全是同一个 mix,只是插值的「锚点」不同——对比度以中灰为锚,饱和度以自身亮度为锚。理解了锚点思想,你可以自己发明调色参数:
4卷积:模糊、锐化与描边
第三把刀:多点采样。以自己为中心,把周围一圈(通常 3×3)的颜色各乘一个权重再求和——这个操作叫卷积(Convolution),权重表叫卷积核(Kernel)。换一张权重表,效果天差地别:
| 核 | 权重(3×3) | 效果 |
|---|---|---|
| Box Blur | 全 1/9 | 均值模糊:平均掉细节 |
| Gaussian | 1 2 1 / 2 4 2 / 1 2 1 (÷16) | 高斯模糊:中心权重大,更自然 |
| Sharpen | 0 -1 0 / -1 5 -1 / 0 -1 0 | 锐化:放大与邻居的差异 |
| Edge(Laplacian) | -1×8 周围 / 8 中心 | 只留边缘,平坦处归零 |
模糊半径翻倍,单 pass 采样数按平方涨:31×31 的核 = 每像素 961 次采样,手机直接冒烟。工业做法:① 拆成横竖两个 pass(31+31=62 次,数学上等价);② 先缩小图再模糊(降采样,模糊本来就不在乎细节);③ 迭代小模糊逼近大模糊。Android 的 RenderEffect.createBlurEffect 内部就是这些组合。自己写着玩用 3×3~5×5 即可。
5效果工坊的通用配方
三把刀就位,可以总结出 Phase 2 之后每个效果的统一骨架——以后看到任何眼花缭乱的图像特效,先按这个框架拆:
vec2 uv = gl_FragCoord.xy / uResolution;
vec2 uv2 = distort(uv); // 刀①:坐标手术(可选)
vec3 color = sampleN(uTex, uv2); // 刀③:采样一次或多次
color = grade(color, uv); // 刀②:逐像素调色(可选)
fragColor = vec4(color, 1.0);
下一章的马赛克 = 刀①(坐标量子化);毛玻璃 = 刀①(随机偏移)+ 刀③(多次采样);CRT = 三把刀全上。你已经有全部零件了。
本章小结
- 纹理是「可以随便读的输入图像」,
texture(uTex, uv)想读哪读哪——模糊的邻居数据由此而来。 - 刀①改坐标:采样前对 UV 做平移/波动/漩涡/噪声,图像就被扭曲。
- 刀②改颜色:灰度用亮度权重 dot;对比度、饱和度都是「换锚点的 mix」。
- 刀③多点采样:卷积核决定效果——均值/高斯是模糊,差分是锐化和描边。
- 大模糊要拆两 pass + 降采样,别硬堆核大小。
动手练习
- 把水波 Demo 的偏移幅度乘上
smoothstep(0.0, 0.5, length(uv - 0.5)),做出「中心清晰、边缘扭曲」的镜头畸变感。 - 实现「移轴摄影」效果:画面上下 1/3 用模糊,中间清晰,过渡用 smoothstep(拼合模糊与原图两份采样结果)。
- 把边缘检测的结果叠回原图(相加),你会得到一个「数字锐化」;调节叠加比例感受锐化强度。