cd ../articles

学习笔记

为什么游戏设置里有五种抗锯齿

6 min read
游戏渲染设置面板,抗锯齿下拉框展开,列出 SSAA / MSAA / FXAA / SMAA / TAA 五个选项。
游戏渲染设置面板,抗锯齿下拉框展开,列出 SSAA / MSAA / FXAA / SMAA / TAA 五个选项。

开学这阵子课不算满,晚上经常打打游戏——主要是战争雷霆和 P 社那几个。前两天在战争雷霆里翻渲染设置,看到「抗锯齿」那一栏点开,“啪”地掉下来一长串:

抗锯齿:  关闭 ▾
         SSAA
         MSAA
         FXAA
         SMAA
         TAA

我当时就有点懵——不就是把边缘弄平滑点吗,至于整出五种花样?而且全是缩写,一个都看不懂。本着”不查明白睡不着”的劲,我去搜了搜,结果发现这背后还真有点东西,比我想的有意思。这篇就是我把它捋明白之后的笔记。

先搞清楚:锯齿到底是哪来的

我一开始以为锯齿是”屏幕不够清楚”,多花点分辨率就没了。后来发现没那么简单——锯齿是个数学问题,不是清晰度问题。

屏幕是一格一格的像素,是离散的;但游戏里一条斜边、一个圆,是连续的。用一张方格网去描一条斜线,本质上是在”采样“——每个像素只能问一次”我这块到底算啥颜色”,然后填一个色。格子放不下连续的斜线,结果就是楼梯一样的台阶。

后来我查到这事有个正经名字,叫混叠(aliasing),背后是信号处理里的采样定理:要想不丢信息地还原一个信号,采样频率 至少得是信号里最高频率 的两倍。

关键就坏在这。一条边是颜色的突变——从这个色”啪”一下跳到那个色,这种突变在频率上是无穷高的。无穷高意味着:不管你屏幕分辨率多大、采样多密,都永远不够两倍。所以——

锯齿不是没擦干净,是从原理上就采不够。你只能想办法让它不那么显眼

想通这句,我才明白为啥会有五种方案:大家都在跟”采样不够”这同一个敌人打,只是打法、花的钱不一样。下面一个个看。

SSAA:最笨,但最讲道理

SSAA(Super-Sampling)的思路特别耿直:既然采样不够,那我就多采。

比如 2×2 的 SSAA,就是偷偷按 2 倍宽、2 倍高去渲染——一个像素的地方,实际上算了 4 个子样本,最后再把这 4 个平均成一个,填回屏幕:

我多看了两眼这个”平均”,发现它其实就是一次卷积——拿一个所有权重都相等的小核(box filter)去扫:

平均 = 低通滤波 = 把那些”过高的频率”(也就是锯齿)压下去。所以 SSAA 效果是最好的,干净、没有副作用。

代价也最实在:渲染了 4 倍的像素,等于显卡多干 4 倍的活。难怪设置里它通常排第一个,旁边没人敢长期开。我自己开了一下,帧数直接腰斩。

MSAA:只在刀刃上花钱

SSAA 的浪费在哪?它对每个像素都算了 4 遍颜色——可锯齿明明只长在边缘上啊,一大片纯色的墙面里采 4 遍纯属白干。

MSAA(Multi-Sample)就抠在这:它把”算颜色”和”算覆盖”拆开了。一个像素的颜色只算一次,但记录这个像素被几何边缘盖住了几个角(coverage)的时候,按多个采样点来记。于是只有真正横跨边缘的像素,才会去做那个多样本的平均。

SSAA:每个像素都算 N 次颜色          ← 全员加班
MSAA:只在边缘按 N 个覆盖点 resolve   ← 哪有锯齿在哪花钱

便宜了一大截,几何边缘也压得挺好。它的短板我也记一下:它只管几何边缘,对于贴图内部的高频细节、透明度测试(alpha test,比如铁丝网、树叶那种镂空)就照顾不到了——那些地方该糊还是糊。

FXAA / SMAA:等画面画完了,再去抹

前面两位都是在”渲染的时候”多花力气。还有一派思路完全相反:先正常画完,再拿成品图去事后处理。 这类叫后处理抗锯齿,便宜得多,因为它只对最终那张 2D 图片动手,根本不管 3D 场景。

FXAA(Fast Approximate)是里头最快也最糙的。它先把彩色图转成亮度——

这一步我盯着看了会儿,发现就是 RGB 跟一组固定权重做点积,把三个通道压成一个亮度值(绿色权重最大,因为人眼对绿最敏感)。然后比较相邻像素的亮度差,差得大的地方就判定是边,再沿着边的方向糊一糊。

快是真快,糊也是真糊——它分不清那到底是物体的边还是贴图本来的花纹,经常把该清楚的地方一起抹了。

SMAA(Subpixel Morphological)是 FXAA 的进阶版,聪明在它不只看”这儿有没有边”,还去识别边的形状——是 L 形、Z 形还是长直线,匹配出图案后再有针对性地精确混合。所以它比 FXAA 锐利干净不少,代价也只是稍微贵一点点。这俩是性价比党的最爱。

TAA:把时间也借来当采样

最后这个 TAA(Temporal)最有意思,也是我查得最久的。前面几种都在一帧之内想办法,TAA 干脆把时间这一维也拉进来用了。

它的玩法是这样:每一帧,悄悄给摄像机加一个不到一个像素的微小抖动(jitter),让采样点在像素格子里换着位置落脚。这一帧采左上角,下一帧采右下角……单看一帧都不够,但好几帧攒起来,等效于在一个像素里采了很多点——这不就又变回超采样了吗,只不过摊到了时间上。

问题是物体和镜头都在动,上一帧那个点,这一帧跑哪去了?得把它找回来。这一步叫重投影(reprojection),我查到的时候看到一个 4×4 的矩阵乘法——当时线代刚学个开头,只能大概懂个意思:拿当前像素对应的世界坐标,用上一帧的视图投影矩阵,反算出它在上一帧画面里的位置(齐次坐标下,就是一次 4×4 矩阵乘):

找到历史像素之后,跟当前帧做个加权混合,一点点累积( 通常很小,约 0.1):

也就是说,每一帧只信当前画面一点点(10%),剩下 90% 沿用攒下来的历史。几帧滚下来,样本越攒越多,边缘就越来越平滑。

整个循环大概长这样:

flowchart LR
    A["亚像素抖动<br/>jitter"] --> B["渲染当前帧"]
    B --> C["重投影<br/>找回历史像素"]
    C --> D["按 α 混合<br/>当前 + 历史"]
    D --> E["输出这一帧"]
    E -->|"作为下一帧的历史"| C

TAA 几乎不怎么吃性能,静止画面里效果还特别好,所以现在新游戏基本默认就是它。它的毛病我也体会到了:一动起来就容易糊,还会拖影(ghosting)——因为你复用的是”过时”的历史像素,镜头一甩,旧信息跟不上,就拖出残影了。我截图里那台机器最后选的就是 TAA,大概也是图它省又稳。

摆在一起看

捋完一轮,我用一句话各自概括了下:

方案思路画质开销短板
SSAA整张图多倍采样再平均最好最贵帧数腰斩
MSAA只在几何边缘多采样较贵管不了贴图/镂空
FXAA成品图上检边再糊一般极省容易糊
SMAA检边 + 识别形状再混合较好比 FXAA 略贵
TAA跨帧抖动累积好(静止)动起来拖影

放一起才看明白:没有谁绝对最好,全是在画质、性能、副作用之间做取舍。 卡顿就退到 FXAA/SMAA,显卡有富余又想要干净就上 MSAA/SSAA,图省心就 TAA。

写在最后

本来只是打游戏顺手点开个设置,没想到查着查着,把混叠、采样定理、卷积、还有线代里那个矩阵乘法都串了一遍——这些名词原来不是孤零零摆在课本里,是真在我屏幕上每一帧地干活。

当然我这理解还很表面:采样定理的严格证明、卷积核到底怎么设计、TAA 那套重投影和拒绝历史(防拖影)的细节,我都还没真啃下来,矩阵那块也才学个皮毛。这篇大概率有不准的地方,等以后学深了再回来改。

但有个感觉先记下来:很多看着唬人的缩写,扒开都是同一个朴素的问题在打转。 这次是”采样不够”。先记到这。

// related

作者头像
yen@harvey:~$ exit 0