渲染流水线的三个阶段#
-
应用阶段
应用阶段是完全由自己编程实现的,是CPU进行工作的部分。在应用阶段,我们要做的有以下几个任务:- 准备场景数据,包括摄像机的位置、视锥体、场景中的模型、使用的光源等;
- 为了提高渲染性能而进行的遮挡提出(culling)工作;
- 设置渲染状态,渲染状态包括每个模型使用的材质、纹理、shader等等。
应用阶段最重要的任务就是在最后输出渲染图元,这些渲染图元将传递给几何阶段。在应用阶段的最后,我们会调用DrawCall命令,它指向了一个等待被渲染的图元列表,然后GPU根据渲染状态中设置的材质纹理等等信息,对所有输入的图元进行几何阶段和光栅化阶段的计算。所以在实际的应用中,我们针对DrawCall的优化就是尽可能的复用材质,然后根据材质来排序合并模型顶点,以减少DrawCall调用次数,因为CPU同GPU的通信是非常消耗性能的。
-
几何阶段
几何阶段将会执行逐顶点、逐多边形的操作,几何阶段是在GPU上执行的。在几何阶段,我们主要执行以下几个阶段任务:- 顶点着色器阶段
顶点着色器(Vertex Shader)必须由开发者实现,是完全可编程的,它通常用于实现顶点的着色和空间变换。输入进来的每个顶点都会调用一次顶点着色器,由于GPU有较多的并行运算单元,所以执行速度会非常快,但同时我们无法通过顶点着色器创建或销毁任何顶点,也无法得到任何顶点与顶点间的关系对顶点的着色包括逐顶点光照,逐顶点着色等等,对顶点的空间变化必须包括将顶点从模型空间转换到齐次裁剪空间,后面硬件会做透视除法,得到归一化的设备坐标NDC,以用于以后进行的裁剪操作。对顶点的空间变化也可以通过UV进行一些其他的变化以实现一些特殊的效果,例如动态的水面等等。关于这些图形理论和数学细节可以查阅相关文档,我有空也会进行一些博客的更新。
- 曲面细分阶段
曲面细分着色器(Tessellation Shader)是一个可选择的着色器,主要用于细分图元。具体的讲,就是通过增加三角形来对一个网格的三角形进行细分,使得网格细节更加丰富。我们可以通过使用曲面细分操作来执行LOD(level -of-detail),也可以在低细节的模型上处理动画和物理效果,降低对CPU的性能消耗,而同时在GPU中进行曲面细分,增加细节表现。同时,在内存中维护一个低细节模型,而在GPU中动态的增加模型细节也是个很好的节省内存的方法。
在Direct3D11之前,曲面细分是在CPU中完成的,这显然是种吃力不讨好的办法。而在Direct3D 11中提供了一个可以完全在硬件上实现的曲面细分API,曲面细分就显得很有价值了。
- 几何着色器阶段
几何着色器(Geometry Shader)也是一个可选择的着色器。这个阶段的输入是完整的图元,也就是说,如果图元的基本单位是三角形,那么输入的就是三角形的三个顶点,这三个顶点是由顶点着色器或曲面细分着色器输入的。几何着色器可以根据某些条件完整的剔除图元,或者是拓展创建新的图元,例如将一个点拓展为一个四边形等。这与顶点着色器阶段不同,顶点着色器是输入一个顶点就输出一个顶点。
- 裁剪阶段
顾名思义,裁剪就是将齐次裁剪空间外的图元舍弃,在齐次裁剪空间内的图元保留,部分在齐次裁剪空间内的图元进行裁剪。这样做的好处是减少GPU要处理图元数量,提升性能。难点在于处理部分在齐次裁剪空间内的图元,这里会在图元和齐次裁剪空间相交的部分生成新的顶点,而原来在外部的顶点被舍弃。这个阶段是不可编程的,但是可以通过给定一个裁剪操作来进行配置。裁剪完成后,会将齐次裁剪空间做透视除法,得到归一化的设备坐标NDC。
- 屏幕映射
屏幕映射阶段(Screen Mapping)的输入是NDC内的坐标,我们的目标是将每个顶点的x,y坐标转换到屏幕坐标系(Screen Coordinates)。根据屏幕分辨率的不同,或者说窗口大小的不同,可能会对图元进行一些拉伸。屏幕映射不会对z坐标做任何处理,同时,屏幕映射后的x,y坐标和z坐标一起构成了窗口坐标系,这些顶点值会传递给光栅化阶段。
注意一点,direct3d的屏幕坐标系原点在左上角,而opengl的坐标系原点在左下角,d3d中NDC的z值为[0,1]而opengl是[-1,1]。
-
光栅化阶段
现在我们得到了屏幕坐标系下,每个顶点的xy坐标以及他们的深度值z,还有诸如顶点的法线方向、视角方向等。在光栅化阶段我们主要要计算每个图元占据哪些像素,以及这些像素的颜色。光栅化流水线有以下阶段:- 三角形设置
在顶点着色器阶段我们得到的都是一个三角形的顶点信息,即每个三角形每条边的两个端点,我们要光栅化一个三角形网格,就是要得到这个三角形网格对每个像素的覆盖情况,那么我们就必须计算每条边上的像素坐标,为了这个目标,我们就要知道三角形边界的表示方式,这样的一个计算三角形网格表示方式的过程就叫三角形设置(Triangle Setup),它为下个阶段做准备。
- 三角形遍历
三角形遍历(Triangle Traversal)会计算每个三角形网格覆盖的像素,并用顶点对覆盖区域像素进行插值。如果一个像素被一个三角形网格所覆盖,那么就会生成一个片元,这个片元包含了屏幕坐标、深度信息、法线信息、纹理坐标等等,用于计算像素的最终颜色,但片元并不是真正的像素,它与像素最大的区别在于,一个像素可能对应着深度不同的多个片元。
- 片元着色器
片元着色器(Fragment Shader)在direct3d也叫像素着色器(Pixel Shader),可是片元并不是真正的像素,所以这个名字并不准确。在三角形设置和三角形遍历阶段,我们得到了每个片元的数据信息,这些信息包括屏幕坐标、纹理坐标、法线、深度等等,我们通过对片元信息的处理,得到每个片元的最终颜色数据,供后面的阶段使用。
片元着色器是一个完全可编程的阶段,所以我们可以在这个阶段完成很多的渲染技术,最重要的例如纹理采样。我们的片元是通过对顶点信息进行插值得到的,在顶点着色器阶段,我们输出每个顶点对应的纹理坐标,通过插值,我们得到了每个片元对应的纹理坐标。
片元着色器虽然可以完成很多重要的效果,但是它每次只能影响单个片元,不能将自身的结果发送给其他的片元。
- 逐片元操作
逐片元操作(Per-Fragment Operations)在direct3d中叫输出合并阶段(Output Merger,OM),这一阶段对片元依次进行模板测试、深度测试、混合后,得到最终的像素颜色。
模板测试(Stencil Test)是做什么的呢?对于每个像素,给定一个模板参考值,这个参考值可以指定,对每个片元,拥有一个模板值,将每个片元的模板值和其所对应的像素的模板参考值进行比较,比较的函数也可以由开发者指定,符合的通过,不符合的舍弃。当然,最终是否通过,还要取决于深度测试。最后还要根据比较结果,对该模板参考值做指定的修改处理,这个修改处理也可以由开发者指定,例如在模板测试失败的时候,模板缓冲区保持不变,在通过时将模板缓冲区对应的位置的模板参考值加1等。模板测试通常用于限制渲染的区域。也可以做一些更高级的使用,例如渲染阴影、渲染轮廓等等。
深度测试(Depth Test)也是高度可配置的。深度测试会将每个片元的深度值和深度缓冲区中的深度参考值进行比较,这个比较函数可以由开发者设置。通常情况下,当一个片元的深度值大于深度缓冲区中的值的时候,我们就会舍弃这个片元,因为我们只想显示出离摄像机最近的物体,而被其他物体遮挡的物体就没必要显示在摄像机上面了。和模板测试不同的是,一个片元如果没有通过深度测试,那么他就没有权利修改修改深度模板缓冲区对应的深度参考值,即使一个片元通过了深度测试,他是否能够覆盖深度模板缓冲区的参考值,还取决于是否开启了深度写入。这让我们很方便的制作透明效果。
对于不透明物体,我们关闭混合(Blend),让它的颜色值直接覆盖颜色缓冲区的像素值,对于透明物体,我们使用混合操作来让这个物体看起来是透明的。混合操作也是高度可配置的,不论是对原像素值进行相加、相减、还是相乘等,通常和透明通道相关。
在Unity3d中,深度测试被放到了片元着色器之前,这个技术叫做Early-Z技术,不难理解,我们希望在片元着色器中处理更少的片元,而那些早就被抛弃的片元应该在片元着色器处理之前就舍弃掉。但是这也带来了一个问题,可能会与我们在片元着色器中的一些操作冲突,例如如果我们在片元着色器中进行透明度测试,如果一个片元没有通过透明度测试,我们手动调用API将其舍弃了,但是在深度测试的时候,我们已经舍弃了它后面的片元,这就会导致显示出现错误。这种冲突导致我们无法使用Early-Z,所以在现代的GPU中,会判断片元着色器是否进行了一些会导致冲突的操作,如果有这种操作,那么就会禁用Early-Z,这也带来了一定的性能下降。