技术美术百人计划 | 《4.4 抗锯齿》笔记

前言:文中补充的内容很多来自链接里的,建议看看链接的文章。

一、锯齿

(一) 什么是锯齿

在学习渲染的旅途中,你可能会时不时遇到模型边缘有锯齿的情况。这些锯齿边缘(Jagged Edges)的产生和光栅器将顶点数据转化为片段的方式有关。在下面的例子中,你可以看到,我们只是绘制了一个简单的立方体,你就能注意到它存在锯齿边缘了:

可能不是非常明显,但如果你离近仔细观察立方体的边缘,你就应该能够看到锯齿状的图案。如果放大的话,你会看到下面的图案:

这很明显不是我们想要在最终程序中所实现的效果。你能够清楚看见形成边缘的像素。这种现象被称之为走样(Aliasing)。有很多种抗锯齿(Anti-aliasing,也被称为反走样)的技术能够帮助我们缓解这种现象,从而产生更平滑的边缘。

具体到实时渲染领域中,可以将锯齿(走样)分为以下三种:

  • 几何走样(Geometry Aliasing),几何物体的边缘有锯齿,是对几何边缘采样不足导致。
  • 着色走样(Shading Aliasing),渲染方程也是一个连续函数,对某些部分(比如法线,高光等)在空间变化较快(高频部分)采样不足也会造成走样,比较明显的现象就是高光闪烁或高光噪点。
  • 时间走样,主要是对高速运动的物体采样不足导致。比如游戏中播放的动画发生跳变等。

(二) 锯齿如何产生

在计算机图形学中,锯齿(jaggies)的产生是由于采样错误引发的。这种错误通常发生在光栅化过程中,即当三维空间中的连续物体被离散化为由像素表示的图像时。由于显示器上的像素是离散化的点,而几何图形是连续的坐标连接实现的,因此在图形的边缘必然会产生锯齿。特别是在屏幕分辨率较低的情况下,这种离散化更为明显,导致锯齿效果更加显著。锯齿的生成原理可以追溯到采样理论,即当信号变化的速度太快(主要指高频信号变化太快),而采样的速度太慢时,最终结果就会导致采样错误,从而产生锯齿状边缘。

二、抗锯齿方法

(一) 超采样抗锯齿 SSAA

最开始我们有一种叫做超采样抗锯齿(Super Sample Anti-aliasing, SSAA)的技术,它会使用比正常分辨率更高的分辨率(即超采样)来渲染场景,当图像输出在帧缓冲中更新时,分辨率会被下采样(Downsample)至正常的分辨率。这些额外的分辨率会被用来防止锯齿边缘的产生。虽然它确实能够解决走样的问题,但是由于这样比平时要绘制更多的片段,它也会带来很大的性能开销。所以这项技术只拥有了短暂的辉煌。

例如,把原来的每个像素点进行细分,将每个像素点细分成了4个采样点,但是劣势也很明显,光栅化和着色的计算负荷都比原来多了4倍,render target的大小也涨了4倍。

再根据采样点在图形内的数量比例去决定最终的颜色,这样边缘会被模糊,弱化了锯齿的感觉。

(二) 多重采样抗锯齿 MSAA

然而,在SSAA这项技术的基础上也诞生了更为现代的技术,叫做多重采样抗锯齿(Multisample Anti-aliasing, MSAA)。它借鉴了SSAA背后的理念,但却以更加高效的方式实现了抗锯齿。

MSAA(Multi-Sampling AA)

  • 在光栅化阶段判断一个三角形是否被像素覆盖的时候会计算多个覆盖样本(Coverage sample)。
  • 但是在片元着色器阶段计算像素颜色的时候每个像素还是只计算一次

例如下图是4xMSAA,三角形只覆盖了4个coverage sample中的2个0和1。所以这个三角形需要生成一个fragment在片元着色器里着色,只不过生成的fragment还是在像素中央(位置,法线等信息插值到像素中央)然后只运行一次片元着色器,最后得到的结果在resolve阶段会乘以0.5,因为这个三角形只覆盖了一半的sample。

现代所有GPU都在硬件上实现了这个算法,而且在shading的运算量远大于光栅化的今天,这个方法远比SSAA快很多。

SSAA和MSAA区别:

一句话:同样是2x2=4x,SSAA做4次深度测试和4次shading,MSAA做4次深度测试和1次shading。

(三) FXAA 快速近似抗锯齿

参考:

FXAA算法演义 - 知乎 (zhihu.com)

FXAA(Fast Approximate Anti-Aliasing)是由 NVDIA 发明的高效后处理抗锯齿方案,也是目前所有后处理抗锯齿方案里面同等设置下效率最高的抗锯齿算法。

FXAA的思路很简单,图像锯齿通常出现在与背景呈现高对比度的物体边缘(视觉比较明显)。那我们可以通过计算像素和其周围像素的亮度对比,来判定一个像素是不是边缘像素。针对非边缘的像素,我们什么都不做,而对于边缘像素,我们可以让其和周围的像素进行Blend混合,从而起到模糊的效果。

所以FXAA算法的核心思路就两点:

  1. 边缘提取
  2. 边缘混合

1. 初代FXAA

一、边缘检测

FXAA1.0使用的边缘检测算法如下:

  • 读取上下左右4个方向 + 自身的像素亮度,筛选出其中的最大值lumaMax和最小值lumaMin
  • 令对比度lumaContrast = lumaMax - lumaMin,当对比度超过一定阈值时,便认为当前像素为边缘像素

//采集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;

白色为高对比度像素,黑色为低对比度像素,物件的边缘被很好的勾勒了出来。接下来我们给定一个对比度阈值FXAA_ABSOLUTE_LUMA_THRESHOLD,当lumaContrast大于该阈值时,便认为是边缘像素:

#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 * 相对阈值比例)

这个修正带来以下效果:明亮的地方需要更高的周边对比度才能被判定为边缘

二、边缘混合

对于非边缘像素,我们什么都不做,原样返回颜色即可。这个叫做Early Exit

对于边缘像素,接下来要让其和周边像素进行混合,以起到模糊边缘的效果。这里有两个问题需要解决:

  • 上下左右4个方向,到底与哪个方向混合?
  • 混合的比例是多少?

(一) 混合方向

针对第一个问题,当我们从像素的微观角度去看时,边缘像素只有三种情况:在横边上,在纵边上,在角上。

初始想法是计算上下两个像素的亮度差以及左右两个像素的亮度差,如果前者大,那就是横边,反之则为纵边。伪代码如下:

float lumaGradV = abs(lumaN - lumaS); 
float lumaGradH = abs(lumaE - lumaW);
bool isHorz = lumaGradV > lumaGradH;

但这种方式有个缺陷,即对于单像素的线,无法正确判定其走向。对于1位置的像素,lumaGradV将与lumaGradH相等。

单像素宽度线

为了解决这个问题,改进如下。首先计算像素在S、N、W、E 4个方向的亮度变化梯度:

float lumaGradS = lumaS - lumaM;
float lumaGradN = lumaN - lumaM;
float lumaGradW = lumaW - lumaM;
float lumaGradE = lumaE - lumaM;

然后对垂直和水平方向的梯度分别相加,取绝对值,比较它们的大小,这样就成功的解决单像素线的问题。

float lumaGradV = abs(lumaGradS + lumaGradN);
float lumaGradH = abs(lumaGradW + lumaGradE);
bool isHorz = lumaGradV > lumaGradH;

(二) 法线朝向

成功判定了边缘像素横纵状态后,我们接下来需要计算其法线朝向。对于像素1,其边缘法向应当是朝左。对于像素2,其边缘法线则是朝上。边缘法线即表征了目标混合像素的方向。

边缘法线

这个就简单了,哪个方向的梯度大,法线就朝哪边。于是我们有:

float2 normal = float2(0,0);
if(isHorz)
{normal.y = sign(abs(lumaGradN) - abs(lumaGradS));//sign()取符号,正为1,负为-1,0不变
}
else
{normal.x = sign(abs(lumaGradE) - abs(lumaGradW));
}

(normal + 1) * 0.5作为颜色输出到画面,我们有如下效果:

4种颜色分别代表了边缘法线N、S、E、W4个朝向。

(三) 混合因子

我们期望的混合因子应当符合以下两个要素:

  • 需要在"高对比度的像素之间"构造出渐变。例如lumaN和lumaS对比度很高,那么lumaM就应该将自己修正为接近 (lumaN + lumaS) / 2。
  • 不破坏像素之间正常的渐变关系。(例如软阴影)

于是首先对NSEW4个方向的像素亮度求平均,计算出中间像素的期望亮度:

half lumaL = (lumaN + lumaS + lumaE + lumaW) * 0.25;

然后考察中间像素的实际亮度与期望亮度之差:

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)偏移

改进混合因子后,抗锯齿效果如下图:

左为无抗锯齿,右为FXAA1.0

软阴影部分完全没受到影响,物体边缘也呈现出了模糊渐变效果。但也可以发现一些不足之处:物体边缘有些"过于"模糊了(相比较于MSAA而言)。这个不难理解。因为如我们前面所述的边缘查找算法里,边缘像素是呈"一对"存在的。1,2在黑色阵营里是边缘像素,那么1,2 normal朝向的白色像素,在白色阵营里自然也属于边缘像素。经过Blend之后,边缘渐变至少会覆盖两个像素宽度,于是就显得有些"厚"。

于是添加了两个参数

  • FXAA_QUALITY_SUBPIX_TRIM 用来把混合因子整体往0靠靠,这样边缘就不会那么的糊了。
  • FXAA_QUALITY_SUBPIX_CAP 用来把混合因子进行一个整体的scale(0.x),也是为了不那么糊,至少保留住一点高频信息吧。
//float blend = lumaDeltaML / lumaContrast;
float blend = max(0,lumaDeltaML / lumaContrast - FXAA_QUALITY_SUBPIX_TRIM) * FXAA_QUALITY_SUBPIX_CAP;

2. 改进版FXAA 3.11

自2009年发布了FXAA初代目,Timothy在两年后的SIGGRAPH上发布了FXAA 3.11。与FXAA初代目相比,最大的不同就是改进了混合因子算法。Timothy觉得,要真正解决边缘模糊"太厚"的问题,就要深入分析锯齿产生的本质原因。

我们知道,SSAA和MSAA是通过在一个像素内部增加额外的采样点,然后进行颜色加权混合达到抗锯齿目的的。如上图所示,一个采样点,如果落入几何内部,那么整个像素就都是红色。如果落到几何体外部,那么整个像素都是蓝色。

一根线,穿过一个像素方块,将其劈成两半。一半归入蓝色阵营,一半归入红色阵营。很明显,我们可以将两部分面积分别作为蓝色和红色的权重,进行加权平均计算,最终得到混合色。当我们观察锯齿的一段局部线条区域时,容易发现,红色面积权重是随着线条方向逐渐减小的。当红色权重少于0.5时,自然像素就翻转成蓝色了。

基于这个,Timothy发明了边缘搜索算法。原理如下:

  • 首先确定边缘像素的横纵向
  • 往边缘两侧按照给定的步长进行边缘终点搜索
  • 确定了边缘线段的两个端点后,可以计算出当前像素在线段中的位置
  • 根据像素在线段中所处的位置来计算出混合因子

于是以上算法归纳出两个待解决的问题:

  1. 如何判定搜索的目标像素为边缘终点?
  2. 边缘上每个点的位置与混合因子满足怎样的关系?
一、边缘终点判定

首先我们可以将搜索的起始点定为像素往normal方向偏移0.5个单位的位置。如下图:

为什么要往normal方向偏移0.5个像素进行采样呢?假如我们不进行偏移,比如直接从黑色像素中心往两边搜索,那么左侧很显然无法查找到正确的端点。

使用线性插值采样,那么采样结果将是边缘两侧像素的平均亮度。不如将起始点亮度记为lumaStart。假设我们往两侧一个个像素进行判定,直到发现亮度与lumaStart的差值超过一定阈值,便认为到头了。并且这个阈值应当跟搜索起始点两侧像素的对比度lumaStartContrast呈正相关,其中0.25只是一个经验性的系数。

float searchEndThreshold = lumaStartContrast * 0.25

所谓边缘,是由边缘两侧的像素对比所产生的。在搜索的时候一定要同时考虑边缘两侧的像素。

那为什么不在搜索的通过计算两侧的亮度差来确认终点,而是要计算两侧的平均亮度呢? Timothy说你仔细想想,当然是为了少一次采样啊。计算差要采样两个像素做减法,而我用平均,可以直接使用Bilinear Filter一次采样足矣。

二、混合因子计算

混合因子的计算方式:

  • 设边缘线长为edgeLength(可以通过搜索到的两个端点位置相减得到)
  • 计算出距离当前像素比较近的一个端点,记为targetP,距离记为dst
  • 考察targetP与当前像素是否属于同一阵营。
  • 如果是,则blend为0,即当前像素保持原样,不进行混合。
  • 如果否,则blend = abs(0.5 - dst/edgeLength)(个人理解:偏移中心的程度)

那么如何考察边缘端点与当前像素是否属同一阵营呢?不妨假设搜索结束时,已经得到距离当前像素较近的边缘端点亮度为lumaEnd。我们知道,搜索起始点的亮度lumaStart为两个阵营的平均亮度。

  • 因此可以将当前像素亮度(lumaM)与阵营平均亮度做比较: lumaM - lumaStart
  • 同时将端点亮度(lumaEnd)也与阵营平均亮度做比较: lumaEnd - lumaStart

如果两者符号一致,则属于同一阵营,否则属于不同阵营。

于是blend计算公式如下:

float blend;
if((lumaM - lumaStart) * (lumaEnd - lumaStart) > 0)//同一阵营
{blend = 0;
}
else//不同阵营
{blend = 0.5 - dst/edgeLength;
}
三、搜索步长优化

在进行边缘搜索时,一个个像素搜索是不现实的,我们需要一个策略来将采样次数控制到限定范围内。很容易想到的一种方式是,给定一个最大搜索次数MAX_EDGE_SEARCH_SAMPLE_COUNT,然后在搜索时逐渐增大步长。

FXAA3.11 Quality原算法里提供了很多组预设,可以参考GitHub上的源码。例如Preset12的定义如下

#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[]数组定义了每步搜索的步长。自然给予的最大搜索步骤越多、步长越小,边缘的渐变就能更细腻。 通过使用不同的参数,我们可以在图像质量与性能之间寻求平衡。

到此为止, 结果如下:

无抗锯齿对比图

带瑕疵的FXAA Quality

水平边缘的抗锯齿效果很不错,边缘渐变层足够"薄",一举解决了FXAA初代目令边缘太模糊的问题。

可是垂直方向却出现了一些瑕疵点。这是怎么回事呢?

四、额外对角线采样

打开边缘法线调试效果Debug一下:

4种颜色,代表了边缘法线的4种朝向

原来垂直向的噪点是由于混合方向计算不正确引起的。仔细深入分析了一下,Timothy终于找到了原因。在早先我们计算边缘法线的时候,只采样了N,S,E,W4个方向的像素亮度。再看一下当时的计算方式:

float2 normal = float2(0,0);
if(isHorz)
{normal.y = sign(abs(lumaGradN) - abs(lumaGradS));
}
else
{normal.x = sign(abs(lumaGradE) - abs(lumaGradW));
}

如果仅有这4个方向,不足以分析如下的情况:考察像素,其lumaGradN(上)lumaGradE(右)梯度相同,lumaGradS(下)lumaGradE(左)梯度相同,因此假如只考虑上下左右4个方向的像素亮度,我们是无法计算出像素1的边缘法线的,这时候就要额外考虑其4个对角线像素的亮度。

于是Timothy将边缘法线计算公式修改如下:

//第一行,第二行和第三行的梯度
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); //纵-右

意思的在水平方向,我们同时计算第一行,第二行和第三行的梯度,对应纵向是第一、二、三列,将他们加权求和后进行对比来确认边缘法线朝向。如此这般就可以得到正确结果了。

增加对角线采样后的效果:

可以发现垂直边缘的瑕疵点消失了。

3. FAXX Console版

边缘搜索,即便是只进行5步的搜索,也需要10次采样(至多),再加上需要进行对角线采样,因此累计采样次数竟多达19次! 这个开销还是比较大的。

对主机版的需求:

  • 将采样次数控制在尽量少
  • 依旧要满足边缘渐变足够"薄"

Timothy心想,既然如此,也不去判断什么局部边缘的横纵轴了,我直接利用局部亮度信息,去估算一下该局部区域边缘的斜率(切线走向)。具体怎么做呢?

首先在当前像素的四个角进行4次采样,如下图所示。

注意,这里只偏移0.5个像素位,并非采样对角像素。因此通过Bilinear插值采样是能够一次性获得4个像素的平均亮度的。不妨记4个角采样到的亮度为lumaNW,lumaNE,lumaSW,lumaSE

Timothy给出的边缘走向估计如下:

float2 dir;
dir.x = (lumaSW + lumaSE) - (lumaNW + lumaNE);
dir.y = (lumaNW + lumaSW) - (lumaNE + lumaSE);
dir = normalize(dir);

这个估计是什么意思呢?其实等于以下的滤波核:

 -1/4 | -1/2 | -1/40   | 0    | 0   1/4 | 1/2  | 1/4

它表征了当前像素上方3个像素和下方3个像素的对比强度,以此作为边缘切线在x轴向的投影分量,这个是合理的。y轴也是同理。

既然决定只采样4个角,那么局部亮度对比度的计算也只能从这4个采样点中去估计了。局部对比度计算公式如下:

float lumaMax = max(lumaSW,lumaSE,lumaNW,lumaNE,lumaM);
float lumaMin = min(lumaSW,lumaSE,lumaNW,lumaNE,lumaM);
float lumaContrast = lumaMax - lumaMin;

当对比度大于阈值时,视为边缘像素。 阈值方式同初代目版本。在估算出切线走向后,接下来要想办法进行混合。为了让边缘能够Sharp一些,不像初代目那般边缘模糊,Timothy决定不向normal方向进行混合。他决计在切线的正反两个方向,偏移一定距离(0~1),各进行一次采样,求平均作为当前像素的颜色。这样求得的颜色,就在切线方向起到了一个渐变过渡作用。

沿切线方向的两次采样

不难看出,偏移距离决定了当前像素颜色在最终颜色里所占的比重。很明显,Timothy的这个思路是优先照顾那些45度角的锯齿边缘,抛弃那些接近于水平或者垂直的锯齿边。对于这种接近水平/垂直的边缘,沿切线方向进行少量偏移采样极难覆盖到敌方阵营的像素。因而也就几乎失去了混合的效果。

无法有效抗锯齿的水平边缘

为了补救这个问题,Timothy额外加入了两次采样。这两次采样的偏移距离根据切线的斜率来决定。越趋于水平/垂直,那么偏移距离就越远,企图以此覆盖到敌对阵营。计算公式如下:

//FXAA_SHARPNESS是一个暴露给用户的可调节参数
float dirAbsMinTimesC = min(abs(dir.x),abs(dir.y)) * FXAA_SHARPNESS;
float2 dir2 = clamp(dir / dirAbsMinTimesC,-2,2) * 2;

我们知道,切线为45度角的情况下,dir.x == dir.y ~= 0.7,dir.length=1,因此随着切线角度变化

  • dirAbsMinTimesC的取值范围为[0 , 0.7 * FXAA_SHARPNESS]
  • dir2的范围为 (-2/FXAA_SHARPNESS , -4)U(2/FXAA_SHARPNESS , 4)

因此随着FXAA_SHARPNESS增大,dir2会越保守,越靠近当前像素,边缘就会越锐利。

需要额外注意的是,由于我们估计的只是局部边缘切线,在dir2较大的情况下,可能会采样到差异很大的像素。因此需要针对额外的两次采样做一下噪点过滤。过滤规则为,若额外两次采样的亮度,超出局部亮度范围[lumaMin,lumaMax],那就丢弃,只使用前两次的采样结果。这样我们对最终合法的采样结果(2或4次采样)求平均作为当前像素颜色即可。

但总得来说,这只能算是"补救"。FXAA Console针对水平/垂直的边缘抗锯齿效果的确比较勉强。水平和垂直的FXAA-Console抗锯齿效果图如下,局部会出现一些不自然的过渡点。

FXAA Console水平/垂直抗锯齿效果

对于斜边或者曲线的抗锯齿效果则很不错

FXAA Console 曲边/斜边抗锯齿效果

4. 最终大比拼

三种方式对比

采样次数

缺陷

FXAA初代

5

边缘太模糊

FXAA3.11 Quality

9 + N * 2(N至少>=3才有比较好的效果)

相对而言比较吃性能

FXAA3.11 Console

9

水平/垂直边缘抗锯齿较弱

同一个场景对比(画面放大6倍后):

5. 补充

下面说说FXAA的最佳使用规范:

  • 最好在完成所有后处理效果之后再执行FXAA
  • 最好在sRGB空间执行
  • 需要在LDR空间执行

这些规范不难理解。因为FXAA图像抗锯齿是基于"视觉的"而不是基于物理的。因此在我们进行亮度计算时,必须时基于"视觉"的亮度,而不是"物理"的亮度。另一方面,HDR到LDR的ToneMap不是线性的,所以即便在HDR空间中执行FXAA,ToneMap后也会失效。

优势:

  1. 减少可见的锯齿,同时保持清晰度并保持在游戏引擎的实际毫秒/帧成本范围内。对于 FXAA 预设 2,在 GTX480 上处理 1920x1200 帧的成本低于 1 毫秒。
  2. 以三角形边缘和着色器结果中的锯齿为目标。FXAA 能够减少单像素和子像素锯齿:抖动采样阴影区域中点画锯齿的减少。
  3. 易于集成到单个像素着色器中。FXAA 作为单通道滤镜在单样本彩色图像上运行。与 MSAA 相比,FXAA 可以提供内存优势,尤其是在立体和多显示器渲染目标或后台缓冲区上。
  4. 与使用 MSAA 和对多个样本进行着色相比,可以为延迟渲染提供性能优势。

(四) 时域抗锯齿 TAA

参考:深入浅出Temporal Antialising - 知乎 (zhihu.com)

抗锯齿技术可以从原理上分为两个大类:

  • 空域抗锯齿,代表是FXAA和SMAA
    • 是通过超采样几何边缘达到模糊边缘的效果,从理论上解释就是通过低通滤波抑制几何边缘的高频信息,它只能缓解几何锯齿,对着色锯齿无效。帧间抗锯齿技术,是通过加权混合相邻多帧达到抗锯齿效果,从理论上解释就是将计算量分摊(Amortized)至多帧的超采样。超采样能有效地提高采样率,帧间抗锯齿能有效缓解几何锯齿和着色锯齿。
  • 时域抗锯齿,代表是TAA。
    • 帧间抗锯齿技术的实现依赖于帧间相关性(frame-to-frame coherence)。我们看到的视频或者游戏画面,上一帧画面中绝大部分内容也会出现在下一帧画面中,通常是平滑过渡的,比较少出现突变。也正是因为受到帧间相关性地启发,才有了帧间抗锯齿技术。

TAA/TXAA(Temporal Anti-Aliasing)并不是一个单独的算法,更像是一个算法框架,有很多的变种。有些引擎,将TAA与一些空域抗锯齿技术结合起来使用,以期待达到更好的抗锯齿效果。

TAA是英伟达设计的更高画质的抗锯齿技术,可以达到和SSAA接近的效果,只不过多采样是基于历史帧缓冲,从历史帧中采样。缺点是随着历史颜色的累积,会导致不可绝对消除的模糊(运动模糊),尤其在移动过快的镜头或物体情况下,会导致重影现象,尤其VR设备中此方法不适用。

帧间抗锯齿技术的核心思想就是将运算量分摊至多帧,如图所示,任意一帧只计算一个像素点,随着t个帧的累积,就有t个采样像素,再混合结果,就相当于做了t个子像素的超采样。当然,每帧中像素需要做一定的抖动(Jitter),不然不同帧相同位置的像素值就完全一样,也就没有了分摊多帧的意义。通过对像素分摊至多帧的超采样,来提高采样效率,达到缓解锯齿的目的。

尽管通过分摊超采样的方法,将计算量分摊到了不同帧,但是保存多帧也是不切实际的。那就有了只混合相邻两帧的思路,本篇文章分别称之为当前帧和历史帧(History Frame)。若当前帧的像素值是

,前t-1帧的历史帧像素是

,将它们加权混合就得到分摊多帧超采样得到的结果,如等式所示。

称这种方法为指数平滑滤过滤(Exponential Smoothing Filter),其中,α是一个加权值,称为指数平滑系数,可以是一个固定值,也可以是一个自适应变化的值。它有点类似于累积缓存器(Accumulation Buffer)的原理,不断累积结果。

重投影

在实际应用中,画面并不是完全静止的,镜头在运动、模型在动,例如当前帧模型上的一个像素点位于(100, 520),它在历史帧对应的位置可能是(99,530)。那么,我们就需要计算当前帧像素点在历史帧所处的位置,这个过程称为重投影(Reprojection)。如果只有镜头在动,那通过镜头的变换矩阵就可以计算出历史帧的位置;但是如果模型本身也在动,就需要计算模型顶点在当前帧与历史帧的位置差值,这个差值称之为运动矢量(Motion Vector),可以把场景内运动模型的运动矢量存储在一个缓存中,这个缓存就称为速度缓存(Velocity Buffer)。重投影是基于当前帧和历史帧的视图投影矩阵与速度缓存,推算出屏幕上当前帧像素点的位置p在历史帧对应位置

的过程。

修正历史帧像素

至此,已经得到当前帧像素在历史帧中的位置,也就容易根据历史帧采样到历史帧像素值

。但是,由于每帧渲染时有像素抖动、模型运动、渲染环境变化(例如光照条件)等,它们会导致渲染结果发生变化,此时得到的历史帧像素就可能不具参考性。如图7所示,当前帧位置p的像素,在历史帧对应位置的像素被遮挡住了,那么重投影得到的历史帧像素与当前帧像素无法对应。那么,就需要一个验证历史帧像素并修正的阶段,修正(Rectify)历史帧像素值,再将它代入等式算出最终结果。

相反,如果不对历史帧像素进行修正,就可能会出现鬼影(Ghosting)现象


重新复盘下帧间抗锯齿技术的流程,如图所示。对镜头进行抖动,渲染场景至主缓存中;如果模型发生运动,需要将渲染运动模型的运动矢量存至速度缓存。在一个TAA的后处理历程中,通过重投影获取当前帧像素在历史帧中的位置,获取历史帧像素,验证并修正历史帧像素,将历史帧像素和当前帧像素根据等式加权混合,输出结果。

三、移动端抗锯齿的建议

开发者选项中的“4倍抗锯齿”对游戏有没有用?用两部手机测一测就知道了_安卓手机_什么值得买 (smzdm.com)

如果我们在开发者模式中强制开启4倍抗锯齿,并且始终使用GPU来处理图像画面,在一定程度上可以改善手游的画质,让游戏运行变得更加流畅吗?

通过新旧两部手机的两轮测试,我们可以得到以下结论:

  • 开发者模式中的“强制启用4×MSAA”、“停用HW叠加层”两个选项,一定程度上可以让游戏运行变得更加流畅,但改善画质真的不能体现,因为开启与否平均帧率都非常相近,这代表单位时间内加载的画幅数量是相近的,所以看不出画质有本质的提升;所以尽量从设计上避免引起锯齿。
  • 旗舰手机在芯片性能溢出的前提下,可以选择开启这两个选项提高游戏的流畅度,但付出的代价是温度,手机明显会更烫;
  • 非旗舰手机、旧手机不建议这样折腾,因为芯片算力本身就吃紧,渲染任务全推给GPU后,游戏运行不光更卡,机身也更烫,游戏体验非常糟糕。

参考链接:

(3 封私信) 请问FXAA、FSAA与MSAA有什么区别?效果和性能上哪个好? - 知乎 (zhihu.com)

抗锯齿 - LearnOpenGL CN (learnopengl-cn.github.io)

photoshop中羽化的操作技巧-百度经验 (baidu.com)

https://de45xmedrsdbp.cloudfront.net/Resources/files/TemporalAA_small-59732822.pdf

GeForce Tech Demo: MFAA

https://zhuanlan.zhihu.com/p/90982812?utm_source=qq

【Metal2研发笔录(七):抗锯齿之基于A11 Imageblock特性的增强MSAA - 知乎 (zhihu.com)

Anti-Aliasing 技术盘点 - 知乎 (zhihu.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/145319.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Mobile net V系列详解 理论+实战(1)

Mobilenet 系列 论文精讲部分0.摘要1. 引文2. 引文3. MobileNet 模型架构3.0 卷积个人理解3.1 深度可分离卷积3.2 网络结构和训练3.3 宽度乘数:更细的模型 α3.4 分辨率乘数:降低表示的维度ρ 4. 实验4.1 模型选择4.2. 模型缩减超参数4.3. 细粒度识别4.4…

人力资源数据集分析(二)_随机森林与逻辑回归

数据入口:人力资源分析数据集 - Heywhale.com 数据说明 字段说明EmpID唯一的员工IDAge年龄AgeGroup年龄组Attrition是否离职BusinessTravel出差:很少、频繁、不出差DailyRate日薪Department任职部门:研发部门、销售部门、人力资源部门Dista…

Linux 进程3

进程地址空间 CPU读取数据都需要地址,在计算机中所有东西都是一种数据,包括我们的进程。 这是一个进程空间示意图,操作系统通过task_struct结构体链表来管理每一个进程,结构体里面有一个指针指向操作系统为进程开辟的一段空间&am…

2-100 基于matlab的水果识别

基于matlab的水果识别。从面积特征、似圆形特征,颜色(rgb值和hsv值)特征对图像中的梨子、苹果、桃子、香蕉和菠萝进行特征提取,边缘检测识别,最后按照筛选出来的特征对水果进行识别。程序已调通,可直接运行。 下载源程序请点链接…

【CustomPainter】渐变圆环

说明 实现一个渐变圆环,起点位置为- π / 2。 效果 源码 GradientCircularPainter1 class GradientCircularPainter1 extends CustomPainter {final double progress;GradientCircularPainter1(this.progress);overridevoid paint(Canvas canvas, Size size) {c…

VCNet论文阅读笔记

VCNet论文阅读笔记 0、基本信息 信息细节英文题目VCNet and Functional Targeted Regularization For Learning Causal Effects of Continuous Treatments翻译VCNet和功能目标正则化用于学习连续处理的因果效应单位芝加哥大学年份2021论文链接[2103.07861] VCNet和功能定向正…

OpenCV特征检测(5)检测图像中的角点函数cornerMinEigenVal()的使用

操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 计算用于角点检测的梯度矩阵的最小特征值。 该函数类似于 cornerEigenValsAndVecs,但它计算并存储协方差矩阵导数的最小特征值&…

2024上海工博会,正运动激光振镜运动控制器应用预览(二)

■展会名称: 第二十四届中国国际工业博览会(以下简称“上海工博会”) ■展会日期 2024年9月24日–28日 ■展馆地点 中国国家会展中心(上海) ■展位号 6.1H-E261 正运动激光加工控制解决方案主要分为激光振镜运动…

24 小时不关机的挂机云电脑,还能这么玩?

云电脑技术为我们提供了无限可能。特别是对于游戏爱好者,挂机云电脑不仅解决了传统电脑的局限性,还带来了更为便利的游戏体验。除此之外云电脑还有什么其他玩法呢? 01 挂机云电脑的优势 首先要知道,什么是挂机云电脑&#xff1f…

解锁自动化新境界:KeymouseGo,让键盘和鼠标动起来!

文章目录 解锁自动化新境界:KeymouseGo,让键盘和鼠标动起来!背景:为何选择KeymouseGo?KeymouseGo简介安装KeymouseGo简单函数使用应用场景常见问题与解决方案总结 解锁自动化新境界:KeymouseGo,…

操作系统 | 学习笔记 | | 王道 | 5.1 I/O管理概述

5.1 I/O管理概述 5.1.1 I/O设备 注:块设备可以寻址,但是字符设备是不可寻址的 I/O设备是将数据输入到计算机中,或者可以接收计算机输出数据的外部设备,属于计算机中的硬件部件; 设备的分类 按使用特性分类&#xff…

from tqdm.auto import tqdm用法详细介绍

tqdm 是一个 Python 库,用于在长时间运行的任务中显示进度条。tqdm.auto 是 tqdm 的一个版本,能够自动适配输出环境(如 Jupyter Notebook、命令行等),以确保进度条在各种环境下显示正确。下面是 tqdm.auto 的详细用法介…

英飞凌 PSoC6 评估板 RT-Thread 开发环境搭建

本文介绍如何搭建基于 RT-Thread Studio IDE 工具的 PSoC6 RTT 评估板的开发环境,通过搭建一个简单的工程,将代码编译、下载到 PSoC6 RTT 开发板。 安装软件包 首先需要安装 RT-Thread Studio,如果你还没安装,可以点击这里下载安…

MySQL 中的 UTF-8 与 UTF8MB4:差异解析

在 MySQL 数据库中,字符集的选择对于数据的存储和处理至关重要。其中,UTF-8 和 UTF8MB4 是两个常见的字符集选项。那么,它们之间到底有什么区别呢? 一、字符集简介 UTF-8 UTF-8(8-bit Unicode Transformation Format&…

中伟视界:AI边端云一体化管控平台的特色功能介绍及其工作原理

在当前的数字化浪潮中,人工智能(AI)与物联网(IoT)技术逐渐渗透到各行各业,而如何在复杂、多层次的技术环境中高效管理和控制这些系统,成为了企业追求自动化、智能化发展的关键挑战。为了满足这种…

docker-compose up 报错:KeyError: ‘ContainerConfig‘

使用命令查看所有容器: docker ps -a 找到有异常的容器删除 docker rm {容器id} 后续发现还是会出现这种情况,尝试使用更高版本的docker-compose后解决

Java开发-面试题-0035-Spring代理方式有哪些

Java开发-面试题-0035-Spring代理方式有哪些 更多内容欢迎关注我(持续更新中,欢迎Star✨) Github:CodeZeng1998/Java-Developer-Work-Note (技术)微信公众号:CodeZeng1998 (生活&…

【Python报错已解决】SyntaxError invalid syntax

🎬 鸽芷咕:个人主页 🔥 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想,就是为了理想的生活! 专栏介绍 在软件开发和日常使用中,BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

怎么解除BitLocker对磁盘的加密?

BitLocker是一种Windows操作系统内置的加密功能,用于保护用户的数据安全。它通过对整个磁盘进行加密,防止数据被未经授权的用户访问。BitLocker主要用于保护笔记本电脑和台式机中的重要数据,尤其是在设备丢失或被盗的情况下。怎么解除BitLock…

【Redis入门到精通二】Redis核心数据类型(String,Hash)详解

目录 Redis数据类型 1.String类型 (1)常见命令 (2)内部编码 2.Hash类型 (1)常见命令 (2)内部编码 Redis数据类型 查阅Redis官方文档可知,Redis提供给用户的核心数据…