Unity Shader入门精要学习笔记 - 第2章 渲染流水线

Unity Shader入门精要学习笔记 - 第2章 渲染流水线

本系列为UnityShader入门精要读书笔记总结,
原作者博客链接:http://blog.csdn.net/candycat1992/article/
书籍链接:http://product.dangdang.com/23972910.html

第2章 渲染流水线

2.1 综述

渲染流水线的最终目的在于生成或者说是渲染一张二维纹理,即我们在电脑屏幕上看到的所有效果。它的输入是一个虚拟摄像机、一些光源、一些Shader以及纹理等。
渲染流程分成3个阶段:应用阶段、几何阶段、光栅化阶段。
这里写图片描述

应用阶段:
这个阶段开发者主导,通常由CPU负责实现。
在这个阶段三个主要任务:
首先,准备好场景数据,例如摄像机的位置、场景中模型、使用光源等;
其次,粗粒度剔除工作,剔除不可见物体,省去后续几何阶段进行处理;
最后,设置好每个模型的渲染状态。这些渲染状态包含材质、纹理、Shader等。

输出的是渲染所需的几何信息,即渲染图元,这些渲染图元将被传递给下一阶段:几何阶段。

几何阶段:(对应的顶点函数)
几何阶段通常在GPU上进行,负责和每个渲染图元打交道,进行逐顶点、逐多边形的操作。
几何阶段的一个重要任务就是把顶点坐标变换到屏幕空间中, 再交给光栅器进行处理。
这一阶段将会输出屏幕空间的二维顶点坐标、 每个顶点对应的深度值、 着色等相关信息, 并传递给下一个阶段。

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

2.2 CPU和GPU之间的通信

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

把数据加载到显存中:
所有渲染所需的数据都需要从硬盘中加载到系统内存中。然后,网格和纹理等数据又被加载到显卡的存储空间——显存中。真实渲染中需要加载到显存中的数据往往比图所示复杂许多。 例如, 顶
点的位置信息、 法线方向、 顶点颜色、 纹理坐标等。
这里写图片描述
设置渲染状态:
渲染状态通俗的解释是,这些状态定义了场景中的网格是怎样被渲染的。
例如,使用哪个着色器、光源属性、材质等。
如果我们没有更改渲染状态,那么所有的网格都将使用同一种渲染状态,如下图显示了当使用同一种渲染状态时,渲染3个不同网格的内容。
这里写图片描述
准备好上述工作后,CPU会调用渲染命令Draw Call来告诉GPU开始渲染。

调用Draw Call:
Draw Call 实际上是一个命令,它的发起方是CPU,接收方是GPU。
这个命令仅仅会指向一个需要被渲染的图元列表,而不会再包含任何材质信息——这是因为我们再上个阶段中完成了。
当给定了一个Draw Call 时,GPU就会根据渲染状态和所有输入的顶点数据来进行计算,最终输出成屏幕上显示的那些漂亮的像素。而这个过程,就是GPU 流水线。
这里写图片描述

2.3 GPU 流水线

几何阶段和光栅化阶段,开发者无法拥有绝对的控制权,其实现的载体是GPU、GPU通过实现流水线化,大大加快了渲染速度。
几何阶段和光栅化极端可分成若干更小的流水线阶段。
这里写图片描述

谐音记忆 顶曲几裁屏 三三片逐屏

绿色:完全可编程
黄色:可配置不可编程
蓝色:流水线固定实现

实线的框:开发者必须编程实践
虚线的框:可选的

几何阶段:
顶点着色器(Vertext Shader):完全可编程的,它通常用于实现顶点的空间变换、顶点着色等功能。
曲面细分着色器(Tessellation Shader):一个可选的着色器,它用于细分图元。
几何着色器(Geometry Shader):可选的着色器,用于执行逐图元的着色操作,或者产生更多的图元。
裁剪:将不在摄像机视野内的顶点裁减掉,并剔除某些三角图元的面片。这个阶段是可配置的。
屏幕映射:这一阶段是不可配置和编程的,它负责把每个图元的坐标转换到屏幕坐标系中。

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

顶点着色器:
顶点着色器是流水线的第一个阶段,它的输入来自于CPU。顶点着色器的处理单位是顶点,也就是说,输入进来的每个顶点都会调用一次顶点着色器。

顶点着色器需要完成的工作主要有:坐标变换和逐顶点光照

坐标变换:对顶点的坐标进行某种变换。一个最基本的顶点着色器必须完成的一个工作是,把顶点坐标从模型空间转换到齐次裁剪空间,我们在顶点着色器中经常可以看到这样的代码:

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

裁剪:
裁剪掉在摄像机视野范围外的东西,以及图元部分在摄像机视野外的部分。
这里写图片描述
屏幕映射:
把每个图元的x和y坐标转换到屏幕坐标系下。屏幕坐标系是一个二维坐标系,屏幕映射不会对输入的坐标进行任何处理。实际上,屏幕坐标系和z坐标一起构成了一个坐标系,叫窗口坐标系。
这里写图片描述

三角形设置:
这一步开始进入了光栅化阶段。从上一个阶段输出的信息是屏幕坐标系下的顶点位置以及和它们相关的额外信息,如深度值(z坐标)、法线方向、视角方向等。

光栅化阶段都有两个最重要的目标:计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色。

三角形设置这个阶段会计算光栅化一个三角网格所需的信息。这样一个计算三角网格表示数据的过程就叫做三角形设置。它的输出是为了给下一个阶段做准备。

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

三角形遍历阶段会根据上一个阶段计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值。如下图:
这里写图片描述

片元着色器:
它是一个非常重要的可编程着色器阶段。
光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据信息,用来表述一个三角网格是怎样覆盖每个像素的。而每个片元就负责存储这样一系列数据。

真正会对像素产生影响的阶段是下一个流水线阶段——逐片元操作。

片元着色器的输入是上一个阶段对顶点信息插值得到的结果,更具体的说,是根据那些从顶点着色器中输出的数据插值得到的。而它的输出是一个或者多个颜色值。如下图
这里写图片描述

逐片元操作:

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

需要指明的是,逐片元操作阶段是高度可配置性的,即我们可以设置每一步的操作细节。
这个阶段首先要解决每个片元的可见性问题。这需要进行一系列测试。
这里写图片描述
这里写图片描述

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

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

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

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

上面给出的测试顺序并不是唯一的,而且虽然从逻辑上来说这些测试是在片元着色器之后进行的,但对于大多数GPU 来说,它们会尽可能在执行片元着色器之前就进行这些测试。这是可以理解的,想象一下,当GPU 在片元着色器阶段花了很大力气终于计算出片元颜色后,却发现这个片元根本没有通过这些检验,也就是说这个片元还是被舍弃了,那之前花费的计算成本全都浪费了!
这里写图片描述

图场景中包含了两个对象,球和长方体,绘制顺序是先绘制球(在屏幕上显示为圆),再绘制长方体(在屏幕上显示为长方形)。如果深度测试在片元着色器之后执行,那么渲染长方体时,虽然它的大部分区域都被遮挡在球的后面,即它覆盖的绝大部分片元根本无法通过深度测试,但是我们仍然需要对这些片元执行片元着色器,造成很大的性能浪费。
当模型的图元机关经过了上面层层计算和测试后,就会显示到我们的屏幕上。我们的屏幕显示的就是颜色缓冲区中的颜色值。但是,为了避免我们看到那些正在进行光栅化的图元,GPU会使用双重缓冲的策略。这意味着,对场景的渲染是在幕后发生的,即在后置缓冲中。一旦场景已经被渲染到了后置缓冲中,GPU就会交换后置缓冲区和前置缓冲区中的内容,而前置缓冲区是之前显示在屏幕上的图像,由此,保证了我们看到的图像总是连续的。
实际上,真正的实现过程远比上面讲到的复杂。需要注意的是,读者可能会发现这里给出的流水线名称、顺序可能和在一些资料上看到的不同。一个原因是由于图像编程接口(如OpenGL 和 DirectX)的实现不尽相同,另一个原因是GPU在底层可能做了很多优化例如上面提到的会在片元着色器之前就进行深度测试。
虽然渲染流水线比较复杂,但Unity作为一个非常出色的平台为我们封装了很多功能。更多时候,我们只要在一个Unity Shader 设置一些输入、编写顶点着色器和片元着色器、设置一些状态可以达到大部分常见的屏幕效果。

猜你喜欢

转载自blog.csdn.net/wwlcsdn000/article/details/78900149