计算机图形学笔记(一)渲染管线概述

计算机图形学笔记(一)渲染管线概述

写在前面:因为之前有学习过图形学,博文是对过去知识的拾遗和对学习过程中的总结记录,所以在内容组织上可能不符合逻辑,某些比较熟悉的内容不会体现或简述;在整理资料的时候发现,在学习初期就对渲染管线进行总结会使得内容非常片面,所以随着学习的深入会对这部分内容不断补充、修正。不足之处还请见谅。
※内容大部分参考了《Unity Shader入门精要》。
※墙裂推荐本文最后的参考,大佬们的资料真的非常细致易懂。

1. 什么是渲染管线

渲染管线(渲染流水线)是将三维场景模型转换到屏幕像素空间输出的过程。图形渲染管线接受一组3D坐标,然后把它们转变为屏幕上的有色2D像素输出。

流水线可以类比汽车工业的发展,在1913年前福特开发出汽车流水线前,汽车组装只能让一位位工人逐工序完成,年产不过12台,效率极低;而引入了流水线概念后,每位工人只需要做不停地做同一道工序,所有工序并行进行,极大地提高了工厂的生产效率,生产效率提高了8倍。
GPU对图像处理的高效率体现了同样的思路,GPU采用了数量众多的计算单元和超长的流水线,但每一个部分只有非常简单的控制逻辑(如同《摩登时代》中一个流水线工人只负责拧一个螺丝)。尽管计算能力不如CPU,但耐不住人多力量大;这就好比拿出一百道十以内加减法运算题给一百个小学生和一个资深大学教授来做,尽管小学生能力并不如何,但这么多个小学生同时做这些题消耗的总时长,总比一个学识渊博大学教授做要来得快。

流水线可抽象为三个阶段:应用阶段、几何阶段、光栅化阶段。
应用阶段:这是一个由开发者完全控制的阶段,在这一阶段将进行数据准备,并通过CPU向GPU输送数据,例如顶点数据、摄像机位置、视锥体数据、场景模型数据、光源等等;此外,为了提高渲染性能,还会对这些数据进行处理,比如剔除不可见物体;最后还要设置每个模型的渲染状态,这些渲染状态包括但不限于所使用的材质、纹理、shader等。这一阶段最重要的输出是渲染所需的几何信息,即渲染图元,通俗来讲渲染图元可以是点、线、面等。
几何阶段:几何阶段运行在GPU中,几何阶段用于处理我们要绘制的几何相关事情,它和每个渲染图元打交道。几何阶段最重要的任务是将顶点坐标变换到屏幕空间中。后面会对几何阶段进行更详细的表述。
光栅化阶段:光栅化阶段运行在GPU中,其主要任务是决定每个渲染图元中哪些像素应该被绘制在屏幕上,它需要对上一阶段得到的逐顶点数据进行插值,然后进行逐像素处理。

2. CPU与GPU之间的通信

渲染管线的起点是CPU,CPU与GPU的通信即上文的应用阶段,在这个阶段可分为以下三个步骤:
(1)把数据加载到显存:大多数显卡没有直接访问RAM的能力,将数据加载到显存中使GPU能更快的访问这些数据。当把数据加载到显存后,内存中的数据便可以释放了,但对于一些还需要使用的数据则需要继续保留在内存中,如CPU需要网格数据进行碰撞检测。
(2)设置渲染状态:渲染状态的一个通俗解释就是,定义了场景中的网格是怎样被渲染的。例如,使用哪个顶点着色器/片段着色器、光源属性、材质等。如果不设置渲染状态,那所有的网格将使用同一种渲染,显然这是不希望得到的结果。
(3)调用Draw Call:当所有的数据准备好后,CPU就需要调用一个渲染指令告诉GPU,按照上述设置进行渲染,这个渲染命令就是Draw Call。Draw Call命令仅仅会指向一个需要被渲染的图元列表,而不包含任何材质信息,因为这些信息已经在上一个阶段中完成。执行DrawCall后GPU就会按照渲染流水线进行渲染计算,并输出到显示设备中,所执行的操作便是下述GPU渲染管线的内容

3. GPU渲染管线

上述是对渲染管线抽象的概述,这里开始对每个阶段进行展开。图形渲染管线可以被划分为如下图所示的几个阶段,每个阶段将会把前一个阶段的输出作为输入,所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行,正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个渲染管线阶段运行各自的小程序,从而在图形渲染管线中快速处理数据。

GPU具有成千上万的小处理核心,每个核心的运算能力相比GPU要弱得多,但GPU的“人海战术”体现了其强大的并行处理能力,而运行在这些核心上的小程序就叫做Shader。

上图所示的渲染管线,绿色部分代表可编程的阶段,因此也被称为可编程渲染管线(这区别于一些较旧的GPU渲染管线,这种渲染管线只提供一些配置操作,开发者没有对流水线的完全控制权),接下来对各个阶段进行展开。
顶点数据:顶点数据是渲染流水线的输入,顶点数据包括顶点坐标、法线、切线、顶点颜色、纹理坐标等。

顶点着色器:顶点着色器的处理单位是顶点,即对于输入的每个顶点都会调用一次顶点着色器。顶点着色器主要功能是进行坐标系变换操作,所输入的顶点坐标等位于模型局部坐标空间,在这一阶段需要将顶点坐标数据变换为到齐次裁剪空间。当顶点坐标被变换到齐次裁剪空间后,通常再由硬件做透视除法,最终得到归一化的设备坐标(NDC)。顶点着色器还可执行顶点光照计算(高洛德着色),但顶点光照效果通常不尽人意,因此通常在片元着色器中执行逐片元光照计算。

曲面细分着色器:曲面细分着色器是一个可选的阶段。曲面细分是利用镶嵌化处理技术对三角形进行细分,以此来增加物体表面的三角面数量。

在这一阶段,程序员可以进行曲面细分操作,看起来就像在原有的图元内加入了更多的顶点。对于一些有大量曲面的模型,进行曲面细分可以让曲面更加圆润;如果为这些细分的顶点再准备一些位置信息,那么这些细分的顶点将有助于我们展现一个细节更加丰富的模型。这也是贴图置换(Displacement Mapping)的基本思路。

几何着色器:几何着色器也是一个可选的阶段。顶点着色器以顶点数据作为输入,而几何着色器则以完整的图元(Primitive)作为输入数据。例如,以三角形的三个顶点作为输入,然后输出对应的图元。与顶点着色器不能销毁或创建顶点不同,几何着色器的主要亮点就是可以创建或销毁几何图元,此功能让GPU可以实现一些有趣的效果。例如,根据输入图元类型扩展为一个或更多其他类型的图元,或者不输出任何图元。需要注意的是,几何着色器的输出图元不一定和输入图元相同。几何着色器的一个拿手好戏就是将一个点扩展为一个四边形(即两个三角形)。

裁剪:裁剪操作就是将相机看不到的物体、顶点剔除,使其不被下一阶段处理。只有当图元完全位于视锥体内时,才会将它送到下一阶段,对于部分位于视锥体内的图元,外部的顶点将被剔除掉。由于已经知道在NDC下的顶点位置(即顶点位置在一个立方体内),因此裁剪就变得简单:只需要将图元裁剪到单位立方体内。裁剪这一步骤是硬件的固定操作,因此是不可编程的。
在这里插入图片描述

屏幕映射:这一步输入的坐标仍是三维坐标(范围在单位立方体内),屏幕映射的任务就是将每个图元的x、y值变换到屏幕坐标系(屏幕坐标系是一个2D空间)。由于输入坐标范围在[-1,1],因此这是一个拉伸到屏幕分辨率大小的过程。对于输入的坐标z值不做任何处理(实际上屏幕坐标系和z坐标一起构成窗口坐标系),这些值会被一起传递到光栅化阶段。
屏幕坐标系在OpenGL和DirectX中的定义方式不同。
在这里插入图片描述

三角形设置光栅化第的第一个流水线阶段是三角形设置,这个阶段会计算光栅化一个三角形所需的信息。具体来说上一阶段输出的都是三角形的顶点,但如果要得到整个三角形网格对像素的覆盖情况,就必须计算每条边上的像素坐标。为了能计算边界像素的坐标信息,就需要得到三角形边界的表示方式。这样一个计算三角网格表示数据的过程就叫做三角形设置。它的输出是为下一阶段做准备的。

三角形遍历:该阶段会检查每个像素是否被一个三角形网格所覆盖。若被覆盖的话就会生成一个片元。而这样一个找到哪些像素被三角形网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换。三角形遍历阶段会根据上一个阶段的计算结果来判断一个三角形网格覆盖了哪些像素,并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值。

关于三角形设置三角形遍历,Dmitry V. Sokolov老师的tinyrenderer开源项目中的Lesson1和Lesson2对这部分内容进行了体现。这两节课从使用Bresenham算法绘制一根直线开始直至通过线扫绘制成一个三角形。(根据Dmitry V. Sokolov的说法,正文所述扫描变换似乎是较旧的CPU渲染算法?)
关于三角形顶点的插值与三角形的重心坐标系相关,三角形填充也会产生锯齿的问题,相关内容后续进行学习补充。

片元着色器:片元着色器是一个非常重要的可编程着色器阶段,前面的光栅化阶段实际上并不会影响每个像素的颜色值,而是会产生一系列的数据信息,用来表述一个三角网格是怎样覆盖每个像素的,而片元就负责存储这样一系列信息,真正会对产生影响的是下一个阶段逐片元操作
片元着色器的输入是上一个阶段对顶点信息进行插值的结果(是根据从顶点着色器输出的数据插值得到的),而它的输出是像素颜色值。这一阶段可以完成很多重要的渲染技术,其中最重要的技术有纹理采样、逐片光照计算等覆盖片元的纹理坐标是通过前述的阶段的顶点数据插值得到的

逐片元操作(输出合并):逐片元操作是渲染管线的最后一个阶段,这一阶段有几个重要任务:

(1)决定每个片元的可见性,这涉及到很多测试功能,例如深度测试、模板测试
模板测试与之相关的是模板缓冲,模板测试通常用来限制渲染的区域,渲染阴影,轮廓渲染等。如果开启了模板测试,GPU会首先读取(使用读取掩码)模板缓冲中该片元位置的模板值,然后将该值和读取(使用读取掩码)到的参考值进行比较,这个比较函数可以是由开发者指定的。模板测试是高度可配置的,无论一个片元有没有经过模板测试,都可以根据模板测试和下面的深度测试结果来修改模板缓冲区。

深度测试同样是高度可配置的,如果开启了深度测试,GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较,这个比较函数也是可以由开发者设置,通常这个值是小于等于的关系,因为我们总想显示出离相机最近的物体(不包括透明/半透明),而那些被遮挡的就不需要出现在屏幕。如果一个片元通过了测试,那么开发者可以指定是否要用这个片元的深度值覆盖所有的深度值。

测试顺序并不是唯一的,逻辑上来说测试是在片元着色器之后进行,但对于大多数GPU而言,会尽可能的在执行片元着色器之前就执行测试操作,从而避免将不需要渲染的图元流入到后续的运算中。但将测试提前可能会与片元着色器中的一些操作冲突,如透明度测试,因此需要对其进行判断。

(2)如果一个片元通过了所有测试,就需要把这些片元的颜色值和颜色缓冲中已有的颜色值进行混合。
颜色混合的一个重要应用就是渲染出透明效果。开发者可以选择开启/关闭混合功能,如果没有开启混合,就会直接覆盖掉颜色缓冲区中的颜色。如果开启了混合,GPU会取出片元着色器得到的颜色(源颜色)和颜色缓冲区存在的颜色(目标颜色),之后按照设定的函数进行混合,这个混合函数通常和透明度通道息息相关,例如可以根据透明通道的值进行相加、相减、相乘等。

4. 渲染管线中的坐标系变换

从输入的顶点局部坐标最终到屏幕坐标需要经过一系列的变换,才能显示到屏幕上,变换过程如上图所示。
局部空间:局部空间也叫“模型空间”,局部空间的原点和坐标轴通常是在建模软件中定义好的,每个模型都有自己的局部空间,当模型移动、旋转时,局部空间也会跟着移动、旋转。

世界空间:世界空间是描述虚拟世界的最大空间,顶点变换的第一步就是将顶点坐标从模型空间中变换到世界空间中,这个变换通常叫做“模型变换”。在模型变换时,通常还需要将法线变换到世界空间中(后续执行光照计算),对于法线变换不能简单的使用模型变换矩阵来变换法线,对于包含非均匀缩放的变换,需要求解模型变换的逆转置矩阵。具体参考法线变换矩阵公式推导

观察空间:观察空间也被称为摄像机空间,是一种特殊的模型空间,相机决定了渲染所使用的视角。按照OpenGL的传统,观察空间的相机位于原点,面向-z方向,+y轴指向上方。

裁剪空间:裁剪空间的目标是能够方便的对图元进行裁剪,这块空间是由视锥体决定的,视锥体外为看不到的区域,也就是被裁剪的部分。如果直接使用视锥体定义的空间进行裁剪,那么不同的视锥体就需要不同的处理过程,而且对于透视投影来说,要判断一个顶点是否位于视锥体内是比较麻烦的。因此,通过一个投影矩阵将顶点转换到裁剪空间中是一种通用、方便的方式。用于变换的到裁剪空间的矩阵叫做“投影矩阵”,常见的投影矩阵有透视投影和正交投影。实际上,投影矩阵并没有真正的进行投影工作,而是为投影做准备,真正的投影发生在齐次除法过程中,这发生在屏幕映射阶段,经过投影矩阵变换后,顶点的w分量具有特殊意义(判断x、y、z是否位于[-w,w]范围,说明是否需要裁剪)。详细参考:齐次坐标系与Unity投影矩阵的推导齐次坐标与投影几何

屏幕空间:经过投影矩阵变换,就可以进行裁剪操作。当完成所有裁剪工作后,就需要进行真正的投影了,经过这一步才会得到真正的像素位置。首先需要进行齐次除法,实际上就是用齐次坐标系的w分别除x、y、z分量。经过这一步,就可以把坐标从齐次裁剪空间转换到NDC空间中。

然后,可以根据变换后的x、y坐标来映射输出窗口的对于像素坐标,这个映射过程就是一个x、y的缩放过程。齐次除法和屏幕映射过程可以用下面公式总结:

上面的式子对x和y分量都进行了处理,z分量会被用于深度缓冲,一个传统的方式是把 C l i p z C l i p w \frac{Clip_z}{Clip_w} ClipwClipz 的值直接存储到深度缓冲中,但驱动生产厂商会根据硬件选择最好的存储格式,此时 C l i p w Clip_w Clipw也不会被丢弃,它在后续的一些工作中仍起到重要左右,如进行透视矫正插值

Reference

  1. 细说渲染管线
  2. 猴子也能看懂的渲染管线(Render Pipeline)
  3. 《Unity Shader入门精要》冯乐乐
  4. 傅老师,OpenGL课程
  5. 几何着色器
  6. tinyrenderer Lesson 1和Lesson 2
  7. 透视矫正插值

猜你喜欢

转载自blog.csdn.net/qq_15017307/article/details/126292739