渲染流水线
0. 整体流程
通常,渲染流程分成3个阶段:应用阶段(Application Stage)、几何阶段(Geometry Stage)、光栅化阶段(Rasterizer Stage)。
其中,应用阶段在CPU中进行,在CPU完成应用阶段后,GPU从CPU那里得到渲染命令后,开始几何阶段往后的系列流水线操作。
(在Unity shader入门精要一书中,逐片元操作在光栅化阶段中。)
1. 应用阶段
在应用阶段中,CPU需要处理4个主要步骤。
1.1 基本场景数据
CPU首先需要准备好场景数据,例如摄像机的位置、视锥体、场景中包含了哪些模型、使用了哪些光源等等;在这一阶段,需要布置好场景数据。
1.2 粗粒度剔除
为了提高渲染性能,CPU需要做一个粗粒度剔除(culling)工作,目的是为了把不可见的物体剔除出去,这样就不需要再移交给几何阶段进行处理;
1.3 渲染设置
然后需要设置好每个模型的渲染状态。这些渲染状态包括但不限于它使用的材质(漫反射颜色、高光反射颜色)、使用的纹理、使用的Shader等。这一段最重要的输出是渲染所需的几何信息,即渲染图元(rendering primitives)。通俗来讲,渲染图元可以是点、线、三角面等。
1.4 输出到显存
所有渲染所需的数据都需要从硬盘(Hard Disk Drive, HDD)中加载到系统内存(Random Access Memory, RAM)中。然后,网格和纹理等数据又被加载到显卡上的存储空间——显存(Video Random Access Memory, VRAM)中。这是因为,显卡对于显存的访问速度更快,并且大多数显卡对于RAM没有直接的访问权利。
渲染状态
这些状态定义了场景中的网格是怎样被渲染的。例如,使用哪个顶点着色器(Vertex Shader)/片元着色器(Fragment Shader)、光源属性、材质等。
当准备好上述工作后,CPU便调用“Draw Call”这个渲染命令来告诉GPU开始渲染。
值得一提的是:一次Draw Call 会指向本次调用需要渲染的图元列表。而CPU与GPU的处理速度不同,他们之间的通信次数会影响整体速度。因此优化的其中一个思路便是减少“Draw Call”的次数,尽量使一次“Draw Call”让GPU处理更多的图元。
2. 几何阶段
几何阶段包含以下几个部分
2.1 顶点着色器——视图变换
该阶段是完全可编程的,通常用于实现顶点的空间变化、顶点着色等功能。
- 坐标变换:对顶点的坐标进行变换。
这在 顶点动画中非常有用:可以通过改变顶点位置来模拟水面、布料等。
但需要注意,前提,一个最基本的顶点着色器必须完成的一个工作是,把顶点坐标从模型空间转换到齐次裁剪空间。o.pos = mul(UNITY_MVP, v.position);
类似上面代码的功能就是把顶点坐标转换到齐次裁剪坐标系下,接着再由硬件做透视除法后,最终得到归一化的设备坐标(Normalized Device Coordinates, NDC)。
几个坐标系的关系:
2.2 曲面细分
这是一个可选的着色器,它用于细分图元。
2.3 几何着色器
同样是一个可选的着色器,可以被用于执行逐图元(Per-Primitive)的着色操作,或者被用于产生更多的图元。比如下图的变换图元:
2.4 投影
Perspective projection ( P ) 透视投影
Orthographic projection ( O ) 正交投影
2.5 裁剪
只有在单位立方体的图元才需要被继续处理,因此部分不完全在单位立方体的图元会被裁剪,生成一个新的图元。
和顶点着色器不同,这一步是不可编程的,即我们无法通过编程来控制裁剪的过程。
2.6 屏幕映射
**屏幕映射(Screen Mapping)的任务是把每个图元的x和y坐标转换到屏幕坐标系(Screen Coordinates)**下。屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。
3. 光栅化阶段
从上一个阶段输出的信息是屏幕坐标系下的顶点位置以及和它们相关的额外信息,如深度值(z坐标)、法线方向、视角方向等。光栅化阶段有两个最重要的目标:计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色。
3.1 三角形设置(Triangle Setup)
上一个阶段输出的都是三角网格的顶点,但如果我们要得到整个三角网格对像素的覆盖情况,我们就必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式。这个计算三角网格表示数据的过程就叫做三角形设置。
3.2 三角形遍历(Triangle Travelsal)
检查每个像素是否被一个三角网格所覆盖。如果被覆盖,则会生成一个片元(fragment),而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换(Scan Conversion)
这一步的输出就是得到一个片元序列,注意,一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了(但不限于)它的屏幕坐标、深度信息,以及其他从几何阶段输出的顶点信息,例如法线、纹理坐标等。
片元着色器(Fragment Shader)
片元着色器是另一个非常重要的可编程着色器阶段。
前面的光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据信息,用来表述一个三角网格是怎样覆盖每个像素的。每个片元就负责存储这一系列数据。
片元着色器的输入是上一个阶段对顶点信息插值得到的结果,更具体来说,是根据上一步插值后的片元信息,片元着色器计算该片元的输出颜色。
这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。
4. 逐片元操作(Per-Fragment Operations)
渲染流水线的最后一步,逐片元操作,在DirectX中,这一阶段被称为输出合并阶段(Outpu-Merger)。
这一阶段有几个主要任务:
- 决定每个片元的可见性。这涉及很多测试工作,例如深度测试、模板测试、透明度测试等。
- 如果一个片元通过了所有的测试,那么再把这个片元的颜色值和已经存储在颜色缓冲区的颜色进行合并,或者说是混合。(如同PS中图层操作一般,下图层先进入颜色缓冲区,上图层通过了测试,最后该像素呈现的颜色就是上图层与下图层混合的颜色,而混合模式就如同PS中的图层模式)
5. 后处理
例如bloom、HDR、Depth of View(景深效果)等等都属于后处理范畴。