【计算机图形学】GAMES101学习笔记(2):光栅化

​本篇内容对应GAMES101课程的Lecture 5-6、以及Lecture 7的开头,对应[1]的Chapter 7-9。

本文主要参考资料:

[1] S. Marschner, P. Shirley, et al., Fundamentals of Computer Graphics, 4th ed. Boca Raton, FL, USA: CRC Press, 2016.
[2] 郑君里, 应启珩, 杨为理. 信号与系统 (第三版). 北京: 高等教育出版社, 2011.
[3] 傅里叶分析之掐死教程(完整版)更新于2014.06.06
[4] 积分变换(2)——傅里叶变换


在上一篇文章中,我们成功将空间中的各物体在世界空间下的世界坐标转换到了在相机空间下的相机坐标,并把相机空间中的任意一个截锥内的物体投影到了xy-平面上的标准正方形(相机的接收区域) [ − 1 , 1 ] 2 [-1,1]^2 [1,1]2中。我们接下来要做的,就是把接收区域内的点变换到计算机屏幕的对应位置上,这个变换就叫视口变换。这就好比监控,摄像头捕捉到了画面还不够,还得把画面显示在电脑屏幕上人们才能看到它。为此,我们需要先定义什么叫屏幕和像素。

视口变换

计算机图形学中,计算机的屏幕 (screen)被抽象地定义为一个二维数组,数组中的每一个元素称为像素(pixel),数组的大小(即所含像素的个数)称为分辨率(resolution)。

在现阶段,我们认为像素是在真实的屏幕上的一个个小正方形,同一个像素内部具有同一种颜色,这种颜色是红、绿、蓝三色按一定比例的混合。

设屏幕的大小为 w i d t h × h e i g h t width\times height width×height,在 R 2 \mathbb{R}^2 R2上,屏幕覆盖的区域为 [ 0 , w i d t h ] × [ 0 , h e i g h t ] [0,width]\times [0,height] [0,width]×[0,height],这个二维空间称为屏幕空间(screen space);像素覆盖了 1 × 1 1\times 1 1×1的区域,它的编号用 ( x , y ) (x,y) (x,y)表示,其中 x ∈ { 0 , 1 , ⋯   , w i d t h − 1 } x\in \{ 0, 1, \cdots ,width-1 \} x{0,1,,width1} y ∈ { 0 , 1 , ⋯   , h e i g h t − 1 } y\in \{ 0, 1, \cdots ,height-1 \} y{0,1,,height1},它指示了像素的左下角在屏幕空间上的坐标。像素 ( x , y ) (x,y) (x,y)的中心的坐标是 ( x + 0.5 , y + 0.5 ) (x+0.5,y+0.5) (x+0.5,y+0.5)


现在,我们可以很容易地写出从 [ − 1 , 1 ] 2 [-1,1]^2 [1,1]2 [ 0 , w i d t h ] × [ 0 , h e i g h t ] [0,width]\times [0,height] [0,width]×[0,height]的视口变换的矩阵:
M v i e w p o r t = ( w i d t h 2 0 0 w i d t h 2 0 h e i g h t 2 0 h e i g h t 2 0 0 1 0 0 0 0 1 ) . M_{viewport}=\begin{pmatrix} \frac{width}{2} & 0 & 0 & \frac{width}{2} \\ 0 & \frac{height}{2} & 0 & \frac{height}{2} \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}. Mviewport= 2width00002height0000102width2height01 .

当然,当 w i d t h ≠ h e i g h t width\ne height width=height时,这个变换实际上使图像发生了拉伸或压缩。

还需要注意的是,该变换保持z坐标不变。虽然这是一个二维平面上的变换,但z坐标仍然是有意义的,它关系到了物体之间的遮挡关系,而显然该变换保持遮挡关系不变。见本文最后一节“深度缓存”。

光栅化

现在我们已经把物体变换到了屏幕空间中,下一步就是把它们“绘制”到屏幕上。注意到一个问题:计算机的屏幕是“离散”的,它由一个个像素组成,但要屏幕空间中的物体可能非常精细,以至于一个像素覆盖的区域内可能包含很多信息。如何确定每个像素应该呈现什么内容(颜色)呢?这就是光栅化要解决的问题。

准确地说,光栅化(rasterization)指找出屏幕空间中的物体所覆盖的像素、并确定这些像素应该呈现什么颜色,从而实现将物体绘制到屏幕上这一操作。顺便说一句,光栅(raster)的英文来源于德语,指屏幕。

在实际建模中,一个物体的表面往往是由很多多边形组成的,而因为每个边数大于3的多边形都可以分解为若干个三角形,并且三角形具有诸多良好的性质,因此,研究三角形的光栅化具有重要的意义。本文主要围绕其展开。

[1]中还介绍了线段的光栅化方法,文末的补充资料里简述了这个方法,但未进行详细讨论,感兴趣的读者可以自行参阅(需要注意的是[1]的屏幕和像素的位置的定义方式和GAMES101课程中的定义方式不太一样,不过得到的结果是一样的)。

研究三角形的光栅化本质上是确定这样一个函数:输入一个三角形(由其三个顶点表示),输出能够尽可能准确地显示这个三角形的一系列像素的位置及它们的颜色。为此,我们先引入采样的概念。

在这里插入图片描述

采样

采样(sampling)是信号处理中的重要概念,简单来说,就是查询一个函数在某一点处的值。通过采样,可以将一个连续的函数离散化。

光栅化就是一个采样的典型例子:把“连续”的图形在每个像素处采样,得到图形在采样处的值,并以此为基础生成以像素为单元的最终图像。在三角形光栅化的问题中,函数可认为是这样一个二值函数: i n s i d e ( T , x , y ) = { 1 , ( x , y )  在三角形 T 中 , 0 , ( x , y )  不在三角形 T 中 . \mathrm{inside}(T,x,y)=\left\{ \begin{array}{ll} 1,\quad (x,y) \ 在三角形T中, \\ 0, \quad (x,y) \ 不在三角形T中. \end{array} \right. inside(T,x,y)={1,(x,y) 在三角形T,0,(x,y) 不在三角形T.

其中, T T T是屏幕空间中的某个三角形, ( x , y ) (x,y) (x,y)是屏幕空间中的某个点(未必是整数)。我们可以对屏幕上每个像素的中心进行采样,根据采样结果是否为1判断是否要对该像素着以三角形的颜色。当然,如果点在(一个或者多个)三角形的边界上,我们可以不做处理(不着色),也可以特殊处理,现阶段这不是重点。当然,有些图形API会对此类情况进行严格处理。

事实上,采样的应用范围十分广泛。例如,可以对时间(1维)、面积(2维)、方向(2维)、体积(3维)采样,等等。

代码如下:

for (int x = 0; x < width; x++)
	for (int y = 0; y < height; y++)
		image[x][y] = inside(tri, x + 0.5, y + 0.5);

i n s i d e \mathrm{inside} inside这个函数怎么写呢?可以利用向量叉积的性质。已知位于xy-平面内的三角形 A B C ABC ABC的三个顶点和点 P P P的坐标,我们有如下定理: A P → × A B → \overrightarrow{AP}\times \overrightarrow{AB} AP ×AB B P → × B C → \overrightarrow{BP}\times \overrightarrow{BC} BP ×BC C P → × C A → \overrightarrow{CP}\times \overrightarrow{CA} CP ×CA 的z坐标同为正数或同为负数,则 P P P △ A B C \bigtriangleup ABC ABC内,不然的话,若三个叉积的结果有一个为0,则 P P P △ A B C \bigtriangleup ABC ABC的边界上,否则 P P P △ A B C \bigtriangleup ABC ABC外。

在这里插入图片描述

这个定理的数学原理是:在xy-平面内,若点 P P P在向量 A B → \overrightarrow{AB} AB 的左侧(从 A A A B B B看去),则 A P → × A B → \overrightarrow{AP}\times \overrightarrow{AB} AP ×AB 的z坐标小于0;若点 P P P在向量 A B → \overrightarrow{AB} AB 的右侧,则 A P → × A B → \overrightarrow{AP}\times \overrightarrow{AB} AP ×AB 的z坐标大于0。

数学上定义叉积:
已知 R 3 \mathbb{R}^3 R3中向量 α = ( x a , y a , z a ) T \boldsymbol{\alpha}=(x_a,y_a,z_a)^{\mathrm{T}} α=(xa,ya,za)T β = ( x b , y b , z b ) T \boldsymbol{\beta}=(x_b,y_b,z_b)^{\mathrm{T}} β=(xb,yb,zb)T,则定义 α × β = ( y a z b − y b z a z a x b − z b x a x a y b − x b y a ) = ( 0 − z a y a z a 0 − x a − y a x a 0 ) ( x b y b z b ) . \boldsymbol{\alpha}\times \boldsymbol{\beta}=\begin{pmatrix} y_az_b-y_bz_a \\ z_ax_b-z_bx_a \\ x_ay_b-x_by_a \end{pmatrix}=\begin{pmatrix} 0 & -z_a & y_a \\ z_a & 0 & -x_a \\ -y_a & x_a & 0 \end{pmatrix}\begin{pmatrix} x_b \\ y_b \\ z_b \end{pmatrix}. α×β= yazbybzazaxbzbxaxaybxbya = 0zayaza0xayaxa0 xbybzb .

α \boldsymbol{\alpha} α β \boldsymbol{\beta} β不共线(即 α × β ≠ 0 \boldsymbol{\alpha}\times \boldsymbol{\beta}\ne \mathbf{0} α×β=0),则 α × β \boldsymbol{\alpha}\times \boldsymbol{\beta} α×β总是垂直于 α \boldsymbol{\alpha} α β \boldsymbol{\beta} β所形成的平面,方向遵循右手螺旋定则。

回到代码中来。我们发现,每对一个三角形进行光栅化,都要遍历屏幕上的所有像素。实际上这个算法还可以更高效:我们无需遍历所有像素,只需根据三角形的三个顶点分别确定三角形最上、最下、最左、最右的点的坐标,找出这些点所在的像素的编号,即可找出覆盖三角形的最小的、四边平行于坐标轴的、且包含了完整像素的矩形,这个矩形称为包围盒(bounding box)。遍历包围盒内的所有像素即可。

for (int x = l; x <= r; x++)
	for (int y = b; y <= t; y++)
		image[x][y] = inside(tri, x + 0.5, y + 0.5);

当然,我们还可以再对算法进行加速。我们可以对三角形覆盖的区域被像素划分后的每一行都找出它覆盖的最左和最右的像素,这样的话,一个像素都不会多考虑。这个算法对于很扁且倾斜角度较大的三角形尤为适用(此时它的包围盒很大,之前的算法反而效率不高)。不过这个算法实现起来并不容易,此处不再多说。

在这里插入图片描述

设原图为

在这里插入图片描述

光栅化后效果如下:

在这里插入图片描述

可以看到效果还是不错的,但有一个明显的问题:出现了锯齿,这极大影响了图像的质量。我们接下来的目标就是尽可能地消除锯齿。

反走样

(声明:本人在此之前从未学过任何有关信号处理的知识,因此对于本部分知识理解较浅,本节内容完全基于初学者视角,或有错漏之处,敬请谅解。如果发现问题,欢迎指出,不胜感激!)

“锯齿”更为学术的说法是走样混叠(aliasing或aliases),对应地,消除走样的过程称为反走样(antialiasing)。要想要实现反走样,我们得先弄清走样的原因。

计算机科学中一个常见的术语是artifact,或artificially introduced flaw,字面意思即“人为引入的缺陷”。走样就是一种常见的“visual artifact”。

从采样的角度分析,走样产生的一大原因是采样的频率远远低于被采样的信号的变化频率,使得两个采样点之间的大量细节被忽略,最终导致仅从采样的结果无法较准确地重构出原信号的变化情况。 由此引发的走样被称为undersampling artifact。下面的示意图有助于理解这种走样。

在这里插入图片描述

从图中可以看到,当采样点间距不变(采样的频率不变)时,从上到下,随着信号的频率越来越高,采样点之间的连线所“拟合”出的结果的频率并没有随之增加,例如 f 5 f_5 f5的采样结果可能会被误认为是一个更低频率的信号的采样结果。也就是说,在某些特定的采样频率下,两个频率不同的信号可能呈现相同的结果,进而无法区分,就好像混在了一起,这也是为什么aliasing又被翻译成“混叠”。

光栅化产生的锯齿也可以认为是一种混叠:若屏幕的分辨率过低,即像素数量(采样数)过少,则锯齿十分明显,从下图的三角形中无法区分原图到底是一个通常意义下的三角形,还是只是由一个个方格组成的、类似三角形的几何图形。

在这里插入图片描述
如何减轻这种走样?我们先给出答案:先对三角形进行模糊化处理,再采样。 如图,效果明显好了很多:

在这里插入图片描述
这里,模糊化处理被称为“filter”,原因我们下文就会解释。

需要注意的是模糊与采样的顺序不能颠倒。如果先采样再对采样结果做模糊化处理,锯齿不会消失:

在这里插入图片描述
为了解释“先模糊再采样”这种做法的合理性,我们必须从图像处理的层次上分析。

在接下来的内容中,我们把图像(已被光栅化)当做一个有界的、二维的、离散的信号,而把屏幕空间中的物体(还未被光栅化)当做有界的、二维的、连续的信号。 于是,对图像的处理本质上属于信号处理的一部分。时刻记住:我们的目标是分析与处理信号,为此,我们需要发明与运用各种各样的方法。Fourier变换是信号处理领域最普适、最有力的数学工具之一。

Fourier变换

在引入Fourier变换之前,我们还需简单介绍有关Fourier级数的前置知识。

对于任何一个定义在 R \mathbb{R} R上的周期函数 f f f(设其周期为 T T T),只要它具有较好的数学性质(实际情况中一般都会满足),则都可以写成可数无穷个正弦函数和余弦函数(统称为正弦波)的和: f ( t ) = a 0 2 + ∑ n = 1 ∞ ( a n c o s ( n ω t ) + b n s i n ( n ω t ) ) = a 0 2 + ∑ n = 1 ∞ a n 2 + b n 2 c o s ( n ω t + φ n ) . \begin{align} f(t) & =\frac{a_0}{2}+\sum_{n=1}^{\infty}\Big( a_n\mathrm{cos}(n\omega t)+b_n\mathrm{sin}(n\omega t) \Big) \\ & = \frac{a_0}{2}+\sum_{n=1}^{\infty} \sqrt{a_n^2+b_n^2} \mathrm{cos}(n\omega t + \varphi_n). \end{align} f(t)=2a0+n=1(ancos(t)+bnsin(t))=2a0+n=1an2+bn2 cos(t+φn).

其中,
ω = 2 π T , a n = 2 T ∫ t 0 t 0 + T f ( x ) c o s ( n ω x ) d x , b n = 2 T ∫ t 0 t 0 + T f ( x ) s i n ( n ω x ) d x , φ n = { − a r c t a n b n a n , a n ≠ 0 , − π 2 , a n = 0. \begin{align} \omega & = \dfrac{2\pi}{T}, \\ a_n & = \frac{2}{T}\int_{t_0}^{t_0+T} f(x)\mathrm{cos}(n\omega x)\mathrm{d}x, \\ b_n & = \frac{2}{T}\int_{t_0}^{t_0+T} f(x)\mathrm{sin}(n\omega x)\mathrm{d}x, \\ \varphi_n & = \left\{ \begin{array}{ll} -\mathrm{arctan}\dfrac{b_n}{a_n}, & a_n\ne 0, \\ -\dfrac{\pi}{2}, & a_n=0. \end{array} \right. \end{align} ωanbnφn=T2π,=T2t0t0+Tf(x)cos(x)dx,=T2t0t0+Tf(x)sin(x)dx,= arctananbn,2π,an=0,an=0.

这种表示称为 f f fFourier级数展开,展开式中的每一个正弦波被称为 f f f谐波分量,简称分量。式 ( 4 ) (4) (4) ( 5 ) (5) (5)中的 t 0 t_0 t0可以取 R \mathbb{R} R中的任意值,为方便,一般取为0或 − T 2 -\dfrac{T}{2} 2T

对于定义在 R \mathbb{R} R的有界区间上的函数,我们可以通过对其进行周期延拓,使之同样可以进行Fourier级数展开。

如果我们把函数的图象以 t t t为横坐标、 f ( t ) f(t) f(t)为纵坐标画出,就会像这样(以矩形方波 f ( t ) = A 0 2 + 2 A π ∑ n = 0 ∞ ( − 1 ) n c o s ( ( 2 n + 1 ) ω t ) 2 n + 1 f(t)=\dfrac{A_0}{2}+\dfrac{2A}{\pi}\displaystyle\sum_{n=0}^{\infty}(-1)^n\frac{\mathrm{cos}((2n+1)\omega t)}{2n+1} f(t)=2A0+π2An=0(1)n2n+1cos((2n+1)ωt)为例):

这样一个以 t t t为横坐标、 f ( t ) f(t) f(t)为纵坐标的二维坐标空间被称为时域(time domain)。

已知 f f f和它的Fourier级数展开,我们还可以以Fourier级数的每一项的频率 n ω 2 π \dfrac{n\omega}{2\pi} 2π为横坐标、振幅 a n 2 + b n 2 \sqrt{a_n^2+b_n^2} an2+bn2 为纵坐标绘制出一个新的图象:

在这里插入图片描述

这个图象被称为函数的(幅度)频谱(spectrum),它所在的以频率为横坐标、频谱幅度(即具有对应频率的正弦波分量的系数,可以理解为正弦波的振幅)为纵坐标的二维坐标空间被称为频域(frequency domain)。幅度频谱是离散谱。

时域和频域之间的关系还可以从下图中获得一个感性的认识(同样以矩形方波为例)。正所谓“横看成时侧成频”。

在这里插入图片描述

对于定义在 R \mathbb{R} R上的非周期函数,不存在类似式 ( 1 ) (1) (1)(由可数无穷个正弦波相加)的Fourier级数展开,但可以写成具有连续频率的“Fourier级数展开”的形式,此时表达式中的求和符号变为积分符号(积分可认为是自变量连续的求和),这就是我们下面要介绍的Fourier变换。

非周期函数也可以认为是周期无穷大的函数,从而从Fourier级数到Fourier变换可认为是当 T → ∞ T\to \infty T时,相邻两个正弦波的频率差 1 T → 0 \dfrac{1}{T}\to 0 T10时的过渡,这种过渡使得原来离散分布的频率变得连续分布。

函数 f f fFourier变换定义如下: f ^ ( u ) = ∫ − ∞ ∞ f ( x ) e − i u x d x . \hat{f}(u)=\int_{-\infty}^{\infty}f(x)\mathrm{e}^{-iux}\mathrm{d}x. f^(u)=f(x)eiuxdx.

对应地,我们有函数 f ^ \hat{f} f^Fourier逆变换 f ( x ) = 1 2 π ∫ − ∞ ∞ f ^ ( u ) e i x u d u . f(x)=\frac{1}{2\pi}\int_{-\infty}^{\infty}\hat{f}(u)\mathrm{e}^{ixu}\mathrm{d}u. f(x)=2π1f^(u)eixudu.

有的教材定义 f ^ ( u ) = ∫ − ∞ ∞ f ( x ) e − 2 π i u x d x \displaystyle \hat{f}(u)=\int_{-\infty}^{\infty}f(x)\mathrm{e}^{-2\pi iux}\mathrm{d}x f^(u)=f(x)e2πiuxdx f ( x ) = ∫ − ∞ ∞ f ^ ( u ) e 2 π i x u d u \displaystyle f(x)=\int_{-\infty}^{\infty}\hat{f}(u)\mathrm{e}^{2\pi ixu}\mathrm{d}u f(x)=f^(u)e2πixudu,两个定义其实是一样的,只不过相差了一个尺度变换而已(见“卷积”一节中Fourier变换的性质),第二个定义实际上对应于把Fourier级数定义为 f ( t ) = a 0 2 + ∑ n = 1 ∞ ( a n c o s ( 2 π n ω t ) + b n s i n ( 2 π n ω t ) ) f(t) =\displaystyle\frac{a_0}{2}+\sum_{n=1}^{\infty}\Big( a_n\mathrm{cos}(2\pi n\omega t)+b_n\mathrm{sin}(2\pi n\omega t) \Big) f(t)=2a0+n=1(ancos(2πnωt)+bnsin(2πnωt))。若应用了这个尺度变换,下文有些式子需要作调整。

Fourier变换的作用是:把一个信号从它的时域变换到它的频域,得到它的频谱。Fourier变换和逆变换的性质在“卷积”一节中会介绍。

注意到 f ^ \hat{f} f^其实是复值函数,因此频域的纵坐标一般取为 f ^ \hat{f} f^的模长 ∣ f ^ ∣ |\hat{f}| f^

f f f是信号在时域中的图象,呈现为 f f f的“Fourier级数展开”(其中有不可数多个项,因为组成 f f f的正弦波的频率是连续的)中的每一项叠加(积分)起来的结果,则 ∣ f ^ ∣ |\hat{f}| f^就是信号在频域中的图象。此时频谱不再像周期函数的频谱那样是离散的,而是连续谱。相应地,频域的纵坐标不再是频谱幅度,而是所谓的频谱密度,即正弦波分量的系数(振幅)的分布密度(函数 f ^ \hat{f} f^本身同样被称为频谱密度),单位是 幅度的单位 / H z 幅度的单位/\mathrm{Hz} 幅度的单位/Hz。具体来说, ∣ f ^ ( u ) ∣ |\hat{f}(u)| f^(u)指示了 f f f在频率 u u u处的单位“频率区间”(称为频带)内的频谱幅度的相对大小,也就是组成 f f f的正弦波分量中频率位于 [ u , u + Δ u ] [u,u+\Delta u] [u,u+Δu]内的振幅平均值的相对大小。位于 [ u , u + Δ u ] [u,u+\Delta u] [u,u+Δu]内的分量的总振幅用对 ∣ f ^ ∣ |\hat{f}| f^的积分表示,为 1 2 π ∫ u u + Δ u ∣ f ^ ( ω ) ∣ d ω ≈ ∣ f ^ ( u ) ∣ Δ u 2 π \dfrac{1}{2\pi}\displaystyle \int_{u}^{u+\Delta u}|\hat{f}(\omega)|\mathrm{d}\omega \approx |\hat{f}(u)|\dfrac{\Delta u}{2\pi} 2π1uu+Δuf^(ω)dωf^(u)2πΔu,单位是 幅度的单位 幅度的单位 幅度的单位

之所以我们使用频谱密度而非频谱幅度,是因为对于连续的频谱而言, T T T趋于无穷大,由式 ( 2 ) (2) (2) ( 4 ) (4) (4) ( 5 ) (5) (5)可知每个频率点上的信号幅度趋于无穷小,所以再讨论频谱幅度没有意义,只有对信号幅度进行积分后值才能不为零。

另一方面,Fourier逆变换把函数从它的频域变回它的时域里。Fourier变换与Fourier逆变换是互逆的过程。

在这里插入图片描述

总结一下:如果我们对一个周期信号(包括定义在有界区间上的信号)在时域中做Fourier级数展开的话,我们能得到它在频域中的离散的函数;如果我们对一个非周期信号做Fourier变换的话,我们能得到它在频域中的连续的函数。因此,Fourier变换可以认为是对Fourier级数的某种频率连续版本的推广。

对连续的周期函数做Fourier变换会引发数学上的错误,但是我们依然可以通过引入Dirac δ \delta δ 函数(又称单位冲激函数)解决,由此产生广义Fourier变换 δ \delta δ函数在采样理论中还有重要作用,我们将在“卷积”一节中介绍之,但不会介绍具体的广义Fourier变换的方法,感兴趣的读者可以参阅[2]的3.9节或者[4]。

在很多实际情况下,信号 f f f是离散的,例如声音采样后的结果等,此时,我们有离散Fourier变换(DFT),即在时域和频域上都呈现离散形式的Fourier变换。DFT可认为是对连续信号进行抽样后的结果的Fourier变换。如果一个离散信号是有限长的,即只在有界区间上不为0,我们可以先将其进行周期延拓后再对延拓后的离散周期函数进行DFT。事实上,计算机只能进行有限量的运算,也就是说,计算机只能进行DFT。

由一个个像素组成的图像可以认为是离散的二维信号,对其进行的Fourier变换称为二维离散Fourier变换,其基本思想是和DFT一样的。

我们即将介绍的Fourier变换的卷积定理是信号处理领域应用最广的Fourier变换性质之一。

卷积

数学上,定义在 R \mathbb{R} R上的函数 f f f g g g的卷积也是一个定义在 R \mathbb{R} R上的函数,定义为 ( f ∗ g ) ( x ) = ∫ − ∞ ∞ f ( t ) g ( x − t ) d t . (f*g)(x)=\int_{-\infty}^{\infty}f(t)g(x-t)\mathrm{d}t. (fg)(x)=f(t)g(xt)dt.

卷积遵循如下定律:

(1)交换律: f ∗ g = g ∗ f f*g=g*f fg=gf

(2)结合律: f ∗ ( g ∗ h ) = ( f ∗ g ) ∗ h f*(g*h)=(f*g)*h f(gh)=(fg)h

(3)对加法的分配律: f ∗ ( g + h ) = f ∗ g + f ∗ h ,   ( f + g ) ∗ h = f ∗ h + g ∗ h f*(g+h)=f*g+f*h, \ (f+g)*h=f*h+g*h f(g+h)=fg+fh, (f+g)h=fh+gh

现在来叙述Fourier变换的部分性质:

(1)尺度变换: f ( x b ) ^ = b f ^ ( b x ) ( b ≠ 0 ) \widehat{f(\dfrac{x}{b})}=b\hat{f}(bx)\quad (b\ne 0) f(bx) =bf^(bx)(b=0)

(2) f ∗ g ^ = f ^ g ^ \widehat{f*g}=\hat{f}\hat{g} fg =f^g^

(3) f g ^ = f ^ ∗ g ^ \widehat{fg}=\hat{f}*\hat{g} fg =f^g^

性质(2)和(3)统称为Fourier变换的卷积定理,它们说明了:若在时域中把两个信号 f f f g g g的卷积进行Fourier变换,在频域中得到的是 f f f g g g的Fourier变换的乘积;若把 f f f g g g的乘积进行Fourier变换,则得到 f f f g g g的Fourier变换的卷积。 下图阐明了乘积、卷积和Fourier变换之间的关系,其中“Space domain”就是时域(对图像而言),★代表卷积。

在这里插入图片描述

回忆:光栅化本质是某种对图形的采样。为了从数学上描述采样这种操作,我们引入 δ \delta δ函数。

δ \delta δ函数定义如下:

(1) δ ( x ) = 0 ,   ∀   x ≠ 0 \delta(x)=0, \ \forall \, x \ne 0 δ(x)=0, x=0

(2) ∫ − ∞ ∞ δ ( x ) d x = 1 \displaystyle\int_{-\infty}^{\infty}\delta(x)\mathrm{d}x=1 δ(x)dx=1

显然, δ \delta δ函数并非通常意义下的函数,也不能简单地记为 δ ( x ) = { ∞ , x = 0 0 , x ≠ 0 \delta(x)=\left\{\begin{array}{ll} \infty, & x=0\\ 0, & x\ne 0\end{array}\right. δ(x)={,0,x=0x=0,它是一种广义函数,不能用数集的映射来定义。

δ \delta δ函数的性质如下:

(1)筛选性质: f ( x ) δ ( x − x 0 ) = f ( x 0 ) δ ( x − x 0 ) f(x)\delta (x-x_0)=f(x_0)\delta (x-x_0) f(x)δ(xx0)=f(x0)δ(xx0)

(2)取样性质: ∫ − ∞ ∞ f ( x ) δ ( x − x 0 ) d x = f ( x 0 ) \displaystyle \int_{-\infty}^{\infty}f(x)\delta (x-x_0)\mathrm{d}x=f(x_0) f(x)δ(xx0)dx=f(x0)

(3)对任意函数 f f f f ( x ) ∗ δ ( x − x 0 ) = δ ( x − x 0 ) ∗ f ( x ) = f ( x − x 0 ) ,   ∀   x 0 ∈ R f(x) * \delta(x-x_0) = \delta(x-x_0) * f(x) =f(x-x_0), \ \forall \, x_0 \in \mathbb{R} f(x)δ(xx0)=δ(xx0)f(x)=f(xx0), x0R。特别地, δ \delta δ是卷积不变量: ( f ∗ δ ) = ( δ ∗ f ) = f (f*\delta)=(\delta * f)=f (fδ)=(δf)=f

(4)尺度变换: δ ( a x ) = 1 ∣ a ∣ δ ( x ) ( a ≠ 0 ) \delta(ax)=\dfrac{1}{|a|}\delta(x) \quad (a\ne 0) δ(ax)=a1δ(x)(a=0)

(5) δ ^ = 1 \hat{\delta}=1 δ^=1,其中等号右边是值为1的常函数。我们称频域中的常函数1为均匀频谱白色频谱

一般地, A δ ( x − x 0 ) A\delta(x-x_0) Aδ(xx0) A A A x 0 x_0 x0是常数)称为冲激函数(impulse),其中 A A A被称为冲激强度。冲激函数的图象通常用一箭头表示,箭头的头部的纵坐标表示其冲激强度,如图。

在这里插入图片描述
对函数 f f f x 0 x_0 x0处采样后得到的函数为 f ( x ) δ ( x − x 0 ) f(x)\delta(x-x_0) f(x)δ(xx0)

以上只是对 f f f在一个点处的采样,如何对 f f f在多个点,特别地,在 R \mathbb{R} R上等距地采样?我们还可以对冲激函数进行加和,获得以 T T T为周期的冲激序列(impulse train): s T ( x ) = ∑ i = − ∞ ∞ δ ( x − T i ) . s_T(x)=\sum_{i=-\infty}^{\infty}\delta(x-Ti). sT(x)=i=δ(xTi).

于是,对 f f f T T T为周期等间距地采样(并且在原点处有采样),得到的结果是 f ( x ) s T ( x ) = f ( x ) ∑ i = − ∞ ∞ δ ( x − T i ) \displaystyle f(x)s_T(x)=f(x)\sum_{i=-\infty}^{\infty}\delta(x-Ti) f(x)sT(x)=f(x)i=δ(xTi)

冲激序列有如下性质:对一个冲激序列进行Fourier变换后会得到另一个冲激序列。冲激序列的周期越长,其经Fourier变换后的周期越短,反之亦然。定量描述即 s T ^ = s 1 T . \widehat{s_T}=s_{\frac{1}{T}}. sT =sT1.

在这里插入图片描述

为什么函数 f f f x 0 x_0 x0处采样后的结果不写成更符合直觉的(狭义)函数 f ( x ) 1 x 0 ( x ) f(x)1_{x_0}(x) f(x)1x0(x)(其中 1 x 0 ( x ) = { 1 , x = x 0 0 , x ≠ x 0 1_{x_0}(x)=\left\{ \begin{array}{ll} 1, & x=x_0 \\ 0, & x \ne x_0 \end{array} \right. 1x0(x)={1,0,x=x0x=x0),而是要写成(广义)函数 f ( x ) δ ( x − x 0 ) f(x)\delta(x-x_0) f(x)δ(xx0)?因为后者保持了函数的积分性质,而前者没有: 1 x 0 ^ ( x ) ≡ 0 \widehat{1_{x_0}}(x)\equiv 0 1x0 (x)0,故 f 1 x 0 ^ = f ^ ∗ 1 x 0 ^ ≡ 0 \widehat{f1_{x_0}}=\hat{f}*\widehat{1_{x_0}}\equiv 0 f1x0 =f^1x0 0,从而 f ( x ) 1 x 0 ( x ) f(x)1_{x_0}(x) f(x)1x0(x)的频谱是零函数,丢失了 f f f的所有信息,这不是我们希望看到的。

为了获得采样后的函数的频谱,我们需要对其进行Fourier变换,得到 f ( x ) s T ( x ) ^ = f ^ ( x ) ∗ s 1 T ( x ) = f ^ ( x ) ∗ ( ∑ i = − ∞ ∞ δ ( x − 1 T i ) ) = ∑ i = − ∞ ∞ ( f ^ ( x ) ∗ δ ( x − 1 T i ) ) = ∑ i = − ∞ ∞ f ^ ( x − 1 T i ) . \begin{align*}\widehat{f(x)s_{T}(x)} & =\hat{f}(x)*s_{\frac{1}{T}}(x) \\ & = \hat{f}(x)*\Big(\sum_{i=-\infty}^{\infty}\delta(x-\frac{1}{T}i)\Big) \\ & = \sum_{i=-\infty}^{\infty}\Big(\hat{f}(x)*\delta(x-\frac{1}{T}i)\Big) \\ & = \sum_{i=-\infty}^{\infty}\hat{f}(x-\frac{1}{T}i).\end{align*} f(x)sT(x) =f^(x)sT1(x)=f^(x)(i=δ(xT1i))=i=(f^(x)δ(xT1i))=i=f^(xT1i).

于是,我们有重要结论:(1)在时域中对 f f f进行以 T T T为周期的采样后再进行Fourier变换,在频域中等价于将 f f f的频谱以 1 T \dfrac{1}{T} T1为周期进行平移复制;(2)采样频率越高,频谱周期越大、显得越稀疏;采样频率越低,频谱周期越小、显得越密集。

在这里插入图片描述

到现在,我们终于可以解释走样的成因了:当我们采样频率过低时,频谱周期较小,原信号频谱的相邻两个复制谱之间可能会有重叠,即在同一个频率上可能有来自不止一个复制谱的分量。 我们无法判断具有这个频率的分量是来自哪一个复制谱,从而无法准确还原出原信号频谱。换句话说,在这些重叠区域,不同原始信号经过采样后可能得到相同的波形,这就产生了波的混叠,即走样。

在这里插入图片描述
如何反走样?答案已经一目了然:本质上只需让相邻复制谱之间不产生重叠。我们看频谱中以纵轴为对称轴的复制谱,它实际上就是原信号的频谱,设它的最高频率为 f m a x f_{max} fmax,设采样频率,即频谱中相邻复制谱之间的频率差为 f s = 1 T f_s=\dfrac{1}{T} fs=T1,则若要不产生重叠,则必有 f m a x < f s − f m a x f_{max}<f_s-f_{max} fmax<fsfmax,即 f s > 2 f m a x f_s>2f_{max} fs>2fmax。事实上,这正是采样定理(又称Nyquist定理):当采样频率大于信号中最高频率的2倍时,采样之后的信号完整地保留了原始信号中的信息

具体实现反走样的方法主要有两种:(1)增加 f s f_s fs,即增加分辨率。这是最直接的方法,但相应地要生产更好的屏幕,花销较大;(2)减小 f m a x f_{max} fmax,即把原信号的频谱中高频的部分“删去”。我们主要介绍(2)的实现方法。

在信号处理中,常常出现把信号中频率位于某个区间内的分量删除的操作,这种操作就叫滤波(filtering)。

滤波

如何滤波?Fourier变换和卷积定理再次给我们启示:对一个信号进行Fourier变换,得到它的频谱。之后用某个函数与这个频谱相乘,使得频谱的低频分量不变,而高频分量变为0。最后再对新的频谱进行Fourier逆变换变回去,就能获得经滤波后的信号。

但是图形(而非光栅化后的图像)是连续的,我们无法在计算机上对其进行Fourier变换和Fourier逆变换。有什么什么方法能够不进行Fourier变换就能实现滤波呢?

回忆卷积定理:两个函数的Fourier变换在频域内相乘,等价于它们自己在时域内的卷积。因此,我们只需在时域内将信号与特定的函数做卷积,即可实现滤波操作。 这种特定的函数被称为滤波器(filter)。

滤波分为高通滤波(high-pass filtering)、低通滤波(low-pass filtering)等,对应的滤波器称为高通滤波器(high-pass filter)、低通滤波器(low-pass filter)等。高通滤波是滤去信号的低频分量,保留高频分量;低通滤波则是滤去高频分量,保留低频分量。可见要想实现反走样,我们需要进行低通滤波。

我们用图像和它的频谱演示这些滤波操作。对图像进行二维离散Fourier变换,得到它的频谱:

在这里插入图片描述

进行高通滤波:

在这里插入图片描述

进行低通滤波:

在这里插入图片描述

我们可以看到:高通滤波保留了图像的轮廓,即那些变化剧烈的部分,也就是图像作为信号的高频分量;低通滤波“舍弃”了图像的轮廓,保留了变化缓慢的部分,也就是图形的低频分量,达成的效果是使得图片变模糊。这和我们的直觉是一样的: f f f变化愈剧烈,所包含的高频分量愈丰富;变化愈缓慢,所包含的低频分量愈丰富。

于是,我们知道了:模糊≈低通滤波≈某种卷积

现在来实现这种卷积。我们的目标是实现三角形的光栅化(为方便,设三角形为单色的。非单色三角形的光栅化易推广得),于是信号就是一个定义在屏幕空间(为 R 2 \mathbb{R}^2 R2的一个子集)上的二值函数 i n s i d e ( T , x , y ) \mathrm{inside}(T,x,y) inside(T,x,y),其中对于单个三角形, T T T确定, x x x y y y可以连续地取值。

对屏幕空间中的任意 ( x , y ) (x,y) (x,y),令 f ( x , y ) f(x,y) f(x,y)为以 ( x , y ) (x,y) (x,y)为中心的一个像素大小区域 [ x − 0.5 , x + 0.5 ] × [ y − 0.5 , y + 0.5 ] [x-0.5,x+0.5]\times [y-0.5,y+0.5] [x0.5,x+0.5]×[y0.5,y+0.5](注意 ( x , y ) (x,y) (x,y)本身可以不是某个像素的中心)内位于三角形内的面积占比。则 f f f就是 i n s i d e \mathrm{inside} inside与低通滤波器 g ( x , y ) = { 1 , ( x , y ) ∈ [ − 0.5 , 0.5 ] 2 0 , ( x , y ) ∉ [ − 0.5 , 0.5 ] 2 g(x,y)=\left\{ \begin{array}{ll} 1, & (x,y)\in [-0.5,0.5]^2 \\ 0, & (x,y) \notin [-0.5,0.5]^2 \end{array}\right. g(x,y)={1,0,(x,y)[0.5,0.5]2(x,y)/[0.5,0.5]2 卷积后的结果,这种滤波器被称为(1像素宽的)盒状滤波器

在这里插入图片描述
上图左边是 g g g,右边是 g ^ \hat{g} g^。可见 g g g还不是理想的低通滤波器:它高频的分量仍不为零,这使得 i n s i d e \mathrm{inside} inside与之卷积的结果中仍存在少量高频分量。然而,理想的低通滤波器,例如盒状函数 g g g的Fourier逆变换,形式较为复杂,不适合用于与原信号做卷积。

f f f可认为是把该像素区域内的颜色求平均后的结果赋给区域的中心点,于是我们又知道了:模糊≈平均化

在这里插入图片描述

对于1维的信号,矩形脉冲 f ( t ) = { 1 , t ∈ [ − τ 2 , τ 2 ] 0 , t ∉ [ − τ 2 , τ 2 ] f(t)=\left\{ \begin{array}{ll} 1, & t\in [-\frac{\tau}{2} ,\frac{\tau}{2}] \\ 0, & t\notin [-\frac{\tau}{2} ,\frac{\tau}{2}] \end{array}\right. f(t)={1,0,t[2τ,2τ]t/[2τ,2τ] g g g的1维版本)就是一个低通滤波器,其Fourier变换为 F ( ω ) = τ S a ( ω τ 2 ) = 2 ω s i n ( ω τ 2 ) F(\omega)=\tau\mathrm{Sa}(\dfrac{\omega \tau}{2})=\dfrac{2}{\omega}\mathrm{sin}(\dfrac{\omega \tau}{2}) F(ω)=τSa(2ωτ)=ω2sin(2ωτ)。其中 S a \mathrm{Sa} Sa抽样函数 S a ( x ) = s i n x x \mathrm{Sa}(x)=\dfrac{\mathrm{sin}x}{x} Sa(x)=xsinx

S a ^ ( ω ) = { π , ω ∈ [ − 1 , 1 ] 0 , ω ∉ [ − 1 , 1 ] \widehat{\mathrm{Sa}}( \omega)=\left\{ \begin{array}{ll} \pi, & \omega \in [-1,1] \\ 0, & \omega \notin [-1,1] \end{array}\right. Sa (ω)={π,0,ω[1,1]ω/[1,1]形如矩形脉冲。函数 ω c π S a ( ω c t ) \dfrac{\omega_c}{\pi}\mathrm{Sa}(\omega_c t) πωcSa(ωct)的Fourier变换是 { 1 , ω ∈ [ − ω c , ω c ] 0 , ω ∉ [ − ω c , ω c ] \left\{ \begin{array}{ll} 1, & \omega \in [-\omega_c,\omega_c] \\ 0, & \omega \notin [-\omega_c,\omega_c] \end{array}\right. {1,0,ω[ωc,ωc]ω/[ωc,ωc],它可认为是“理想的”低通滤波器。

在这里插入图片描述

但是计算机实际上无法进行上述的卷积操作:毕竟卷积是在屏幕空间里进行的, x x x y y y都是连续量,计算机只能处理离散的量。所幸我们根本无需求出 f f f在每一个点的值,我们只需求出 f f f在屏幕上每个像素的中心处的值,因为光栅化只要求获得这些点的值,这些值即为这个像素最终呈现的颜色。

因此,做卷积(滤波)和光栅化(采样)两步看起来像是合并成了一步:在采样点求卷积值。

我们现在就可以回答之前的一个问题:为什么“先模糊(滤波)再采样”是对的,而“先采样再模糊”是错的呢?因为对采样后的结果进行滤波,并无法消除波形重叠的部分。采样后已经产生了波形混叠,滤波要么使之变为0(丢失所有信息),要么使之乘以一个非零的系数。无论如何,无法将混叠的波分开。读者自行画图便可对此有直观的认识。

问题仍没有解决:我们不知道如何计算 f f f,即不知道如何计算1像素区域内三角形的面积占比。我们采样一个折中的方法:把一个像素拆成很多个“子像素”(尽管实际上并没有),在每个子像素中心采样(即计算该位置的 i n s i d e \mathrm{inside} inside),所有子像素的值做平均后的结果即为这个像素最终的值。这个方法被称为Antialiasing By Supersampling(MSAA),它并没有增加屏幕的分辨率,只不过在每个像素内部做了更多的采样,用这些采样的结果的平均近似该区域三角形面积的占比。相比于不做反走样,代价自然是花费了更多时间。

在这里插入图片描述

在这里插入图片描述

最后,用一段话总结“反走样”一节所有的内容:走样的原因是采样后的图像的频谱中,原图形的不同复制谱之间存在波形重叠。反走样的目的是消除这些重叠。为此,我们需要对图形信号进行低通滤波,使其高频分量尽可能变为零。低通滤波通过对图形与盒状滤波器做卷积实现。实际情况下,我们通过在每个像素内多个位置进行采样,将采样值做平均的结果作为这个像素的值。

深度缓存

回忆“视口变换”一节,视口变换保持z坐标不变。虽然在光栅化单个三角形的过程中z坐标没有实际意义,但如果考虑多个三角形,则它们之间的遮挡关系通过z坐标反映出来。让我们回到上一篇文章“透视投影”一节,此时所有物体刚被投影到长方体中,x、y坐标都位于 [ − 1 , 1 ] 2 [-1,1]^2 [1,1]2中,相机位于原点处,看向-z方向。下一步是要通过只取x、y坐标的方式把它们都投影到 [ − 1 , 1 ] 2 [-1,1]^2 [1,1]2中。问题是:不同物体上的不同点可能具有相同的x、y坐标,但z坐标不同,将会被投影到同一个位置,但这个位置只能呈现一个点。怎么确定这个位置最终呈现哪个物体上的点,即谁最终会出现在屏幕上呢?解决问题的关键是判断哪些物体能被相机看到。

为方便,在此我们不考虑z坐标大于0的物体,将其余物体的z坐标统一乘-1,即都变得大于0。z越小,离相机越近,从而越“不会被其他物体遮挡”。具体来说,一个物体能出现在屏幕上,当且仅当它与相机之间不存在其他物体,当且仅当它的z坐标最小。

但是,把物体当做整体考虑彼此的遮挡关系存在不可逾越的困难。例如下图:

在这里插入图片描述

该图中,P、Q、R三个三角形存在互相遮挡的关系,即P遮挡Q、Q遮挡R、R遮挡P。因此,没有一个三角形不被其他任何三角形遮挡,故无法确定应该显示哪个三角形!

解决方案是:不是一个一个物体地考虑,而是一个一个像素地考虑。回忆之前做的所有变换,z坐标相对关系均保持不变。在每一个像素处,存储当前x、y坐标位于该像素内并且z坐标最小的点的z坐标 z m i n ( x , y ) z_{min}(x,y) zmin(x,y)。在光栅化生成渲染图的同时,生成一张“深度图”,里面每一个像素处存储的不是最终呈现的颜色,而是 z m i n ( x , y ) z_{min}(x,y) zmin(x,y) 这种算法叫做深度缓存(z-buffer)。

在这里插入图片描述

如图,左图是最终显示到屏幕上的渲染图,每个像素处存储了应当呈现的颜色;右图是左图对应的深度图,颜色越深代表z坐标越小、离相机越近,颜色越浅代表z坐标越大、离相机越远。

在这里,我们假设任意两个三角形上的任意两点不会在同一个像素处拥有相同的z坐标。实际上这种情况一般也不会发生:三角形上点的坐标使用浮点型变量存储,而两个浮点型变量很难被判断为相等。

要想实现这个算法,我们需要事先初始化所有像素处的z坐标 z b u f f e r [ x , y ] \mathrm{zbuffer}[x,y] zbuffer[x,y] ∞ \infty ,并在光栅化的同时,执行如下伪代码:

for (each triangle T)
	for (each sample (x,y,z) in T)
		if (z < zbuffer[x,y])
			framebuffer[x,y] = rgb; //更新渲染图中该像素处的颜色
			zbuffer[x,y] = z; //更新深度图中该像素处的深度
		//else 什么也不做,该点已被遮挡

深度缓存算法有一个优点:遍历三角形的顺序和遍历其中像素的顺序(伪代码中的两个循环)不会影响最终的结果,最终生成的深度图和渲染图都是一样的。

在这里插入图片描述

如果我们在光栅化的时候使用了MSAA算法进行反走样,那我们就不是在每个像素处进行深度采样,而是在每个“子像素”(采样点)处进行深度采样。

现在来分析深度缓存算法的时间复杂度。根据伪代码,设一共有n个三角形,每个三角形本身(不考虑相互的遮挡关系)覆盖的像素个数(即伪代码第二个循环中采样点的个数)相近,为常数个,则算法的时间复杂度是 O ( n ) O(n) O(n)

当然,该算法也有不足之处:它无法处理透明物体。对于这种情况,我们需要进行特殊处理,此处不再多说。

补充资料:线段的光栅化

给定线段的两个端点 ( x 0 , y 0 ) (x_0,y_0) (x0,y0) ( x 1 , y 1 ) (x_1,y_1) (x1,y1),不妨假设 x 0 ≤ x 1 x_0 \le x_1 x0x1,我们有它对应的方程(隐式表示) f ( x , y ) = ( y 0 − y 1 ) x + ( x 1 − x 0 ) y + x 0 y 1 − x 1 y 0 = 0 ( x 0 ≤ x ≤ x 1 ) . f(x,y)=(y_0-y_1)x+(x_1-x_0)y+x_0y_1-x_1y_0=0 \quad (x_0 \le x \le x_1). f(x,y)=(y0y1)x+(x1x0)y+x0y1x1y0=0(x0xx1).

这里我们进一步假设 x 0 < x 1 x_0 < x_1 x0<x1 y 0 ≠ y 1 y_0 \ne y_1 y0=y1(否则线段垂直于坐标轴,可以直接画出)。则线段的斜率 m = y 1 − y 0 x 1 − x 0 . m=\frac{y_1-y_0}{x_1-x_0}. m=x1x0y1y0.

我们只讨论 m ∈ ( 0 , 1 ] m \in (0,1] m(0,1]时的情况,其他情况讨论方法类似。

代码如下:

y = y0;

for(int x = x0; x <= x1; x++) {
    draw(x, y); //绘制坐标为(x, y)的像素
    if (f(x + 1, y + 0.5) < 0) //如果点(x + 1, y + 0.5)在线段下方
        y = y + 1; //y坐标向上移动一个单位
}

需要说明的是,代码里 x 0 \mathrm{x0} x0 y 0 \mathrm{y0} y0 x 1 \mathrm{x1} x1代表像素的位置编号,亦即像素左下角在屏幕空间中的坐标,它们均为整数,通过以下关系确定:线段的端点 ( x i , y i ) (x_i,y_i) (xi,yi)位于像素 ( x i , y i ) (\mathrm{xi},\mathrm{yi}) (xi,yi)内,即 ( x i , y i ) ∈ [ x i , x i + 1 ) × [ y i , y i + 1 ) (x_i,y_i) \in [\mathrm{xi},\mathrm{xi}+1) \times [\mathrm{yi},\mathrm{yi}+1) (xi,yi)[xi,xi+1)×[yi,yi+1),其中 i ∈ { 1 , 2 } i \in \{ 1,2 \} i{1,2}

这个算法的名字是中点算法(midpoint algorithm),结合上面的代码和下面的示意图可以大概理解它的原理。

在这里插入图片描述

函数f的计算比较复杂,我们可以对算法进行一定的优化。优化后代码如下:

int y = y0;
int d = f(x0 + 1, y0 + 0.5);

for(int x = x0; x <= x1; x++) {
    draw(x, y);
    if (d < 0) {
        y = y + 1;
        d = d + (x1 - x0) + (y0 - y1);
    }
    else
        d = d + (y0 - y1);
}
  • 19
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值