首页 > 代码库 > 渲染管线

渲染管线

2.1为什么要介绍渲染管线?

??在微软DirectX10.0规范的统一渲染架构发布以前,渲染管线曾经是选购显卡的一项重要指标。然而采用流处理器渲染架构,由于硬件工作效率更高,目前已经逐渐取代了采用渲染管线的传统架构,在消费领域渲染管线的概念慢慢被淡化了。但是在计算机图形学领域,渲染管线却依然有着举足轻重的地位,因为它依然是实时渲染的基本原理。虽然在上一章提到了渲染管线这一概念,但并没有给出详细的解释,在这一章我要对渲染管线进行一个深入的分析。

2.2什么是渲染管线

??渲染就是把3D场景转换到屏幕上2D图像的一个处理过程。

技术分享
3D渲染过程

??有了渲染概念,渲染管线的概念就更加清晰了,它是显示芯片内部处理图形信号相互独立的的并行处理单元。每个阶段从上一个阶段接收输入,处理完成后输出到下一个阶段,整个过程就是为了完成一帧画面渲染。在某种程度上可以把渲染管线比喻为工厂里面常见的各种生产流水线,工厂里的生产流水线是为了提高产品的生产能力和效率,而渲染管线则是提高显卡的工作能力和效率。
??在这里还得解释一下渲染管线和GPU渲染管线的关系。在GPU问世的初期,处理能力非常有限,渲染管线很大程度上依赖于CPU,整个管线的任务是由CPU和GPU共同协作完成的。当然随着GPU日益强大,GPU承担了管线中所有的任务,所以现在渲染管线这一概念就等同于GPU渲染管线了。

2.3渲染管线的结构

??在Real-Time Rendering一书中,作者把渲染管线分成了三个阶段:应用程序阶段、几何阶段以及光栅阶段。
??应用程序阶段,使用高级编程语言进行开发,主要和CPU 、内存打交道,诸如碰撞检测、场景图建立、空间八叉树更新、视锥裁剪等经典算法都在此阶段执行。在该阶段的末端,几何体数据(顶点坐标、法向量、纹理坐标、纹理等)通过数据总线传送到图形硬件。
??几何阶段,主要负责顶点坐标变换、光照、裁剪、投影以及屏幕映射,该阶段基于 GPU 进行运算,在该阶段的末端得到了经过变换和投影之后的顶点坐标、颜色、以及纹理坐标。
??光栅阶段,基于几何阶段的输出数据,为像素正确配色,以便绘制完整图像,该阶段进行的都是单个像素的操作,每个像素的信息存储在帧缓存中。
??但是我并不完全认同Real-Time Rendering中的阶段划分,目前主流的阶段划分方式只有后面两个阶段。尽管应用程序阶段是图形渲染的重要一环,但是它处理的任务只是优化渲染过程的输入数据以及相关逻辑处理,并不是真正的核心的渲染过程,而且该过程主要在CPU中完成。所以在接下来我将详细地阐述这两个阶段的过程。
??渲染管线根据硬件架构的不同分为固定渲染管线和可编程渲染管线,焦点在于GPU是否具有可编程性。

2.4固定渲染管线

??在可编程渲染管线出现之前,渲染管线就是指固定渲染管线,后来为了便于区分才添加了“固定”二字。固定渲染管线具备了渲染管线流水线作业的优势,将复杂的过程分解成小的阶段,几何数据按工作流程依次经过各个阶段的处理,大大增强了渲染作业的处理效率。同时几何阶段和光栅阶段算法都是固化在GPU中,编程人员在开发时无须关注几何阶段的各种变换算法以及光栅阶段的着色算法,开发效率大幅提高。图2- 2展示了固定渲染管线的整个流程。

技术分享
固定渲染管线流程

2.4.1几何阶段

??几何阶段的主要工作就是三维顶点坐标变换和光照计算,由显卡中的“T&L”硬件来完成。
??什么是“T&L”硬件?“T&L”英文全称是“Transform&Lingting”,中文意思几何变换和光照。“T&L”硬件其实一个硬件级别的几何与光照转换引擎,它在GPU中的出现,使得CPU从复杂运算中解脱出来。这样一来,一个场景中可以添加更多的物体和几何细节,最终渲染效果更加细腻,同时渲染效率也大幅提升。
??那么为什么需要进行三维顶点坐标的变换?原因很简单,我们的真实世界是一个三维的空间,而显示屏幕是二维的平面,为了将三维的数据更加真实地绘制到屏幕上,并达到“跃然纸上”的效果,我们就需要顶点变换。通过一系列顶点变换,物体在世界空间被转换到屏幕空间。
??根据顶点坐标变换的先后顺序,主要有这样几个坐标空间:物体空间、世界空间、观视空间以及屏幕空间。

物体空间到世界空间

??物体空间的是一个相对独立的局部空间,与其他物体没有任何参照关系。物体空间中的坐标都是在3DS MAX这类建模工具中生成的。
??而世界空间与物体空间的关键区别就在于前者中所有的物体把需要坐标原点作为参考点。物体导入场景中之后,需要为物体指定一个位置,这个位置就是物体在世界空间的一个全局坐标。物体空间到世界空间的变换由一个一个四阶矩阵来完成,称作世界矩阵。
??光照计算通常是在世界坐标空间中进行的,这也符合人类的生活常识。当然,也可以在观视空间中得到相同的光照效果,因为,在同一观察空间中物体之间的相对关系是保存不变的。
在这里有一点非常重要。就是物体顶点的法相量是在物体空间中生成的,它也是需要变换到世界空间才能正常使用,和顶点坐标类似。但是二者的转换矩阵却是不一样的,法向量变换的矩阵是世界矩阵转置的逆矩阵。在固定管线中,虽然这一变换不需要我们手动完成,但是理解清楚对于后面的可编程管线非常重要。

世界空间到观视空间

??每个人都是从各自的视点出发观察这个世界,无论是主观世界还是客观世界。同样,在计算机中每次只能从唯一的视角出发渲染物体。在游戏中,都会提供视点漫游的功能,屏幕显示的内容随着视点的变化而变化。这是因为GPU 将物体顶点坐标从世界空间转换到了观视空间。
??所谓观视空间,即以视点为原点,由视线方向、视角和远近平面,共同组成一个梯形体的三维空间,称之为视锥,如图所示。近平面,是梯形体较小的矩形面,作为投影平面,远平面是梯形体较大的矩形,在这个梯形体中的所有顶点数据是可见的,而超出这个梯形体之外的场景数据,会被视锥裁剪去掉。

技术分享
视锥

观视空间到投影空间

??理论上讲视锥剔除应该在投影之前完成,事实上这样也是可以的,但是有一个问题就是视锥是一个不规则的几何体,在其中完成多边形剔除并不是一件很容易的事。所以目前采用的方式是先投影,后剔除。
??但是这个投影并非简简单单投影到一个二维平面上,而是将观视空间中的场景投影到一个单位立方体中,俗称CCV(Canonical view volume)[5]。三维场景投影到三维空间,听起来也许很诧异。其实本质上还是投影到了二维平面上,因为CCV近平面的X、Y坐标与屏幕坐标相对应,Z坐标表示像素的深度值。我们常用的投影方式有两种:正投影和透视投影。从人眼观察世界的物理原理上来看,透视投影更加符合人类的视觉习惯。
从数学上来讲,这个投影过程就是观视空间的顶点坐标乘以投影矩阵,顶点就变换到了投影空间CVV中。
??视锥剔除过程现在变得简单一些了,因为视体由视锥变成了CVV,位于CVV外面的图元全部被裁剔除,部分位于CVV之外的需要进行裁剪。这一过程在下面的图元装配时完成。

图元装配

??顶点流经过变换后,按照管线的顺序,下一个流程就是图元装配。所谓图元装配,就是根据之前的图元分类信息把顶点装配成图元。在这个过程中,将生成一些三角形、线段和点。之前是对顶点处理,现在就是对图元就行裁剪,对于超出屏幕之外的图元进行裁剪。对于游戏来讲,一般装配的图元都是三角形,如果三角形的一部分在屏幕外面,那么经过裁剪之后就变成了四边形,这个四边形又需要分割成两个小三角形。到现在这个阶段,就只剩下基本的图元,不存在复杂的几何图形。
??到此为止,渲染管线的几何阶段就完成了,几何阶段的工作比较简单明晰,就是顶点变换和光照。接下来就是非常重要的光栅阶段。

2.4.2光栅阶段

光栅化

??图元装配后生成的图元要显示在屏幕上必须经过光栅化。光栅化就是决定哪些像素被几何图元覆盖的过程。每种图元根据指定的规则分别被光栅化,涉及一些填充算法如扫描线多边形填充算法、边界填充算法等,这里不做讨论。光栅化的结果就是片段位置的集合。当光栅化之后,一个图元拥有的顶点数目和产生的片段数目之间没有任何关系。比如,一个由三个顶点组成的三角形,占据整个屏幕需要上百万的片段。
??这个有必要解释一下片段的概念。说到片段,不得不提到像素。像素是图像元素的简称。一个像素代表帧缓存中某个指定位置的信息,比如颜色、深度和其他与这个位置关联的值。而片段就是像素形成之前的一个状态,可以算作潜在的像素。在完成最后的光栅操作,更新片段信息至帧缓存后,片段就成了名副其实的像素。

插值、贴图以及着色

??当图元被光栅化成一个或者多个片段后,就会根据需要对片段进行插值、执行一系列的贴图以及数学操作,然后为每个片段确定最终颜色。除了确定最终的颜色意外,还需要确定最终的深度值,或者丢弃这个片段以避免更新帧缓存中对应像素。

光栅操作

??光栅操作是在更新帧缓存之前,执行的最后的一系列片段操作。这些操作是Direct3D和OpenGL的一个标准组成部分。
光栅操作会根据许多测试来检查每一个片段,这些测试包括裁剪测试、alpha测试、模板测试以及深度测试。如果其中一项测试不通过,片段就会被丢弃。如果所有测试都通过,执行完后续操作后,就会更新帧缓存中的对应像素值。
技术分享
标准OpenGL和Direct3D的光栅操作流程

2.5可编程渲染管线

??固定渲染管线看似很完美,预制好的算法,操作简单,整个流程各环节分工明确,配合紧密。但是这样经典的模型同样存在着诸多短板,导致整个渲染的效率和质量很难再大幅提升。曾经的优势随着科技的进步反倒成了劣势,算法都固化在硬件内部,所有开发人员只能采用同一种固定的渲染方式,这样极大的束缚了开发人员的常造型思维。固定渲染管线已经完全不能满足时代对于渲染的要求,所以一种新的渲染模型——可编程渲染管线诞生了。
可编程渲染管线顾名思义,它的关键就在于GPU内部提供了可编程性。这种可编程性的出现,对开发人员来说是一种思想的解放。

2.5.1可编程渲染管线VS固定渲染管线

??对比一下可编程渲染管线与固定渲染管线的流程图,可以很明显的发现他们之间的共性和差异。
??关于共性,整个渲染流程大体上都是一致的,都是分为几何阶段和光栅阶段,其中流水线中顶点变换、图元装配、光栅化、插值、光栅操作等关键操作都是一致的。只是在具体操作中采用的方式不同而已。
??重点就在于二者的差异。可以看到图中的可编程渲染管线多了两条分支——可编程顶点处理器和可编程片段处理器。在几何阶段没有了一系列坐标变换的过程,这缘于“T&L”硬件被移除了,取而代之的是可编程顶点处理器,顶点变换和光照计算都由它来完成。在光栅阶段,颜色计算过程由可编程片段处理器取代了。
??接下来将要详细介绍可编程处理器。

2.5.2可编程处理器

??可编程处理器是内嵌在GPU上的一种可编程单元,拥有非常强大的并行计算能力,并且擅长不高于4阶的矩阵运算。而且随着技术的发展,对浮点运算能力越来越强大。
??可编程处理器还有个别名——着色器,使用的比较广泛。所以上面的两种可编程处理器也叫做顶点着色器和片段着色器(或者像素着色器)。
??可编程处理器只是一个单纯的硬件,还需要有运行在处理器上的程序来驱动,而这种程序叫做着色器程序,也叫shader程序。程序和处理器之间有一一对应关系,顶点shader程序运行在顶点着色器上,片段shader程序运行片段着色器上。前面已经提到顶点着色器接管了“T&L”硬件的任务,片段着色器负责颜色计算。那么shader程序也有各自分工,顶点shader程序负责顶点变换和光照计算,片段shader程序负责颜色计算,而且前者的输出是后者的输入。
??由于每一代可编程处理器的架构有一定差异,没有一个绝对统一的标准。本文将以遵循Shader Model 2.0规范的可编程处理器为例来介绍。

可编程顶点处理器

??在渲染3D场景的时候,顶点信息是通过Direct3D或者OpenGL这类3D渲染API传递给GPU。当GPU接收到顶点信息,它会为应用程序提交的每一个顶点调用一次顶点着色器程序。图2- 8就是可编程顶点处理器的架构图。
技术分享
可编程顶点处理器架构

??从图中我们可以看到顶点信息是以流的形式传递进来,这些信息包括顶点坐标、纹理坐标、颜色等等。信息传递完毕后,会被存放顶点数据寄存器(也叫输入寄存器)中。输入寄存器是一种只读寄存器,从图中可以看到输入寄存器有16个,v0到v15,说明最多可以存储16个顶点属性。然后,顶点处理器会通过访问其他的一些寄存器来完成几何阶段的任务。
??常量寄存器也是一种只读寄存器,存储一些预先设定的静态参数。
??临时寄存器可以进行读写操作,存放计算的中间结果。注意其中的a0和aL寄存器,他们主要用于循环计数和按索引寻址。
??由于顶点处理器可以随意访问以上三种寄存器,所以编程人员可以按照自己的方式处理操作和处理寄存器中的数据。这在固定渲染管线时代是不可想象的事情。计算完毕,定点处理器要把计算结果存入输出寄存器。这里必须把屏幕空间的顶点坐标放入oPos寄存器中,当然还可以传递其他的信息比如颜色、纹理坐标等等到其他寄存器中。结果存放完毕后,接下来就进入了图元装配阶段和光栅化阶段,这个与固定渲染管线一致,不具备可编程性。

可编程片段处理器

??光栅化操作完后,生成了一系列片段,这时候片段处理器会为每个片段调用片段着色程序。
技术分享
可编程片段着色器架构图

??片段处理器内部架构和顶点处理器基本框架是差不多的,只是有些细节上的差异。这里是输入寄存器变成了颜色寄存器和纹理寄存器,因为现在操作的对象是片段。寄存器v0和v1分别表示插值过以后的漫反射和镜面反射分量。t0到tN这一系列寄存是用于存储纹理查找坐标。在片段处理阶段,采样寄存器指向需要使用的纹理。常量寄存器和临时寄存器基本一致。
??计算完毕后,输出寄存器要把计算出的最终颜色存入寄存器。这里的寄存器除了oC0以外,还有oDepth,它里面的深度值是在光栅操作中做深度测试用的。
??两种可编程处理器介绍完了,在这里有两点需要说明一下。第一点就是在本章最开始提到的统一渲染架构,已经取消了专门的可编程处理器,shader程序与处理器之间没有对应关系。取而代之的是统一的流处理器,能运行所有类型的shader程序,这样的好处是在各类场景渲染时能保持负载均衡。第二点是逻辑上的可编程处理器还有一类可编程几何处理器,目前只有较新的硬件支持,应用不广泛,本文不做介绍。

2.6本章小结

??本章介绍了GPU 图形渲染管线,并对相关的图形硬件进行了阐述。图形渲染管线是GPU 编程的基础,事实上顶点着色程序和片段着色程序正是按照图渲染制管线而划分的。

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    渲染管线