概念性阶段划分
应用阶段->几何阶段->光栅化阶段
应用阶段:由CPU负责输出渲染所需的几何信息(即渲染图元(rendering primitives)),渲染图元可以是点、线、三角面等,这些渲染图元被传递给几何阶段。
应用阶段大致可分为下面3个阶段:
(1) 把数据加载到显存中。
(2) 设置渲染状态。
(3) 调用Draw Call
把数据加载到显存中
所有渲染所需的数据都需要从硬盘(Hard Disk Drive, HDD)中加载到系统内存(Random Access Memory, RAM)中。然后,网格和纹理等数据又被加载到显卡上的存储空间——显存(Video Random Access Memory, VRAM)中。这是因为,显卡对于显存的访问速度更快,而且大多数显卡对于RAM没有直接的访问权利。
渲染状态
这些状态定义了场景中的网格是怎样被渲染的。例如,使用哪个顶点着色器(Vertex Shader)/片元着色器(Fragment Shader)、光源属性、材质等。
调用Draw Call
Draw Call就是一个命令,CPU通过调用Draw Call来告诉GPU开始进行一个渲染过程。一个Draw Call会指向本次调用需要渲染的图元列表。
几何阶段:这一阶段在GPU上进行,几何阶段负责和每个图元打交道,进行逐顶点、逐多边形的操作。这阶段的一个重要任务就是把顶点坐标变换到屏幕空间中,再交给光栅器进行处理。
光栅化阶段:这一阶段将会使用上个阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。这一阶段也是在GPU上运行。光栅化的任务主要是决定每个渲染图元中的哪些像素应该被绘制在屏幕上。它需要对上一阶段得到的逐顶点数据(例如纹理坐标、顶点颜色等)进行插值,然后再进行逐像素处理。
GPU流水线
上图为GPU渲染流水线实现。颜色表示了不同阶段的可配置性或可编程性:绿色表示该流水线阶段是完全可编程控制的,黄色表示该流水线阶段可以配置但不可编程,蓝色表示该流水线是由GPU固定实现的,开发者没有任何控制权。实线表示该Shader必须由开发者编程实现,虚线表示该Shader是可选的。
顶点数据:这些顶点数据是由应用阶段加载到显存中,再由Draw Call指定的。这些数据随后被传递给顶点着色器。
顶点着色器(Vertex Shader):是完全可编程的,它通常用于实现顶点的空间变换、顶点着色等功能。
顶点着色器必须完成的工作是:把顶点坐标从模型空间转换到齐次裁剪空间。
例如: o.pos = mul(UNITY_MVP, v.position);
顶点着色器会将模型顶点的位置变换到齐次裁剪坐标空间下,进行输出后再由硬件做透视除法得到NDC(设备坐标)下的坐标。
需要注意的是,齐次空间的z分量,在OpenGL中同时也是Unity使用的NDC中的范围在[-1, 1]。而在DirectX中,NDC的z分量范围是[0, 1]。顶点着色器可以有不同的输出方式。最常见的输出路径是经光栅化后交给片元着色器进行处理。而在现代的Shader Model中,它还可以把数据发送给曲面细分着色器或几何着色器。
齐次空间:一个单位立方体空间。
曲面细分着色器(Tessellation Shader):是一个可选的着色器,它用于细分图元。
几何着色器(Geometry Shader):同样是一个可选的着色器,它可以被用于执行逐图元(Per-Primitive)的着色操作,或者被用于产生更多的图元。
裁剪(Clipping):这一阶段的目的是将那些不在摄像机视野内的顶点裁剪掉,并剔除某些三角图元的面片。这个阶段是可配置的。例如,我们可以使用自定义的裁剪平面来配置裁剪区域,也可以通过指令控制裁剪三角图元的正面还是背面。
简单的说就是裁剪掉不在齐次空间中的顶点。
屏幕映射(Screen Mapping):这一阶段是不可配置和编程的。它负责把每个图元的坐标转换到屏幕坐标系中。即,把图元在齐次空间中的坐标转成屏幕坐标。屏幕坐标系的原点在OpenGL中是左下角在DirectX中是左上角。
三角形设置:这个阶段会计算光栅化一个三角网格所需的信息。具体来说,上一个阶段输出的都是三角网格的顶点,即我们得到的是三角网格每条边的两个端点。但如果要得到整个三角网格对像素的覆盖情况,我们就必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式。这样,一个计算三角网格表示数据的过程就叫做三角形设置。
三角形遍历:此阶段会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话,就会生成一个片元(fragment)。而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换(Scan Conversion)。片元中的状态是对3个顶点信息进行插值得到的。这一步的输出就是一个片元序列。需要注意的是,一个片元不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了(但不限于)它的屏幕坐标、深度信息,以及其他从几何阶段输出的顶点信息,例如法线、纹理坐标等。
片元着色器(Fragment Shader):是另一个非常重要的可编程着色阶段。在DirectX中,片元着色器被称为像素着色器(Pixel Shader)。但片元着色器是一个更合适的名字,因为此时的片元并不是一个真正意义上的像素。
前面的光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据信息,用来表示一个三角网格是怎样覆盖每个像素的。而每个片元就负责存储这样一系列数据。真正会对像素产生影响的阶段是下一个流水线阶段——逐片元操作(Per-Fragment Operations)。
逐片元操作(Per-Fragment Operations):逐片元操作是OpenGL中的说法,在DirectX中,这一阶段被称为输出合并阶段(Output-Merger)。Merger这个词可能更容易让读者明白这一步骤的目的:合并。而OpenGL中的名字可以让读者明白这个阶段的操作单位,即是对每一个片元进行一些操作。
逐片元操作的过程:
片元->模板测试->测试测试->混合->颜色缓冲区
光栅化概念阶段中的三角形设置(Triangle Setup)和三角形遍历(Triangle Traversal)阶段也都是固定函数(Fixed-Function)的阶段。接下来的片元着色器(Fragment Shader),则是完全可编程的,它用于实现逐片元(Per-Fragment)的着色操作。最后,逐片元操作(Per-Fragment Operations)阶段负责执行很多重要的操作,例如修改颜色、深度缓冲、进行混合等,它不是可编程的,但具有很高的可配置性。
注意:
OpenGL2.0才开始支持可编程管线。OpenGL3.2中已经完全移除了固定管线的概念。
OpenGL ES2.0才开始支持可编程管线。
DirectX8.0才开始支持可线程管线。