图形学补完计划(二):Rendering Pipeline
参考:
图形渲染管线
图形渲染管线是实时渲染的核心组件。图形渲染管线实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程。渲染管线是实时渲染的重要工具,实时渲染离不开渲染管线。
图形渲染管线主要包括两个功能:一是将物体3D坐标转变为屏幕空间2D坐标,二是为屏幕每个像素点进行着色。渲染管线的一般流程如下图所示。分别是:顶点数据的输入、顶点着色器、曲面细分过程、几何着色器、图元组装、裁剪剔除、光栅化、片段着色器以及混合测试。
渲染管线的一个特点就是每个阶段都会把前一个阶段的输出作为该阶段的输入。例如,片段着色器会将光栅化后的片段(以及片段的数据块)作为输入进行光照计算。除了图元组装和光栅化几个阶段是由硬件自动完成之外,管线的其他阶段管线都是可编程/可配置的。其中顶点着色器、曲面细分相关着色器、几何着色器和片段着色器是可编程的阶段,而混合测试是可高度配置的阶段。管线的可编程/可配置是渲染管理的另一个特点。因为早期的渲染管线采用的是立即渲染模式(Immediate mode,也就是固定渲染管线),不允许开发人员改变GPU渲染的方式,而核心渲染默认(Core-profile mode)允许开发人员定制化GPU的渲染方式。
渲染管线并非严格这样划分,不同的教材会有不同的划分方法。《Real Time Rendering》一书将渲染管线划分为以下四个阶段:应用程序阶段(Application)、几何处理阶段(Geometry Processing)、光栅化(Rasterization)和像素处理阶段(Pixel Processing)。应用阶段通常是在CPU端进行处理,包括碰撞检测、动画物理模拟以及视椎体剔除等任务,这个阶段会将数据送到渲染管线中;几何处理阶段主要执行顶点着色器、投影变换、裁剪和屏幕映射的功能;光栅化阶段和我们上面讨论的差不多,都是将图元离散化片段的过程;像素处理阶段包括像素着色和混合的功能。我们可以发现,虽然管线的划分粒度不一样,但是每个阶段的具体功能其实是差不多的,原理也是一样的,并没有太大的差异。
本节就按照 RTR4 中对于渲染管线的划分方式,分别分析渲染管线各个阶段的定义及作用。
应用程序阶段
应用程序阶段的主要任务,是识别出潜在可视的网格实例,并把它们及其材质呈交至图形硬件以供渲染。该阶段包含三大任务:可见性判断、控制着色器参数和渲染状态、提交图元至GPU硬件以供渲染。应用程序阶段在CPU端完成,后面的所有阶段都是在GPU端完成。
应用程序阶段,该阶段主要是在软件层面上执行的一些工作,包括空间加速算法、视锥剔除、碰撞检测、动画物理模拟等。大体逻辑是:执行视锥剔除,查询出可能需要绘制的图元并生成渲染数据,设置渲染状态和绑定各种 Shader 参数,调用 DrawCall,进入到下一个阶段,GPU渲染管线。
几何阶段
几何阶段主要负责大部分多边形操作和顶点操作,将三维空间的数据转换为二维空间的数据,可以分为顶点着色(Vertex Shader)、投影变换、裁剪和屏幕映射阶段。
从上述的流程中可以看出,VertexData 对应的就是应用程序阶段 CPU 将数据提交给 GPU 渲染的步骤。
几何阶段和上述管线中对应的步骤有:顶点着色器、曲面细分、几何着色器、图元组装、裁剪;其中,曲面细分和几何着色器是可选阶段。
我们知道,从输入的顶点局部坐标到最终的屏幕坐标需要经过一系列的坐标变换,才能最终显示到屏幕上。下面的图片展示了顶点坐标的一系列变换过程。我们从建模工具得到的是物体的局部坐标(Local Coordinate),局部坐标通过模型矩阵Model变换到世界坐标(World Coordinate),世界坐标通过观察矩阵View变换到观察坐标(View Coordinate),观察坐标经过投影矩阵Projection变换到裁剪坐标(Clip Coordinate),裁剪坐标经过透射除法(Perspective Division)得到标准设备空间(Normalized Device Coordinates,NDC),NDC坐标通过视口变换(Viewport Transformation)变换到窗口坐标进行显示。
关于这部分的详细推导与内容可以查看上一篇博客
顶点着色器
顶点着色器主要功能是进行坐标变换。将输入的局部坐标变换到世界坐标、观察坐标和裁剪坐标。虽然我们也会在顶点着色器进行光照计算(称作高洛德着色),然后经过光栅化插值得到各个片段的颜色,但由于这种方法得到的光照比较不自然,所以一般在片段着色器进行光照计算。
总的来说,Vertex Shader 就是对顶点数据进行 MVP 矩阵变换,最终将输入顶点从局部坐标系转换到裁剪坐标系中。
高洛德着色 (Gouraud Shading)
-
平面着色 (Flat Shading)
根据光照计算的不同,可以将着色方式分为平面着色、高洛德着色和冯氏着色。其中,最简单的着色方式就是平面着色。平面着色是使用一个顶点颜色来代表整个三角面的颜色,默认是使用索引中第一个顶点的颜色,例如,第一个顶点是红色,那么整个三角面都是红色的。上图最左边的图像就是利用平面着色渲染得到的,我们可以看到平面着色几乎无法展示高光效果。 -
高洛德着色 (Gouraud Shading)
高洛德着色就是在顶点着色器中计算顶点的光照信息,所以也叫作逐顶点着色。高洛德着色是平面着色和冯氏着色的折中方案,该方法会在顶点着色器中计算三个顶点的光照信息,然后在光栅化阶段插值得到三角形内部各个片段的光照信息。上图中间的图像就是利用高洛德着色渲染得到的,我们可以看到在高光部分高洛德着色表现的并不是很令人满意,原因在于高光并非是线性变换的,所以通过插值得到的效果比较差。 -
冯氏着色 (Phong Shading)
冯氏着色就是在片段着色器中计算每个片段的光照信息,所以也叫作逐片段着色。冯氏着色是三种着色方式中效果最好的,也是性能消耗最大的着色方式。冯氏着色会在根据输入的顶点法线信息在光栅化阶段插值得到各个片段的法线信息,然后在片段着色器中利用法线、纹理坐标、位置等信息计算每个片段的光照信息。开销最大,同时效果也是最好的,我们可以从上图右边的图像看到冯氏着色渲染得到的,通过对比高洛德着色和冯氏着色的高光部分,我们看发现冯氏着色处理得更加自然真实。Phong Shading 作用于 Fragment Shader 阶段,放在这里只是为了查看方便。
从上面的讨论我们发现:这三种着色方案中平面着色效果最差(计算量最少),高洛德着色其次(计算量其次),冯氏着色最好(计算量最多)。我们需要根据实际的需求选择对应的着色方式,需要在画面效果和性能开销之间进行取舍。
曲面细分
曲面细分是利用镶嵌化处理技术对三角面进行细分,以此来增加物体表面的三角面的数量,是渲染管线一个可选的阶段。它由外壳着色器(Hull Shader)、镶嵌器(Tessellator)和域着色器(Domain Shader)构成,其中外壳着色器和域着色器是可编程的,而镶嵌器是有硬件管理的。我们可以借助曲面细分的技术实现细节层次(Level-of-Detail, LOD)的机制,使得离摄像机越近的物体具有更加丰富的细节,而远离摄像机的物体具有较少的细节。
我们不创建高模(high-poly)来丰富网格信息主要是考虑到以下几个原因:第一,基于GPU可以实现动态的LOD技术,可以根据物体距离摄像机的远近来调整多边形网格的细节,比如说,若物体距离摄像机比较远,则按照高模的规格对它进行渲染会造成浪费,因为我们根本看不清网格的所有具体细节。随着物体和摄像机之间距离的拉近,我们可以实现连续镶嵌化处理,增加物体的细节;第二,节省内存。我们可以在各种存储器中保存低模网格信息,再根据需求用GPU动态地增加物体表面的细节。
几何着色器
几何着色器(Geometry Shader)属于渲染管线的一个可选阶段,位于曲面细分(Tessellation)和光栅化(Rasterization)之间。顶点着色器以顶点数据作为输入数据,而几何着色器则以完整的图元(Primitive)作为输入数据。例如,以三角形的三个顶点作为输入,然后输出对应的图元。与顶点着色器不能销毁或创建顶点不同,几何着色器的主要亮点就是可以创建或销毁几何图元,此功能让GPU可以实现一些有趣的效果。例如,根据输入图元类型扩展为一个或更多其他类型的图元,或者不输出任何图元。需要注意的是,几何着色器的输出图元不一定和输入图元相同。几何着色器的一个拿手好戏就是将一个点扩展为一个四边形(即两个三角形)。几何着色器通常用来实现一种叫做公告栏(BillBoards)的视觉效果。
几何着色器优点很明显:可以修改网格(增删改模型的顶点及三角面等)。但缺点也很明显:几何体着色器并行调用硬件困难,并行程度低。且对移动端不友好,需要Shader Model 4.0以上
几何着色器输出的图元由顶点列表定义而成,而且顶点必须变换到裁剪空间。也就是说,经过几何着色器处理后,得到的是一系列位于齐次裁剪空间的顶点所组成的图元。这些顶点会在后面的裁剪、透视除法和光栅化阶段得到进一步处理。
一个问题是,图元装配与几何着色器的顺序,这部分内容可以参考 OpenGL Wiki。
大致可以理解为,如果几何着色器或图元细分(Tessellation)启用,则会将这部分所需图元进行提前进行装配。所以说图元装配和几何着色器二者哪个在前都有一定道理。
图元装配(Primitive Assembly)
所谓图元就是虚拟空间中构成物体的最基本的几何单位,如点、线、三角形等,程序可以直接绘制任何一个图元。
这个阶段主要有两个任务,一个是图元组装,另一个是图元处理。图元组装将输入的顶点组装成指定的图元;例如,点绘制方式下仅需要一个单独的顶点,每个顶点为一个图元;线段绘制方式则需要两个顶点,每两个顶点构成一个图元。图元处理会进行裁剪和背面剔除相关的优化,以减少进入光栅化的图元的数量,加速渲染过程。在光栅化之前,还会进行屏幕映射的操作:透视除法和视口变换。
关于透视除法和视口变换到底属于流水线的那个阶段并没有一个权威的说法,某些资料将这两个操作归入到图元组装阶段,某些资料将它归入到光栅化过程,但对我们理解整个渲染管线并没有太大的影响,我们只需要知道在光栅化前需要进行屏幕映射就可以了,所以我们这里将屏幕映射放到了图元组装过程。这两个操作主要是硬件实现,不同厂商会有不同的设计。
裁剪(Clipping)
只有当图元部分或全部位于视椎体内时,我们才会将它送到流水线的下个阶段,也就是光栅化阶段。而完全位于视椎体外部的图元会被裁剪掉,不会对它们进行渲染。对于部分位于视椎体的图元,我们需要对他们进行所谓的剪裁操作,例如,线段的两个顶点一个位于视椎体内而另一个位于视椎体外,那么位于外部的顶点将被裁剪掉,而且在视椎体与线段的交界处产生新的顶点。通过之前顶点着色器的投影变换后,视椎体是一个立方体,这有利于我们进行裁剪操作,使得该操作变得更加容易和一致。
从上图中我们可以看到红色的三角形被丢弃了,而绿色的三角形被送到了光栅化阶段,需要特别留意的是黄色三角形被部分裁剪了,位于视椎体外的顶点被裁剪了,并且在交界处产生了两个新的顶点代替原来的旧顶点。裁剪阶段使用的是4-分量的齐次坐标,通过执行透射除法我们可以得到归一化设备坐标(Normalized Device Coordinates,NDC),然后通过视口变换将NDC坐标变换到屏幕坐标。我们注意到透射除法在裁剪操作之后进行,可以保证透射除法中 w != 0。常见的裁剪算法有Cohen-Sutherland算法、Liang-Barsky算法和Sutherland-Hodgman多边形裁剪算法。
在应用程序阶段,已经由 CPU 进行了一次视锥剔除,即只将视锥体内的物体送到 GPU 进行渲染;为什么有了视锥剔除,到这个阶段还需要进行一次裁剪呢?简单来说就是两次裁剪的粒度不同,前者是在物体对象层面的,一般对对象的包围盒做剔除,剔除掉不在视锥体内的物体,几何阶段的裁剪是在三角形层面做的,裁剪掉不在屏幕内的像素。
背面剔除(Back-Face Culling)
背面剔除指的是剔除那些背对摄像机的图元,如下图所以,图元t1背向摄像机,需要被剔除,而图元t2需要被保留。我们利用三角形顶点的环绕顺序(Winding Order)来确定所谓的正面(front-face)和背面(back-face)。通常情况下,三角形的3个顶点是逆时针顺序(couter-clockwise,ccw)进行排列时,我们会认为是正面,而顺时针(clockwise,cw)排序时,我们会认为是背面。例如t2的三个顶点顺序为逆时针的(v1,v2,v3),所以是正面,需要保留。将t1和t2投影到XY平面后,我们可以清楚的看到它们顶点的环绕顺序。
从上图可以看到,这里我们可以使用行列式(determinant)来确定投影后的2D三角形到底是CW还是CCW顺序。行列式的第一行由顶点v1和v2坐标确定,而第二行由顶点v1和v3坐标确定。如果行列式的值为负数,那么该三角面是背面朝向;如果为正数,则是正面朝向。
虽然背面剔除可以大概减少50%的渲染图元,但是在渲染半透明或不透明物体时,不能使用该技术,否则会出现穿帮的情况,因为半透明或不透明物体可以看到物体背后的东西。
光栅化阶段
经过图元组装以及屏幕映射阶段后,我们将物体坐标变换到了窗口坐标。光栅化是个离散化的过程,将3D连续的物体转化为离散屏幕像素点的过程。包括三角形组装和三角形遍历两个阶段。光栅化会确定图元所覆盖的片段,利用顶点属性插值得到片段的属性信息,然后送到片段着色器进行颜色计算,我们这里需要注意到片段是像素的候选者,只有通过后续的测试,片段才会成为最终显示的像素点。
这里有个概念需要我们注意,就是如何定义图元覆盖(Overlap)一个片段。如果我们采用最简单的点采样(Point Sampling)而且采样点位于片段的中央位置。当采样点位于图元的内部时,我们认为图元覆盖了该片段。这是最简单的采样方式,除此还有超级采样和多重采样技术,我们会在抗锯齿的部分展开介绍。
三角形组装 (Triangle Setup)
三角形组装会计算出三角形的一些重要数据(如三条边的方程、深度值等)以供三角形遍历阶段使用,这些数据同样可用于各种着色数据的插值。
三角形遍历 (Triangle Traversal)
三角形遍历会找到哪些像素被三角形所覆盖,并对这些像素的属性值进行插值。通过判断像素的中心采样点是否被三角形覆盖来决定该像素是否要生成片段。通过三角形三个顶点的属性数据,插值得到每个像素的属性值。此外透视校正插值也在这个阶段执行。里对法线的插值进行了可视化处理。不过我们这里只是进行一个简单的示范,一般情况下,我们很少去插值颜色值,通常都是利用片段着色器对片段进行着色。
这两个阶段是完全硬件控制的,不可进行任何操作。
像素处理阶段
这一阶段包括片段着色器(Fragment Shader)与测试混合两个部分
片段着色器(Fragment Shader)
片段着色器在DirectX中也成为像素着色器(Pixel Shader)。片段着色器用来决定屏幕上像素的最终颜色。在这个阶段会进行光照计算以及阴影处理,是渲染管线高级效果产生的地方。这部分内容非常多,开个坑下一篇博客介绍。
测试与混合(Tests & Blending)
管线的最后一个阶段是测试混合阶段。测试包括裁切测试、Alpha测试、模板测试和深度测试。各种测试的相对顺序:裁剪->Alpha->模板->深度。
没有经过测试的片段会被丢弃,不需要进行混合阶段;经过测试的片段会进入混合阶段。
值得注意的是,半透明物体的绘制需要遵循画家算法(painter Algorithm)由远及近进行绘制,因为半透明的混合跟物体的顺序有严格的对应关系。从下面两张图我们可以看到,先绘制红色还是先绘制绿色对最终颜色的有这很大的影响。所以,绘制半透明物体之前,我们需要按照距离远近对场景中的物体进行严格排序,然而这是一个非常棘手的问题。比如,我们如何排序下面几个三角形呢?所以当进行半透明物体渲染时,一般会使用顺序无关的半透明渲染技术(Order-independent transparency,OIT)。
经过了测试合并阶段,并存到帧缓冲的像素值,才是最终呈现在屏幕上的图像。
-
裁剪测试
在裁剪测试中,允许程序员开设一个裁剪框,只有在裁剪框内的片元才会被显示出来,在裁剪框外的片元皆被剔除。裁切测试可以避免当视口比屏幕窗口小时造成的渲染浪费问题。通常情况下,我们会让视口的大小和屏幕空间一样大,此时可以不需要使用到裁切测试。但当两者大小不一样大时,我们需要用到裁切测试来避免其产生的一些问题。如下图所示:
图中的绿色方框表示视口范围,大的黑色方框表示屏幕范围,当视口小于屏幕的时候,如果不用裁剪测试,则会将视口范围外的背景白色也绘制出来,导致渲染浪费,结果也不正确。
-
Alpha 测试
像素值一般是由RGBA四个分量来表示的,其中的A是alpha,表示的是物体的不透明度。1代表完全不透明,0代表完全透明。可选的 alpha 测试可在深度测试执行前在传入片段上运行。片段的 alpha 值与参考值作某些特定的测试(如等于,大于等),如果片段未能通过测试,它将不再进行进一步的处理。 alpha 测试经常用于不影响深度缓存的全透明片段的处理。简单来说,就是根据物体的透明度来决定是否渲染。
-
模板测试
模板缓冲是用于记录所呈现图元位置的离屏缓存,如下图所示,如果使用了模板缓冲,就相当于在屏幕上有一块模板盖在上面,只有位于这个模板中的图元片段,才会被渲染出来。模板测试就是用片段指定的参考值与模板缓冲中的模板值进行比较,如果达到预设的比较结果,模板测试就通过了,然后用这个参考值更新模板缓冲中的模板值;如果没有达到预设的比较结果,就是没有通过测试,就不更新模板缓冲。简单来说,就是根据物体的位置范围决定是否渲染。
模板测试在 UGUI 中的应用:Mask 组件
-
深度测试
我们在观察物体的时候,位于前面的物体会把后面的物体挡住,所以在渲染的时候,图形管线会先对每一个位置的像素存储一个深度值,称为深度缓冲,代表了该像素点在3D世界中离相机最近物体的深度值。于是在计算每一个物体的像素值的时候,都会将它的深度值和缓冲器当中的深度值进行比较,如果这个深度值小于缓冲器中的深度值,就更新深度缓冲和颜色缓冲的值,否则就丢弃;简单来说,就是根据物体的深度决定是否渲染。
Early-Z是一种提前深度测试的技术,它位于光栅化阶段之后,像素处理阶段之前,目的是减少进入像素着色阶段的片段,优化性能。Early-Z会带来透明测试的冲突,例如某个片元A虽然遮挡了另一个片元B,但A却是透明的,GPU应当渲染的是片元B,这就产生了矛盾,这就是透明度测试会导致性能下降的原因(因为无法用Early-Z),但是有一种叫PreZ的技术可以解决这个问题。
在经过了上述的裁切测试、Alpha测试、模板测试和深度测试后,最终还要经过Alpha混合。从前面我们已经知道,颜色一般采用RGBA四分量来进行表示,其中颜色的Alpha值用来表示物体本身的不透明度,引入Alpha技术是为了实现半透明的效果。当场景中既有不透明物体,又有半透明物体时,我们需要先渲染不透明物体,渲染顺序为从前往后(Front-to-Back);然后再渲染半透明物体,渲染顺序为从后往前(Back-to-Front)。
我们需要对不透明和半透明物体分开渲染是因为:我们可以透过半透明物体看到半透明物体背后的东西,所以对半透明物体进行渲染时需要后面图层的信息,才能够正确进行混合。
总结
到此为止,基本上就把图形渲染管线的各个阶段分析清楚了,光栅化阶段的抗锯齿方法与片段着色器的具体内容留到其他的文章中分析。
现在再看这些描述渲染管线流程的示意图,是不是就清晰多了。