在 Transformation 部分,我们已经可以把一个三维空间里的三角形投影到平面上。但怎么把一个纯色三角形绘制到屏幕上呢?我们知道屏幕由大量的像素点组成,那么怎么确定每个像素点是什么颜色呢?
我们通过采样给每个像素点涂上颜色。我们可以检查每个像素的中心点是否在这个三角形内部,如果在则涂上三角形的颜色。虽然这样绘制的三角形会有锯齿,效果一般,但基本思想就是这样。
那么如果有多个三角形,而且他们之间有遮挡关系呢?这就需要 Z-Buffer 出场了。
Z-Buffer
Z-Buffer 的算法如下面的伪代码所示。思路是对每个像素所在的所有三角形,取 z 值最小的三角形的颜色作为这个像素的颜色。
foreach (Triangle triangle in triangles) {
foreach (Vector3 sample in triangle.GetSamples()) {
// z 值越小显示越靠前
if (sample.z < zBuffer[sample.x, sample.y]) {
frameBuffer[sample.x, sample.y] = sample.rgb;
zBuffer[sample.x, sample.y] = sample.z;
}
}
}
这也有良好的并行性,因为三角形的遍历顺序和最终结果无关,像素的绘制顺序也和最终结果无关。
虎书还提到我们会将 $z$ 值映射到 $[0, B-1]$,用整数存储 $z$ 值,但其实这是现在不再使用的方法,我们现在通常使用 24 位或 32 位的浮点数来存储深度值。(怪不得我查了好久资料都没查到“整数映射”的具体代码,原来早就不用了!!!)
走样现象
我们之前说过,简单的采样会出现锯齿,如下图所示。
锯齿、摩尔纹之类的采样图像与原图不符的现象被统称为走样(Aliasing)现象,走样的实质是原图的高频信号被错误采样。为了明确什么是”高频信号”并找到缓解走样现象的方法,我们先看看一些数学知识。
数学知识
为了不让文章太长,这里我们省略所有的证明。不过所有的证明都并不困难,有空的话可以自己证一下试试。
卷积
我们先来看看三种卷积——离散-离散卷积、连续-连续卷积、离散-连续卷积。下面的 $f_{\rightarrow t}$ 表示将函数 $f$ 向右平移 $t$ 长度得到的新函数:
离散-离散卷积
连续-连续卷积
离散-连续卷积
我们会注意到,卷积可以表示为函数平移后的加权和。
卷积是过会儿会用到的妙妙小工具。
傅里叶级数和傅里叶变换
我们之前提到,走样的实质是原图的高频信号被错误采样。图像作为一个 $R^2\rightarrow \text{RGBA
}$的函数,怎么会有高频和低频之分呢?uh actually☝️🤓 我们处理的绝大多数函数都有频域,这个频域可以通过傅里叶变换得到。
先回顾一下傅里叶级数。熟知在 $[-\frac{T}{2},\frac{T}{2}]$ 内,函数 $f(x)$ 可以表示为
其中
这本质上是函数在闭区间内的正交基展开,这个展开的周期为 $T$.
当 $T \rightarrow \infty$ 时,令 $w_n=nw_0$,$\Delta w=w_n-w_{n-1}=w_0$ 再结合积分的定义,我们就能(不太严谨地)求出
我们可以令 $u=\frac{w}{2\pi}$ 从而去掉积分外面的那个系数
而 $e^{2\pi iux}$ 的系数
就是 $f$ 的傅里叶变换了,它也记作 $\mathcal{F}(f)$.
把 $\hat f$ 代入就得到了逆傅里叶变换
逆傅里叶变换把函数变成了不同频率的三角函数的积分/求和,这就是我们之前所说的“高频信号”和“低频信号”的含义。之后我们会介绍采样导致高频信号丢失的原因,不过在此之前我们先看看傅里叶变换的一些性质。
傅里叶变换的性质
我们列举傅里叶变换的几个常用的性质。
- 如果 $f$ 是实函数,$\hat f$ 是偶函数。
函数和傅里叶变换的平方积分相等
原函数拉长,傅里叶变换收紧
狄拉克脉冲函数和冲激串
狄拉克脉冲函数的定义如下:
我们可以把连续信号的均匀间隔采样表示为冲激串
与原函数 $f$ 的乘积,这里的 $T$ 表示两个采样点之间的间隔。
$s_T$ 的傅里叶变换为
它仍然是一系列狄拉克函数的和。
卷积定理
之前说过,将 $s_T$ 和原函数 $f$ 相乘能获得许多重要的采样性质,我们很快就会讨论他们了,在此之前我们还要补充最后一个知识——卷积定理。
这就是说,时域的卷积对应频域的乘积,频域的乘积对应时域的卷积。
走样的原因
一开始我们就说过,走样的实质是原图的高频信号被错误采样。现在我们的数学工具已经足以分析究竟为什么发生了错误采样,以及如何缓解他们了。
先来看看为什么我们没有正确采样高频信号。
假设我们希望对这样的函数进行采样,通过傅里叶变换我们可以得到其频域(右一)
我们之前说过,可以把连续函数的均匀间隔采样表示为冲激串 $s_{T}(x)=\sum_{n=-\infty}^{\infty}\delta(x-nT)$ 和原函数 $f$ 的乘积,而我们也已经知道冲激串的傅里叶变换还是冲激串
再结合卷积定理 $\mathcal{F}(s_Tf)=\hat s_T * \hat f$,以及“卷积就是函数平移后的加权和”,我们就能推出,采样结果的傅里叶变换就是原函数的傅里叶变换的无穷个复制各按 $\frac{n}{T}$ 平移的和。
由于我们的采样间隔 $T$ 不够小,即采样率 $\frac{1}{T}$ 不够大,所以相邻的两个复制间发生了重叠,导致高频信号和低频信号产生混合,这就引起了走样。而这正是“高频信号被错误采样”的实质。
当我们在光栅化时,我们还把像素填上了颜色,这是在“reconstruction”即重建图像。不过由于在采样时我们已经发生了走样,无论怎么重建都不会有一个非常完美的结果了。
滤波器和走样的缓解方法
为了缓解走样,我们自然就要避免发生重叠。避免发生重叠的思路主要有两种:
- 增加采样率,这样就能让相邻的两个复制距离增大。
- 用滤波器减弱高频信号的强度。
对图像的光栅化来说,增加分辨率就对应前者;在采样前对原图应用各种滤波器就对应后者。
我们当然会问,增加采样率能缓解走样,那增加到多大合适呢?我们希望相邻的两个复制间的距离足够大以至于不发生重叠,这就需要输入信号的最高频率小于采样频率的一半。采样频率的一半也被称为奈奎斯特频率。理论上说,只要奈奎斯特频率高于被采样信号的最高频率,我们就能完美复原原信号。
接下来我们看看几个常见的滤波器,它们常被用于和别的函数做卷积。由于我们知道卷积可以被理解为一种“加权平均”,所以我们希望滤波器都是归一化的。
名称 | 公式 |
---|---|
Box filter(离散) | $a_{\text{box},r}[i] = \begin{cases} 1/(2r + 1) & |i| \le r, \\ 0 & \text{otherwise}. \end{cases}$ |
Box filter(连续) | $f_{\text{box},r}(x) = \begin{cases} 1/(2r) & -r \le x < r, \\ 0 & \text{otherwise}. \end{cases}$ |
Tent filter | $f_{\text{tent}}(x) = \begin{cases} 1 - |x| & |x| < 1, \\ 0 & \text{otherwise}; \end{cases}$ |
Gaussian filter | $f_{g, \sigma}(x) = \frac{1}{\sigma\sqrt{2\pi}}e^{-x^2/2\sigma^2}$ |
名称 | 公式 |
---|---|
B-Spline Cubic Filter | $f_B(x) = \frac{1}{6} \begin{cases} -3(1 - |x|)^3 + 3(1 - |x|)^2 + 3(1 - |x|) + 1 & -1 \le x \le 1, \\ (2 - |x|)^3 & 1 \le |x| \le 2, \\ 0 & \text{otherwise}. \end{cases}$ |
Catmull-Rom Cubic Filter | $f_C(x) = \frac{1}{2} \begin{cases} -3(1 - |x|)^3 + 4(1 - |x|)^2 + (1 - |x|) & -1 \le x \le 1, \\ (2 - |x|)^3 - (2 - |x|)^2 & 1 \le |x| \le 2, \\ 0 & \text{otherwise}. \end{cases}$ |
Mitchell-Netravali Cubic Filter | $f_M(x) = \frac{1}{3}f_B(x) + \frac{2}{3}f_C(x) \\ = \frac{1}{18} \begin{cases} -21(1 - |x|)^3 + 27(1 - |x|)^2 + 9(1 - |x|) + 1 & -1 \le x \le 1, \\ 7(2 - |x|)^3 - 6(2 - |x|)^2 & 1 \le |x| \le 2, \\ 0 & \text{otherwise}. \end{cases}$ |
除了过滤高频信号之外,滤波器还在重建图像时发挥着重要作用。还记得吗,离散-连续卷积能把一系列离散点变成一个连续函数。在根据采样点重建图像时,我们基本就是在做采样点和滤波器的卷积。在光栅化的“把每个像素点的中心采样颜色填到像素点上”这一步中,我们就是在将采样点和 Box filter 做卷积。
总而言之,为了缓解走样,我们可以以图像细节丰富度为代价,先用低通滤波器滤波,再采样+重建。当然,如果我们有更高分辨率的屏幕就更好了~
其他小知识
常见滤波器的傅里叶变换
卷积的单位元
任何离散信号和单位脉冲序列做卷积,结果还是原本的离散信号
任何连续信号和狄拉克函数做卷积,结果还是原本的连续信号
二维卷积和二维滤波器
我们这里只给出连续-连续的二维卷积,别的情况都差不多。
对滤波器 $f(x)$,我们简单地定义 $g(x,y)=f(x)f(y)$ 就得到了这个滤波器对应的二维滤波器。一个比较好的性质是,如果 $f$ 是归一化的,那么 $g$ 也是归一化的。
伽马值
显示器的显示亮度关于输入信号不是线性关系。假设输入信号为 $a\in[0,1]$,则有
这里的 $\gamma$ 一般在 $2.2$ 左右。虽然在最开始这是 CRT 显示器的物理特性所致,但有趣的是,人眼也恰好对暗部变化比亮部变化更敏感,所以它现在作为一个刻意的设计保留了下来。
伽马矫正在影像系统中也有极大作用。假设我们在拍摄一个苹果的照片,苹果的物理亮度是 $0.5$,相机忠实地把它记录了下来。而当显示器显示时,就显示出了 $0.5^{2.2}\approx
0.22$ 的物理亮度,这显然不是我们想要的。因此在我们拍摄照片之后,相机的图像处理器就会自动对照片进行伽马矫正。
图像锐化
Unsharp Mask 是一个经典的图像锐化算法。令高斯模糊核为 $G$,冲激函数为 $\delta$,它先提取原始图像的高频信号
再把这些高频信号加入回原图中
综合起来就是
图像缩放
直接对一个像素化的图像采样虽然效率很高,但效果不佳。先用连续的滤波器重建信号,再用低通滤波器过滤高频信号,再采样会得到更好的结果,这被称为 resampling,重采样。