1. 机制:
流水线很常见,比如工厂中的生产流水线。为什么引入流水线?不引入的话,一个面包需要20分钟完成才能做下一个面包,引入流水线后,流水线的各个阶段能够并行执行,每个子阶段需要5分钟,每5分钟后就能做一个蛋糕的子阶段,各个子阶段并行执行则时间消耗为原来的时间消耗除以子阶段数。所以,流水线的主要特征是并行性,引入流水线的目的是提高效率。最慢的子阶段是这个流水线的瓶颈。
图形渲染管线大致分为4个子阶段:应用阶段、几何阶段、光栅化阶段和像素处理阶段。这几个阶段自身又可能包含几个子阶段。
渲染速度可以用fps、毫秒、hz表示。fps通常表示一秒钟渲染的帧数,一般用来表示某个特定帧花费的时间或者一段时间的平均帧率。毫米表示渲染一帧花费的时间,跟每帧需要的渲染的图片的复杂度有关。Hz一般跟硬件有关,比如显示器,一般是固定值。
应用阶段一般在cpu上执行,cpu一般有多核心,所以能用多线程并行的方式执行。应用阶段上的任务一般有碰撞检测、接收输入信息并反应、全局加速算法、动画、物理模拟等,应用程序不同任务也不同。应用阶段也提供后面阶段需要的数据,比如图元位置、摄像机位置、灯光信息、图源着色信息、投影矩阵等。
几何阶段处理转换、投影等操作,主要用来决定画什么,怎么画、在哪里画的问题。在gpu上执行,gpu上包含许多可编程核心和固定函数硬件。
光栅化阶段一般输入3个顶点,组成一个三角形,并将所有在这个三角形中的像素输出到下个阶段。
像素处理执行一个像素shader,得到像素颜色,也会进行深度测试,还可能做模板测试、颜色混合。
光栅化阶段和像素处理阶段也完全在gpu上执行。
2. 应用程序阶段:
由于这个阶段一般是在cpu上执行的,所以开发者能完全控制。也有可能有些应用阶段的任务要在gpu上完成,比如一些计算可以用compute shader执行。这时,gpu不是传统意义的渲染功能,而是被当成高并行通用处理器。应用阶段的操作会影响后面管线的性能。
应用阶段最后会将几何阶段需要的数据输出给后面阶段,比如图元信息,这是这个阶段的主要职责。
应用阶段一般包含碰撞检测、动画、接收输入、加速算法、剔除算法等。
3. 几何处理阶段:
gpu上的几何处理阶段一般负责逐像素和逐顶点的操作,一般可包含顶点shader、投影、剔除、屏幕映射这几个子阶段。
3.1 顶点shader
计算坐标和着色需要的信息。模型坐标转换到世界坐标,世界坐标转换到相机坐标,相机坐标转换到投影坐标。投影又分正交投影和透视投影,正交投影是平行投影的一种。这两种投影都能用一个4*4的矩阵表示。投影变换后,顶点就位于裁剪坐标系中,也就是除以w之前,是齐次坐标系。为了下个阶段:裁剪阶段的正常工作,这个阶段必须输出裁剪坐标系的坐标。尽管这些矩阵将顶点从一个体积中变换到另一个体积中,为啥叫这个为投影呢?因为显示后,z存储到z-buffer中,不再存储在贴图中了。
3.2 可选顶点处理阶段
这些阶段不太常用,也是在gpu上执行,是否应用依赖于硬件和开发者意愿。这几个阶段是互相独立的。
细分shader:当一个球面数很低的时候,离近看,效果不好;但如果这个球面数很高,离远了,性能会比较费。所以这时候就需要有细分阶段。经过细分,一个曲面能生成合适数目的三角形。顶点可以用来表示一个曲面,一个曲面由多个片构成,片由顶点集合构成。场景摄像机决定生成顶点的数目,片离得越近,生成的三角形越多;片离得越远,生成的三角形越少。
几何shader:出现的比细分shader早,所以用的也更普遍。比细分shader简单,因为在规模和输出图元类型上有一些限制。几何shader一个普遍的应用是粒子生成,比如烟花,由一个顶点生成一个面对摄像机的四边形。
Steam Output阶段:这个阶段让我们能把gpu当作几何引擎来用。每个顶点不是必须要映射到屏幕进行渲染,可以输出为数组,用来进一步处理,可以给cpu用,也可以给gpu自身用。这个阶段一般可以用来做粒子模拟。
这3个阶段的执行顺序是:细分、几何和流输出,每个阶段都是可选的。如果我们继续往下,会有一些在裁剪坐标系中的顶点,用来判断是否对相机是可见的。
3.3 裁剪
完全在视锥体内的图元,会传输到下个阶段,完全不在视锥体内的图元,不会被传输到下个阶段。要进行裁剪的,是那些跟视锥体相交的图元。比如一个线段,一半在视锥体内,一半在视锥体外,那么,在相交的地方会生成一个新的顶点,代替原来的图元。投影矩阵的使用意味着图元要基于单位体积进行裁剪。进行投影变换也是为了裁剪操作能够统一。
用户也能用额外的裁剪平面对物体进行裁剪。
裁剪阶段用齐次裁剪坐标进行裁剪。插值不是在三角形中进行线性插值的,w值也会被考虑,这样用透视投影的时候,插值会更正确。
最后,会应用透视除法,将坐标转换到ndc坐标系下。
3.4 屏幕映射
进入这个阶段时,坐标仍然是3维的,xy就会转换到屏幕坐标空间。屏幕坐标和z坐标一起成为窗口坐标。z坐标也被映射到一个范围,openGL(-1,1),DirectX(0,1).这个窗口坐标和重新映射后的z值被传输到光栅化阶段。
对于OpenGL来说,左下角坐标为(0,0);对于DirectX来说,左上角为(0,0).
4 光栅化阶段
查找在一个图元中的所有像素。分为两个子阶段:三角形建立(图元组装)和三角形遍历。因为图元类型可能是线段等,但是三角形比较普遍,所以这两个阶段名称里都带着“三角形”。光栅化阶段也叫扫描转换阶段。将屏幕空间的二维坐标的顶点转化为屏幕上的像素。
一个像素是否被认为和一个三角形重叠,取决于你怎么设置管线。最简单的方法,在一个像素的中心用点的采样,如果中心点在三角形中,那么说这个像素在三角形中。也可以用超级采样或者多采样抗锯齿技术,一个像素上进行1次以上的采样。也可以用保守的方法,只要像素的一部分在三角形中,就认为这个像素在三角形中。
4.1 三角形建立阶段
三角形的特征,边缘公式会被计算,用于三角形遍历阶段。三角形上顶点处理阶段输出的的着色数据也会被插值。这个任务用固定函数硬件实现。
4.2 三角形遍历阶段
这个阶段检测像素是否被三角形覆盖,会为被三角形覆盖的像素生成一个片元。查找哪些像素或者采样在三角形中的过程被称为遍历。每个片元的属性用三角形顶点的属性进行插值得到,这些属性包括深度和着色属性。插值过程中,透视正确被考虑到。图元中的所有采样和像素都会被送到像素处理阶段。
5. 像素处理阶段
分为像素着色和merge阶段。
5.1 像素着色
像素着色阶段会输出一个或者多个颜色。逐像素着色计算都在这个阶段进行,用插值的着色数据作为输入。纹理采样是这个阶段用的最多的技术。
5.2 merge
像素的颜色存储在colorbuffer中,merge阶段的目标是将像素输出的片元颜色与color buffer中的颜色进行混合。这个阶段不是完全可编程的,但是是可配置的。这个阶段会判断这个片元是否可见,通过z-buffer,z-buffer的复杂度为O(n),如果当前z比zbuffer的值小,则更新color buffer和zbuffer的值。zbuffer允许图元以任意顺序渲染,这是它流行的一个原因。但zbuffer的一个缺陷在于,它只能存一个值,他不能用于半透明物体。所以,半透明物体要以从后向前的顺序进行渲染或者用一种分开的,基于顺序的方式进行渲染。
stencil buffer一般用来标识图元位置,用来实现一些效果,控制color buffer和zbuffer的渲染。一般stencil buffer有8位。
管线最后的这些功能都被叫做光栅操作或者混合操作。将color buffer中的颜色和像素处理生成的颜色进行混合。一般是用api配置的,不是完全可编程的。
经过光栅化,被摄像机看到的像素会输出到屏幕上。为了避免用户看到光栅化的过程,用双缓存机制。一定时间,交换buffer。
6 总结
这不是唯一一种管线方式,还有其他类型的管线比如光线追踪等。