[Shader 入门精要]——整体概述

渲染流水线

1.1 综述

1.1.1 什么是渲染流水线

渲染一张图片由GPU和CPU完成,渲染流程分为三个阶段: 应用阶段几何阶段光栅化阶段

在这里插入图片描述

应用阶段:准备好场景数据,做一个 粗粒度提出(culling),以把不可见物体剔除。最后,设置好每个模型的渲染状态这一阶段最重要的输出是渲染所需的几何信息.即渲染图元,通俗来讲渲染图元可以是点、线、面。这些渲染图元会传递给下一个阶段——几何阶段

•几何阶段:对每个渲染图元进行逐顶点、逐多边形操作。 几何阶段的重要任务是把顶点坐标变换到屏幕空间,再交给光栅器进行处理,这一阶段将会输出屏幕空间的二位顶点坐标,并传递给下一阶段。

•光栅化阶段:使用上个阶段的数据来产生屏幕像素,并渲染出最终图像 。这阶段在GPU进行,主要任务是决定每个渲染图元中那些像素应该被绘制在屏幕上。它需要堆上一阶段得到的逐顶点数据进行插值,然后进行逐像素处理。

1.2 CPU和GPU之间通信

渲染流水线的起点是CPU,即应用阶段。应用阶段大致分为下面3个阶段:

(1)把数据加载到显存
(2)设置渲染状态
**(3)调用Draw Call **

1.2.1 把数据加载到显存中

所有渲染数据需要从硬盘加载到系统内存网格和纹理等数据被加载到显卡上的存储空间——显存。因为GPU对显存的访问速度更快,而且大部分显卡对RAM没有直接访问权限。

当数据加载到显存中,RAM中的数据可以移除了,在这之后开发者需要通过CPU来设置渲染状态。

1.2.2 设置渲染状态

状态定义了场景中网格怎样被渲染。使用那个顶点着色器\材质等。如果没有更改渲染状态,那么所有网格都将使用同一种渲染状态。

1.2.3 调用Draw Call

Dall Call 就是个命令,它的发起方是CPU,接受方是GPU,命令只指向需要被渲染的图元列表,而不包含任何材质信息。

1.3 GPU 流水线

1.3.1 概述

调用Drall Call 命令GPU进行渲染,这个GPU渲染过程就是GPU流水线。

我们无法控制几何阶段和光栅化阶段的细节,但是GPU向开发者开放了很多控制权

在这里插入图片描述

顶点数据——几何阶段**:顶点——曲面——几何——裁剪——屏幕映射————光栅化阶段:三角形设置——三角形遍历——片元——逐片元——屏幕图像

顶点着色器是完全可编程的,通常用于实现顶点的空间变化顶点着色等功能。

曲面细分着色器是一个可选的着色器,用于细分图元

几何着色器是一个可选着色器,用于执行逐图元的着色操作,或产生更多图元。

裁剪: 这阶段的目的是将不在摄像头视野内顶点裁剪掉。这个阶段可配置。

屏幕映射: 这阶段不可配置和编程,负责把每个图元坐标转换到屏幕坐标系。

光栅化阶段三角形设置三角形遍历阶段都是固定函数的阶段。

片元着色器则是完全可编程的,用于实现逐片元着色操作。

逐片元操作阶段负责执行很多重要操作,例如修改颜色,深度缓冲等,它不是不可编程,但是有很高的配置性。

1.3.2 顶点着色器

顶点着色器(Vertex Shader)是流水线的第一个阶段,输入来自于CPU。顶点着色器处理每个输入进来的顶点,但是本身不创建和销毁顶点。因为相互独立性所以处理速度快。

顶点着色器需要完成的工作主要有:坐标变换和逐顶点光照。顶点着色器还可以输出后续阶段所需的数据。

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

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

上述代码就是把顶点坐标转换到齐次裁剪空间

在这里插入图片描述

1.3.3 裁剪

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

一个图元和摄像机视野的关系有3种:完全在视野内部分在视野内完全在视野外。完全在视野内的图元就继续传递给下一个流水线阶段,完全在视野外的图元不会继续向下传递,因为它们不需要被渲染。而那些部分在视野内的图元需要进行一个处理,这就是裁剪。

在这里插入图片描述

1.3. 4 屏幕映射

**屏幕映射(ScreenMapping)的任务是把每个图元的x和y坐标转换到屏幕坐标系(Screen Coordinates)**下。屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。

屏幕坐标系和z坐标一起构成了一个坐标系,叫做窗口坐标系(Window Coordinates)

在这里插入图片描述

有一个需要引起注意的地方是,屏幕坐标系在OpenGL和DirectX之间的差异问题。OpenGL把屏幕的左下角当成最小的窗口坐标值,而DirectX则定义了屏幕的左上角为最小的窗口坐标值。

在这里插入图片描述

1.3.5 三角形设置

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

1.3.6 三角形遍历

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

在这里插入图片描述

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

1.3.7 片元着色器

片元着色器(Fragment Shader)是另一个非常重要的可编程着色器阶段。在DirectX中,片元着色器被称为像素着色器(Pixel Shader)前面的光栅化阶段实际上并不会影响屏幕上每个像素的颜色值而是会产生一系列的数据信息,用来表述一个三角网格是怎样覆盖每个像素的。而每个片元就负责存储这样一系列数据。真正会对像素产生影响的阶段是下一个流水线阶段——逐片元操作(Per-Fragment Operations)。我们随后就会讲到。片元着色器的输入是上一个阶段对顶点信息插值得到的结果,更具体来说,是根据那些从顶点着色器中输出的数据插值得到的。而它的输出是一个或者多个颜色值。

在这里插入图片描述

1.2.8 逐片元操作

终于到了渲染流水线的最后一步。逐片元操作(Per-Fragment Operations)是OpenGL中的说法,在DirectX中,这一阶段被称为输出合并阶段(Output-Merger)。Merger这个词可能更容易让读者明白这一步骤的目的:合并。而OpenGL中的名字可以让读者明白这个阶段的操作单位,即是对每一个片元进行一些操作。

				**最终操作,对每个片元进行操作处理,来决定渲染的最终效果**

这一阶段有几个主要任务。

(1)决定每个片元的可见性。这涉及了很多测试工作,例如深度测试、模板测试等。

(2)如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。

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

在这里插入图片描述

决定片元可见性的测试

深度测试和模板测试的实现过程

在这里插入图片描述

模板测试(Stencil Test)。与之相关的是模板缓冲(Stencil Buffer)。如果开启了模板测试,GPU会首先读取(使用读取掩码)模板缓冲区中该片元位置的模板值,然后将该值和读取(使用读取掩码)到的参考值(reference value)进行比较,这个比较函数可以是由开发者指定的,例如小于时舍弃该片元,或者大于等于时舍弃该片元。

如果这个片元没有通过这个测试,该片元就会被舍弃。不管一个片元有没有通过模板测试,我们都可以根据模板测试和下面的深度测试结果来修改模板缓冲区,这个修改操作也是由开发者指定的。开发者可以设置不同结果下的修改操作,例如,在失败时模板缓冲区保持不变,通过时将模板缓冲区中对应位置的值加1等。模板测试通常用于限制渲染的区域。另外,模板测试还有一些更高级的用法,如渲染阴影、轮廓渲染等。

如果开启了深度测试,GPU会把该片元的深度值已经存在于深度缓冲区中的深度值进行比较。这个比较函数也是可由开发者设置的,例如小于时舍弃该片元,或者大于等于时舍弃该片元。

通常这个比较函数是小于等于的关系,即如果这个片元的深度值大于等于当前深度缓冲区中的值,那么就会舍弃它。这是因为,我们总想只显示出离摄像机最近的物体,而那些被其他物体遮挡的就不需要出现在屏幕上。如果这个片元没有通过这个测试,该片元就会被舍弃。和**模板测试有些不同的是,如果一个片元没有通过深度测试,它就没有权利更改深度缓冲区中的值。**而如果它通过了测试,开发者还可以指定是否要用这个片元的深度值覆盖掉原有的深度值,这是通过开启/关闭深度写入来做到的。**我们在后面的学习中会发现,透明效果和深度测试以及深度写入的关系非常密切。**如果一个幸运的片元通过了上面的所有测试,它就可以自豪地来到合并功能的面前。

为什么需要合并?我们要知道,这里所讨论的渲染过程是一个物体接着一个物体画到屏幕上的。而每个像素的颜色信息被存储在一个名为颜色缓冲的地方。因此,当我们执行这次渲染时,颜色缓冲中往往已经有了上次渲染之后的颜色结果,那么,我们是使用这次渲染得到的颜色完全覆盖掉之前的结果,还是进行其他处理?这就是合并需要解决的问题。对于不透明物体,开发者可以关闭混合(Blend)操作。这样片元着色器计算得到的颜色值就会直接覆盖掉颜色缓冲区中的像素值。但对于半透明物体,我们就需要使用混合操作来让这个物体看起来是透明的。图2.16展示了一个简化版的混合操作的流程图。

在这里插入图片描述

从流程图中我们可以发现,混合操作也是可以高度配置的:开发者可以选择开启/关闭混合功能。如果没有开启混合功能,就会直接使用片元的颜色覆盖掉颜色缓冲区中的颜色,而这也是很多初学者发现无法得到透明效果的原因(没有开启混合功能)。如果开启了混合,GPU会取出源颜色和目标颜色,将两种颜色进行混合。源颜色指的是片元着色器得到的颜色值,而目标颜色则是已经存在于颜色缓冲区中的颜色值。之后,就会使用一个混合函数来进行混合操作。混合函数通常和透明通道息息相关,例如根据透明通道的值进行相加、相减、相乘等。混合很像Photoshop中对图层的操作:每一层图层可以选择混合模式,**混合模式决定了该图层和下层图层的混合结果,而我们看到的图片就是混合后的图片。上面给出的测试顺序并不是唯一的,而且虽然从逻辑上来说这些测试是在片元着色器之后进行的,但对于大多数GPU来说,它们会尽可能在执行片元着色器之前就进行这些测试。**这是可以理解的,想象一下,当GPU在片元着色器阶段花了很大力气终于计算出片元的颜色后,却发现这个片元根本没有通过这些检验,也就是说这个片元还是被舍弃了,那之前花费的计算成本全都浪费了!

在这里插入图片描述

作为一个想充分提高性能的GPU,它会希望尽可能早地知道哪些片元是会被舍弃的,对于这些片元就不需要再使用片元着色器来计算它们的颜色。在Unity给出的渲染流水线中,我们也可以发现它给出的深度测试是在片元着色器之前。这种将深度测试提前执行的技术通常也被称为Early-Z技术。希望读者看到这里时不会因此感到困惑。在本书后面的章节中,我们还会继续讨论这个问题。但是,如果将这些测试提前的话,其检验结果可能会与片元着色器中的一些操作冲突。

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

2.3.9 总结

虽然我们上面讲了很多,但其真正的实现过程远比上面讲到的要复杂。需要注意的是,读者可能会发现这里给出的流水线名称、顺序可能和在一些资料上看到的不同。一个原因是由于图像编程接口(如OpenGL和DirectX)的实现不尽相同,另一个原因是GPU在底层可能做了很多优化,例如上面提到的会在片元着色器之前就进行深度测试,似避免无谓的计算。虽然渲染流水线比较复杂,但Unity作为一个非常出色的平台为我们封装了很多功能。更多时候,我们只需要在一个**Unity Shader设置一些输入、编写顶点着色器和片元着色器、设置一些状态就可以达到大部分常见的屏幕效果。**这是Unity吸引人的魅力之处,但这样的缺点在于,封装性会导致编程自由度下降,使很多初学者迷失方向,无法掌握其背后的原理,并在出现问题时,往往无法找到错误原因,这是在学习Unity Shader时普遍的遭遇。

  • 21
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值