《Real-Time Rendering》图形渲染管线
本文同时发布在我的个人博客https://dragon_boy.gitee.io
图形渲染管线的主要目的是根据一个给定的虚拟摄像机、一些三维物体、光源和其它的物体来生成或渲染一张二维图像。因此图形渲染管线是实时渲染的最根本的工具。二维图像中的物体的位置和形状有它们的几何形状和场景中的摄像机和角色决定。物体的颜色由材质属性、光源、纹理和着色等式决定。
架构
在真实世界中,管线的概念可由多种不同的方式表明,比如工厂生产快餐的装配流水线。这一概念同样应用于图形渲染。一个渲染管线包含几个阶段,每个阶段执行复杂工作中的一部分。
管线的各个阶段并行执行,每个阶段依赖于上一阶段的结果。理论上,一个非管线系统如果被分为n个管线阶段,将会加速n倍,这也是在渲染中使用管线的理由。例如,大量的三明治可以通过一系列的工人快速做出——一个人准备面包,另一个人添加肉,还有一个人添加配料。每一个人将做出的东西传给下一个人后便马上开始制作下一个东西。如果每个人使用20秒来执行他们的任务,那么一分钟大概可以制作3个三明治。管线的各个阶段并行执行,但整体执行速度会被速度最慢的阶段拖累。比如,制作三明治添加肉的阶段的时间稍微复杂些,为30秒左右,那么1分钟大概就只能做2个三明治。那么针对这个管线,添加肉的阶段就是一个瓶颈,它决定整体的速度,而添加配料阶段就会等待较长的时间。
上述管线的概念被应用于实时渲染图形渲染。渲染管线可以被粗略地分为四个阶段:应用
、几何处理
、光栅化
、像素处理
。这一结构是渲染管线的核心,每一阶段也拥有一个自己的管线。
渲染速度可以用每秒帧数即FPS
表示,也可以使用Hz
表示,即刷新频率,也可以单纯地使用时间,毫秒ms
来表示渲染一张图像所需时间。生成一张图像的时间是多样的,决定于每帧计算的复杂度。FPS被用来表示一个特定帧速率或一段持续时间的平均性能。Hz由硬件使用。
应用阶段由应用驱动,在一些CPU上实现,这些CPU通常拥有多个可以并行处理多线程的核。传统意义上运行在CPU上的任务包括碰撞检测,全局优化算法,动画,物理模拟等等,由应用程序的类型决定。下一个主要的阶段是几何处理,处理变换、投影和其它类型的几何处理。这一阶段会计算出哪些东西会被绘制,它们会怎么绘制,它们应该绘制在哪儿。几何处理阶段一般运行在GPU上,这些GPU包含许多可编程的核和固定处理硬件。光栅化阶段典型地将三个顶点作为输入,组成一个三角形,并寻找位于三角形内的像素,并将这些输出到下一阶段。最后,像素处理阶段逐像素地执行一个程序来决定它地颜色,也会执行深度测试来决定某一像素是否可见。也可能执行一些其它的逐像素操作,如混合。光栅化和像素处理阶段也完全在GPU上执行。
应用阶段
开发者可以完全控制应用阶段,因为它经常在CPU上执行。在这一阶段的修改可以影响子阶段的性能。例如,一些应用阶段算法或设置可以减少要渲染三角形的数量。
一些应用工作可以在GPU上执行,如一个单独的模块——计算着色器。这一模块将GPU当作一个高并行的处理器,忽略它针对渲染管线的特殊功能。
在应用阶段末尾,要渲染的几何体被传递给几何处理阶段,这些几何体被称为渲染基本体,如点、线和三角形。
这一阶段的基于软件实现的结果是它不会被分为子阶段,这一点区别于几何处理、光栅化和像素处理。然而,为了提升性能,这一阶段经常并行运行在几个处理器核心上。在CPU设计中,这被称为超标量体系结构。
经常在应用阶段实现的处理是碰撞检测。当检测到两个物体发生碰撞后,信号可能生成和传输回碰撞物体或力反馈装置。应用阶段也考虑如来自键盘、鼠标等的输入。加速算法,如特殊的裁剪算法也在这一阶段实现。
几何处理
在GPU上几何处理阶段进行大多数的逐三角形和逐顶点操作,这一阶段可以进一步分为四个阶段:顶点着色器
、投影
、裁剪
、屏幕映射
。
顶点着色器
顶点着色器阶段有两个主要任务:计算顶点位置;估计开发者想要的顶点输出,如法线和纹理坐标。在过去的传统方法,大多数的物体的着色计算通过将灯光应用于每个顶点的位置和法线并将唯一的颜色存储在顶点中完成,这些颜色接下来沿着三角形插值。这一可编程的处理单元被称为顶点着色器。随着现代GPU的出现,着色由逐像素的处理方式替代,现在的顶点着色器一般不用来进行计算着色。顶点着色器如今倾向于处理每个顶点的数据,如,顶点着色器可以用来做动画。
我们首先解释如何计算一个顶点的坐标。从开始到传输到屏幕上,一个模型被变换到不同的空间或坐标系下。一开始,一个模型位于自己的模型空间中,接着,每个模型可以绑定一个模型变换,来让模型平移和旋转和缩放。一个物体的坐标被称为模型坐标,在经历模型变换后,物体位于世界空间中。世界空间是唯一的。
只有摄像机观察到的物体可以被渲染。摄像机拥有一个世界空间位置和方向,它们用来防止和指向摄像机。为了让投影和裁剪更容易,摄像机和模型被变换到视图空间,通过一个视图变换。视图变换的目的是将摄像机放在中心并让其指向-z轴,同时+y轴指向上方,+x轴指向右方。摄像机指向的轴由API指定,这里约定为-z轴。
接下来我们描述顶点着色器的第二种输出。为了生成一个真实的场景,渲染形状和位置是不够的,它们的外表也需要被构建。这一描述包括每个物体的材质,和灯光对每个物体的颜色反应。材质和灯光有多种方式构建,简单的颜色或者详细的物理描述。
上述的过程被称为着色,它包含在物体的不同的点上计算一个着色等式。典型地,一些着色等式在几何处理阶段实行,另一些在逐像素阶段实现。顶点着色器的结果被传递到光栅化阶段和像素阶段插值,接着用来计算表面的着色。
作为顶点着色的一部分,渲染系统执行投影,接着进行裁剪,将视锥体变换为一个单位立方体,极端顶点坐标是(1,1,1)和(-1,-1,-1)。投影首先被执行,由顶点着色器执行。通常的投影模式由两种:正交投影和透视投影。
正交摄像机的视锥体是一个长方体,正交投影变换将这个长方体转化为一个单位立方体,这一变换由平移和缩放组合而成。
透视摄像机的视锥体是一个锥体的一部分,终点在某一远处汇聚为一点。透视投影也将这一视锥体转化为一个单位立方体。在进行投影后的模型的坐标称为裁剪坐标,或者齐次坐标。
进行投影后,z坐标将会存储在z缓冲中。
可选的顶点处理
可选的顶点处理有:细分曲面、几何着色器、流输出。
第一个可选阶段是细分曲面。比如一个球体,我们用一系列三角形表示,在质量和性能方面会有问题。这个球体在远处看起来可能不错,但凑近了看就会发现棱角感很强,增加顶点数会改善质量问题,但同时性能会大打折扣。通过使用细分曲面,一个曲面可以根据恰当数量的三角形生成。
顶点可以用来表示点、线、三角形或其它物体,顶点也可以用来表示曲面,例如一个球。这些面可以用一系列片表示,每个片由一系列顶点表示。细分曲面阶段包括三个子阶段——外壳着色器、镶嵌器和域着色器,它们将一系列片顶点转化为大量的顶点,接着用来生成新的三角形。场景中的摄像机可以用来决定生成三角形的数量:片越近生成的越多。
下一可选阶段是几何着色器。这一着色器的出现时间早于细分曲面着色器。几何着色器的功能类似于细分曲面着色器,将一系列基本体作为输入,然后生成新的顶点。几何着色器有一些用途,其中最流行的是粒子生成。比如模拟烟花爆炸,每个火球可以用一个点,一个顶点表示。几何着色器可以将一个点作为输入,然后根据其生成一个面向观察者的四边形,然后对其进行着色。
最后一个可选阶段是流输出。这一阶段让我们可以将GPU作为一个几何引擎使用。我们不再将我们处理好的顶点传递至管线的其它阶段然后渲染到屏幕上,而是有选择将它们输出为一个队列来做更近一步的处理。这些数据可以被CPU使用,或者GPU自己。这一阶段典型地用于粒子模拟,例如烟花粒子。
裁剪
只有出现在视锥体范围内的基本体需要被传递至光栅化阶段。一个完全出现在视锥体内的基本体会直接传递至下一阶段,完全在视锥体范围外的基本体将会被忽略,而部分位于视锥体范围内的基本体需要被裁减。例如,一条线段的一个顶点在视锥体外面,另一个在范围内,那么在视锥体范围外的顶点会被一个新的顶点替代,这个新的顶点位于线段和视锥体的交点处。使用投影的好处就是裁剪在一个单位立方体内进行。
裁剪阶段使用通过投影产生的4值齐次坐标来执行裁剪。坐标值并不通过遍历透视空间的三角形的通常的线性插值生成。第四个值是必须的,所以当执行透视投影后,数据才会进行正确的插值和裁剪。最后,透视除法被执行,他将结果的三角形位置转化为三维标准设备坐标,范围为(-1,-1,-1)到(1,1,1)。几何阶段的最后一步就是将这一空间的坐标转化为窗口坐标。
屏幕映射
只有经过裁剪后的位于视锥体内的基本体会传递至光栅化阶段,此时坐标仍是三维的,但xy坐标被转化为屏幕坐标,屏幕坐标和z坐标被统称为窗口坐标。比如一个场景需要被渲染到一个窗口,窗口的左下角坐标为,右上角坐标为,。屏幕映射就是将上一阶段的顶点的xy坐标缩放到窗口的大小。z坐标也被映射到。
接下来,我们解释如何将整型和浮点型数据与像素连接在一起。给定一个水平队列的像素,并使用笛卡尔坐标表示,最左侧的像素是0.0,中间的像素是0.5,那么位于[0,9]范围的像素的坐标跨度为[0.0,10.0)。这个转化为:
是离散的像素索引,是连续的像素坐标。
光栅化
几何处理阶段将顶点进行变换、投影,连接着色数据后,下一阶段的目的就是找到位于基本体内的像素,这一阶段被称为光栅化。光栅化阶段可以被分为两个子阶段:三角形准备和三角形遍历。光栅化阶段也被称为扫描转化,也就是将屏幕空间的二维顶点转化到屏幕的像素。光栅化也被认为是几何处理阶段和像素处理阶段的同步点,这一阶段组装三角形并传递到下一阶段。
光栅化主要是使用每个像素的采样点来决定是否位于三角形内,根据采样点的数量可以决定三角形的边缘平滑程度。
三角形准备
这一阶段,三角形的微分,边界等式,其它数据会被计算。这些数据会用来进行三角形遍历,也用来插值顶点着色数据。
三角形遍历
这一阶段每个像素会根据其采样点来判断是否在三角形中。在像素和三角形的重合处一个片段会生成。
像素处理
在这一阶段,所有像素都被考虑位于一个三角形内。像素处理阶段被分为像素着色器和结合。
像素着色器
所有的逐像素着色计算都在像素着色器进行,使用着色数据的插值作为输入。在像素着色器中使用最广泛的技术是纹理。
结合
每个像素的信息被存储在颜色缓冲中,结合阶段的任务是将片段的颜色和当前存储在颜色缓冲中的颜色进行混合,这一阶段也被称为ROP,即光栅操作管线或渲染输出单元。这一阶段不可编程但可高度配置。
这一阶段也处理可见性。这意味着当整个场景被渲染时,颜色缓冲应该包含场景中可见的基本体的颜色。对大多数的硬件来说,这一操作由深度缓冲完成。深度缓冲和颜色缓冲的大小和空间维度相同,对每个像素存储当前最近的基本体的z值,这也就意味着一个基本体被渲染到特定的像素时,当前基本体的深度值会被计算,接着和存储在当前像素的深度缓冲中的深度值进行比较,如果新的深度值小于深度缓冲中的深度值,那么基本体就会绘制,接着颜色缓冲和深度缓冲会根据这个基本体进行更新。如果大于的话,这个基本体就会被忽略,颜色缓冲和深度缓冲也不会更新。深度缓冲算法很简单,收敛。然而,深度缓冲不适用于半透明和透明物体,所以它们要在所有的不透明物体渲染结束后渲染。
颜色缓冲中的Alpha值存储当前像素的透明度,所以可以根据这个值进行透明度测试。
模板缓冲是一个离屏缓冲,用来记录渲染基本体的位置,每个像素通常包含8bit值。基本体可以使用多种函数渲染到模板缓冲,模板缓冲可以用来控制渲染到颜色缓冲和深度缓冲。例如,一个填充圆被渲染到模板缓冲中,我们就可以根据这个模板缓冲来显示场景中位于填充圆内的像素。所有这些管线末尾的方法被称为光栅操作或混合操作。
一个系统上的帧缓冲一般包含上述所有缓冲。
管线结束后,屏幕上显示颜色缓冲中的颜色,为了避免看见渲染的过程,我们使用双缓冲技术。所有场景的渲染发生在屏幕外,在后置缓冲中,当场景渲染好后,后置缓冲将和显示在屏幕上的前置缓冲中的内容交换。这一交换经常发生在垂直扫描期间。