unity shader基础之——渲染流水线和GPU流水线

学习shader之前有必要了解什么是渲染流水线的,渲染流水线的最终目的在于生成或者说是渲染一张二维纹理,即我们在电脑屏幕上看到的所有效果,它的输入是一个虚拟摄像机、一些光源、一些shader以及纹理等。

一、综述

要学会怎么使用shader,首先要了解shader是怎么工作的,shader仅仅是渲染流水线的一个环节,想要让我们的shader发挥作用,我们就需要它在渲染流水线中扮演了怎样的角色。

1.1 什么是渲染流水线

渲染流水线的工作任务在于由一个三维场景出发、生成(渲染)一张二维图像。计算机需要从一系列的顶点数据、纹理等信息出发,把这些信息最终转换成一张人眼可以看到的图像,而这个工作通常是由CPU和GPU共同完成的。

渲染流程一般分为三个阶段:应用阶段、几何阶段、光栅化阶段。下图显示了三个概念阶段之间的关系:

unity shader基础之——渲染流水线和GPU流水线

1. 应用阶段:

这个阶段由我们的应用主导,通常由CPU实现。在这一阶段中,开发者有三个任务:首先准备好场景数据,例如摄像机的位置、视椎体、场景中包含了哪些模型、使用了哪些光源等等;其次为了提高渲染性能,我们往往需要做一个粗粒度剔除工作,把那些不可见的物体剔除出去,这样就不需要再移交给几何阶段进行处理;最后设置好每个模型的渲染状态。这些渲染状态包括但不限于它使用的材质(漫反射颜色、高光反射颜色)、使用的纹理、使用的shader等。这一阶段最重要的输出是渲染所需的几何信息,即渲染图元。通俗来讲渲染图元可以是点、线、三角面等。这些渲染图元将会被传递给下一个阶段——几何阶段。

2. 几何阶段:

几何阶段用于处理所有和我们要绘制的几何相关的事情。例如决定要绘制的图元是什么,怎样绘制它们,在哪里绘制它们,这一阶段通常在GPU上进行。

几何阶段负责和每个渲染图元打交道,进行逐顶点,逐多边形的操作。这个阶段可以进一步分成更小的流水线阶段。几何阶段的一个重要任务就是把顶点坐标变换到屏幕空间中,再交给光栅器进行处理。通过对输入的渲染图元进行多步处理后,这一阶段将会输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息,并传递给下一个阶段。

3. 光栅化阶段:

这一阶段将会使用上个阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。这一阶段也是在GPU上运行,光栅化的任务主要是决定每个渲染图元中哪些像素应该被绘制在屏幕上,它需要对上一个阶段得到的逐顶点数据(例如纹理坐标、顶点颜色等)进行插值,然后再进行逐像素处理。光栅化阶段也可以分成更小的流水线阶段。

注意:这里的流水线是概念流水线,是为了给一个渲染流程进行基本的功能划分而提出来的,下面要介绍的GPU流水线,则是硬件真正用于实现上述概念的流水线。

二、CPU和GPU的之间的通信

渲染流水线的起点是CPU,即应用阶段。应用阶段大致可分为下面3个阶段:
(1)把数据加载到显存中。
(2)设置渲染状态。
(3)调用Draw Call。

2.1 把数据加载到显存中

所有渲染所需的数据都需要从硬盘中加载到系统内存中,然后网格和纹理等数据又被加载到显卡上的存储空间——显存中,这是因为显卡对于显存的访问速度更快,而且大多数显卡对于RAM没有直接访问的权利。

当把数据加载到显存中后,RAM中的数据就可以移除了。但对于一些数据来说,CPU仍然需要访问他们(例如希望CPU可以访问网格数据来进行碰撞检测),那么我们就可能不希望这些数据被移除,因为从硬盘加载到RAM的过程是十分耗时的。

在这之后,开发者还需要通过CPU来设置渲染状态,从而指导GPU如何进行渲染工作。

2.2 设置渲染状态

渲染状态的通俗解释是:这些状态定义了场景中的网格是怎样被渲染的。例如使用哪个顶点着色器/片元着色器、光源属性、材质等。如果我们没有更改渲染状态,那么所有网格都将使用同一种渲染状态。

在准备好上述所有工作后,CPU就需要调用一个渲染命令(就是Draw Call)告诉GPU数据准备好了,可以按照我的设置来渲染了。

2.3 调用Draw Call

Draw Call是一个命令,它的发起方是CPU,接收方是GPU。这个命令仅仅会指向一个需要被渲染的图元列表,而不会再包含任何材质信息——这是因为我们已经在上一个阶段中完成了。

当给定了一个Draw Call时,GPU就会根据渲染状态(例如材质、纹理、着色器等)和所有输入的顶点数据来进行计算,最终输出成屏幕上显示的那些漂亮的像素。而这个过程就是后面要讲的GPU流水线。

三、GPU流水线

当GPU从CPU那里得到渲染命令后,就会进行一系列流水线操作,最终把图元渲染到屏幕上。

3.1 概述

几何阶段和光栅化阶段可以分成若干更小的流水线阶段,这些流水线阶段由GPU来实现,每个阶段GPU提供了不同的可配置性或可编程性。如下图:

unity shader基础之——渲染流水线和GPU流水线

从图中可以看出,GPU的渲染流水线接收顶点数据作为输入。这些顶点数据是由应用阶段加载到显存中,再由Draw Call指定的。这些数据随后被传递给顶点着色器。

顶点着色器是完全可编程的,它通常用于实现顶点的空间变换、顶点着色等功能,曲面细分着色器是一个可选的着色器,它用于细分图元。几何着色器同样是一个可选着色器,它可以被用于执行逐图元的着色操作,或者被用于产生更多的图元。下一个流水线阶段是裁剪,这一阶段的目的是将那些不在摄像机视野内的顶点裁减掉,并剔除某些三角图元的面片。这个阶段是可配置的。例如我们可以使用自定义的裁剪平面来配置裁剪区域,也可以通过指令控制裁剪三角图元的正面还是背面。几何概念的最后一个流水线是屏幕映射,这一阶段是不可以配置和编程的,它负责把每个图元的坐标转换到屏幕坐标系中。

光栅化概念阶段中的三角形设置和三角形遍历阶段也都是固定函数的阶段。接下来的片元着色器则是完全可编程的,它用于实现逐片元的着色操作。最后逐片元操作阶段负责执行很多重要的操作,例如修改颜色、深度缓冲、进行混合等,它不是可编程的,但具有很高的可配置性。

接下来会对其中主要的流水线阶段进行更加详细的解释。

3.2 顶点着色器

顶点着色器是流水线的第一个阶段,它的输入来自于CPU。顶点着色器的处理单位是顶点,也就是说输入进来的每个顶点都会调用一次顶点着色器。顶点着色器本身不可以创建或者销毁任何顶点,而且无法得到顶点与顶点之间的关系。例如我们无法得知两个顶点是否属于同一个三角网格。但正是因为这样的相互独立性,GPU可以利用本身的特性并行化处理每一个顶点,这意味着这一阶段的处理速度会很快。

顶点着色器需要完成的工作主要有:坐标变换和逐顶点光照。当然除了这两个主要任务外,顶点着色器还可以输出后续阶段所需的数据。下图展示了在顶点着色器中对顶点位置进行坐标变换并计算顶点颜色的过程:

unity shader基础之——渲染流水线和GPU流水线

坐标变换,就是对顶点的坐标位置进行某种变换。顶点着色器可以在这一步中改变顶点的位置,这在顶点动画中是非常有用的。例如我们可以通过改变顶点的位置来模拟水面、布料等。但需要注意的是,无论我们在顶点着色器中怎样改变顶点的位置,一个最基本的顶点着色器必须要完成的工作是,把顶点坐标从模型空间转换到齐次裁剪空间。我们经常会看到下面类型的代码:

o.pos=mul(UNITY_MVP,v.position);

类似上面这句代码的功能,就是把顶点坐标转换到齐次裁剪坐标系下,接着通常再由硬件做透视除法后,最终得到归一化的设备坐标(NDC),具体数学上的实现细节后面的文章中会讲。

unity shader基础之——渲染流水线和GPU流水线

需要注意的是,上面给出的坐标范围是OpenGL同时也是unity使用的NDC,它的z分量范围在[-1,1]之间,而在DirectX中,NDC的z分量范围是[0,1]。顶点着色器可以有不同的输出方式,最常见的输出路径是经光栅化后交给片元着色器进行处理。而现代的shader model中,它还可以把数据发送给曲面细分着色器或几何着色器。

3.3 裁剪

由于场景可能会很大,而摄像机的视野范围很有可能不会覆盖所有的场景物体,一个很自然的想法就是不在摄像机视野范围内的物体不需要被处理。而裁剪就是为了完成这个目的而被提出来的。

一个图元和摄像机视野的关系有三种:完全在视野内,部分在视野内、完全在视野外。完全在视野内的图元就继续传递给下一个流水线阶段,完全在视野外的图元不会继续向下传递,因为它们不需要被渲染。而那些部分在视野内的图元需要进行一个处理,这就是裁剪。例如一条线段的一个顶点在视野内,而另一个顶点不在视野内,那么在视野外部的顶点就应该使用一个新的顶点来代替,这个新的顶点位于这条线段和视野边界的交点。

由于我们已知在NDC下的顶点位置,即顶点位置在一个立方体内,因此裁剪变得很简单:只需要将图元裁剪到单位立方体内,下图展示了这样一个过程:

unity shader基础之——渲染流水线和GPU流水线

和单位立方体相交的图元会被裁剪,新的顶点会被生成,原来在外边的顶点会被舍弃。

和顶点着色器不同,这一步是不可编程的,不能控制裁剪的过程,而是硬件上的固定操作,但我们可以自定义一个裁剪操作来对这一步进行配置。

3.4 屏幕映射

这一步输入的坐标仍然是三维坐标系下的坐标(范围在单位立方体内)。屏幕映射的任务是把每个图元的x和y坐标转换到屏幕坐标系下。屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。

假设我们需要把场景渲染到一个窗口上,窗口的范围是从最小的窗口坐标(x1,y1)到最大的窗口坐标(x2,y2),其中x1<x2,且y1<y2.由于我们输入坐标范围在-1到1,因此可以想象到,这个过程实际是一个缩放的过程,屏幕映射不会对输入的z坐标做任何处理,实际上屏幕坐标系和z坐标一起构成了一个坐标系,叫做窗口坐标系。这些值会一起被传递到光栅化阶段。

unity shader基础之——渲染流水线和GPU流水线

屏幕映射得到的屏幕坐标决定了这个顶点对应屏幕上哪个像素以及距离这个像素有多远。有一个需要引起注意的地方是,屏幕坐标系在OpenGL和DirectX之间的差异问题。OpenGL把屏幕的左下角当成最小的窗口坐标值,而DirectX则定义了屏幕的左上角为最小窗口坐标值。

unity shader基础之——渲染流水线和GPU流水线

产生这种差异的原因是,微软的窗口都使用了这样的坐标系统,因为这和我们的阅读方式是一致的:从左到右,从上到下,并且很多图像文件也都是按照这样的格式进行存储的。要时刻小心这样的差异,如果发现得到的图像是倒转的,那么很有可能就是这个原因造成的。

3.5 三角形设置

由这一步开始就进入了光栅化阶段。从上一个阶段输出的信息是屏幕坐标系下的顶点位置以及和它们相关的额外信息,如深度值(z坐标)、法线方向、视角方向等。光栅化阶段有两个最重要的目标:计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色。

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

3.6 三角形遍历

三角形遍历阶段将会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话就会生成一个片元。而这样一个找到哪些像素被三角形网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换。

三角形遍历阶段会根据上一个阶段的计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值,下图展示了三角形遍历阶段的简化计算过程:

unity shader基础之——渲染流水线和GPU流水线

这一步的输出就是得到一个片元序列。需要注意的是,一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了(但不限于)它的屏幕坐标、深度信息、以及其他从几何阶段输出的顶点信息,例如法线、纹理坐标等。

3.7 片元着色器

片元着色器是另一个非常重要的可编程着色器阶段。在DirectX中,片元着色器被称为像素着色器,但片元着色器是一个更合适的名字,因为此时的片元并不是一个真正意义上的像素。

前面的光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据信息,用来表示一个三角网格是怎样覆盖每个像素的。而每个片元就负责存储这样一系列的数据。真正会对像素产生影响的阶段是下一个流水线阶段——逐片元操作。

片元着色器的输入是上一个阶段对顶点信息插值得到的结果,更具体来说,是根据那些从顶点着色器中输出的数据插值得到的。而他的输出是一个或者多个颜色值。下图显示了这样一个过程:

unity shader基础之——渲染流水线和GPU流水线

这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。为了在片元着色器中进行纹理采样,我们通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。

虽然片元着色器可以完成很多重要的效果,但是它的局限在于,它仅可以影响单个片元。也就是说当执行片元着色器时,他不可以将自己的任何结果直接发送给它的邻居们。有一个情况例外,就是片元着色器可以访问到导数信息。

3.8 逐片元操作

流水线的最后一步,逐片元操作是OpenGL中的说法,在DirectX中这一阶段被称为输出合并阶段。对每一个片元进行操作。那么要合并哪些数据,进行哪些操作呢?

这一阶段的主要任务;
(1)决定每个片元的可见性,这涉及了很多测试工作,例如深度测试、模板测试等。
(2)如果一个片元通过了所有的测试,就需要把这个片元着色器的值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。

逐片元操作阶段是高度可配置性的,即我们可以设置每一步的操作细节。

这个阶段首先需要解决每个片元的可见性问题,需要进行一系列测试,通过所有测试才能最终获得和GPU谈判的资格,这个资格指的是可以和颜色缓冲区进行合并。如果,没有通过某一个测试,之前为了产生这个片元所做的所有工作都是白费的,因为这个片元会被舍弃掉。下图是逐片元操作所做的操作:

unity shader基础之——渲染流水线和GPU流水线

测试的过程比较复杂,下图是深度测试和模板测试的实现过程:

unity shader基础之——渲染流水线和GPU流水线

先看模板测试,与之相关的是模板缓冲。模板缓冲和我们经常听到的颜色缓冲、深度缓冲几乎是一类东西,如果开始了模板测试,GPU首先读取(使用读取掩码)模板缓冲区该片元位置的模板值,然后将该值和读取(使用读取掩码)到的参考值进行比较,这个比较函数可与由开发者指定,例如小于时舍弃该片元,或大于等于时舍弃该片元。如果这个片元没有通过测试该片元就会被舍弃。不管一个片元有没有通过模板测试,我们都可以根据模板测试和下面的深度测试结果来修改模板缓冲区,这个操作也是由开发者指定。开发者可以设置不同结果下的修改操作,例如失败时模板缓冲区保持不变等等。模板测试通常用于限制渲染的区域,另外模板测试还有一些更高级的用法,如渲染阴影,轮廓渲染等。

如果一个片元幸运地通过了模板测试,那么就进入深度测试。这个同样可以高度配置的,如果开启了深度测试,GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较,这个比较函数也可以自己设置,例如小于时舍弃该片元,或者大于等于舍弃。通常这个函数是小于等于的关系,即如果这个片元的深度值大于等于当前深度缓冲区的值,那么就会舍弃它。因为我们总想只显示出离摄像机最近的物体,而那些被其他物体遮挡的就不需要出现在屏幕上。如果这个片元没有通过深度测试,它就没有权利更改深度缓冲区的值。如果通过了,开发者还可以指定是否要用这个片元的深度值覆盖掉原有的深度值,这是通过开启/关闭深度写入做到的。透明效果和深度测试以及深度写入关系很密切。

如果片元通过了上面所有测试,就可以进行合并。这里所讨论的渲染过程是一个物体接着一个物体画到屏幕上的,每个像素值的颜色信息被存储在一个名为颜色缓冲的地方。因此当我们执行这次渲染时,颜色缓冲往往已经有了上次渲染之后的颜色结果,那么我们使用这次渲染得到的颜色完全覆盖掉之前的结果还是其他处理?这就是合并需要解决的问题。

对于不透明物体,开发者可以关闭混合操作,这样片元着色器计算得到的颜色值就会直接覆盖掉颜色缓冲中的像素值。但对于半透明物体,我们就需要使用混合操作来让这个物体看起来是透明的。下面是一个简化版的混合操作流程图:

unity shader基础之——渲染流水线和GPU流水线

从流程图中可以发现,混合操作也可以高度配置的,开发者可以选择开启/关闭混合功能,如果开启混合功能会直接使用片元的颜色覆盖掉颜色缓冲区的颜色,而这特殊很多人发现无法得到透明效果的原因(没有开启混合功能)。如果开启了混合,GPU会取出已经存在于颜色缓冲区的颜色值。之后使用一个混合函数来进行混合操作。这个混合函数通常和透明通道息息相关,例如会根据透明通道的值进行混合操作。混合很像ps中对图层的操作:每一层图层可以选择混合模式,混合模式决定了该图层和下层图层的混合结果,而我们看到的图片就行混合后的图片。

上面的测试顺序不是唯一的,从逻辑上来说这些测试是在片元着色器后进行的,但对大多数GPU来说,会尽可能在执行片元着色器之前进行这些测试。因为当CPU在片元着色器阶段花了很大力气终于计算出片元颜色,却发现没有通过这些检验,片元被舍弃,那之前的计算成本全浪费了。

作为一个想提高性能的GPU,他会希望尽可能早的知道哪些片元是被舍弃的,对于这些片元就不需要在使用片元着色器计算它们的颜色。在unity给出的渲染流水线中,我们也可以发现他给出的深度测试是在片元着色器之前,这将深度测试提前执行的技术通常称为Early-Z技术。

但是如果将这些测试提前,检验结果可能会与片元着色器中的一些操作冲突。例如我们在片元着色器进行透明度测试,这个片元没有通过透明度测试,我们将会在着色器中调用API(例如clip函数)来手动舍弃掉。这就导致GPU无法提前执行各种测试。现代GPU会判断片元着色器中操作是否和提前测试发生冲突,如果冲突会禁用提前测试,这样会造成性能上的下降,因为有更多片元需要被处理了。这也是透明度测试会导致性能下降的原因。

当模型的图元经过上面层层计算和测试后就会显示到屏幕上,我们的屏幕显示的就是颜色缓冲区的颜色值,为了避免我们看到那些正在进行光栅化的图元,GPU会使用双重缓冲测试,这意味着对场景的渲染是在幕后发生的,即在后置缓冲中。一旦场景已经被渲染到了后置缓冲中,GPU就会交换后置缓冲区和前置缓冲中的内容,而前置缓冲区是之前显示在屏幕上的图像,由此保证了我们看到的图像是连续的。

3.9 总结

虽然渲染流水线比较复杂,但unity为我们封装了很多功能。更多时候只需要在shader设置一些输入,编写顶点和片元着色器,设置一些状态就可以达到大部分场景屏幕效果。缺点是编程自由度下降,初学者容易迷失方向,无法掌握背后的原理,出现问题无法找到错误的原因。

版权声明:本文为CSDN博主「小橙子0」的原创文章,
遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cgy56191948/article/details/101849639

最新文章