我们总在说渲染管线,几乎每一个与TA有关的课程第一课都和渲染管线相关,这是一切图像的基础。就拿最近在用的Unity内置渲染管线为例吧,其中包含了CPU和GPU两个部分,总的流程图如下所示:

应用阶段/CPU处理阶段

image.png

应用阶段的处理场所在CPU。这一阶段主要处理以下几件事:

  • 准备好场景数据
  • 剔除不需要渲染的部分
  • 设置渲染顺序
  • 加载数据到显存
  • 调用DrawCall
  • 输出渲染图元(Rendering Primitives)作为几何阶段的输入。通俗来讲,渲染图元可以是点、线、三角面等,这些信息会传递给GPU渲染管线处理。

剔除(Culling)

CPU阶段的剔除是粗粒度的,和GPU阶段的裁剪不同。粗粒度剔除发生在物体层面,剔除是以物体1为单位进行的。这一步包含了三种剔除:

  1. 视锥体剔除
    视锥体剔除指的是剔除掉相机视锥体之外的元素,视锥体即相机的可视范围,全部在外的物体(比如下图的鸟)不需要渲染,部分在外的物体会在GPU渲染部分被裁剪。
    image.png

  2. 层级剔除
    层级剔除指物体所处的层(Layer)是否被相机所渲染。如果相机的渲染层不包含该物体的layer,则该物体不被渲染。譬如在上文的例子中,如果不渲染树木层,就会是下面的画面:
    image.png

  3. 遮挡剔除
    遮挡剔除指当一个物体被其他物体遮挡而不在摄像机的可视范围内时,不对其进行渲染。对这些物体进行渲染是浪费时间和性能的,所以要先剔除掉这部分元素以节省接下来的时间。

设置渲染顺序

渲染顺序主要由渲染队列(Render Queue)决定的,序号越小的越先渲染。Unity默认渲染队列序号如下:

  • Background(1000)
    最早被渲染的物体的队列。
  • Geometry(2000)
    不透明物体的渲染队列。大多数物体都应该使用该队列进行渲染,也是Unity Shader中默认的渲染队列。
  • AlphaTest(2450)
    有透明通道,需要进行Alpha Test的物体的队列,比在Geomerty中更有效。
  • Transparent(3000)
    半透物体的渲染队列。一般是不写深度的物体,Alpha Blend等的在该队列渲染。
  • Overlay(4000)
    最后被渲染的物体的队列,一般是覆盖效果,比如镜头光晕,屏幕贴片之类的

当渲染队列相同时,不透明队列(RenderQueue < 2500),根据摄像机距离从前往后排序,这样先渲染离摄像机近的物体,远处的物体被遮挡剔除;半透明队列(RenderQueue > 2500),根据摄像机距离从后往前排序,这是为了保证渲染正确性,例如半透明黄色和蓝色物体,不同的渲染顺序会出现不一样的颜色。

打包数据到显存

在CPU阶段,数据会先从硬盘加载到RAM中,随后网格、纹理等数据又被加载到显存上。加载到显存上之后,数据就会被移除。
之所以这么做是因为GPU访问显存的速度比访问前两者更快,而且多数显卡可能无法直接访问RAM。打包的数据主要包括:

  1. 模型信息,包括顶点坐标、法线、uv、切线、顶点颜色等等
  2. 变换矩阵V和P,这是由相机的位置和FOV决定的
  3. 灯光信息
  4. 模型的材质参数,设置对应的渲染状态

调用SetPass Call和Draw Call

  • SetPass Call
    Shader脚本中一个Pass语义块就是一个完整的渲染流程,一个着色器可以包含多个Pass语义块,每当GPU运行一个Pass之前,就会产生一个SetPassCall,所以可以理解为调用一个完整渲染流程。

  • DrawCall
    CPU每次调用图像编程接口命令GPU渲染的操作称为一次Draw Call。Draw Call就是一次渲染命令的调用,它指向一个需要被渲染的图元(primitive)列表,不包含任何材质信息。GPU收到指令就会根据渲染状态(例如材质、纹理、着色器等)和所有输入的顶点数据来进行计算,最终输出成屏幕上显示的那些漂亮的像素。

几何阶段/GPU处理阶段

处理场所:GPU
主要流程:

  1. 顶点处理
  2. 图元装配
  3. 光栅化
  4. 片元着色器
  5. 输出合并
image.png

顶点处理

在顶点处理阶段,每个顶点都会经过一次顶点着色器。虽然一个模型可能有很多很多顶点,但得益于GPU的高并发的特点,可以同时处理大量的数据,所以实际上是很快的(虽然快但不代表可以无限精细模型)。在顶点阶段完成的最基本的操作是顶点坐标的变换,从模型空间经过MVP处理变换到齐次裁剪空间。在这个阶段也可以计算顶点的光照(高洛德着色),但效果不佳。

除了顶点着色器之外,顶点处理阶段还包括曲面细分着色器几何着色器。这两种着色器是非必须的,可选可不选。

曲面细分着色器(Tessellation Shader)

主要作用是用于细分图元。曲面细分着色器是一个可选的着色器,主要是对三角面进行细分,以此来增加物体表面的三角面的数量。借助它可以实现细节层次(LOD,Level-of-Detail)的机制,使得离摄像机越近的物体具有更加丰富的细节,而远离摄像机的物体具有较少的细节。

几何着色器

几何着色器也是一个可选的着色器,它以完整的图元(比如点)作为输入数据,输出可以是一个或多个其他的图元(比如三角面),或者不输出任何的图元。几何着色器的拿手好戏就是将输入的点或线扩展成多边形。我们可以用几何着色器完成很多工作,例如绘制mesh的法线、

图元装配

裁剪(Clipping)

CPU阶段的剔除已经帮助我们去掉了所有整个在视锥体外、不需要渲染的物体。但有一些物体部分在视锥体内部分在视锥体外,针对这些物体,在视锥体外的部分就需要进行裁剪,使用一些新的顶点来代替被裁剪的部分,不再渲染视锥体外被裁减的部分,减少开销。

image.png

标准化设备坐标(Normalized Device Coordinates,NDC)

在处理完模型顶点的裁剪空间的基础上,进行透视除法(perspective division,即除w)后会得到一个长宽高均为2的正方体,这就是标准化设备坐标(NDC)。之所以边长是2是因为从原点出发三个轴的范围都在[0,1]之间。

image.png

获得NDC坐标是为了实现屏幕坐标的转换,与硬件无关。

背面剔除(Back-Face Culling)

这一步在NDC之后,剔除所有背对摄像机的三角面。上篇文章(GPU渲染管线)中我们讲到过模型数据中含有顶点和mesh的索引列表,列表中的三个点组成一个三角片,默认情况下:

  • 三个点是顺时针排列的,认为背对摄像机
  • 三个点是逆时针排列的,认为正对摄像机

关于是顺时针还是逆时针,可以通过三角形任意两边叉乘的方向判断。判断为背面则剔除,除非开启了双面渲染等设置。

屏幕映射(Screen Mapping)

屏幕映射将NDC中每个图元的x、y坐标转换到屏幕坐标系(Screen Coordinates, z坐标不做任何处理(但并不是无用的),因为屏幕是二维的。

屏幕坐标系和z坐标一起被称作窗口坐标系(Window Coordinates)。

光栅化

光栅化的主要流程是计算每个图元覆盖了哪些像素,并计算它们的颜色。即将屏幕空间的图元离散化为片元的过程,包括:

  • 三角形设置
  • 三角形遍历

三角形设置(Triangle Setup)

我们从上一个阶段获得图元的顶点信息,也就是三角面每条边的两个端点,但如果要得到整个三角网格对像素的覆盖情况,我们就必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式。这样一个计算三角网格表示数据的过程就叫做三角形设置。

三角形遍历(Triangle Traversal)

检查每个像素是否被一个三角形网格覆盖,如果覆盖的话则生成一个片元(Fragment), 并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值。这个阶段也被成为扫描变换(Scan Conversion)

经过三角形遍历我们会得到一个片元序列,但片元不等同于像素,而是包含了屏幕坐标、深度信息(通过插值得到)、顶点信息等的状态合集。

片元着色器(Fragment Shader)

片元着色器最主要的任务就是着色,光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据信息,用来表述一个三角网格是怎样覆盖每个像素的。而每个片元就负责存储这样一系列数据。着色有两种最常见的技术,分别是纹理贴图光照技术

输出合并(Output Operations)

这个阶段也被成为逐片元操作(Per-Fragment Operations),主要决定了每个片元的可见性(在这个阶段需要对片元进行多种测试,包括透明度测试、模板测试和深度测试等等)和片元的混合。

透明度测试(Alpha Test)

这是输出合并阶段片元经历的第一个测试,也是一个非常简单粗暴的测试。通过片元数据,可以获取该片元的alpha值,如果alpha值小于某个数的话,则直接将该片元丢弃,不进行渲染(即只渲染透明度在某一范围内的片元),可以用来做一些树叶镂空的效果。

模板测试(Stencil Test)

模板测试是输出合并阶段经历的第二个测试。模板测试将模板缓冲区中的模板值参考值进行比较,可以自己配置比较的方式,比较的结果决定一个片元是否通过测试。通过模板测试的片元可以参与下一阶段的深度测试(如果开启了的话),否则被舍弃。

image.png

深度测试(Depth Test)

深度测试是最后一个测试。深度测试比较了片元的深度值和已存在于深度缓冲区中的深度值,可以自己配置比较的方式,比较的结果决定一个片元是否通过深度测试。如果没有通过,需要舍弃该片元。

深度测试在正常的思维中是近处覆盖远处(深度值小覆盖深度值大,同时缓冲区内的深度值被替换成当前深度值),但通过修改深度测试的规则,可以允许物体永远出现在前方,或仅在遮挡时显示。

混合(Blend)

混合操作对于半透明物体而言十分重要。如果没有开启混合功能,GPU会直接使用片元的颜色覆盖掉颜色缓冲区中的颜色。而如果开启了混合功能,GPU会取出颜色缓冲区中的颜色(目标颜色)与片元的颜色(源颜色)根据设置的混合模式进行混合

对混合更好的理解是绘图软件(例如PS)中的混合模式(比如叠加、正片叠底、滤色等等),实际上就是混合图层与下面的图层进行混合。在片元着色器的混合中,因为存在深度(z轴参数不同),所以实际上片元也存在图层的前后关系。

提前深度测试 (Early-z)
深度测试和模板测试是在片段着色器之后进行的,这里就存在一个问题:我们费尽千辛万苦计算了一个片元的颜色,然后它没有通过测试,被抛弃了!那我们花在这个片元上的计算岂不是就浪费了吗?Yes,真是这样。
提前深度测试正是在这种情况下出现的,它在顶点着色器和片元着色器之间进行,可以在片元着色器计算之前把那些无法通过深度测试的片元剔除。不过,Early-Z Culling不是管线标准,只是硬件厂商用来加速渲染的一种优化手段,所以在不同的硬件上会有不同的实现,而且Early-Z Culling并不保证一定有效,它需要硬件的支持。