目录
2.曲面细分着色器(Tessellation Shader)
1.三角形设置(Triangle Setup)【固定函数阶段】
2.三角形遍历(Triangle Traversal)【固定函数阶段】
4.逐片元操作(Per-Fragment Operations)
一、什么是渲染流水线
流水线在工厂生产中是一个极为重要的概念,一个完整产品的制作一般会分成许多工序,由不同的部门进行制作,在流水线未被提及时,一般一个产品会一直生产直至生产完成,第二个产品才会继续开始。
所谓流水线,就是一个产品进行第一个工序后传递给第二个工序,此时第二个产品进入到第一个工序,这样就提高单位时间的生产量。
而渲染流水线的工作任务在于由一个三维场景出发,生成(或者说是渲染)一张二维图像,换句话说,计算机需要从一系列的顶点数据、纹理等信息出发,把这些信息最终转换成一张人眼可以看到的图像。而这些工作通常是由CPU和GPU共同完成的。
二、渲染流程的三个概念性阶段
1.应用阶段(Application Stage)
这个阶段由应用主导,通常是由CPU负责实现,我们开发者对这个阶段具有绝对的控制权。
在这个阶段,开发者有三个主要的任务:
一、准备好场景数据
比如摄像机的位置、视锥体、场景中包含了什么模型,使用了哪些光源等。
二、粗粒度剔除
为了提高性能,这一步是需要进行的,把那些不可见的物体剔除出去,这样就不需要再移交给几何阶段进行处理。
三、设置渲染状态
这一步需要设置好模型的渲染状态,这些渲染状态包含但不限于它使用的材质(漫反射颜色、高光反射颜色)、使用的纹理、Shader等。
这一阶段最重要的输出是渲染所需要的几何信息,即渲染图元(通俗来讲,渲染图元可以是点、线、三角面等,这些图元会被传递给下一个阶段——几何阶段)
2.几何阶段(Geometry Stage)
几何阶段处理所有和我们要绘制的几何相关的事物(例如决定需要绘制的图元是什么,怎么样绘制它们,在什么地方绘制),这一阶段通常在GPU上进行
几何阶段负责和每个渲染图元打交道,进行逐顶点、逐多边形操作
几何阶段一个重要的任务就是把顶点坐标变换到屏幕空间中,再交给光栅器进行处理。
(这一阶段将会输出屏幕空间的二维顶点坐标、每个顶点对应的深度值。着色等相关信息,并转递给下一个阶段——光栅化阶段)
3.光栅化阶段(Rasterizer Stage)
这一阶段使用几何阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像,这一阶段在GPU上运行。
光栅化的任务主要是决定每个渲染图元中的哪些像素应该被绘制在屏幕上,它需要对上一个阶段得到的逐顶点数据(如纹理坐标,顶点颜色等)进行插值,然后再进行逐像素处理
三、CPU和GPU的通信
渲染流水线的七点是CPU,即应用阶段
应用阶段可以大致可以分为3个阶段:
1.把数据加载到显存中
所有渲染所需要的数据都需要从硬盘(Hard Disk Drive ,HDD)中加载到系统内存(Random Access Memory,RAM)中,然后网格和纹理等数据又被加载到显卡上的存储空间——显存(Video Random Access Memory,VRAM)中
原因:显卡对于显存的访问速度更快,且大多数的显卡不能够直接访问系统内存
当把数据加载到显存后,RAM中的数据就可以移除了。但是对于一些CPU仍需要访问的数据是不需要被移除的,因为从硬盘加载到RAM的过程十分耗时。
2.设置渲染状态
通俗来讲,渲染状态定义了场景中的网格是怎样被渲染的(比如使用了哪个顶点着色器、光源属性、材质等),如果没有更改渲染状态,那么所有的网格都将使用同一种渲染状态。
3.调用Draw Call
Draw Call是一种命令,它的发起方是CPU,接收方是GPU。这个命令仅仅指向一个需要被渲染的图元列表,而不会再包含任何材质信息。
当给定一个Draw Call时,GPU就会根据渲染状态和所有的输入顶点数据进行计算,最终输出成屏幕上显示的像素,这个计算过程就是GPU流水线。
四、GPU流水线
GPU渲染过程就是GPU流水线。
几何阶段和光栅化阶段可以分成若干更小的流水线阶段,这些流水线阶段由GPU来实现,每个阶段GPU提供了不同的可配置性或者可编程性。
GPU流水线接收顶点数据作为输入,这些数据是由应用阶段加载到显存中,再由Draw Call指定的。
几何阶段
1.顶点着色器(Vertex Shader)
GPU流水线接收到的顶点数据转递给顶点着色器,顶点着色器是完全可编程的,通常用于实现顶点的空间变换,顶点着色等功能。
它是流水线的第一个阶段,输入来自CPU,处理单位是顶点。
输入进来的每个顶点都会调用一次顶点着色器。
顶点着色器本身不可以创建或者销毁任何顶点,而且无法得知顶点与顶点之间的关系(比如我们无法得治两个顶点是否属于同一个三角网格),因为这一个相互独立性,GPU可以利用本身的特性并行化处理每一个顶点,加快处理速度。
顶点着色器的主要工作有:坐标变换和逐顶点光照(除此之外还可以输出后续阶段所需要的数据)
坐标变换
对顶点的坐标(即位置)进行某种变换,但是一个最基本的顶点着色器必须完成的一个工作是,把所有顶点从模型空间转换到齐次裁剪空间。
o.pos=mul(UNITY_MVP,v.position)
类似这句代码的功能就是把顶点坐标转换到齐次裁剪坐标系下,接着通常再由硬件做透视除法后,最终得到归一化的设备坐标(Normalized Device Coordinates, NDC)。
2.曲面细分着色器(Tessellation Shader)
曲面细分着色器是一个可选的着色器,用于细分图元。
3.裁剪(Clipping)
将那些不在摄像机视野内的顶点裁剪掉,并剔除某些三角图元的面片,这个过程是可配置的但是是不可编程的。
一个图元和视野的关系有3中:完全在视野内,部分在视野内,完全在视野外。
那些部分在视野内的图元需要进行裁剪处理。
已知在NDC下的顶点位置,即顶点位置在一个立方体内,只需要将图元裁剪到立方体内即可。
4.屏幕映射(Screen Mapping)
这一阶段是不可配置和编程的,它负责把每个图元的坐标转换到屏幕坐标系中。
这一步的输入是三维坐标系下的坐标(范围在单位立方体内),屏幕映射的任务是把每个图元的x和y坐标转换到屏幕坐标系下,屏幕坐标系是一个二维坐标系。它和我们用于显示画面的分辨率有很大关系。
实际上,屏幕坐标系和z坐标系一起构成了一个坐标系,叫做窗口坐标系,这些值会被一起传递到光栅化阶段
光栅化阶段
这一步开始进入到光栅化阶段,从上一个阶段输出的信息是屏幕坐标系下的顶点位置以及和它们相关的额外信息(如深度值、法线方向、视角方向等)。光栅化阶段有两个重要的目标:计算每个图元覆盖率那些像素,以及为这些像素计算它们的颜色。
1.三角形设置(Triangle Setup)【固定函数阶段】
光栅化的第一个阶段,会计算光栅化三角网格所需的信息。
具体来说,上一个阶段输出的都是三角网格的顶点,即我们得到的是三角网格每条边的两个端点。如果需要计算整个三角网格对像素的覆盖情况,就必须计算每条边上的像素坐标。
为了能够计算边界像素的坐标信息,就需要得到三角形边界的表示方式,这样一个计算三角网格表示数据的过程就叫做三角形设置。
2.三角形遍历(Triangle Traversal)【固定函数阶段】
此阶段会检查每个像素是否被一个三角网格覆盖,如果覆盖,则生成一个片元。
这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,也被称为扫描变换(Scan Conversion)。
这一步的输出是一个片元序列,需要注意的是,一个片元并不是真正意义上的像素,而是包含了很多状态的几何,这些状态用于计算每个像素的最终颜色。
3.片元着色器(Fragment Shader)
片元着色器是另一个非常重要的可编程着色器阶段,在DirectX中也被称为像素着色器(Pixel Shader)。
片元着色器的输入是上一个阶段对顶点数据信息插值得到的结果,更具体来说,是根据那些从顶点着色器中输出的数据插值得到的,它的输出是一个或者多个颜色值。
这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。
4.逐片元操作(Per-Fragment Operations)
这个是渲染流水线的最后一步。逐片元操作时OpenGL中的说法,在DirectX中被称为输出合并阶段(Output-Merger)。此阶段时高度可配置的,即我们需要设置每一步的操作细节。
这一阶段的几个主要任务:
1.决定每个片元的可见性,这涉及了很多测试工作,如深度测试,模板测试等
2.如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。
测试的过程实际上是个比较复杂的过程,而且不同图形接口的实现细节不尽相同。最基本的两个测试——深度测试和模板测试:
模板测试(Stencil Test)
与之相关的时模板缓冲(Stencil Buffer),这个测试是高度可配置的。
如果开启了模板测试,GPU会首先读取(使用读取掩码)模板缓冲中改片元的位置的模板值,然后将该值和读取(使用读取掩码)到的参考值进行比较,这个比较是可以由开发者自定的【比如小于时舍弃该片元】。如果该片元没有通过这个测试,该片元就会被舍弃。
不管一个片元有没有通过模板测试,我们都可以根据模板测试和下面的深度测试结果来修改模板缓冲区,这个修改操作也是由开发者自定的,
模板测试常用于限制渲染的区域。另外模板测试还有一些高级的用法,不如渲染阴影。轮廓渲染等。
深度测试(Depth Test)
这个测试是高度可配置的。
如果开启了深度测试,GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较。这个比较函数也是可由开发之设置的【比如小于时舍弃该片元】。通常这个比较函数时小于等于的关系,这是因为我们总想只显示出离摄像机最近的物体,而那些被其他物体遮挡的就不需要出现在屏幕上。如果没有通过测试,该片元就会被舍弃。
但与模板测试不同的是,如果一个片元没有通过深度测试,它就没有权利更改深度缓冲区中的值。而如果它通过了,开发者也可以指定是否要用这个片元的深度值覆盖掉原有的深度值。
合并
如果一个片元通过了上面的所有测试,即进入合并阶段。
为什么需要合并?
渲染过程是一个接着一个物体画到屏幕上的,而每个像素的颜色信息被存储在一个名为颜色缓冲区的地方。因此,当我们执行这次渲染时,颜色缓冲中往往以及有了上次渲染之后的颜色结果,那么,我们是使用测试渲染得到的颜色完全覆盖之前的结果还是进行其他处理,则是合并需要解决的问题。
对于不透明物体,开发者可以关闭混合(Blend)操作,这样片元着色器计算得到的颜色值就会直接覆盖颜色缓冲区的像素值。但对于半透明物体,就需要混合操作来让这个物体看起来是透明的。
混合(Blend)
混合操作也是高度可配置的,如果没有开启,则会直接使用片元的颜色覆盖掉颜色缓冲区中的颜色,如果开启,GPU会取出源颜色(片元着色器得到的颜色值)和目标颜色(已经存在于颜色缓冲区中的颜色值),将两种颜色进行混合【采用一个混合函数来进行混合操作】。
当模型的图元经过了上面层层计算和测试后,就会显示到我们的屏幕上。我们屏幕显示的就是颜色缓冲区中的颜色值。为了避免我们看到那些正在进行光栅化的图元,GPU会使用双重缓冲(Double Buffering),这意味着对场景的渲染是在幕后进行的,即在后置缓冲(Back Buffer)中。一旦场景已经被渲染到后置缓冲中,GPU就会交换后置缓冲区和前置缓冲(Front Buffer)中的内容,而前置缓冲区是之前显示在屏幕上的图像,这样就保证了我们看到的图像是连续的。
一些问题:
CPU和GPU是如何实现并行工作的?
命令缓冲区(Command Buffer),包含了一个命令队列,由CPU添加命令,GPU从中读取命令,添加和读取都是相互独立的。当CPU需要渲染一些对象时,它可以想命令缓冲区添加命令,而当GPU完成了上一次的渲染任务后,它就可以从命令队列中再读取一个命令并执行,Draw Call命令是其中一种命令。