采样与滤波
本篇位置:模块一 · 第 7 篇 | 前置:插值 | 预计阅读:30 分钟
学习目标
- 用奈奎斯特准则(Nyquist criterion)解释走样(aliasing)到底从哪来,而不是把它当成只能靠经验规避的玄学现象。
- 分清「重建滤波」(reconstruction filter)和「预滤波 / 抗走样」(pre-filter / anti-aliasing)两个方向——核可能长得很像,但解决的问题正好相反。
- 会写 3×3/5×5 卷积核,说出盒式、高斯、锐化三种常见核的形状与用途,理解分离卷积(separable convolution)为什么能省算力。
- 把 mipmap、MSAA、TAA 都放进「对抗欠采样」的统一框架,看清它们各自补的是哪一种采样不足。
渲染 = 对连续场景采样
上一篇讲"怎么用已知样本重建未知位置的值"——lerp、双线性、Catmull-Rom 核,全是在猜。这一篇往前退一步,问一个更根本的问题:那些"已知样本"最初是怎么来的,又丢了什么。答案是:整个渲染过程从头到尾就是一次采样(sampling)。场景在空间、角度、时间这些维度上都是连续的,而最终产出的是一张有限分辨率的像素网格——从连续到离散这一步不是细节,它就是渲染这件事本身。
一个常见的直觉误区值得先纠正:把像素想象成紧密拼接的小方块(little square),仿佛每个像素完整覆盖了对应的一小块场景,因此不存在"漏采"。图形学经典备忘录《A Pixel Is Not A Little Square》(Alvy Ray Smith)明确反对这种理解:像素是对连续图像函数在某个位置的一次点采样(point sample),不是一块有面积的瓷砖。承认像素是"点",就必须正视:光栅化最朴素的做法是每像素只在中心取一次样(1 sample per pixel)来决定颜色和可见性,而几何边缘、高频纹理、镜面高光往往在远比像素间距精细的尺度上变化——用一次采样代表一片剧烈起伏的信号,天然会丢信息、认错内容。这是本篇一切走样的共同根源,也是 MSAA、mipmap、TAA 这些技术存在的理由。
奈奎斯特与走样
丢信息不是含糊地"变模糊",它有精确的数学描述。奈奎斯特-香农采样定理(Nyquist-Shannon sampling theorem)给出的判据是:要无失真地表达一个最高频率为 的信号,采样率 必须大于 ,这个上限 叫做奈奎斯特频率(Nyquist frequency)。一旦信号里混入高于奈奎斯特频率的成分,麻烦不是"这部分被丢弃"这么简单——它会被折叠(fold back)成一个更低的、原始信号里根本不存在的虚假频率。这个现象就是走样(aliasing):高频成分采样后伪装成低频,且两者在采样点上完全无法区分。
labs/m1/lab_07_sampling.py 的第一张图把这个折叠算给你看:真实频率 的正弦信号,用 采样(奈奎斯特频率只有 ,明显违反准则),折叠出的虚假频率可以直接算出来:。数值验证很干净——在全部采样点上, 与 的取值严格重合(差异在 量级):8 个样本既能解释成摆了 7 个周期的快曲线,也能解释成只摆 1 个周期、方向还相反的慢曲线,而采样器拿到的只有这些点,两种解释它分不出来。
图:灰色细线是真实的 7 Hz 信号,蓝色圆点是 8 Hz 采样得到的离散样本,橙色粗线是这些样本"伪装"出来的 1 Hz 虚假信号——两条曲线在采样点上严格重合,仅凭样本无法分辨真实频率与折叠频率。
这条规律不是时间信号独有的,空间维度的对应物随处可见:远处一排细密的栏杆、一块斜着延伸向地平线的棋盘格地板,它们的空间频率(单位视角内的条纹数)随距离增大不断升高,一旦超过屏幕像素网格的奈奎斯特极限,同样的折叠就会发生——密集的重复图案变成位置、形状都不对的粗条纹,相机稍动还会抖动闪烁。这正是下一节实验要复现的效果。
摩尔纹实验
lab_07_sampling.py 的第二张图把上面的空间域直觉做成可控实验:构造一张 3 像素为一格的高对比度棋盘图(512×512),再把它 4× 下采样。3px 的格子对原始分辨率是安全的,但对缩小四倍后的新分辨率来说远超奈奎斯特极限。图里对比两种下采样做法:一种是直接抽点(naive decimate)——每隔 4 个像素取 1 个,其余原样丢弃;另一种是先做 4×4 均值滤波、把每个小块压成一个平均值,再抽点(box prefilter + decimate)。
图:左图是原始的 3px 棋盘;中图直接每隔 4 像素抽点,规整的棋盘变成大块、扭曲、位置错乱的摩尔纹(moiré pattern)——高频信息折叠成虚假低频图案的视觉版本;右图先对每个 4×4 块取平均再抽点,黑白交替被平均成接近均匀的灰色(灰度仅在 0.5 附近小幅波动),残留图案的幅度远小于直接抽点,肉眼基本觉察不到。
直接抽点丢掉的不是"多余的细节",而是合法表示这个信号所必需的信息:按奈奎斯特准则,新分辨率根本不足以区分这么密的格子,强行抽点等于用不够密的样本去猜一个猜不动的高频信号,结果看似规整、实则完全错误。先滤波再抽点则诚实地承认分辨率装不下这么细的格子,提前抹平超限高频,留下的灰色是该信号在新分辨率下唯一诚实的表示——棋盘黑白各半,真实的平均值本来就是灰。这正是纹理不开 mipmap 时闪烁摩尔纹的机理:远处或斜视角下,一个屏幕像素对应的纹理区域远大于一个纹素,对原始纹理做点采样等价于"直接抽点",且每帧视角微变抽到的点都不同,于是摩尔纹叠加逐帧闪烁。mipmap 解决的正是这个问题,下一节先补上滤波的数学工具,第六节再放进统一框架。
卷积与滤波核
前两节用的"均值滤波"只是滤波家族里最简单的一种。图像处理里几乎所有的模糊、锐化、边缘检测都能写成同一个操作:离散卷积(discrete convolution)。给定图像 和一个 的核(kernel),输出的每个像素是核覆盖范围内原图像素的加权和:
严格的卷积要求核先做一次上下左右翻转,但下面三个常用核都中心对称,翻转前后一样,所以工程实现直接滑窗做加权和即可。三个最常见的 3×3 核,落笔前都用 Python 验证过元素和:
盒式核(box kernel)——邻域权重相等的均值滤波,正是上一节 box prefilter 用的核:
高斯核(Gaussian kernel)——用二项式系数近似高斯分布的标准 3×3 权重,中心最大、向外衰减,过渡比盒式核平滑:
这套构造可以直接扩展到更大尺寸: 是 的二项式系数,取 的系数 (和为 16),它与自身的外积除以 就是标准的 5×5 高斯核,元素和同样为 1——需要更大的模糊半径按同样规律往上加即可。
锐化核(sharpen kernel)——恒等核(只在中心取 1)叠加一个和为零的拉普拉斯算子(Laplacian,二阶差分的边缘检测核),逐项相加得到:
三个核的元素和都严格等于 1(数值验证:、、拉普拉斯和为 0 叠加恒等核仍为 1),这不是巧合——和为 1 意味着纯色图卷积后颜色不变,滤波不会改变图像的整体亮度。盒式核、高斯核权重全为正,效果是把邻域拉平、抹掉高频差异,对应低通滤波(low-pass);锐化核混入了和为零的高通分量,放大中心像素与邻域的差值让边缘更锐利——两者元素和相同,频域特性截然相反。
分离卷积(separable convolution)是把 2D 卷积拆成两次 1D 的技巧:若 核能写成两个一维向量的外积 ,就可以先逐行做 1D 卷积、再对结果逐列做 1D 卷积,数学上与原始 2D 卷积完全等价。盒式核 、高斯核 都满足(外积结果与上面的矩阵逐项相等,已验证);锐化核因为混入十字形拉普拉斯分量,矩阵秩为 2,不可分离——工程上通常改用"可分离模糊 + 与原图混合"的反锐化蒙版(unsharp masking)间接吃到这份红利。分离带来的收益是数量级的: 核每像素 次乘加,拆开后只要 次——以 Bloom、景深预模糊常用的 9×9 高斯为例,81 次降到 18 次,还没算纹理带宽的节省。这正是 shader 里做模糊的标准手法:横向一个 pass、纵向一个 pass,而不是双重循环的 2D 卷积。
滤波的两个方向
前面反复出现的"预滤波"和上一篇的"重建",是时候摆在一起讲清楚——两者用的核经常长得很像(都可能是 box、都可能接近高斯),初学最容易混用,但解决的问题、发生的时机、信息丢失的方向完全相反:
预滤波(pre-filter,抗走样 anti-aliasing 里"抗"的那一步)发生在降分辨率之前:mipmap 生成、SSAA(先在更高分辨率渲染、再滤波平均降回目标分辨率)都是这个方向,目的是在丢样本之前先把新分辨率装不下的高频滤掉,防止折叠成虚假图案——本质是在丢信息,但丢得克制、不产生错误内容。重建滤波(reconstruction filter)方向相反:手上只有稀疏的离散样本,要在样本之间的空位猜出值,上一篇的 box/tent/Catmull-Rom 核就是干这个的——本质是在编造信息,核只能按自己的形状假设给出最合理的猜测。把两者搞反是很实际的 bug 来源:降分辨率时只做点采样(该预滤波时没滤)直接产生摩尔纹;反过来指望放大时靠"更讲究的核"补回丢失的细节,则永远补不回来——该丢的信息在采样那一刻就真的丢了,核往哪个方向用都不能违反这一点。
统一框架看抗走样
有了方向区分,实时渲染里几个常被当成互不相关的"抗锯齿技术"就能放进同一个框架:哪个维度采样不够,就在哪个维度补采样。
几何走样(边缘锯齿)→ MSAA。 光栅化默认每像素只做 1 次覆盖率采样;但多边形边缘是阶跃状硬边界,等效于无穷高频,1 个采样点表达不出边缘在像素内穿过的位置。MSAA(multisample anti-aliasing)补的是空间覆盖率维度:同一像素内取多个子采样点分别做覆盖率测试,但只做一次着色,再按覆盖比例混合——花覆盖率采样的代价,省着色的代价。
纹理走样(摩尔纹/闪烁)→ mipmap。 这正是第三节的场景:纹理缩小时内容频率超过屏幕像素的采样密度。mipmap 补的是预滤波这一维——提前生成一串逐级滤波、逐级缩小的纹理金字塔(把"box prefilter + decimate"按各种缩放比例提前算好),渲染时按投影密度选层,等价于永远"先滤波再抽点"。
着色走样(高光/细节的时域闪烁)→ TAA。 即使几何和纹理都处理好了,着色结果本身(高频法线贴图上的镜面高光、细小镂空)在运动中逐帧独立的单次着色采样也会随时间跳变,这是时间轴上的欠采样。TAA(temporal anti-aliasing)补的是时域采样这一维:每帧给采样位置加亚像素抖动(jitter),把多帧结果累积(accumulation)进历史缓冲,用跨帧攒起来的采样点逼近单帧内做不起的高密度采样——用时间换空间分辨率。
三者的共同点是第一节那句话的三次应用:默认的"每像素一次采样"在覆盖率、纹理、着色三个维度上分别不够用,MSAA、mipmap、TAA 各自把采样密度补上,同时摊薄代价(共享着色、预计算金字塔、跨帧复用)。MSAA 的硬件细节、TAA 的鬼影抑制留到 M2 抗锯齿篇(预计第 7 篇);mipmap 的选层公式与各向异性采样留到 M2 纹理与采样管线篇(预计第 10 篇)。
🎯 岗位关联
超分辨率(super-resolution)的本质困难,就是第一节那句话的直接后果:游戏以低分辨率渲染时,高于目标奈奎斯特极限的细节在采样那一刻就已经真实地丢了——不是藏起来了,是不在那些像素值里了。任何超分算法,无论是经典 bicubic 还是 FSR/DLSS 这类结合时域信息的方案,本质都是在赌一个重建核(手工设计的或数据学出来的)能从不完整的样本里猜出足够接近真实的高分辨率画面;"猜得准"和"信息没丢"是两回事,这也是超分质量强烈依赖场景内容、运动幅度和历史帧数量的原因。插帧的伪影同样可以理解为时间轴上的走样:帧率就是时间维度的采样率,运动越快、画面内容在时间轴上的频率越高,一旦帧率跟不上,遮挡/去遮挡区域(上一篇讲过成因)就会出现鬼影和撕裂——正是第六节"时域欠采样"的另一个具体案例。理解了这套框架,再读 FSR/DLSS 的技术文档,jitter(TAA 式的逐帧亚像素采样点抖动)、accumulation(历史帧采样的累积缓冲)、clamp(历史样本失效时用邻域颜色钳制累积值、拒绝不可信历史以防鬼影)这几个高频词就不再是孤立术语,而是本篇和上一篇机制在产品文档里的落地说法。
小结
走样类型 → 成因 → 对策
| 走样类型 | 成因 | 对策 |
|---|---|---|
| 一般信号走样 | 采样率不满足奈奎斯特准则(),高频折叠成虚假低频 | 提高采样率;或采样前低通预滤波,去掉超限高频 |
| 几何走样(边缘锯齿) | 每像素仅 1 次覆盖率采样,表达不出边缘的阶跃状高频 | MSAA:每像素多次覆盖率采样,共享一次着色 |
| 纹理走样(摩尔纹/闪烁) | 纹理缩小时内容频率超过屏幕像素采样密度 | mipmap(预滤波金字塔)+ 各向异性过滤 |
| 着色走样(时域闪烁) | 每帧仅 1 次着色采样,高频着色结果在时间轴上欠采样 | TAA:亚像素 jitter + 跨帧 accumulation |
| 插帧时域鬼影 | 遮挡/去遮挡区域运动矢量无效,该处时域信息本就没被采到 | 遮挡检测 + 历史样本 clamp/拒绝,而非盲目 lerp |
延伸阅读
- Alvy Ray Smith,A Pixel Is Not A Little Square(像素是点采样而非小方块的经典备忘录):http://alvyray.com/Memos/CG/Microsoft/6_pixel.pdf
- GAMES101 Lecture 6(Rasterization 2: Antialiasing and Z-Buffering):https://games-cn.org/intro-graphics/
- Bart Wronski,The Nyquist Problem(bilinear down/upsampling、像素网格与半像素偏移):https://bartwronski.com/2021/02/15/bilinear-down-upsampling-pixel-grids-and-that-half-pixel-offset/
