一、渲染流水线
渲染流水线的定义:通常由CPU和GPU共同完成把一系列的顶点数据、纹理信息最终转换成一张人眼可以看到的图像。
1.应用阶段
- 主要由CPU实现,开发者拥有绝对控制权。
- 开发者的3个主要任务:
(1)准备好场景数据。例如:摄像机位置、视锥体、场景中使用的模型、使用的光源等;
(2)粗粒度剔除。为了提高渲染性能,把那些不可见的物体剔除;
(3)设置好每个模型的渲染状态。这些渲染状态包括但不限于它使用的材质(漫反射色彩、高光反射颜色)、使用的纹理、使用的Shader等。
- 此阶段最重要的输出是渲染图元,这些渲染图元将会传递给下一个阶段——几何阶段。
- 渲染流水线的起点是CPU,即应用阶段,可大致分为下面3个阶段:1.把数据加载到显存中;2.设置渲染状态;3.调用DrawCall
(1)把数据加载到显存中
所有渲染所需的数据都需要从硬盘(Hard Disk Drive,HDD)中加载到系统内存(Random Access Memory,RAM)。然后,网格和纹理数据又加载到显存(Video Random Access Memory,VRAM)。
在这之后,开发者需要通过CPU来设置渲染状态,来指导GPU如何进行渲染工作。
(2)设置渲染状态
渲染状态就是,定义了场景中的网格是怎样被渲染的,例如:使用哪个顶点着色器(vertex Shader)/片元着色器(Fragment Shader)、光源属性、材质等。
准备好上述工作后,CPU就需要调用一个渲染命令来告诉GPU:“万事俱备,就差你执行了!”,这个渲染命令就是Draw Call。
(3)调用Draw Call
Draw Call就是一个命令,发起方是CPU,接收方是GPU。这个命令仅仅会指向一个需要被渲染的图元(primitives)列表,而不会再包含任何材质信息。
2.几何阶段
- 通常在GPU上进行,几何阶段用于处理所有和我们要绘制的几何相关的事情,负责和每个渲染图元打交到,进行逐顶点、逐多边形的操作。
- 重要任务:把顶点坐标变换到屏幕空间中,再交给光栅器处理。
- 此阶段的重要输出是屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息,并传递给下一个阶段。
- 从上图中可以看出,GPU的渲染流水线接收顶点数据作为输入,这些顶点数据是由应用阶段加载到显存中,再由DrawCall指定。
- 顶点着色器(Vertex Shader)……………………是完全可编程的,用于实现顶点的空间变换、顶点着色等功能;
- 曲面细分着色器(Tessellation Shader)……是可选着色器,用于细分图元;
- 几何着色器(Geometry Shader)………………是可选着色器,用于执行逐图元(Per-Primitive)的着色操作,或者被用于产生更多的图元;
- 裁剪(Clipping)………………………………是可配置的,目的是将那些不在摄像机视野内的顶点裁剪掉,并剔除某些三角图元的面片;
- 屏幕映射(ScreenMapping)……………………是不可配置和不可编程的,负责把每个图元坐标转换到屏幕坐标系中。
(1)顶点着色器
- 它的输入来自CPU,处理单位是顶点,顶点着色器本身不可以创建或销毁任何顶点,而且无法得到顶点与顶点之间的关系(例,无法得知两个顶点是否属于同一个网格)。因此,这阶段的处理速度会比较快。
- 主要工作:坐标转换 和 逐顶点光照
- 必须完成的一个工作是:把顶点坐标从模型空间转换到齐次裁剪空间(坐标转换)。然后再有硬件做透视除法后,最终得到归一化的设备坐标(Normalized DeviceCoordinates,NDC)。
- 还可以输出后续阶段需要的数据。
- 注:Unity使用的NDC和OpenGL是相同的,它的z分量的范围在[-1,1]之间,而在DirectX中,NDC的z分量范围是[0,1]。
- 顶点着色器可以有不同的输出方式,最常见的输出路径是经光栅化后交给片元着色器进行处理。
- 在现代Shader Model中,还可以把数据发送给曲面细分着色器或几何着色器。
(2)裁剪
- 一个图元和摄像机视野的关系有3种:完全在视野内、部分在视野内、完全在视野外。
- 此步不可以编程,是硬件上的固定操作,但可以自定义一个裁剪操作来对这一步进行配置。
(3)屏幕映射
- 输入坐标为三维坐标(范围在单位立方体内)。
- 主要任务:把每个图元的x和y坐标转换到屏幕坐标系(Screen Coordinates)下。
- 屏幕坐标系和用于显示画面的分辨率有很大关系。
- 屏幕映射不会对输入的z坐标做任何处理
- 屏幕坐标系和z坐标一起构成了窗口坐标系,这些值会一起传递到光栅化阶段。
- 屏幕坐标系在OpenGL和DirectX之间的差异:OpenGL的最小窗口坐标值在左下角,DirectX的最小窗口坐标值在左上角。
从上一阶段输出的信息是屏幕坐标系下的顶点位置以及和它们相关的额外信息,如深度值(z坐标)、法线方向、视角方向等。
3.光栅化阶段
- 通常在GPU上进行,光栅化阶段需要对上一个阶段得到的逐顶点数据(例如:纹理坐标、顶点颜色等)进行插值,然后再进行逐像素处理。
- 主要任务:决定每个渲染图元中哪些像素应该被绘制在屏幕上。
- 此阶段的重要输出是使用上个阶段传递的数据来产生屏幕上的像素,并渲染出最终图像。
- 2个重要目标:计算每个图元覆盖了哪些像素;计算这些像素的颜色。
- 三角形设置(Triangle Setup)和三角形遍历(Triangle Traversal)…………都是固定函数(Fixed-Function)的阶段;
- 片元着色器(Fragment Shader)…………………………………………………是完全可编程的,用于实现逐片元(Per-Fragment)的着色操作;
- 逐片元操作(Per-Fragment Operations)………………………………………是不可编程的,但有很高的可配置性,负责执行很多重要的操作,例如:修改颜色、深度缓冲、进行混合等。
(1)三角形设置
- 此阶段会计算光栅化一个三角网格所需的信息
- 上一个阶段得到三角网格每条边的两个端点,必须计算每条边上的像素坐标,才能得到整个三角网格对像素的覆盖情况
- 为了计算边界像素的坐标信息,需要的到三角形边界的表示方式。
- 这样一个计算三角网格表示数据的过程就叫三角形设置。
(2)三角形遍历
- 三角形遍历阶段会检查每个像素是否被一份三角网格所覆盖。
- 如果被覆盖,就生成一个片元。
- 这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,也被称为扫描变换(Scan Conversion)
- 此阶段会根据上一个阶段的计算结果来判断一个三角网格覆盖了哪些像素
- 并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值
- 此阶段输出一个片元序列
- 一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色
- 这些状态包括但不限于它的屏幕坐标、深度信息,以及其他从几何阶段输出的顶点信息,如法线、纹理坐标等。
(3)片元着色器
- 片元着色器是一个非常重要的可编程着色器阶段
- DirectX称为像素着色器(Pixel Shader)
- 输入的是上一个阶段对顶点信息(那些从顶点着色器中输出的数据)插值得到的结果
- 这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。
- 纹理采样:通常在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到覆盖的片元的纹理坐标。
- 局限在于,仅影响单个片元
- 有一个情况例外,片元着色器可以访问到导数信息(gradient 或 derivative)
(4)逐片元操作(难点)
- OpenGL称为逐片元操作,DirectX称为输出合并阶段(OutPut-Merger)
- 目的:合并
- 操作单位:片元
- 主要任务:(1)决定每个片元的可见性。如深度测试、模板测试等;(2)如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。
- 此阶段是高度可配置性的,可以设置每一步的操作细节。
- 测试的过程实际上是个比较复杂的过程,而且不同的图形接口(如OpenGL和DirectX)的实现细节也不尽相同。
1.模板测试(Stencil Test)
与之相关的是模板缓冲(Stencil Buffer)。模板缓冲和颜色缓冲、深度缓冲几乎是一类东西。
开启模板测试后,第一步,GPU先读取(使用读取掩码)模板缓冲区中该片元位置的模板值;
第二步,将该值和读取(使用读取掩码)到的参考值(referencen value)进行比较,这个比较函数可以是由开发者指定的,如,小于时舍弃该片元。
不管一个片元有没有通过模板测试,我们都可以根据模板测试和下面的深度测试结果来修改模板缓冲区,这个修改操作也是由开发者指定的。
开发者可以设置不同结果下的修改操作,例如,在失败时模板缓冲区保持不变,通过时将模板缓冲区中对应位置的值加1等。
模板测试通常用于限制渲染的区域。
另外,还有一些高级用法,如渲染阴影、轮廓渲染等。
2.深度测试(Depth Test)
一个片元通过了模板测试,它会进行下一个测试——深度测试(Depth Test)。
深度测试可以高度配置。
开启深度测试后,GPU会把该片元的深度值和已经存在与深度缓冲区中的深度值进行比较。这个比较函数也可由开发者设置,如小于时舍弃该片元。
通常这个比较函数是小于等于的关系,即如果片元的深度值大于时舍弃该片元。
和模板测试不同的是,如果一个片元没有通过深度测试,就没有权利更改深度缓冲区的值。
如果通过了测试,开发者还可以指定是否要用这个片元的深度值覆盖掉原有的深度值,这是通过开启/关闭深度写入来做到的。
透明效果和深度测试以及深度写入的关系非常密切。
3.混合(Blend)
合并需要解决的问题是,使用这次渲染得到的颜色完全覆盖掉之前的结果,还是进行其他处理。
对于不透明物体,开发者可以关闭混合(Blend)操作,这样片元着色器计算的颜色值就会直接覆盖掉颜色缓冲区中的像素值。
对于半透明物体,需要使用混合操作来让这个物体看起来是透明的。
混合是可以高度配置的:没有开启混合功能,就会直接使用片元的颜色覆盖掉颜色缓冲区中的颜色;
开启混合功能,GPU会取出源颜色和目标颜色,将两种颜色使用一个混合函数进行混合。
源颜色:指片元着色器得到的颜色值。
目标颜色:指已经存在于颜色缓冲区中的颜色值。
混合函数:通常和透明通道息息相关,如,根据透明通道的值进行相加、相减、相乘等。
以上测试顺序并不是唯一的,从逻辑上说这些测试是在片元着色器之后进行的,但对于大多数GPU来说,会尽可能在执行片元着色器之前就进行这些测试。
想充分提高GPU的性能,就尽量避免计算被舍弃的片元的颜色。
Unity给出的渲染流水线中,深度测试是在片元着色器之前。
这种将深度测试提前的执行技术被称为Early-Z技术。
将测试提前,其检验结果可能会与片元着色器中的一些操作冲突。
如,在片元着色器进行了透明度测试,而这个片元没有通过透明度测试,我们会在着色器中调用API(例如clip函数)来手动将其舍弃掉。
这就导致GPU无法提前执行各种测试。因此,现代GPU会判断片元着色器中的操作是否和提前测试发生冲突,如果有冲突,就会禁用提前测试。
但是这样也会造成性能上的下降,因为有更多的片元需要被处理了。这也是透明度测试会导致性能下降的原因。
屏幕显示的就是颜色缓冲区中的颜色值。为了避免我们看到那些正在进行光栅化的图元,GPU会使用双重缓冲(Double Buffering)的策略。
这意味着,对场景的渲染是在幕后发生的,即在后置缓冲(Back Buffer)中。一旦场景已经被渲染到了后置缓冲中,GPU就会交换后置缓冲区和前置缓冲区(Four Buffer)中的内容,而前置缓冲区是之前显示在屏幕上的图像。
由此,保证了我们看到的图像总是连续的。