# FXAA算法演义 如要研究图像抗锯齿算法,FXAA自然是逃不开的其中之一。FXAA由Nvidia的Timothy Lottes在2009年提出,经过若干时间演化,到目前的版本为FXAA3.11。本文将主要依据[[1] Timothy Lottes, FXAA White Paper, 2009](https://developer.download.nvidia.cn/assets/gamedev/files/sdk/11/FXAA_WhitePaper.pdf)和 [[2] SIGGRAPH 2011 presentation on FXAA 3.11](http://iryoku.com/aacourse/downloads/09-FXAA-3.11-in-15-Slides.pptx),然后再参考若干网上的文章([[3]](http://blog.simonrodriguez.fr/articles/30-07-2016_implementing_fxaa.html))来深入分析FXAA算法。 由于FXAA算法在发展过程中衍生出了诸多变种版本,本文将着重分析其中的3个版本,从中我们可以窥出FXAA在变化过程中关于图像质量与算法性能的取舍之道。顺带,尝试揣度一下原作者在推演这个算法时的心路历程。 本文将包含以下内容: - FXAA的大致思路 - FXAA初代算法 - FXAA3.11 - Quality 质量版 - FXAA3.11 - Console 主机版(性能版) - FXAA相关公式背后的思考与意义 - Timothy Lottes 的心路历程(伪) 本文将会使用Unity SRP实现上述的算法,但文中仅会放一点关键的Shader代码,助于理解。 # 1. FXAA思路简介 FXAA拥有一种很朴素的算法思想。 它的思路很简单,既然我们要进行图像抗锯齿,那总得区分出来哪些像素是锯齿,哪些像素不是。根据图像锯齿的成因和日常的经验我们知道,它通常出现在与背景呈现高对比度的物体边缘(视觉比较明显)。 如下图所示(看最左图就好了) 在观察了一些常见的图像锯齿之后,Timothy就想,那我是不是可以通过计算像素和其周围像素的亮度对比,来判定一个像素是不是边缘像素呢?针对非边缘的像素,我们什么都不做,而对于边缘像素,我们可以让其和周围的像素进行Blend混合,从而起到模糊的效果。这样一来,不就达到抗锯齿了吗?Good Idea! 所以FXAA算法的核心思路就两点: 1. 边缘判定算法 2. 边缘像素的混合因子计算 诸多变种,无非就是在这两点上做文章。 # 2. FXAA初代目 算法取自Timothy于2009发布的FXAA White Paper,暂且认为是初代目吧。 ## 2.1 边缘检测算法 FXAA1.0使用的边缘检测算法如下: - 读取上下左右4个方向 + 自身的像素亮度 - 筛选出其中的最大值lumaMax和最小值lumaMin - 令对比度lumaContrast = lumaMax - lumaMin - 当对比度超过一定阈值时,便认为当前像素为边缘像素 这个思路其实非常的朴素。一般人都能理解,就算没接触过相关Paper,自己捣鼓一下应该也是能想到的。基于这个思路,我们尝试写下第一段代码。相关的字母意思如下: - N - 北即上 - S - 南即下 - W - 西即左 - E - 东即右 - M - 中即自身 图示如下: 对比度计算如下: ```hlsl //采集uv的上下左右中共计5个像素的RGB和亮度 FXAACrossData cross = SampleCross(tex,uv,offset); //计算对比度 half lumaMinNS = min(cross.N.a,cross.S.a); half lumaMinWE = min(cross.W.a,cross.E.a); half lumaMin = min(cross.M.a,min(lumaMinNS,lumaMinWE)); half lumaMaxNS = max(cross.N.a,cross.S.a); half lumaMaxWE = max(cross.W.a,cross.E.a); half lumaMax = max(cross.M.a,max(lumaMaxNS,lumaMaxWE)); half lumaContrast = lumaMax - lumaMin; ``` 为了测试这段代码,我们搭建一个场景,往场景中丢一根棍子,将画面放大10倍后,可以看到棍子边缘出现经典的锯齿。 将前面计算出的`lumaContrast`直接作为颜色返回,可以看到画面变成如下样子: 白色为高对比度像素,黑色为低对比度像素,物件的边缘被很好的勾勒了出来。接下来我们给定一个对比度阈值,当lumaContrast大于该阈值时,便认为是边缘像素。简单的代码示意如下: ```hlsl #define FXAA_ABSOLUTE_LUMA_THRESHOLD 0.05 bool isEdge = lumaContrast > FXAA_ABSOLUTE_LUMA_THRESHOLD; ``` 然后我们把边缘像素描红返回,效果如下: 可以看到该算法将棍子投下的阴影渐变区域也识别为了边缘。 这也是FXAA的为人诟病的缺点之一,即只要是对比度高的像素,它都视作边缘像素进行处理。这可能会使图像丢失一些局部高频信息,使得画面不够锐利。 为了缓解这个问题,FXAA1.0在阈值判断这里额外加入一个修正参数`FXAA_RELATIVE_LUMA_THRESHOLD`,其关系如下: ``` float edgeThreshold = max(FXAA_ABSOLUTE_LUMA_THRESHOLD,lumaMax * FXAA_RELATIVE_LUMA_THRESHOLD); bool isEdge = lumaContrast > edgeThreshold; ``` 用中文来表示就是 `最终阈值 = max(绝对阈值, lumaMax * 相对阈值比例)` 这个修正带来以下效果: - 明亮的地方需要更高的周边对比度才能被判定为边缘 ## 2.2 计算混合因子 对于非边缘像素,我们什么都不做,原样返回颜色即可。这个叫做`Early Exit`。 对于边缘像素,接下来要让其和周边像素进行混合,以起到模糊边缘的效果。这里有两个问题需要解决: - 上下左右4个方向,到底与哪个方向混合? - 混合的比例是多少? ### 2.2.1 边缘横纵判定 针对第一个问题,我们首先判定该像素所在的边缘到底横向还是纵向。有的人就问了,那斜着的边呢?实际上斜边是宏观上的视觉,当我们从像素的微观角度去看时,像素只有三种情况: - 在横边上 - 在纵边上 - 在角上 如上图,1在纵边,2在横边,3在角上。 Timothy心想,这可太简单了,我只要计算上下两个像素的亮度差以及左右两个像素的亮度差,如果前者大,那就是横边,反之则为纵边。如果一样呢?那先随便吧。伪代码如下: ``` float lumaGradV = abs(lumaN - lumaS); float lumaGradH = abs(lumaE - lumaW); bool isHorz = lumaGradV > lumaGradH; ``` 但随即他就发现这种方式有个缺陷,即对于单像素的线,无法正确判定其走向。参考下图情形: 对于1位置的像素,`lumaGradV`将与`lumaGradH`相等。为了解决这个问题,Timothy对以上算法做了些许的改进如下。 首先计算像素在S、N、W、E 4个方向的亮度变化梯度: ```hlsl float lumaGradS = lumaS - lumaM; float lumaGradN = lumaN - lumaM; float lumaGradW = lumaW - lumaM; float lumaGradE = lumaE - lumaM; ``` 然后对垂直和水平方向的梯度分别相加,取绝对值,比较它们的大小: ```hlsl float lumaGradV = abs(lumaGradS + lumaGradN); float lumaGradH = abs(lumaGradW + lumaGradE); bool isHorz = lumaGradV > lumaGradH; ``` 这样就成功的解决单像素线的问题。 (实际上2009 White Paper里还考虑了对角线的4个像素,但这里我们暂且不提) # 2.2.2 边缘法线计算 成功判定了边缘像素横纵状态后,我们接下来需要计算其法线朝向。 看上图,对于像素1,其边缘法向应当是朝左。对于像素2,其边缘法线则是朝上。边缘法线即表征了目标混合像素的方向。 这个就简单了,哪个方向的梯度大,法线就朝哪边。于是我们有: ```hlsl float2 normal = float2(0,0); if(isHorz){ normal.y = sign(abs(lumaGradN) - abs(lumaGradS)); }else{ normal.x = sign(abs(lumaGradE) - abs(lumaGradW)); } ``` 把 `(normal + 1) * 0.5`作为颜色输出到画面,我们有如下效果: 4种颜色分别代表了边缘法线N、S、E、W4个朝向。 # 2.2.3 混合因子计算 现在混合方向有了,接下来就要计算混合因子了。咱们不管三七二十一,先填个0.5看看吧。 不出所料,效果很糟糕。不仅物体边缘如马赛克一般,好好的阴影渐变也被整的一塌糊涂。 仔细思考一下,我们期望的混合因子应当符合以下两个要素: - 需要在"高对比度的像素之间"构造出渐变。例如lumaN和lumaS对比度很高,那么lumaM就应该将自己修正为接近 (lumaN + lumaS) / 2。 - 不破坏像素之间正常的渐变关系。(例如上图中的软阴影) 于是Timothy首先对NSEW4个方向的像素亮度求平均,计算出中间像素的期望亮度: ``` half lumaL = (lumaN + lumaS + lumaE + lumaW) * 0.25; ``` 然后考察中间像素的实际亮度与期望亮度之差: ```hlsl half lumaDeltaML = abs(lumaM - lumaL); ``` 这个差值如果是0,说明当前的中间像素亮度已经完美符合,不用任何修改。否则,令 ``` float blend = lumaDeltaML / lumaContrast; ``` `lumaContrast`在前面已经说过,是N、S、W、E、M,5个像素中`最大亮度-最小亮度`。易知,blend范围位于`0~1`。 最终颜色采样代码如下: ``` half4 finalColor = SampleLinear(tex,pixelCoord + normal * blendL); ``` 以上公式意味着: **中间像素亮度(lumaM)与期望亮度(lumaL)的差值越大,混合因子(blend)越趋向1,也即中间像素的颜色越往其亮度的最大梯度方向(normal)偏移** 改进混合因子后,抗锯齿效果如下图: 软阴影部分完全没受到影响,物体边缘也呈现出了模糊渐变效果。整体上来看还算OK的,但也可以发现一些不足之处: - 物体边缘有些"过于"模糊了(相比较于MSAA而言)。 这个不难理解。因为如我们前面所述的边缘查找算法里,边缘像素是呈"一对"存在的。 1,2在黑色阵营里是边缘像素,那么1,2normal朝向的白色像素,在白色阵营里自然也属于边缘像素。 经过Blend之后,边缘渐变至少会覆盖两个像素宽度,于是就显得有些"厚"。 但看在`FXAA1.0`如此廉价的份上,这个缺点我们暂且接受了。 Timothy兴致勃勃的发布了这第一个版本,不出所料,有人抱怨FXAA在抗了锯齿的同时,让图像变糊了。 玩家才不管你廉不廉价,你的画面但凡有点瑕疵,他们都能挑的出毛病来。 于是Timothy紧急之下,先加进去两个参数缓一缓: ``` float blend = max(0,lumaDeltaML / lumaContrast - FXAA_QUALITY_SUBPIX_TRIM) * FXAA_QUALITY_SUBPIX_CAP; ``` - FXAA_QUALITY_SUBPIX_TRIM 用来把混合因子整体往0靠靠,这样边缘就不会那么的糊了。 - FXAA_QUALITY_SUBPIX_CAP 用来把混合因子进行一个整体的scale(0.x),也是为了不那么糊,至少保留住一点高频信息吧。 但我觉得以上两个参数实际效果还是比较弱的,属于"反正参数都给你了,你自己调吧"系列。 到此为止,FXAA初代目就完成了。5次纹理采样 + 不算复杂的计算,我们就得到了一个廉价的全屏后处理抗锯齿技术。 下面说说FXAA的最佳使用规范: - 最好在完成所有后处理效果之后再执行FXAA - 最好在sRGB空间执行 - 需要在LDR空间执行 这些规范不难理解。因为FXAA图像抗锯齿是基于"视觉的"而不是基于物理的。因此在我们进行亮度计算时,必须时基于"视觉"的亮度,而不是"物理"的亮度。另一方面,HDR到LDR的ToneMap不是线性的,所以即便在HDR空间中执行FXAA,ToneMap后也会失效。 # 3. FXAA3.11 Quality 打自2009年发布了FXAA初代目,Timothy就对其存在的一些瑕疵耿耿于怀。作为一个精益求精的人,他潜心研究,于是在两年后的SIGGRAPH上发布了[FXAA 3.11](http://iryoku.com/aacourse/downloads/09-FXAA-3.11-in-15-Slides.pptx)。 FXAA3.11与FXAA初代目相比,最大的不同就是改进了混合因子算法。Timothy觉得,要真正解决边缘模糊"太厚"的问题,就要深入分析锯齿产生的本质原因。 我们知道,几何物体的边缘锯齿是由于采样不足而引起的。如上图所示,一个采样点,如果落入几何内部,那么整个像素就都是红色。如果落到几何体外部,那么整个像素都是蓝色。 SSAA和MSAA是通过在一个像素内部增加额外的采样点,然后进行颜色混合达到抗锯齿目的的。 不妨思考一下,如果我们在一个像素内部生成无穷的采样点,那么最终混合的颜色应当怎么计算呢? 一根线,穿过一个像素方块,将其劈成两半。一半归入蓝色阵营,一半归入红色阵营。很明显,我们可以将两部分面积分别作为蓝色和红色的权重,进行加权平均计算,最终得到混合色。 当我们观察锯齿的一段局部线条区域时: 容易发现,红色面积权重是随着线条方向(朝右)逐渐减小的。当红色权重少于0.5时,自然像素就翻转成蓝色了。 基于这个推理,Timothy心生一计,发明了边缘搜索算法。原理如下: - 首先确定边缘像素的横纵向 - 往边缘两侧按照给定的步长进行边缘终点搜索 - 确定了边缘线段的两个端点后,可以计算出当前像素在线段中的位置 - 根据像素在线段中所处的位置来计算出混合因子 于是以上算法归纳出两个待解决的问题: 1. 如何判定搜索的目标像素为边缘终点? 2. 边缘上每个点的位置与混合因子满足怎样的关系? ## 3.1 边缘终点判定 首先我们可以将搜索的起始点定为像素往normal方向偏移0.5个单位的位置。如下图: 使用线性插值采样,那么采样结果将是边缘两侧像素的平均亮度。不如将起始点亮度记为`lumaStart`。假设我们往两侧一个个像素进行判定,直到发现亮度与`lumaStart`的差值超过一定阈值,便认为到头了。 那么这个阈值多少合理呢?写死肯定是不合理的。它应当跟搜索起始点两侧像素的对比度(lumaStartContrast)呈正相关。Timothy给出的估计为: ```hlsl float searchEndThreshold = lumaStartContrast * 0.25 ``` 0.25只是一个经验性的系数。 有人问Timothy,为什么要往normal方向偏移0.5个像素进行采样呢?Timothy微微一笑说,假如我们不进行偏移,比如直接从黑色像素中心往两边搜索,那么左侧很显然无法查找到正确的端点。 所谓边缘,是由边缘两侧的像素对比所产生的。在搜索的时候一定要同时考虑边缘两侧的像素。 这时候又有人问,那为什么不在搜索的通过计算两侧的亮度差来确认终点,而是要计算两侧的平均亮度呢? Timothy说你仔细想想,当然是为了少一次采样啊。计算差要采样两个像素做减法,而我用平均,可以直接使用Bilinear Filter一次采样足矣。 ## 3.2 混合因子计算 如我们前面所述: > 当我们观察锯齿的一段局部线条区域时,容易发现,红色面积权重是随着线条方向(朝右)逐渐减小的。当红色权重少于0.5时,自然像素就翻转成蓝色了。 边缘像素的锯齿翻转,均是其中一方面积权重越过0.5时发生。为了完美拟合这种现象,我们需要**将边缘终点处的混合因子设为0.5** 因此对于一条锯齿化边缘线,我们是可以假设**几何体的真实边缘正好穿过线段终点像素外侧边的中点**,如下图所示,蓝框为我们考察的锯齿边缘区域,黄色线条为我们预测的真实几何体边缘。 于是从图中很明显可以看出,锯齿边缘线的中点处,权重值正好为1。从边缘线的黑色一侧看来,混合因子应当从中点处开始,向右侧从1渐变到0.5。 这里我们还能给出另外两种锯齿形态: 依旧从黑色像素一侧来看: - 第二种锯齿形态情况下,混合因子应当从中点开始同时向两侧渐变到0.5。 - 第三种形态,则黑色一侧无需渐变。 从以上的几种情况总结下来,混合因子的计算方式已经呼之欲出了: - 设边缘线长为`edgeLength`(可以通过搜索到的两个端点位置相减得到) - 计算出距离当前像素比较近的一个端点,记为`targetP`,距离记为`dst`。 - 考察targetP与当前像素是否属于同一阵营。 - 如果是,则blend为0,即当前像素保持原因,不进行混合。 - 如果否,则`blend = abs(0.5 - dst/edgeLength)` 这样这个Blend的变化就完美符合了我们前面的理论推理。 那么如何考察边缘端点与当前像素是否属同一阵营呢? 不妨假设搜索结束时,已经得到距离当前像素较近的边缘端点亮度为`lumaEnd`。我们知道,搜索起始点的亮度`lumaStart`为两个阵营的平均亮度。 因此可以将当前像素亮度(`lumaM`)与阵营平均亮度做比较: `lumaM - lumaStart` 同时将端点亮度(`lumaEnd`)也与阵营平均亮度做比较: `lumaEnd - lumaStart` 如果两者符号一致,则属于同一阵营,否则属于不同阵营。于是blend计算公式如下: ```hlsl float blend; if((lumaM - lumaStart) * (lumaEnd - lumaStart) > 0){ blend = 0; }else{ blend = 0.5 - dst/edgeLength; } ``` ## 3.3 搜索步长优化 在进行边缘搜索时,一个个像素搜索是不现实的,我们需要一个策略来将采样次数控制到限定范围内。很容易想到的一种方式是,给定一个最大搜索次数`MAX_EDGE_SEARCH_SAMPLE_COUNT`,然后在搜索时逐渐增大步长。 FXAA3.11 Quality原算法里提供了很多组预设,可以参考[GitHub上的源码](https://gist.github.com/kosua20/0c506b81b3812ac900048059d2383126)。 例如Preset12的定义如下 ```hlsl #define FXAA_MAX_EAGE_SEARCH_SAMPLE_COUNT 5 static half edgeSearchSteps[FXAA_MAX_EAGE_SEARCH_SAMPLE_COUNT] = {1,1.5,2,4,12}; ``` 意思是最多执行5步搜索(每步同时往两个方向进行采样,最多会有10次采样),edgeSearchSteps数组定义了每步搜索的步长。自然给予的最大搜索步骤越多、步长越小,边缘的渐变就能更细腻。 通过使用不同的参数,我们可以在图像质量与性能之间寻求平衡。 到此为止, Timothy觉得自己应该是大功告成了。他把预设12的参数输进去,一跑,发现结果如下: 水平边缘的抗锯齿效果很不错,边缘渐变层足够"薄",一举解决了FXAA初代目令边缘太模糊的问题。 可是垂直方向却出现了一些瑕疵点。这是怎么回事呢? ## 3.4 额外对角线采样 打开边缘法线调试效果,如下: 原来垂直向的噪点是由于混合方向计算不正确引起的。仔细深入分析了一下,Timothy终于找到了原因。在早先我们计算边缘法线的时候,只采样了N,S,E,W4个方向的像素亮度。再看一下当时的计算方式: ```hlsl float2 normal = float2(0,0); if(isHorz){ normal.y = sign(abs(lumaGradN) - abs(lumaGradS)); }else{ normal.x = sign(abs(lumaGradE) - abs(lumaGradW)); } ``` 如果仅有这4个方向,不足以分析如下的情况: 考察像素1,其`lumaGradN(上)`与`lumaGradE(右)`梯度相同,`lumaGradS(下)`与`lumaGradE(左)`梯度相同,因此假如只考虑上下左右4个方向的像素亮度,我们是无法计算出像素1的边缘法线的,这时候就要额外考虑其4个对角线像素的亮度。 于是Timothy将边缘法线计算公式修改如下: ```hlsl lumaGradH = abs(lumaNW + lumaNE - 2 * lumaN) //横-上 + 2 * abs(lumaW + lumaE - 2 * lumaM) //横-中 + abs(lumaSW + lumaSE - 2 * lumaS); //横-下 lumaGradV = abs(lumaNW + lumaSW - 2 * lumaW) //纵-左 + 2 * abs(lumaN + lumaS - 2 * lumaM) //纵-中 + abs(lumaNE + lumaSE - 2 * lumaE); //纵-右 ``` 意思的在水平方向,我们同时计算第一行,第二行和第三行的梯度,对应纵向是第一、二、三列,将他们加权求和后进行对比来确认边缘法线朝向。如此这般就可以得到正确结果了。 增加对角线采样后的效果: 可以发现垂直边缘的瑕疵点消失了。至此终于大功告成! # 4. FXAA3.11 Console 边缘搜索,即便是只进行5步的搜索,也需要10次采样(至多),再加上需要进行对角线采样,因此累计采样次数竟多达19次! 这个开销还是比较大的。 老板说,Timothy啊,我们的游戏要上xbox360和ps3的,你这个质量版跑不动啊。能不能整个采样次数少一点的版本呢? 对主机版的需求: - 将采样次数控制在尽量少 - 依旧要满足边缘渐变足够"薄" Timothy说你这不是既要马儿跑,又要马儿不吃草吗? Timothy心想,既然如此,那我就放飞一下自我吧。也不去判断什么局部边缘的横纵轴了,我直接利用局部亮度信息,去估算一下该局部区域边缘的斜率(切线走向)。具体怎么做呢? ## 4.1 估算边缘切线走向 首先在当前像素的四个角进行4次采样,如下图所示。 注意,这里只偏移0.5个像素位,并非采样对角像素。因此通过Bilinear插值采样是能够一次性获得4个像素的平均亮度的。 不妨记4个角采样到的亮度为`lumaNW,lumaNE,lumaSW,lumaSE`。 Timothy给出的边缘走向估计如下: ```hlsl float2 dir; dir.x = (lumaSW + lumaSE) - (lumaNW + lumaNE); dir.y = (lumaNW + lumaSW) - (lumaNE + lumaSE); dir = normalize(dir); ``` 这个估计是什么意思呢? 根据Bilinear插值的原理,易推导出: ``` dir.x = (lumaSW + lumaSE) - (lumaNW + lumaNE); ``` 其实等于以下的滤波核: ``` -1/4 | -1/2 | -1/4 0 | 0 | 0 1/4 | 1/2 | 1/4 ``` 它表征了当前像素上方3个像素和下方3个像素的对比强度,以此作为边缘切线在x轴向的投影分量,这个是合理的。 y轴也是同理。 ## 4.2 边缘像素判定 既然决定只采样4个角,那么局部亮度对比度的计算也只能从这4个采样点中去估计了。局部对比度计算公式如下: ```hlsl float lumaMax = max(lumaSW,lumaSE,lumaNW,lumaNE,lumaM); float lumaMin = min(lumaSW,lumaSE,lumaNW,lumaNE,lumaM); float lumaContrast = lumaMax - lumaMin; ``` 当对比度大于阈值时,视为边缘像素。 阈值方式同初代目版本。 ## 4.3 混合 在估算出切线走向后,接下来要想办法进行混合。为了让边缘能够Sharp一些,不像初代目那般边缘模糊,Timothy决定不向normal方向进行混合。 他决计在切线的正反两个方向,偏移一定距离(0~1),各进行一次采样,求平均作为当前像素的颜色。这样求得的颜色,就在切线方向起到了一个渐变过渡作用。 不难看出,偏移距离决定了当前像素颜色在最终颜色里所占的比重。 很明显,Timothy的这个思路是优先照顾那些45度角的锯齿边缘(亲儿子),抛弃那些接近于水平或者垂直的锯齿边。 对于这种接近水平/垂直的边缘,沿切线方向进行少量偏移采样极难覆盖到敌方阵营的像素。因而也就几乎失去了混合的效果。 为了补救这个问题,Timothy额外加入了两次采样。这两次采样的偏移距离根据切线的斜率来决定。越趋于水平/垂直,那么偏移距离就越远,企图以此覆盖到敌对阵营。 计算公式如下: ```hlsl float dirAbsMinTimesC = min(abs(dir.x),abs(dir.y)) * FXAA_SHARPNESS; float2 dir2 = clamp(dir / dirAbsMinTimesC,-2,2) * 2; ``` - FXAA_SHARPNESS是一个暴露给用户的可调节参数 我们知道,切线为45度角的情况下,`dir.x == dir.y ~= 0.7 `,因此随着切线角度变化,`dirAbsMinTimesC`的取值范围为`[0 , 0.7 * FXAA_SHARPNESS]`,于是dir2分量的范围为 `-2/FXAA_SHARPNESS ~ -4 or 2/FXAA_SHARPNESS ~ 4 ` 因此随着`FXAA_SHARPNESS`增大,dir2会越保守,越靠近当前像素,边缘就会越锐利。 需要额外注意的是,由于我们估计的只是局部边缘切线,在dir2较大的情况下,可能会采样到差异很大的像素。因此需要针对额外的两次采样做一下噪点过滤。过滤规则为,若额外两次采样的亮度,超出局部亮度范围`[lumaMin,lumaMax]`,那就丢弃,只使用前两次的采样结果。 这样我们对最终合法的采样结果(2或4次采样)求平均作为当前像素颜色即可。 但总得来说,这只能算是"补救"。FXAA Console针对水平/垂直的边缘抗锯齿效果的确比较勉强。 这一点Timothy自己也承认了。他在SIGGRAPH 2011 presentation里关于Console版本的评价是: >(Advantages) Very fast, reduces contrast on pixel and sub-pixel aliasing >(Disadvantages) Not very good on near horizontal or vertical edges 水平和垂直的FXAA-Console抗锯齿效果图如下,局部会出现一些不自然的过渡点。 对于斜边或者曲线的抗锯齿效果则很不错 # 5. 最终大比拼 三种方式对比 --|采样次数|缺陷 --|--|--| FXAA初代|5|边缘太模糊| FXAA3.11 Quality|9 + N * 2(N至少>=3才有比较好的效果)|相对而言比较吃性能 FXAA3.11 Console|9|水平/垂直边缘抗锯齿较弱 同一个场景对比(画面放大6倍后): # 参考文献 [[1] Timothy Lottes, FXAA White Paper, 2009](https://developer.download.nvidia.cn/assets/gamedev/files/sdk/11/FXAA_WhitePaper.pdf) [[2] SIGGRAPH 2011 presentation on FXAA 3.11](http://iryoku.com/aacourse/downloads/09-FXAA-3.11-in-15-Slides.pptx) [[3] Simon Rodriguez, Implementing FXAA, 2016](http://blog.simonrodriguez.fr/articles/30-07-2016_implementing_fxaa.html)