GPU Gems 1 · Chapter 24. High-Quality Filtering
从 filter kernel 与 convolution 出发,理解 bicubic、separable filtering、GPU derivatives 和 analytical antialiasing 如何在质量与速度之间取舍。
学习目标
- 能从 source image、target image、kernel 和 convolution 解释一次高质量过滤,而不是只记住某个采样 API
- 能比较 box、bilinear、bicubic 与 separable filtering 的质量、采样、带宽和锐化风险
- 能用
ddx()与ddy()估计纹理 footprint,并把它们用于 procedural antialiasing 或动态 filter 选择 - 能在纹理缩放、条纹、边缘检测和视频格式转换中建立可复现的质量回归与帧预算
GPU 硬件擅长快速做有限种类的纹理过滤,但真实图像处理常常需要自定义 kernel、非标准格式或任意的数学函数。高质量过滤的工程问题不是“采样次数越多越好”,而是要让 source 像素、权重、纹理 footprint 和目标像素的关系清楚可测。
↡描述 source 像素邻域及其相对贡献的权重模式,决定一个 target 像素如何从输入图像获得颜色 是高质量过滤的最小可解释单元。给定输入图像 A,过滤器从 A 取一个局部窗口,按 kernel 权重组合成输出图像 B;缩放、锐化、颜色变化、模糊和边缘检测都可以落在这个接口上。
1. 从 box 到 bicubic:让权重匹配目标质量
↡用邻域像素及其权重相乘累加,把 source image 转换成 target image 的局部图像运算 通常表示按 kernel 对邻域求加权和。box filter 给每个命中的 texel 相同权重,容易构造且接近硬件线性过滤的直觉;但缩小会丢失边缘对比,放大会暴露矩形像素。
float4 boxFilter(float2 uv, float2 texel) {
float4 sum = 0.0;
sum += tex2D(source, uv + texel * float2(-1, -1));
sum += tex2D(source, uv + texel * float2( 1, -1));
sum += tex2D(source, uv + texel * float2(-1, 1));
sum += tex2D(source, uv + texel * float2( 1, 1));
return sum * 0.25;
}↡在 x 和 y 两个方向应用 cubic kernel 的重建过滤,通常用更高的中心权重保持缩放图像的边缘细节 让距离中心较近的 texel 有更高权重,并允许特定 cubic profile 出现负权重,从而产生轻微锐化。官方章节用解析 cubic 函数计算权重,也指出权重函数本身可以放入小型浮点纹理,让 shader 用快速查表取代重复数学。
float4 cubicFilter(float4 a, float4 b, float4 c, float4 d, float4 weights) {
return a * weights.x + b * weights.y + c * weights.z + d * weights.w;
}复杂 kernel 要同时记录 support 半径、权重归一化、负权重是否允许和边界寻址。一个看起来锐利的 kernel 可能在高对比边缘产生 ringing;一个看起来平滑的 kernel 可能吞掉细线。质量评估应同时看静态图和运动中的缩放。
2. Separable filtering:用中间纹理换取可控成本
↡将二维 kernel 拆成 x 方向和 y 方向两个一维 kernel,先后写入中间纹理的过滤方法 把二维卷积拆成两个一维 pass。教材式 bicubic 可以先对四行做 x 方向 cubic,再对四个结果做 y 方向 cubic;更实用的实现会把 x pass 写入 intermediate render target,再让 y pass 读取它。额外的写入和读取换来了更少的重复采样与更易控制的带宽。
float4 horizontal = cubic1D(source, uv, texel.x, kernelTexture);
// render horizontal into an intermediate target
float4 vertical = cubic1D(intermediate, uv, texel.y, kernelTexture);
return vertical;如果过滤在 x 与 y 方向均匀,可以共享同一个权重资源;如果图像有各向异性缩放或旋转,需要分别标定两个方向的 footprint。对视频或采集格式,kernel 还可能是 screen-aligned 的静态模式:权重相对屏幕位置固定,而不是随 sample texel 一起移动。
3. GPU derivatives:知道当前像素覆盖了多大纹理
↡GPU 用相邻 SIMD 像素的值差估计局部变化的函数,常见接口是 `ddx()` 和 `ddy()` 让 shader 询问一个值在屏幕 x、y 邻居之间变化多少。对于随顶点插值的 UV,ddx(UV) 和 ddy(UV) 可以近似当前像素在纹理上的 footprint;对于 uniform,差分为零。它们不是 CPU 意义下可任意组合的高阶解析导数,因此应把它们当作局部采样窗口的证据。
float2 du = ddx(uv);
float2 dv = ddy(uv);
float footprint = length(du) + length(dv);
float4 color = textureFilter(source, uv, footprint);硬件的普通 tex2D() 本身已经根据相邻像素变化自动选择过滤和 mip;显式 derivatives 的价值在于让程序过滤、纹理查表或非标准格式转换知道窗口大小。若硬件 profile 不支持 ddx() 和 ddy(),可以用预计算的 filter-width 纹理近似,但要把精度与内存当成明确 trade-off。
4. Analytical antialiasing:对覆盖区域而非单点采样
↡利用像素内函数的覆盖区间和积分估计平均颜色,减少 procedural texture 或几何边缘的单点锯齿 不是简单地把一个 if 改成更模糊的颜色,而是估计函数在当前像素窗口内占了多少面积。以条纹为例,先得到条纹函数的积分,再用 x0 与 x1 的积分差除以窗口宽度,就能得到像素内的平均 coverage。
float width = abs(ddx(value)) + abs(ddy(value));
float x0 = value - width * 0.5;
float x1 = value + width * 0.5;
float coverage = (stripeIntegral(x1) - stripeIntegral(x0)) / max(width, 0.0001);
return saturate(coverage);这个版本在高频条纹和非真实纹理函数上能消除明显 jaggies,但计算通常比直接查一张黑白纹理更慢。纹理版可以把复杂函数预先编码到一维或二维资源中,速度更好却受限于精度和 mip 设计。API 多重采样还会从几何边缘提供另一层样本;它可能改善 polygon edge,却不一定重新评估每个 sample 的 shader shading,所以 CPU 侧应在开启和关闭 AA 时都验证。
5. 动手实验:用 footprint 选择过滤策略
先预测四个变化:box filter 的成本最低但 alias 更高;bicubic 会增加采样与权重成本并保留更多边缘;footprint 变宽会降低高频 alias 却可能擦掉对比;oversample 和 analytical AA 能提高边缘 coverage,但不能替代对纹理格式、mip 和 API AA 的测量。
High-Quality Filtering Lab
先预测:为什么更宽的 footprint 不一定更清晰?
观察:box 快但易锯齿;bicubic 需要更多权重与采样;analytical AA 用 footprint 积分边缘覆盖,质量更高但数学成本也更高。
实验使用确定性的条纹信号表达过滤关系。发布前要用缩小、放大、斜线、细字、透明边缘、YUV/视频范围、procedural stripe 和高对比单像素线回归,并记录纹理 fetch、ALU、临时 RT、mip 级别、ddx/ddy 支持和不同 API AA 状态。
小结:高质量过滤是可解释的采样窗口
- filter kernel 描述 source 邻域与权重;convolution 把这些权重应用到输入图像以得到 target。
- box 快但质量有限;bicubic 通过更丰富的 profile 提升重建质量,负权重也带来 ringing 风险。
- separable filtering 用水平和垂直 pass 加中间纹理换取更可控的采样与带宽。
- GPU derivatives 用
ddx()和ddy()暴露局部 footprint;它们支持动态过滤,却不是任意高阶导数。 - analytical antialiasing 通过像素覆盖积分减少 procedural alias,但应和纹理查表、mip 以及 API 多重采样一起评估。
练习
问题 1|改 Demo 代码 一个二维过滤器需要 4×4 个颜色样本。如果 x 与 y 方向可以分离,设计两个 pass,并说明中间纹理的作用以及大致的采样变化。
问题 2|推导题 某像素的 ddx(uv) 长度为 0.04,ddy(uv) 长度为 0.06。若过滤器把两者相加作为 footprint,得到多少?当纹理缩放使两个差分都变成原来的两倍时,为什么应该考虑更宽的过滤或更低的 mip?
问题 3|独立实现题 为一个需要高质量缩放和 procedural 条纹的 UI 设计过滤方案。请分别说明静态图像、缩放中的纹理和条纹边缘的策略,以及至少五个回归样本。
名词解释
本章出现的专业名词,用大白话再讲一遍。
- filter kernel
- 描述 source 像素邻域及其相对贡献的权重模式。
- convolution
- 按 kernel 对 source 邻域求加权和并生成 target 像素的图像运算。
- bicubic filtering
- 在 x 与 y 方向使用 cubic kernel 的重建过滤方法。
- separable filtering
- 把二维 kernel 拆成两个一维 pass 并通过中间纹理组合结果的过滤方法。
- GPU derivatives
- 用相邻 GPU 像素差估计局部变化的函数,常见接口是
ddx()和ddy()。 - analytical antialiasing
- 通过像素覆盖区域积分估计平均颜色以减少 procedural alias 的方法。