0%

【计算机图形学】(四)反走样

反走样(Antialiasing)

上一篇文章说到,光栅化的采样过程导致了图形走样,这一篇来讲如何反走样。

1 什么是走样?

要解决问题,就要先了问题。

走样有很多种形式,比如上文中的锯齿

image-20220306191713343

还有我们日常生活中经常遇到的摩尔纹

image-20220306191741271

甚至一些视觉假象,比如一个顺时针旋转的轮子,当他的速度很快时,我们可能看到它是逆时针旋转的。

以上都属于走样,而产生这些走样的原因都一样,那就是采样速度跟不上信号变化的速度

要理解这句话可不容易,接下来我们慢慢来看。

2 产生走样的原因

上面说了,走样产生的原因就是采样速度跟不上信号变化的速度,更细致一点来说就是,采样频率小于信号频率,导致采样后信号在频域发生了交错、重叠。

关于频域,是信号处理中的重要概念,简单来说就是只存在正弦波的一个世界,也就是只能用 sin 和 cos 函数,而我们日常描述一个信号(函数)是在时域中描述的,这个函数可以是任何形状,那么频域存在的意义是什么呢。

频域存在的意义就是任何一个函数都可以分解为多个正弦波函数和常数的组合,也就是傅里叶变换。有了这个伟大的变换,我们就可以把在时域中不好解释、不好研究的问题,放到频域中去研究。

image-20220306193355754

时域中的信号通过傅里叶变换,可以变为频域中的信号,同样,频域中的信号通过傅里叶逆变换,可以变为时域中的信号。

image-20220306193545813

了解了频域,再来回顾一下上篇文章提到的采样,采样就是计算函数在采样点上的值,我们光栅化的过程其实就是采样的过程,在信号中,采样其实也就是在整个信号的波形上选一些点拿出来,比如下面的图:

image-20220306193918481

垂直的虚线就代表一次采样,和不同的波形的交点,就是采样出来的点,然后我们连接这些点,会发现在同样的采样频率下,对频率不同的信号进行采样得到的效果天差地别,例如最上面的信号$f_1(x)$,频率比较小,那我们连接采样点后的折线就和原来的信号非常接近,但是越往下信号频率越大,我们用同样的频率采样得到的折线,就和原来的信号差别越大,$f_4(x)$和$f_5(x)$的折线已经和原信号完全不同了。

再进一步说,如下图所示:

image-20220306194603851

对频率相差很大的两个信号进行同频率采样,我们得到的折线是完全一样的,那我们就无法分辨原来的信号长什么样,尤其如果一个信号是高频信号,如果我们用低频采样,就会得到和原来信号完全不同的结果,这就是走样产生的原因,现在回过头来看开头说的,走样产生的原因就是采样速度跟不上信号变化的速度,是不是就完全理解了。但是这还不够,我们继续。

开头说的另一句话,采样频率小于信号频率,导致采样后信号在频域发生了交错、重叠,这又是什么意思呢。

刚才对于采样的分析,完全是在时域下分析的,那么采样在频域的表现是什么样的呢?

采样在频域表现为信号的重复。如下图:

image-20220306195205688

左边一列是时域信号,右边是它对应的频域信号。图(a)是时域中的一个信号,图(b)是它在频域中的样子,图(c)是采样信号,也叫做冲击信号,图(d)是冲击信号在频域中的样子,图(e)就是用冲击信号对原信号进行采样的过程,冲击的点就取原信号对应的值,图(f)就是这个采样过程在频域中得到的结果,可以看到,每采样一个点,都相当于复制了原信号在频域中的内容,所以采样在频域中表现为信号的重复。

而且采样越密集,在频域中信号间隔越大,如下图所示:

image-20220306195811399

上面的图是使用密集采样得到的结果,采样越密集,频域中间隔越大,所以信号没有发生重叠交错,下面的图是稀疏采样,采样越稀疏,频域间隔越小,也就发生了重叠交错,自然也就产生了走样

到这里,我们差不多搞明白了产生走样的原因,现在回到光栅化上面来,光栅化的过程就是采样的过程,因为我们的像素点相对于三角形来说没有那么密集,所以相当于进行了稀疏采样,也就产生了走样,那么如何来进行反走样呢?

根据上面的理论,只要我们不进行稀疏采样,使采样尽量密集就可以了。

3 滤波

在讨论反走样之前还要了解一个概念,滤波,滤波在数字图像处理和信号处理中都很重要,图像也可以抽象为信号,所以本质都是一样的,那么滤波的意义也就是一样的,对于信号来说,滤波的作用就是去掉信号中特定频率的内容

比如一张图片和它在频域中的图像:

image-20220306200708255

要说明的是,时域到频域的转换,默认信号是周期性的,对于图片来说,就是把一张图片水平和垂直重复的拼在一起,不停的重复这张图片,然后就可以转换到频域中。频域图中越靠近中心的越低频,越发散的越高频。所谓高频,就是指图像中像素变化剧烈的部分,比如边缘。所以频域图中有一个很明显的“十”字的形状,就是图片重复拼接的时候产生的边缘高频信号,其他高频信号就是图像中的边缘。可以看到图片中大部分都集中在低频。

现在我们对图片进行一个边缘提取:

image-20220306201235273

此时图片只剩下了边缘,对应的频域图中低频信号全部被滤掉了,只剩下了高频信号,这种滤波也就是所谓的高通滤波,只允许高频信号通过。

如果我们对图片的边缘进行平滑处理,也就是给图片加模糊:

image-20220306201404735

显然,高频信号都被滤掉了,模糊是低通滤波

如果我们想要某一特定频率的信号,那就可以进行特定的滤波:

image-20220306201511142

这样就得到了图像在某一特定频率下的特征。

搞清了滤波的作用,滤波的过程实际上就是卷积的过程,无论信号还是图像,滤波就是卷积,关于图像卷积就不赘述了。

但还是要特别说明两个重要的性质:

  • 时域中的卷积,相当于频域中的乘积,时域中的乘积相当于频域中的卷积。这是一个非常神奇的性质,利用这个性质,我们对图像卷积时,可以把图像和卷积核转到频域进行乘积,得到的结果再转回时域,就得到卷积后的图片了。

image-20220306201946817

  • 卷积核越大,频率越低,所以我们用越大的模糊核去模糊一张图片时,模糊效果越严重,图片剩下的信号频率越低

image-20220306202316849

image-20220306202325681

4 反走样原理

有了滤波,我们再回顾刚才产生走样的原因:

image-20220306195811399

发生重叠的部分正是信号的高频部分,那我们通过滤波,把高频部分滤掉,再进行采样,就不会重叠了。

image-20220306202657939

对应到图像上,模糊就是低通滤波,所以我们先对图形模糊,再进行采样,这样的光栅化就可以避免走样,这就是反走样

image-20220306202815314

试试效果,这是没有反走样的效果:

image-20220306202929823

这是反走样的效果:

image-20220306203002692

锯齿有了明显改善,更明显的对比:

image-20220306203030537

那么我们怎么进行模糊呢,反走样具体要如何实现?

我们对每一个像素,进行一个1像素的滤波,所谓1像素的滤波是指,每个像素的像素值等于三角形在像素内覆盖的加权平均值。

image-20220306203643492

但这个滤波对于计算机是很难实现的,我们无法准确的判断三角形覆盖了这个像素百分之多少的位置,因此就出现了各种近似的方法,这些方法基本达到了我们理想中反走样的目的。

5 反走样方法

根据上面的理论分析,产生走样是因为我们的像素不够密集,那只要像素足够密集,进行足够密集的采样,走样就可以得到缓解,所以屏幕分辨率越高,显示越清晰。当然,不停提升屏幕分辨率是不现实的,因此需要进行软件反走样,游戏领域中,反走样(AA)最具代表性、应用最广泛的方法有以下几种。

5.1 多重采样反走样-MSAA

MSAA把一个像素分为多个亚像素,然后根据亚像素有多少在三角形内部,对这个像素的像素值做相应比例的改变。

image-20220306204044700

image-20220306204102154

image-20220306204111526

image-20220306204121336

这里给出最简单的MSAA的C++版本代码,在上一节的判断每个像素是否在三角形内部的函数的基础上稍作修改即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static bool insideTriangle(float x, float y, const Vector3f* _v)
{
Vector3f p(float(x), float(y), 1);
float signofTrig = (_v[1].x() - _v[0].x()) * (_v[2].y() - _v[0].y()) - (_v[1].y() - _v[0].y()) * (_v[2].x() - _v[0].x());
float signOfAB = (_v[1].x() - _v[0].x()) * (p.y() - _v[0].y()) - (_v[1].y() - _v[0].y()) * (p.x() - _v[0].x());
float signOfCA = (_v[0].x() - _v[2].x()) * (p.y() - _v[2].y()) - (_v[0].y() - _v[2].y()) * (p.x() - _v[2].x());
float signOfBC = (_v[2].x() - _v[1].x()) * (p.y() - _v[2].y()) - (_v[2].y() - _v[1].y()) * (p.x() - _v[2].x());
bool d1 = (signOfAB * signofTrig > 0);
bool d2 = (signOfCA * signofTrig > 0);
bool d3 = (signOfBC * signofTrig > 0);
return d1 && d2 && d3;
}

float MSAA(int x, int y, const Vector3f* _v)
{
float count = 0;
count += insideTriangle(x + 0.25, y + 0.25, _v) ? 1 : 0;
count += insideTriangle(x + 0.25, y + 0.75, _v) ? 1 : 0;
count += insideTriangle(x + 0.75, y + 0.25, _v) ? 1 : 0;
count += insideTriangle(x + 0.75, y + 0.75, _v) ? 1 : 0;
return float(count / 4.0);
}

看看效果,左边是没使用反走样生成的图形,右边是MSAA的效果:

image-20220308210015557

优点:

  • 对几何反走样效果良好
  • 不支持延迟渲染(关于延迟渲染之后会详细说)
  • 画面更清晰

缺点:

  • 像素的亮度与覆盖区域的面积成正比,而与覆盖区域落在像素内的位置无关,这仍会导致锯齿效应;
  • 只能消除几何走样,无法解决高光区域的着色走样
  • 静态画面表现良好,时域上不稳定
5.2 时域反走样-Temporal AA

Temporal AA严格来说不是在光栅化时进行处理,而是以后处理的方式进行反走样,这也是目前比较主流的方式。Temporal AA 是近年来商业引擎最流行的几种反走样算法之一。

简单来说,Temporal AA是基于历史帧缓冲,从历史帧中采样,在像素范围内进行加权抖动。相机抖动是 TAA 能够反走样最本质原因。相机随时间抖动过程中,引入了额外的子像素信息,对子像素的融合,使我们在时域上获得超采样的效果。具体来说,对于每一帧游戏画面,相机抖动 0.x~1 像素。那么在时域上,我们可以得到当前像素的多个子像素信息。时域上进行加权融合后,得到当前像素的最终颜色。

具体的原理可以查看TAA 反走样算法研究 | 时域超采样技术

优点:

  • 效果好,开销小
  • 时域稳定性强
  • 支持延迟渲染

缺点:

  • 随着历史颜色的累积,会导致不可绝对消除的模糊(运动模糊),尤其在移动过快的镜头或物体情况下,会导致重影现象
  • 需要额外内存开销,保存历史信息
  • 不能应对半透明物体
5.3 快速近似反走样-FXAA

最简单的、高效率的抗锯齿方式,对图形边缘进行后处理。先进行边缘检测,然后通过提取边缘像素周围的颜色信息,通过混合颜色信息来消除高对比所产生的锯齿,其实就是对图像边缘进行柔化。

优点:

  • 性能开销极小

缺点:

  • 画面会更模糊
  • 对像绒毛一类的复杂物体效果不好
5.4 深度学习超采样-DLSS

利用神经网络的重建能力进行图像处理。DLSS 背后使用的技术是 Recurrent CNN,递归神经网络与卷积神经网络的一种结合。因此他能结合时域上的信息保证时域稳定性,即像素具有帧间连贯性,不会出现过多闪烁、跳变现象。其次,结合神经网络的强大图形重建能力,DLSS 能够分别对几何边缘以及着色进行重建。

优点:

  • DLSS能同时在几何、着色、时域上进行反走样
  • 深度学习解决了TAA种画面模糊、透明、遮挡、残影等现象

缺点:

  • 性能开销高,仅仅反走样就占用了画面 20% 的渲染时长
---- 本文结束 知识又增加了亿点点!----

文章版权声明 1、博客名称:LycTechStack
2、博客网址:https://lz328.github.io/LycTechStack.github.io/
3、本博客的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系博主进行删除处理。
4、本博客所有文章版权归博主所有,如需转载请标明出处。