一、什么是渲染流水线
图形的渲染流程被拆分成多个阶段,每个阶段独立并发执行而不用等待整个渲染流程完成后才开始下一个渲染任务,大大的提高了处理速度,这样的工作方式使得GPU能够并发快速的批量处理大量数据,提高渲染速度。
从大概念上来说,渲染流程分为3个阶段:
每个阶段同时又是个流水线,包含一系列更小的流水线阶段。
- 应用程序阶段:在cpu中进行,完全由开发者控制。该阶段主要是准备好场景数据,如场景物件、相机和光源,并设置好渲染数据(顶点数据和纹理等)和渲染状态,准备好之后就可以调用Draw Call通知GPU执行渲染命令了。
- 几何阶段:在GPU上进行,对渲染图元进行逐顶点、逐多边形操作。该阶段的主要任务是把把顶点坐标转换到屏幕空间中,然后交给光栅化阶段
- 光栅化阶段:在GPU中进行,该阶段接收几何阶段处理好的逐顶点数据并决定每个渲染图元所覆盖的像素范围,根据上一阶段输出的逐顶点数据对覆盖的每个像素进行插值,然后进行逐像素处理并渲染出图像。
二、应用程序阶段以及CPU和GPU之间的通信
渲染流水线的起点是CPU,即应用程序阶段,该阶段大概分为3个阶段:
- 数据准备和填充:将网格顶点和纹理数据加载到显存中
- 渲染状态设置
- 调用Draw Call
所有的渲染数据都需要从系统内存(RAM)加载到显存(VRAM)中,这是因为显卡对显存的访问速度更快,而且大多数的显卡都没有直接访问RAM的权利。
加载到显存之后就可以从内存中移除这些数据了,但对于一些在后续仍然需要访问的数据(如网格数据我们需要访问以进行碰撞检测)我们会选择一直持有, 因为实时创建或者从硬盘加载这些数据也很耗性能。- 在该阶段应用开发者需要通过CPU来访问GPU
三、GPU流水线
应用程序阶段之后的2个概念性阶段都是在GPU中进行的,他们一起构成了GPU渲染流水线,他们同时各自包含若干更小的流水线阶段。
- 几何阶段
- 光栅化阶段
光栅化阶段的目标是:计算图元覆盖了哪些像素,并计算这些像素的颜色。
包含以下阶段:
- 顶点着色器
该阶段负责坐标转换和逐顶点光照。
顶点着色器必须要完成一个任务是把顶点坐标从模型空间转换到其次裁剪空间,接着在进行透视除法得到归一化的设备坐标(Normalized Device Coordinates,NDC)。
NDC坐标把视野可见的顶点坐标线性到(-1,1)范围内,OpenGL和Unity中都使用的该范围,在DirectX中的z分量范围是[0,1]。 - 裁剪和剔除
该阶段主要是把不在视野范围(坐标不在NDC范围内)的图元剔除掉,同时剔除不需要绘制的面,一般是背面。 - 屏幕映射
该阶段的输入时NDC坐标仍然是三维坐标,屏幕映射负责把该坐标转换到屏幕空间坐标,这一转换也叫视口变换。 - 图元装配
这一阶段的输入是三角形的顶点信息,图元装配需要根据这些顶点计算出三角形的边界像素和覆盖的像素范围,也就是说这一阶段我们需要得到片元(三角形)的表示形式。 - 图元遍历
该阶段会遍历所有的像素,计算出该片元覆盖了哪些像素,并且根据三角形的三个顶点信息对整个覆盖区域的像素进行插值。 - 片元着色器
片元着色器阶段输入是片元,片元包含其所覆盖的所有像素经过插值得到的顶点信息结果,输入是一个或者多个颜色值。在该阶段最重要的技术是纹理采样。 - 合并/混合阶段
这一阶段的也叫逐片元操作,其操作单位是片元,也即是对每一个片元进行的操作,它的目的是合并。为了完成这一目的它的主要任务有:
- 检测可见性。这一阶段主要进行一些测试工作,如模板测试、深度测试
- 如果通过了所有的测试,接下来就要考虑片元的颜色如何与颜色缓冲中的颜色进行合并(或者说混合)
逻辑上来讲这些测试都是在片元着色器之后进行,但实际应用中为了提高性能,现代GPU都希望尽可能在片元着色器之前进行测试以确定哪些片元应该被舍弃,这样的话就不需要片元着色器计算他们的颜色。
Unity给出的渲染流水线中的深度测试是在片元着色器之前进行的,这种深度测试提前的技术成为Early-Z技术。
但是,测试提前的话不能保证检验结果总是对的,可能会与片元着色器中的一些操作冲突。例如,当我们在片元着色器中开启了透明度测试或者修改了深度值,如果透明度测试失败或者在片元着色器中修改后深度值不通过测试,理应舍弃该片元,但是前面的深度测试可能已经写入深度缓冲了,这就导致了冲突。
因此,现代GPU会检测片元着色器中是否有和提前测试冲突的操作,如果有就禁用提前测试。但是这样的话就有更多的片元经过片元着色器处理后才被舍弃而不是提前就被舍弃掉,这也是透明测试导致性能下降的原因。