前言
此系列是本人对于学习计算机图形学过程中的记录及总结,所看教材是《Fundamentals of Computer Graphics (FOURTH EDITION)》。本文的图片也主要来源此书。以博客作为学习记录,理解之中若有不足还请大佬们指教。
在第四章学习光线追踪时了解到了image-order rendering与object-order rendering,光线追踪是一种image-order rendering。object-order rendering则是按序遍历每一个物体对象,发现可能被物体对象影响的像素值。其中,发现被图元影响的像素值的过程被称为光栅化(rasterization),所以object-order rendering也可以被称为rendering by rasterization(光栅化渲染)
本章将学习图形管线(graphics pipeline),它是从物体对象开始到更新图片像素的一系列操作,也即是对应object-order rendering。
相比image-order rendering,object-order rendering开销更小。在大型场景中,数据的访问方式对性能非常重要,object-order rendering只需要遍历一次场景的geometry,而image-order rendering则需要重复地搜索场景、检索物体对象以更新像素值。
对于object-order rendering的实现并非只有一种方法,有两个非常不同的例子。一个是hardware pipelines,用于支持可交互的渲染,类似的API有OpenGL和Direct3D;一个是software pipelines,用于电影制作,类似的API有RenderMan。hardware pipeline为支持UI、实时游戏、可视化等会跑得更快。software pipelines为渲染光质量的动画及特效,会花费更多的时间。
尽管两种不同的pipeline有不同的设计目的,此章节将着重讲解共同的基础知识。
object-order rendering可以根据rasterization划分阶段,一个是rasterization之前对geometry的操作,一个是rasterization之后对像素的操作。对于各种pipeline,最相似的geometry操作是前两节应用的变换矩阵,最相似的像素操作是hidden surface removal(遮盖表面去除)。各个阶段中也有其他的操作,其多样达成了各种各样的渲染效果。
本章节的目的是从4个阶段讲述图形管线(graphic pipeline)。

首先,一个用顶点描述的几何对象通过交互应用或者场景描述文件被扔进管线,vertex-processing stage对这些点(通过正交视图或者透视视图,注意视图主要处理x、y值,z值用于后续的表面遮盖处理,所以后续得光栅化也是针对二维)处理成为图元(geometric primitive,点、线、多边形,一般为三角形);接着,图元被送进rasterization stage,raseterizer(光栅化器)会将图元分解为许多片元(fragments),一个片元是一个像素的3D投射,其与像素有一样的属性,同时图元会覆盖每一个像素;然后,片元进入fragment processing stage进一步处理;最后片元与对应的像素在fragment blending stage组合。
MDN上有一篇阐述基本三维理论的文章,其中有一个非常好的流程示意图如下:

本章将会先介绍光栅化,再阐述光栅化前的geometry操作及光栅化后的像素操作和它们的目的
8.1光栅化(Rasterization)
光栅化是object-order graphics中至关重要的操作,因此*光栅器(rasterizeer)*对于任何图形管线非常重要。对于每一个进入rasterization stage的图元,光栅器会完成两个工作:
- 枚举图元覆盖的像素
- 对像素值进行插值计算,所获值称为属性(attributes)。属性的作用会在后文讲解到
最后光栅器会输出片元集合,每个片元对应其像素位置,并且都有各自的属性值。
本小节描述如何用光栅化去渲染三维场景。
8.1.1 绘制线(Line Drawing)
许多的图形包包含绘制屏幕坐标系上点到点连线的命令。

比如对(1,1)与(3,2)调用命令,会绘制(1,1)、(3,2)点及填充之间的一个像素。更一般的情况下(x0,y0)及(x1,y1)之间的像素填充后,会使其看起来尽可能像一条直线。
绘制线使用线方程(line equations),有两种方程可使用:隐函数(implicit equation) 和参数方程(parametric equation)
本小节主要讲述隐函数。
使用隐函数绘制直线(Line Drawing Using Implicit Line Equations)
最常用的用于绘制直线的隐函数是midpoint algorithm(Pitteway (1967); van Aken and Novak (1985))。因为midpoint algorithm更加直观,所以取代了之前的Bresenham algorithm(Bresenham, 1965),当然,两者的绘图效果是一样的。
绘制直线,第一件事是找到对应的隐函数:

假设x0≤x1,如不是,交换两点。斜率m为:

再假设m∈(0, 1]。对于m∈(-∞, -1]、m∈(-1, 0]、m∈(1, ∞)可以由其衍生。这四种情况覆盖了所有的可能。
当m∈(0, 1]时,x值增长快于y值,
midpoint algorithm的核心概念是尽可能地画一条薄的、没有间隙的直线。当从左端点到右端点绘制线时,有两种可能性:一是与此点同高处左侧画像素,二是此点更高处画像素。这会保证两端点之间每一列都有一个像素而不会造成间隙。如果有一列是0个像素,会造成间隙;如果有两个像素,会造成线条变粗。如图是几种可能的情况,这几条线段更加”横向发展“。

对于m∈(0, 1],midpoint algorithm先确定最左侧像素及最右侧像素的x值,接着水平遍历(即遍历x值),确定每个像素的y值。算法的基本形式如下:

注意x、y值均是整型。使用此算法的关键是使用高效的方法进行条件判断。一个高效的方法是使用两像素之间的midpoint(中点)。在绘制完前一个像素(x,y)后,下一个像素可以绘制在(x+1,y)或(x+1,y+1),这时midpoint的坐标为(x+1,y+0.5)。如果midpoint在隐函数直线下方,则绘制(x+1,y+1);如果midpoint在隐函数直线上方,则绘制(x+1,y)。如图,上图中midpoint在直线下方,所以应绘制上部的像素;下图中midpoint在直线上方,所以应绘制下部的像素。

具体代码之中,可以通过上方的函数f(x+1,y+0.5)来判断midpoint在直线哪一边。

还有一种更加高效的方法,为增值方法(incremental method)。因为:

于算法之中,减少了f(x,y)函数的计算时间:

8.1.2三角形光栅化(Triangle Rasterization)

我们一般使用屏幕坐标系上的二维点用于绘制二维三角形:p0,p1,p2。绘制三角形与绘制直线有点相似,但有些更细节的问题。一个是使用重心坐标系计算颜色插值:

此颜色插值计算被称为Gouraud interpolation(Gouraud,1971)。
另一个细节是在光栅化后的三角形需要共享顶点和边,可以使用midpoint algorithm先绘制出每个三角形的轮廓,再使用像素对三角形进行填充。这也会使得相邻的三角形均会沿着边绘制像素,如果像素颜色不一致,将会取决于绘制的先后顺序。
为了避免顺序问题和忽略空隙,规定当且仅当像素的中心在三角形之中才可以绘制。如果像素在三角形之中,像素中心的重心坐标值大小必然在(0,1)之间。这种方法又导致了一个新的问题:如果像素中心恰好在三角形边上怎么办?对于这个问题有好几个解决方法,后续再阐述。
至此,光栅化三角形的问题化解为如何高效地计算各像素中心在三角形上的重心坐标系。一个暴力解决算法为如下:

接下来的算法会减小需要遍历的集合以提升性能。
比如找到三角形的矩形框,仅遍历矩形中的像素点,同时使用之前学到的隐函数计算重心坐标:

处理三角形边界上的像素(Dealing with Pixels on Triangle Edges)
两个相邻的三角形有共同的边界,其边界上的像素不能不绘制,也不能均绘制(效率低)。一个解决的方法是使用一个离屏点,离屏点在共享边的哪一侧,就绘制对应侧的边。

将离屏点的坐标代入隐函数得p,顶点代入对应边隐函数得q,若p*q>0则说明其在同一边。
选取(-1,-1)作为离屏点,加入此判断后,对应算法如下:


但是此算法还未完美,还未解决若离屏点在隐函数之上的情况,不过已经帮我们解决了大多数问题。
8.1.3裁剪(Clipping)
直接将图元转换至screen space然后进行光栅化不会取得很好的效果,因为有些图元会在视域体(view volume)之外,比如在e点之后,在光栅化时会导致错误的结果。
回顾透视投影的公式:

对于z坐标,现假设变换前为z,变换后为z',其函数图如下:

发现当z为负值时,z'会变为正值。

如果c点原先在e点(即眼睛)后,变换后c'点会在于n+f平面,这显然是不合理的。因此,在进行光栅化前要先进行*裁剪(clipping)*操作,去除图元出现在e后的部分。
在光栅化前的裁剪中,需要裁掉的边即是视域体外的边。
本小节讲述的是两个基本的裁剪方法:
法一:在世界坐标系中使用六个平面
法二:在四维齐次坐标使用除法前
对每个三角形可使用以下算法:

8.1.4方法一:转换前进行裁剪(Clipping Before the Transform (Option 1))
方法一比较直观,唯一的问题是如何获得六个平面的公式。
可以从对应八点获得。

8.1.5方法二:齐次坐标系中进行裁剪(Clipping in Homogeneous Coordinates (Option 2))
齐次坐标系中用以下公式表明平面:

8.1.6通过平面进行裁剪(Clipping against a Plane)
无论选择上文的何种方式,都需要通过平面进行裁剪。
用隐函数、法向量n及平面通过的一点q来表示p点是否在平面上:

此公式也常被写为:

假设现有线段,两端点为a与b,通过平面来判断两端点是否在同一侧。如果f(a)与f(b)正负号相同,则说明其在同侧。
如果平面截断了线段,可以通过如下方式找到截断点,设点p:

代入公式f(p)=0:

最后可求得t值,然后缩短线段:

关于裁剪,本章只是做简要的介绍,更多可看此书第十二章节。
8.2光栅化前与光栅化后的操作(Operations Before and After Rasterization)
在图元进行光栅化前,vertex processing stage将顶点通过modeling、camera、viewing、projection transformation进行变换,使其从原先的原点坐标系匹配至屏幕空间。同时,像颜色、表面法向量、材质坐标系等等信息也会被转换。
vertex processing stage过程相当于在视图的基础上再添加必要信息的转换。
在图元进行光栅化后,fragment processing stage会计算每一个片元的颜色和深度(depth)。这个过程简单设计的话就只是传递插值颜色,和使用光栅器计算的深度(depth);复杂的话可以包括阴影上的操作。
最后fragment blending stage结合所有片元,计算最终的颜色。比较共同的方式是选择片元中深度最小的颜色(也就是离e最近)。
本小节将使用例子阐述各个不同阶段。
8.2.1简单的二维绘制(Simple 2D Drawing)
最简单的管线是在顶点处理阶段和片元处理阶段什么都不做,在混合(blending)阶段每一个片元的颜色直接进行覆盖。应用直接提供像素坐标系表示的片元,然后光栅器完成了所有的工作。
许多用于绘制UI及二维画面的老API都是这样做的。
8.2.2一个迷你的三维管线(A Minimal 3D Pipeline)
想要绘制三维空间中的物体,可以直接在二维绘制管线上增加vertex-processing stage,把送进来的端点通过modeling、camera、projection、viewport矩阵的变换转化为屏幕空间的三角形。
为了使距离更近的物体覆盖距离较远的物体,使图元的绘制顺序从远向近,达到去除被隐藏表面的效果,所使用的算法为painter's algorithm。painter's algorithm先绘制较远的物体,再绘制较近的物体以完成覆盖,它是一种非常有效的去除被隐藏表面的方法,但其也有缺点。第一个缺点是无法掌控相互交叉的两个三角形或者形成闭环的几个三角形,因为无法找到正确的远近顺序。

第二个缺点是根据图元深度(depth)来排序是很慢的,特别是在大型场景中,使得object-order rendering的速度变慢了。
8.2.3使用z-Buffer来隐藏表面(Using a z-Buffer for Hidden Surfaces)
实践之中painter's algorithm很少被使用,较常使用的是简单而高效的z-buffer算法。
z-buffer的方法非常简单:对于每一个像素,追踪其目前距离最近的表面,然后扔掉所有远于最近距离的片元。
像素的存储值,除了红色,绿色和蓝色值之外,还会分配一个额外的值(被称为深度(depth)或z-buffer)来存储最接近的距离。
z-buffer算法在fragment blending stage运行,运行原理是比较每一个片元的深度和当前存储的z-buffer值,如果片元的深度更近,此片元的颜色及深度均会覆盖当前像素的颜色值及深度值。如果片元的深度更远,那么此片元会被销毁。为了保证第一个片元会通过深度测试(depth test),z-buffer的初始值为最大深度,即远平面的距离。
8.2.4Per-vertex Shading
目前为止,已经能将三维转化为二维,分清物体的前后顺序,但还缺少shading。公式方面,所用的光源公式也是第四章学习时光线追踪时所用到的。
一个进行shading计算的方法是在vertex-processing stage进行。应用提供顶点处的法向量,至于光源的位置、颜色另外提供(因为光源不会覆盖所有表面)。对于每一个顶点来说,其朝向观察者(相机)的方向和朝向光源的方向是基于相机、光源、顶点的位置计算出来的。理想的shading公式会计算出颜色值后,传递给光栅器(rasterizer)去作为顶点颜色。Per-vertex shading有时也被称为Gouraud shading。
选择在哪个坐标系使用shading计算非常重要,比较好的选择是world space或者eye space(camera space)。因为shading计算依赖于向量的夹角,而各种非均匀的缩放、透视投影等变换会改变这个夹角。
Per-vertex shading的缺点是无法产生任何比图元更小的细节,因为其仅仅只为每个顶点计算一次shading,而不会在顶点之间。
举例来说,场景中有一个房间,地板仅仅使用两个大三角形绘制,中间有一个光源进行照明。进行per-vertex shading的话仅仅只有房间角落进行了计算,而中间部分会变得很黑,这种由于插值计算所导致的效果被称为interpolation artifacts。另一个例子,使用高亮灯光照射曲面,也要保证曲面的图元足够小,使其有高亮效果。
如图是使用per-vertex shading的效果。

8.2.5Per-fragment Shading
为了避免per-vertex shading导致的interpolation artifact,我们可以在fragment processing stage,插值(interpolation)计算之后,进行shading计算以避免interpolation color。(。。。这里好拗口。。放下原文:To avoid the interpolation artifacts associated with per-vertex shading, we can avoid interpolating colors by performing the shading computations after the interpolation, in the fragment stage)
在per-fragment shading中,shading公式也是一样的,不过其应用对象是每个片元,使用的是向量插值。per-fragment shading需要的图形信息是作为属性,通过光栅器传递的,所以vertex processing stage与fragment processing stage两个流程要协调好数据。
如下是使用per-fragment shading后的效果。

8.2.6纹理匹配(Texture Mapping)
纹理是可应用于surface的shading上的图片,为surface增添额外细节的。其原理是每次计算shading时,都会从纹理中读取shading计算中使用的值之一(例如,漫反射颜色),而不是使用附加到要渲染的几何图形的属性值。
其操作过程被称为texture lookup:shading代码指定了一个纹理坐标系,坐标点处在纹理域之中。纹理匹配机制寻找到点在纹理图片中的值,然后返回该值。该值被使用于shading计算中。
texture相关部分在本书第十一章中会详议。
8.2.7Shading Frequency
决定在哪个阶段使用shading计算,取决于颜色变化的速度——细节的规模大小(the scale of the details being computed)。计算大规模特征shading时(比如曲面上的漫反射),其细节规模较小,因此可以用low shading frequency进行计算;计算小规模特征shading时(比如高亮或者纹理细节),需要high shading frequency。对于需要锋利等等的细节,shading frequency需要至少对每个像素进行计算。
所以大规模效果(large-scale effects)可以在vertex processing stage进行shading计算。
对于需要精细的计算,若图元足够小,顶点相距很近,则可在vertex processing stage进行计算;如果图元大小大于一像素,则可在fragment processing stage进行计算。
举实际应用中的例子,应用于电脑游戏的hardware pipelines,常常使用覆盖多个像素的图元,然后在fragment processing stage进行shading计算以保证效果。而像RenderMan此类的software pipelines,则使用的是per-vertex shading。其中所有的曲面都会被细分为更小的四边形,称为micropolygons,其大小和像素差不多一样。由于图元很小,per-vertex shading可以达到很高的质量。
8.3简单的抗锯齿(Simple Antialiasing)
如光线追踪一样,如果对于像素的绘制仅仅有绘制或根本不绘制两个选择,光栅化会产生锯齿状的直线和三角形边缘。

上边是抗锯齿处理后的线,下边为锯齿状的线。
在光栅化应用中,有几种抗锯齿的方法。一个方法称为box filtering,这种方法把像素值与像素附近图像的值进行求均值,如上图所示。
详细至具体的操作方法,对于box filtering最简单的做法是supersampling(超采样):生成高分辨率的图像,然后再进行下采样(downsample)。
8.4Culling Primitives for Efficiency
object-order rendering的一个优势是其只需要遍历场景中的几何对象一次,但这在复杂场景中也是一个缺点。比如一个场景中有一整座城市,我们需要呈现的仅仅是城市的一角。尽管绘制所有的图元能显示正确的图像,但很多时间都浪费在了看不见的几何体上,比如被建筑遮盖的建筑,观察者背后的建筑等等,而这些几何对象不会对显示的图像造成影响。
*culling(直译:淘汰,无中文术语。。)*是指辨别和抛弃场景中看不见的几何对象,以节省时间,提高性能。
culling主要有以下三种方法:
- view volume culling:the removal of geometry that is outside the view volume(去除视域外的几何体);
- occlusion culling:the removal of geometry that may be within the view volume but is obscured, or occluded, by other geometry closer to the camera(去除视域内被遮盖的几何体);
- backface culling:the removal of primitives facing away from the camera(去除背对相机的图元).
高性能的culling在一个高性能的系统中是非常复杂的,此处不再展开。
总结
目前多数三维使用的均为基于光栅的图形管线,理解过程之下管线可能没有光线追踪如此直观,但了解每一个步骤的目的后也不难理解。下图是根据MDN的插图所做的进一步基础补充。

以上内容若有不足之处,还请大佬指教。
参考资料
Geometric primitive(图元)- Wikipedia
Fragment (computer graphics) - Wikipedia
Explaining basic 3D theory - MDN
插值(interpolation)- Wikipedia
参数方程(parametric equation)- Wikipedia
隐函数(implicit equation)- Wikipedia
Rasterizing Triangles: The Left Edge - YouTube
Blending - OpenGL Wikipedia
图形管线(Graphics pipeline)- Wikipedia