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 和目标像素的关系清楚可测。

是高质量过滤的最小可解释单元。给定输入图像 A,过滤器从 A 取一个局部窗口,按 kernel 权重组合成输出图像 B;缩放、锐化、颜色变化、模糊和边缘检测都可以落在这个接口上。

filter kernel:用权重定义 source → target 的图像变换box filtercubic / bicubic profile-2-2-1-1001122negative cubic weights may add sharpness, so validate ringing on high-contrast edges
kernel 同时定义采样哪些 texel 和每个 texel 的贡献;box 简单但容易软化,cubic 通过距离相关权重保留更多边缘对比。

1. 从 box 到 bicubic:让权重匹配目标质量

通常表示按 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;
}

让距离中心较近的 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:用中间纹理换取可控成本

把二维卷积拆成两个一维 pass。教材式 bicubic 可以先对四行做 x 方向 cubic,再对四个结果做 y 方向 cubic;更实用的实现会把 x pass 写入 intermediate render target,再让 y pass 读取它。额外的写入和读取换来了更少的重复采样与更易控制的带宽。

separable filtering:两个一维 pass 组合二维结果sourceimage Ahorizontal1D kernelintermediatetemporary RTvertical1D kerneltargetimage Btrade one intermediate write for reusable x/y profiles and predictable bandwidth
bicubic 的教材式二维实现可能需要 16 次颜色采样和额外 kernel lookup;separable 的水平加垂直 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:知道当前像素覆盖了多大纹理

让 shader 询问一个值在屏幕 x、y 邻居之间变化多少。对于随顶点插值的 UV,ddx(UV)ddy(UV) 可以近似当前像素在纹理上的 footprint;对于 uniform,差分为零。它们不是 CPU 意义下可任意组合的高阶解析导数,因此应把它们当作局部采样窗口的证据。

GPU derivatives:ddx(UV) × ddy(UV) 定义 filter footprintscreen pixelsddxddytexture footprintuvintegrate source over this windowuniform values have zero derivative; interpolated values expose local change
ddx 与 ddy 不是二阶数学导数,而是 GPU SIMD 邻居之间的差分;它们提供当前像素需要覆盖多大纹理窗口的线索。
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:对覆盖区域而非单点采样

不是简单地把一个 if 改成更模糊的颜色,而是估计函数在当前像素窗口内占了多少面积。以条纹为例,先得到条纹函数的积分,再用 x0x1 的积分差除以窗口宽度,就能得到像素内的平均 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 时都验证。

analytical antialiasing:从 binary sample 到 pixel coveragenaive stripeone if per pixel → jaggiesintegrated coverageaverage over ddx / ddy windowquality gain costs math; a texture lookup may be faster when the control can be baked
analytical antialiasing 的核心是对像素覆盖区域积分;它比单点 if 更平滑,但会增加计算,并且仍需和 API 多重采样一起评估。

5. 动手实验:用 footprint 选择过滤策略

先预测四个变化:box filter 的成本最低但 alias 更高;bicubic 会增加采样与权重成本并保留更多边缘;footprint 变宽会降低高频 alias 却可能擦掉对比;oversample 和 analytical AA 能提高边缘 coverage,但不能替代对纹理格式、mip 和 API AA 的测量。

High-Quality Filtering Lab

先预测:为什么更宽的 footprint 不一定更清晰?

可交互
filtered edge previewbicubicquality 0.92 · alias 0.14cost 0.88 · texture/math budgetnarrow footprint preserves detail but risks aliasing

观察: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.04ddy(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 的方法。

资料与写作方式声明

本章以GPU Gems 系列权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…