光栅化中的走样,频域与滤波
采样(Sampling)
何为采样?是指从总体中抽取个体或者样品的过程。用程序的思维来解释,就是给定一个连续函数,然后我们通过不同的输入来获取函数的值,如对于函数f(x),我们输入x=1,x=2,x=3…,求以上y的值。采样就是把一个连续的函数给离散化的过程。
在光栅化过程中取每个像素的中心点,其实就是在采样。
锯齿产生的原因:
简单的说就是采样频率不够高。
物体在三维世界中是连续的,而像素是离散的,光栅化的过程就是把连续的物体离散的表示出来。但是会出现采样不足等原因,同时像素本身就有一定的大小,这样就导致有的采样点在三角形内,有的不在,显示出凹凸不平的锯齿状。我们用有限离散的像素点去逼近连续的三角形,自然会出现走样现象。
采样频率
采样频率可以理解为抽取样本的间隔,例如上面我们采样的x=1,x=2.x=3。间隔为1。如果改成x=1,x=3,那么间隔就是2,代表频率变慢。
应用到光栅化里面,我们可以把三角形当做无数的点组成,而我们屏幕又是由一个个的像素组成,这些像素的中心点对应到三角形中的某些点,等同于所有点中抽取出的样本。那么采样的间隔自然是我们像素的实际物理大小。因此若屏幕大小不变,分辨率变高(即像素越多,像素的中心点物理间隔越小),采样频率越高。
走样(Alising)
走样,什么是走样?
汉语字典给的定义是:变样,失去原有的样子。这个就很好理解了,我们光栅化一个三角形,得到的结果因为锯齿导致它变样了(如下图),那么锯齿就是我们走样的一种体现。
图形学中的采样瑕疵(Sampling Artifacts in Computer Graphics)
锯齿(Jaggles)
摩尔纹(Moire)
摩尔纹是一种会使图片出现彩色的高频率不规则的条纹,也是采样所造成的问题之一。我们生活中使用手机拍摄显示器屏幕的时候,拍出的照片出现的扭曲的纹理,就是摩尔纹。
我们要在图像上进行复现也很简单,我们把屏幕上一张大小为MN的图去掉所有奇数行和所有奇数列的像素,然后把剩下的图像拼接到一起(相当于缩小了照片),然后依旧按照原始的mn大小进行显示,就会出现摩尔纹。例如下图:
用采样理解起来也很简单,例如我们一个22的4个像素,原本采样4个点,四个点颜色不尽相同,去掉奇数行奇数列后只剩右上角的1个像素,然后又要显示成22,那么就是四个像素都变成了一个颜色,也就是右上角那个像素的颜色,等同于只采样了右上角1个点。
可以理解成,我们之前每个像素采样一次,现在每4个像素采样一次,使得采样频率降低了,这种情况称之为欠采样。
车轮效应(Wagon wheel effect)
在生活中,有些高速行驶的汽车,但是我们的眼睛看他的车轮却反而像是在倒着转,这也是采样所导致的,我们称之为车轮效应。
造成这个问题的原因是人眼在时间上的采样,跟不上运动的速度,时间上采样慢也就是采样频率低。
造成走样的原因:
从上述种种的Sampling Artifacts 造成的走样来看,都是由于采样频率低(采样慢)跟不上物体的实际变化速度导致的。更专业的说法是,信号(signals)变化的太快(高频,high frequency),而我们的采样太慢。
这里的信号变化怎么理解呢?
车轮效应我们很好理解,信号变化快就是车轮转的太快了,随着时间的变换,车轮的旋转角度发生了变换。但是对于光栅化过程,怎么理解这个信号变化呢?
我们前面讲过光栅化是把真实的场景映射到屏幕上的过程。我们可以把真实的场景内容看作是一个由 mn 个像素组成的(忽略z轴),m和n趋向于无限大,为了方便理解和描述,我们称它们为原始像素。那么我们的光栅化等于把这mn个像素进行缩减,使其显示在 x*y 个像素的屏幕上,x和y远远小于m和n,我们称之为屏幕像素。简单来说,我们等同于把一幅高精度的图像(现实)变成了一幅低精度的图像(图片),这里的信号,我们可以称之为图像信号,信号变化就是原始像素中每个像素间的颜色变化,颜色本质就是一串信息,例如RGB值。同时也说明图像的信号变化并不是随着时间而变换,而是随着空间中x和y的值的变化而变化,这种我们称之为空间域(Spatial Domin)。那么信号变化快就是指原始像素间颜色发生了骤变,例如白色变成黑色等,反之两个像素之间颜色没有发生什么变化就是信号变化慢。
这也就很容易理解走样的说法了,如果信号变化慢,即周边一群像素的颜色相似,那么我们采样慢,即只取其中一点,就不会造成什么问题。但是若信号变化快,周边一群像素都是五花八门的,我们采样慢就会造成走样。这也是锯齿为什么都发生在三角形边缘的原因,因为在三角形内部信号变化慢,但是在三角形边缘信号变化往往会骤增。
一般采样时,要遵循香农-奈奎斯特采样定理
采样频率≥原始信号频谱的最高频率×2,信号可以还原。
反走样(Antialiasing)
既然我们的采样结果走样了,那我们就要反走样。
平常玩游戏的时候就有抗锯齿操作,如古墓丽影。
抗锯齿方法为SMAA4x,画面那是相当的清晰,之后会发文对SMAA进行详解和实现。
模糊操作(Blurring)
模糊就是做一个滤波,也就是卷积,我们可以对图像进行滤波,进行抗锯齿。
但是这就分,先采样后模糊,和先模糊后采样。
事实证明先采样后模糊没啥效果,而先模糊后采样效果比较好。
信号与波形
在前面,我们说过我们采样,采样的是信号(也可以称作是函数),而波形则是信号的表现形式。
我们来看看最简单的两个信号波,分别是正弦波 sin(2πx) 和余弦波 cos(2πx),如下图:
这两个波是在时域中定义的,所谓时域,就是信号随时间的变换而变换。图中的x轴代表着时间。
频率与采样
前面我们说到,走样是因为信号变化快(频率高),而采样的频率低导致的。那么我们现在来通过波形来更直观的看一下走样现象,如下图,我们有三个不同频率的信号(从上到下频率变高),然后我们每间隔 x=1 进行采样一次,就会得到图中的各个点。
采完样后,我们自然是要根据我们的采样来恢复原始信号。也就是我们把这些采样点进行连线,就是我们能够恢复出来的原始信号,如下图。
结果很明显,频率慢的信号和我们的采样恢复结果基本一致,但是频率高的信号就不行了,就出现了严重的走样现象。
同时还可能出现信号完全不同,但是采样结果却完全相同的情况,例如下图,黑色和蓝色波形分别代表着两种不同的信号,但是它们的采样结果(图中的白点)却相同。
以上种种问题都是采样频率不足而导致的。
频域(frequency domain)
前面我们说到时域,是信号在不同时间的状态,即 函数 f(x) 中,x值代表着时间。空间域,是信号在不同位置的状态,即 函数 f(x) 中,x值代表着偏移量。顾名思义,频域就是指在不同的频率下观察信号,即 函数 f(x) 中,x值代表着频率。画成坐标系的话,横坐标代表着频率,画出来的图形我们称之为频谱。
那么在频域上观察有什么好处呢?这个我们在后面介绍。
既然我们要在频率上观察信号,那么就要把原来时域或者空间域的函数转换成频域的,那么如何转换呢?傅里叶变换可以帮助我们。
傅里叶变换(Fourier Transform)
傅里叶级数展开
生活中的很多信号,并不像正弦波余弦波这样简单清晰,例如我们前面所说的图像信号。
就像我们前面处理物体表面一样,把一个表面划分成多个不规则的三角形。对于信号,我们也可以沿用这样的思想,将其进行拆解。法国数学家傅里叶发现,任何周期函数都可以用常数与正弦函数(和/或)余弦函数构成的无穷级数来表示,这种展开方式我们称为傅里叶级数展开。
https://www.zhihu.com/question/279808864/answer/552617806
https://zhuanlan.zhihu.com/p/363284887
图像转换成频谱
前面我们说过,一个时域或空间域上连续重复的信号可以通过傅里叶变换变到频域上。而图像本身就属于空间域上的一种信号,因此我们也可以通过傅里叶变换使其变成频域的表现形式。如下图,左边是一张图像,右边是傅里叶变换后的频谱。
对于右图的解读:图像中心代表着低频率区域,越往外频率越高,图像周围即是高频率区域。同时亮度越亮,说明该频率的信息越多,例如图中中心最亮,往外越来越暗,说明原始图像中低频信息最多,高频信息偏少。而关于图像的频率信息也就是像素间颜色变化,颜色变化越大则频率越高。同时我们也可以通过逆傅里叶变化,将右图变回左图。
我们说重复的信号才能被变换,但是图像的信号怎么看也不像是连续信号。这里我们会把图像做一个平铺处理,也就是无数张相同的图片连接在一起,这样每一张图片就是一个重复的信号。
我们可以把之前傅里叶级数展开例子中的每个凹凸形状的信号当做是一张图片,无数个重复的凹凸形状就是无数张图片。每个凹凸形状通过级数展开变成了无数个频率不同的正弦或余弦函数,那么每张图片也可以展开成不同频率的信号,这些信号就会根据频率,显示在我们的频谱中。
并且频谱中水平和竖直方向各有一条很明显的线就是由于平铺导致的,因此是图片连接在一起,因此图片的右边界连接的是图片的左边界,下边界连接的是上边界,此处往往会导致出现大量的高频信息。
滤波(Filter)
滤波,顾名思义,过滤信号波,根据什么过滤呢?就是我们的频率。
高通滤波(High-Pass Filter)
高通的意思就是高频可以通过,高通滤波也就是过滤掉低频的信号。如下图,我们去掉低频信号后得到的图像。
高通滤波是高的可以通过的滤波,只过滤低频,因此中间被挖空。
可以看出,过滤掉低频信号后,我们能看清的就剩下人物的边界了。这也是因为往往在边界处,会发生较大的颜色变化。
低通滤波(Low-Pass Filter)
有高通滤波自然也有低通滤波,即过滤掉高频的信号,如下图,图像变得模糊了,因为原本清晰的边界都被过滤掉了。
带通滤波(Band-Pass Filter)
带通滤波,允许特定频段的波通过。例如下两图
卷积(Convolution)
从上面的例子中可以发现,所谓滤波的过滤似乎并不是对于某些频率的信号直接抛弃不要,而是把它变成了一个新的信号(低通滤波比较明显,因为如果是直接抛弃某些信号,那显示出来的就是黑色)。并且原本我们一个m*n的信号通过滤波过滤后,依旧还是一个m * n的信号(图像大小没有发生变换)。这是因为滤波的本质是将信号与滤波器在时域上进行卷积的操作。
何为滤波器?我们可以把滤波器看作是一个固定大小的信号,可以是13,33等等。然后滤波器中的每个信号都有一个对应的数值,滤波器也被称之为卷积核。
何为卷积操作?假设我们有一段信号,要使用mn大小的滤波器进行滤波。当对其中第x个信号进行滤波时,实际上是以第x信号为中心点,取其周边mn个信号,与滤波器中的各个值一一对应相乘,然后相乘后值全部相加,得到的新值便是第x个信号进过滤波后的值。
如下图,我们将一段信号使用31的滤波器进行滤波操作:
如图,我们将第二个信号进行 滤波/卷积 操作,因为滤波器是31的,因此我们以第二个信号为中心,取3*1的信号,即第1-3的信号。然后与滤波器各个值相乘然后相加,得到的结果如图。
那么若是第一个或者最后一个信号怎么卷积操作呢?例如第一个信号,左边并没有信号去对应滤波器的第一个值了。对于缺省的信号我们自动补零即可,例如第一个信号取得的3*1的信号值分别为0,1,3,然后进行卷积操作即可,得到的结果为:(0 * 1/4) + (1 * 1/2) + (3 * 1/4) = 1.25。
图像和卷积(Convolution)
了解了卷积操作后,也就很容易理解前面对图像做滤波操作的原理了,即新图像中第(x,y)像素的值,是原本图像中第(x,y)像素和其周边几个像素与滤波器进行卷积操作的值。
如下图,我们把图像和一个 3*3 ,每个值都是 1/9 的滤波器进行卷积操作,得到的就是一张变模糊的图像。因为约等于把几个像素求了个平均,这样颜色偏深的像素会变浅,偏浅的像素会变深,从而相邻的像素变换变小,频率变低。
因此上诉滤波器就属于一种低通滤波器,滤波器中所有值的和为1。
同时我们也可推导出,若我们使用 10*10 的滤波器,每个信号的值为 0.01,那么图像就会变得更加模糊,因为更多的像素参与了卷积操作。
这样也很容易理解其他的一些滤波器,例如高通滤波器如下:
可以发现该滤波器中所有值的和为0。也就是说颜色越相近(低频)的几个像素,得到的结果越接近于0,即黑色。
关于其他滤波器可以参考这篇文章:
图像处理基本知识,理解滤波十分有用
频域和卷积
前面我们可以发现卷积操作是在时域或者空间域上进行的,那么对应到频域上,是怎么计算的呢?如下图:
我们前面说过滤波器也可以看作是一个信号,因此它也可通过傅里叶变换变到频域上,例如上图的低通滤波器,由于每个信号一样,因此都是低频信号,得到的频域自然是中间亮外面黑。然后将它与傅里叶变换后的图片相乘,即可得到对应的结果。
因此时域上的卷积相当于频域上的乘积,同时反过来也是成立的,即时域上的乘积相当于频域上的卷积。
采样与频域
注:这块内容比较抽象,同时需要较多的信号学相关的知识,我们这边只做结果论。也就是知道什么操作会得到什么结果即可,具体推导过程暂时不做深究,因为本人作为萌新也不太懂,希望有大神指点。
冲激函数
如下图,前面我们说了信号就是一个函数,而采样就是在该函数的各个点取值,如下图,虚线便是我们的取样点。
而对于这样的采样方式,我们可以理解为,有一个函数,它只在一些特定的地方有值(需要采样),而在其他地方的值都为0(不需要采样),这样的函数我们称之为冲激函数。
例如最简单的冲激函数即为:
代表着 t=0 时有值(无穷大)而 t 为其他值时,结果都为0。
采样与函数
前面说了信号是一个函数,采样是一个冲激函数,那么采样后的结果在这里我们就可以理解成这两个函数相乘所得到的结果。
如图,a函数代表信号,c函数为冲激函数代表采样,两个相乘得到e函数,即采样结果,也就是信号上各个点的结果。上述就是在时域上的采样原理。
采样与频域
那么采样在频域上是一个怎么样的过程呢?我们知道一个信号可以通过傅里叶变换变到频率上,如下图
a是我们时域上的信号,b是我们频域上的信号。前面我们说过频域是在频率上观察信号,因此b中的坐标系的横轴代表着频率,同时越往外代表频率越高。从b中我们可以看出,信号a大部分还是低频的信号。
接着我们的冲击函数同样可以做一个傅里叶变换,得到的截图如下,c为我们的冲激函数,d为该函数的频谱。(具体为什么变成这样我们这里暂时不做过多的推理)
从中可以看出,将冲激函数傅里叶变换后以及还是冲激函数,只不过间隔发生了变化。
前面我们说过在时域上是两个函数相乘,同时前面我们也学到了时域上的乘积等于频域上的卷积,因此在频域上,我们要把b和d进行卷积,得到的结果如下,e为a和c在时域的乘积结果,f为b和d频域的卷积结果。
可以发现,得到的频域结果就是把原本频域的信号进行重复,因此可以得到结论采样就是在重复信号的频谱。
频谱和走样
从前面我们知道采样是根据冲击函数的频谱间隔在重复信号的频谱,那
么就会存着这么一个问题,冲击函数的间隔小于频谱的大小,如下图:
可以发现,这种情况就会造成一部分的频谱重叠,这就是我们所谓的走样现象。因此在频域角度上,走样就是频谱重复时发生了混合。而重叠的这部分就是我们的高频信号,因为前面b中我们可以看出,一个信号的频谱图外面代表着高频信号,而重叠部分就是外面的这部分。
频谱和反走样
那么解决方法是什么呢?最简单的自然是增加频谱中冲激函数的间隔,使其避免重叠现象。通过前面的学习,我们知道采样频率越高,即时域上的冲激函数间隔越小,走样越少,因此我们也可推出,冲激函数在时域间隔越小,在频域上则间隔越大。
当然了,增加采样频率是最容易理解的解决办法,但并不是最简单的,因为例如一个固定分辨率的屏幕,我们没法更改他的分辨率,即采样频率。那么应该怎么做呢?此时我们可以去除掉这些会被重叠的信号,如下图:
我们把原始信号的频谱的两头(即高频信号)去掉,然后依旧按原来的间隔排列,就会发现重叠的部分消失了。
这也就解释了为什么前面所说的先做模糊操作再采样可以实现反走样,因为模糊操作等于做了个低通滤波,即去掉了高频的信号,减少了信号重叠的情况。
同时先采样在模糊的话,在频域上,采样后频谱已经混叠了,此时再去掉高频信号,等于是把混叠后的结果去掉两端(并不是每一段去掉两端),所以是错误的。
模糊操作应该怎么做?
既然反走样可以通过先模糊再采样的操作来实现,而采样的操作在上一篇我们已经知道如何实现了,那么模糊操作应该如何实现呢?
前面我们说,采样可以看作是把由无数个很细小原始像素组成的真实场景,通过取特殊点的方式,把它映射到有固定数量固定大小的屏幕像素上。因为是先模糊后采样,那么就等于我们要对原始像素的图像信息进行模糊操作。
而模糊操作我们前面也说了,它属于低通滤波,即是一个 nn 大小且每个信号值为 1/n² 的滤波器,也就是说一个原始像素模糊后的值,是它和它周边nn个像素的值,求个平均。
我们知道一个屏幕像素会对应很多很多的原始像素(设mm个),那么我们设想下,如果把一个屏幕像素所对应的所有原始像素的值求个平均,那么得到的这个值,不就是中心的那个原始像素,与mm的滤波器做一次模糊操作的值么。
也就是说我们的模糊操作,只需要把一个屏幕像素内的所有颜色求平均即可,如下图
第一行即是未做处理时,屏幕像素所对应的原始像素的内容,这些屏幕像素可能在三角形的边界,也可能在三角形内部。第二行即是该屏幕像素内所有颜色取完平均后填充整个像素的效果。
疑问1:前面说了模糊操作(卷积)是取当前像素和他周边像素的平均值,一个屏幕像素内,例如其左下角(0,0)对应的原始像素的卷积结果应该不等于中心点(0.5,0.5)对应的原始像素的卷积结果,因为他们周边的原始像素只有四分之一是共同的。可是上图中明显把屏幕像素所对应的所有原始像素颜色都变成了中心点的卷积结果了,这不是错误的么?
严格意义来说,这个模糊操作确实存在问题,但是结合到后面我们的采样过程,就会发现可以这么操作。因为我们的采样过程只采样屏幕像素的中心点所对应的原始像素,所以你屏幕像素其他点到底是什么颜色,我们在采样过程中压根不关心。然后会根据中心点的颜色去填充整个屏幕像素,实际上现在整个屏幕像素就已经都是中心点的颜色了。因此做完上面的操作我们甚至等于完成了采样的操作。
疑问2:例如上面图片的模糊操作中,怎么算出一个屏幕像素内的颜色平均值?也就是怎么通过图片中的第一行得到第二行的结果
当然了,肯定不能通过肉眼观察,例如你看第一行第二个图,黑色白色各占了一半,那么平均值肯定是0.5。解决方法就是接下来我们要学的Supersampling的概念。
疑问3:前面我们说了,先模糊再采样,但是模糊操作后,首先这个三角形形状大小都可能变了,怎么确定像素和它位置关系?其次就算能确定是否在三角形内,三角形的颜色也不是单一的一个颜色了(边缘因为模糊操作颜色变淡),那怎么采样像素中心点的颜色(我们之前采样颜色是通过采样点和三角形的位置关系来确定的)?
其实在疑问1的最后一句提到了,做完模糊后,我们不需要再采样了,因此也就不存在疑问3的操作了。
抗锯齿方法(预演)
SSAA(Supersampling Anti-Aliasing)
拿4xSSAA举例子,假设最终屏幕输出的分辨率是800x600, 4xSSAA就会先渲染到一个分辨率1600x1200的buffer上,然后再直接把这个放大4倍的buffer下采样致800x600.这种做法在数学上是最完美的抗锯齿。但是劣势也很明显,光栅化和着色的计算负荷都比原来多了4倍,render target的大小也涨了4倍。
MSAA(MultiSampling Anti-Aliasing)
只是在光栅化阶段,判断一个三角形是否被像素覆盖的时候会计算多个覆盖样本(Coverage sample),但是在fragment shader着色阶段计算像素颜色的时候每个像素还是只计算一次。例如下图是4xMSAA,三角形只覆盖了4个coverage sample中的2个。所以这个三角形需要生成一个fragment在pixel shader里着色,只不过生成的fragment还是在像素中央(位置,法线等信息插值到像素中央)然后只运行一次fragment shader,最后得到的结果在resolve阶段会乘以0.5,因为这个三角形只cover了一半的sample。现代所有GPU都在硬件上实现了这个算法,而且在shading的运算量远大于光栅化的今天,这个方法远比SSAA快很多。顺便提一下之前NV的CSAA,它就是更进一步的把coverage sample和depth,stencil test分开了。
问题是和延迟渲染不兼容。
FXAA(Fast Approximate Anti-Aliasing)
先得到带有锯齿的图,然后通过一些图像匹配的方法找到这些锯齿边界,然后将这些边界换成没有锯齿的边界,属于图像的后期处理。
SMAA(Subpixel Morphological Anti-Aliasing)
SMAA 是由 CryEngine 开发的更注重效果的后处理抗锯齿方案,当然其效率在大多数移动平台上也是很难被接受的。
它和FXAA的最大区别是会更具边缘的形状选择不同的周围像素进行模糊,尽量还原出合理的SubPixels。和FXAA相比,它不仅本像素是不是锯齿像素,还会关心本周围像素周围的像素,从而推断出该像素处于那种边缘,应该取哪个方向的周围像素来做混合消除这个锯齿像素。所以,SMAA的提取边缘像素这部操作和混合操作是无法在一个Pass中完成的,因为混合时需要边缘提取的计算结果.
SMAA 1x 由以下三个 Pass 组成:
- 边缘检测
SMAA在提取边缘时会严格区分边缘的形状, - 低质量的边缘提取和高质量的边缘提取结果会有很大的差别。所以 SMAA 在低质量(SMAA 1x)的设定下效果反而不如同等级的 FXAA。
TAA (Temporal Anti-Aliasing)
复用上一帧的数据。
将采样点从单帧分布到多个帧上,使得每一帧并不需要多次采样增加计算量。