渲染流水线

渲染(Render)是指通过计算机程序将二维图像或者三维模型经过一系列计算工序处理为图像的过程。将输入的模型、图像按照流程依次处理再输出,因此实施渲染工作的程序也被成为渲染流水线(Rendering Pipeline)

将一个点花到屏幕上,首先需要知道一个 模型空间(Object Space) 或者 本地空间(Local Space),并且知道这个空间中顶点的坐标,然后计算出本地空间到世界空间(World Space) 的变换矩阵,就可以相应得到这个顶点在世界空间中的坐标,最后再得到由世界空间到屏幕空间(Screen Space) 的变换矩阵,就可以计算得到这个顶点的屏幕坐标了。

一个世界中的点是否可以被画到屏幕上,使用 视锥体(View Frustum) 来实现,一种常见的视锥体有一个顶点(观察者的位置)和五个面,在视锥体内部的顶点都是可以被看到的点,而视锥体有两个参数,一个是棱锥张开的角度(即视野Field of View,FOV)和最远视距。除此之外还可以添加一个最近视距来限制最近能看到什么,这时候视锥体也从棱锥变成棱台。

以视锥体为参考系建立一个空间,将所有看不到的东西裁剪掉,这个空间就叫做裁剪空间(Clip Space)

整体的渲染流水线就可以整理为:
信息输入->模型空间->世界空间->裁剪空间->判断点是否能看到->将看得到的点转到屏幕空间->找出各个三角形->对三角形进行插值->根据插值结果涂色

以上就是CPU的渲染流程,即在一个CPU中分步骤地执行以上流程。为了提高效率,考虑GPU。CPU首先将数据传给GPU,然后GPU的各个计算单元分别元对这些数据进行简单的处理,最后组装出完整的图像。完整的处理的过程大致可以分为3个阶段,应用结果输出渲染图元,进入几何阶段输出屏幕空间的顶点信息,最后进入光栅化阶段

应用阶段(Application Stage)

应用阶段主要由 CPU 负责,CPU 从 RAM 得到初始数据然后进行数据处理和剔除,然后将这些数据和如何处理的方式(着色器)丢给GPU

CPU 将数据传递给 GPU 后还需要向 GPU 下达一个渲染指令,即Draw call。CPU向命令缓冲区放入一个个命令,然后由GPU以此取出,但是由于往往GPU的渲染速度超过了CPU提交命令的速度,因此就导致渲染中大部分时间都消耗在了 CPU 提交 Draw Call上,解决的方法是使用批处理(Batching),即把要渲染的模型合并在一起提交给 GPU。

几何阶段(Geometry Stage)

几何阶段由GPU负责,主要就是渲染流水管线,以 OpenGL 为例就是

从应用阶段得到图元数据->顶点着色器->曲面细分着色器->几何着色器->投影->裁剪->屏幕映射

其中顶点着色器曲面细分着色器几何着色器都由开发者完整编程控制,裁剪部分开发者不能完全控制但是可以进行一些配置,投影屏幕映射则已经由 GPU 固定实现。

  1. 顶点着色器: 每个顶点相互独立,计算单元不需要考虑其他顶点的状态。除此之外,GPU 还需要在这里进行模型转化与相机转换(Model- & Camera transformation) ,和之前「把模型从模型空间转移到世界空间」不一样的是,这里为了方便后续运算,还需要将顶点的空间由世界空间映射到摄像机的观察空间

  2. 曲面细分着色器: 在这里进行曲面细分操作,看起来就像在原有的图元内加入了更多的顶点。

  3. 几何着色器: 几何着色器与顶点着色器都可以对顶点的坐标进行修改,但是建议是除了顶点增、删这些仅仅能用几何着色器实现的效果,其余的顶点修改还是由顶点着色器完成,几何体着色器并行调用硬件困难,并行程度低,效率比顶点着色器低很多。

  4. 投影: 在这个阶段 GPU 将顶点从摄像机观察空间转换到裁剪空间,为之后的剔除过程以及投射到二维平面做准备。常见的投影方式有 透视投影(锥体,有远的地方看起来小的效果) 和 正交投影(方体,没有远的地方看起来小的效果) 。计算时需要考虑 远裁剪平面(Far Clipping Plane)近裁剪平面(Near Clipping Plane) ,即摄像机最远看到的位置和最近看到的位置。透视投影需要额外考虑 视野(FOV) ,即视锥体张开的角度。而在正交投影中,因为是方体,远裁剪平面和近裁剪平面一样大,用裁剪平面的 尺寸 来表示它能看到的范围。

  5. 裁剪: 即将摄像机看不到的顶点剔除出去,然后再把顶点映射到屏幕空间,这里得到的 $x、y、z$ 要进行归一化,范围在 $[-1, 1]$ 之间,因为屏幕是 2 维空间,所以只需要 $x$ 和 $y$ 分量,而 $z$ 分量不会被丢弃,而是写入 深度缓冲(Z-Buffer) 之中。

  6. 屏幕映射: 经过裁剪后得到的 $x$ 和 $y$ 坐标在 $[-1,1]$ 之间,在这里进行映射到特定分辨率的屏幕上。

光栅化阶段

光栅化阶段可以分为以下步骤:

从几何阶段收集到的顶点信息->进行图元组装->三角形遍历->片元着色器->逐片元操作

  1. 图元组装: 这个过程做的工作就是把顶点数据收集并组装为简单的基本体(线、点或三角形)

  2. 三角形遍历: 这个过程将检验屏幕上的某个像素是是否被一个三角形网格覆盖,被覆盖的区域将生成一个 片元(Fragment) ,一个像素是一个小正方形,显然像素不一定会被完全覆盖,有3种方案: Standard Rasterization (中心点被覆盖即被划入片元)、Outer-conservative Rasterization (只要覆盖到就划入片元)、Inner-conservative Rasterization (完全被覆盖才划入片元) 。一个片元包含了很多种状态的集合(屏幕坐标、深度、法线、纹理等),GPU 还将对覆盖区域的每个像素的进行插值计算,因为只知道这个三角形片元的三个顶点的信息,中间部分的信息就通过插值得到。

  3. 片元着色器: 它将为每个片元计算颜色,除此之外还可以实现一些诸如法线贴图等效果,这里是对每一个片元进行操作的,每一个片元之间相互独立。

  4. 逐片元操作: 主要的工作有两个,对片元进行 测试(Test) 并进行 合并(Merge) 。测试步骤决定了片元最终会不会被显示出来,主要的测试步骤有 透明度测试(Alpha Test)模板测试(Stencil Test)深度测试(Depth Test)

    • 透明度测试: 对片元的透明度值进行检测,仅仅允许透明度值达到设置的阈值后才可以会绘制。
    • 深度测试: GPU 将读取片元的深度值(之前留下来的坐标 z 分量)与缓冲区的深度值进行比较。
    • 模板测试: GPU 将读取片元的模板值与模板缓冲区的模板值进行比较,比较方式由程序员决定。