(一)UnityShader渲染流水线

什么是渲染流水线


把一个非流水线系统分成n个流水线阶段,且每个阶段耗时相同的话,会使整个系统的效率提升n倍。渲染流水线是计算机类似工厂流水线一样,多道工序可同时渲染,而不是只有一个产片渲染完毕后才进行下一个产品的渲染。

渲染流水线的工作任务是为了将一个三维场景渲染成一张二维图像。即计算机将顶点、纹理等信息整理,将其转换成一张人眼看到的计算机屏幕上的二维图像。这个工作离不开CPU(中央处理器)和GPU(显卡图形处理器)。《Render-Time Rendering,Third Edition》中将渲染流水线分为三个阶段:应用阶段(Application Stage)、几何阶段(Geometry Stage)、光栅化阶段(Rasterizer Stage)。每个阶段本身也都是一个流水线,也包含了很多的子流水线。

 

CPU和GPU之间的通信


 

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

1)把数据加载到显存中

渲染所需数据从硬盘(Hard Disk Drive,HDD)加载到系统内存(Random Access Memory,RAM)。然后,网格和纹理等大量数据又被加载到显卡上的存储空间——显存(Video Random Access Memory,VRAM)。这是因为,显卡对于显存的访问速度更快,而且大多数显卡对于RAM没有直接的访问权利。一般数据加载到显存,RAM中的数据就可以移除了,但有时CPU仍需要访问他们(比如利用网格检测碰撞),所以我们并不希望移除他们,因为从HDD加载到RAM过程对计算机运算是一个不小的开销。在加载完成后,CPU来设置渲染状态来指导GPU如何进行渲染。

2)设置渲染状态                                         

这些渲染状态定义了场景中的网格是如何被渲染出来,例如使用哪个顶点着色器/片元着色器、光源属性、材质等。

3)调用DrawCall命令                    

DrawCall就是一个命令,发起方是CPU,接受方GPU。这个命令仅包含一个需要渲染的图元列表,不再包含任何材质信息(上一个阶段完成了)。

 

GPU流水线                                                                                        


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

 

顶点着色器(Vertex Shader)是完全可编程的,他通常用于实现顶点的空间变换、顶点着色等动能。它处理单位是顶点,输入进来的每个顶点都会调用一次顶点着色器,顶点着色器无权创建或销毁任何顶点,并且无法得到顶点和顶点之间的关系(如两个顶点是否属于同一个三角网格)。顶点着色器需要完成主要工作:坐标变换和逐顶点光照。

一个最最基本的顶点着色器必须完成的一个工作是,把顶点坐标从模型空间转换到齐次裁剪空间。在顶点着色器中常有类似

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

这种代码,其功能就是把顶点坐标从模型空间转换到齐次裁剪空间,接着由硬件做透视除法,最终得到归一化的设备坐标(Normalized Device Coordinates,NDC)。具体实现细节详见第三篇文章。下图给出的坐标范围是OpenGL同时也是Unity使用的NDC,他的Z分量范围在[-1,1]之间,而在DirectX中,NDC的Z分量范围是[0,1]。最常见的输出路径是经过光栅化后交给片元着色器进行处理。而现代ShaderModel中,他还可以把数据发送给曲面细分着色器或几何着色器。

 

 

曲面细分着色器(Tessellation Shader)是一个可选的着色器,它用于细分图元。

几何着色器(Geometry Shader)同样是一个可选着色器,它可以被用于执行逐图元的着色操作,或用于产生更多图元。

裁剪(Clipping)是将那些不存在摄像机视野内的顶点裁剪掉,并剔除某些三角图元的面片,不可编程但可配置的。一个图元和摄像机的关系有3种:完全在视野中、部分在视野中、完全在视野外。完全在视野中的图元继续传递给下一个流水线阶段;完全在视野外的图元不会继续向下传递,因为他们不需要渲染;而部分在视野内的图元需要进行一个处理就是裁剪。如下图(方体为相机视野范围,颜色面片为3种不同位置的图元)

 

屏幕映射(Screen Mapping)可配置并且可编程,它负责把每个图元的xy坐标转换到屏幕坐标系(Screen Coordinates)下。屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大的关系。屏幕映射不会对输入的z坐标做任何处理,屏幕坐标系和z坐标共同构成一个新的坐标系,叫做窗口坐标系(Windows Coordinates)。这些值会一起传到光栅化阶段。屏幕映射得到的屏幕坐标决定了这个顶点对应屏幕上哪个像素以及距离这个像素多远。屏幕坐标系在OpenGl和DirectX之间存在不小的差异,他们的窗口坐标存在差异,开发者要时刻小心注意该问题,如果发现的图像是倒转的,可能这就是导致原因。

 

三角形设置(Triangle Setup)和三角形遍历(Triangle Traversal)都是固定函数(Fixed-Function)的阶段。

这一步开始就正式进入光栅化阶段,光栅化阶段两个最重要的目标:

1)计算每个图元覆盖了哪些像素。2)为这些像素计算他们的颜色。

三角形设置(Triangle Setup)该阶段会计算光栅化一个三角形网格所需的信息,如三角形的边界的表示方式。三角形遍历(Triangle Traversal)阶段会检查每个像素是否被一个三角形网格覆盖,如果覆盖就会生成一个片元(fragment)。而这样一个找到哪些像素被三角形网格覆盖的过程就是三角形遍历,这个阶段也被称为三角形遍历(Scan Conversion)。三角形遍历会根据上一个阶段的计算结果判断一个三角形网格覆盖了哪些像素。需要注意的是,一个片元并不是真正意义上的像素,而是包含了和状态的集合,这些状态用于计算每个像素的最终颜色。

 

 

片元着色器(Fragment Shader)是完全可编程的,它用于实现逐片元(Per-Fragment)的着色操作。在DirectX中片元着色器被称为像素着色器(Pixel Shader),但片元着色器是一个更合适的名字,因为此时的片元并不是一个真正的像素。该阶段重要的技术之一就是纹理采样,我们通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化对三角网格的三个顶点对应的纹理坐标进行插值,既可得到其覆盖的片元纹理坐标,如下图所示。前面的光栅化不影响屏幕上的每个像素颜色值,只是产生一系列数据,用来表述一个三角网络如何覆盖每个像素,而片元就负责存储这一系列数据,真正影响像素产生的是下一个流水线阶段——逐片元操作(Per-Fragment Operations)。片元着色器的局限性在于,它仅可以影响单个片元。当执行片元着色器时,他不可以将自己任何结果直接发送给它的邻居们。但是有一个特殊情况,就是片元着色器可以访问到导数信息(gradient,或者是derivative)。

渲染流水线的最后一步。逐片元操作(Per-Fragment)是OpenGL中的说法,在DirectX中,这一阶段被称为输出合并阶段(Output-Merger)。逐片元操作负责执行很多重要的操作,具有有很高的可配置性,即我们可以设置每一步的操作细节。这一阶段有以下主要的任务:

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

在该阶段需要解决每个片元的可见性问题,一个片元必须通过所有的测试,才能最终获得和GPU谈判的资格,即和颜色缓冲区进行合并,如果没有通过,这个片元将会被舍弃! 

测试是个复杂的过程,而且不同的图形借口例如(OpenGL和DirectX)实现细节也不同。

  1. 模块测试(Stencil Test)。与之相关的是模板缓冲(Stencil Buffer),如果开启了模板测试,GPU会首先读取(使用读取掩码)模板缓冲区中该片元位置的模板值,然后将该值和读取(使用读取掩码)到参考值(reference value)进行比较,这个比较函数可以由开发者指定。例如小于时舍弃该片元,或者大于等于时舍弃该片元。模板测试通常用于限制渲染的区域。也还有其他比较高级的用法,如渲染阴影、轮廓渲染。
  2. 深度测试(Depth Test)。这个测试同样可以进行高度配置,如果开启了深度测试,GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较,这个比较函数也是可以由开发者指定。如果这个片元的深度值大于等于当前深度缓冲区中的值,那么就会舍弃它。因为,我们总想只显示出离摄像机最近的物体,而那些被其他物体遮挡的就不需要出现在屏幕上。如果这个片元没有通过测试,该片元将被舍弃。和模板测试不同的是,如果一个片元没有通过深度测试,他就没有权利更改深度缓冲区中的值。而如果通过,开发者可以指定是否用这个片元的深度值覆盖掉原有的深度值,这是通过开启/关闭深度写入来做到。通过以后的学习,我们会发现,透明效果和深度测试以及深度写入关系非常密切。
  3. 每个像素的颜色信息都被储存在一个叫颜色缓冲的地方。当我们执行这次渲染时,颜色缓冲已经包含上次渲染之后的颜色结果,那么这次得到渲染得到的颜色如何处理?完全覆盖掉?还是其他处理?我们解决的手段,通过混合(Blend)。(1)对于不透明的物体,开发者可以关闭混合,这样片元着色器计算得到的颜色会直接覆盖掉颜色缓冲区的颜色。(2)对于半透明的物体,我们使用混合操作,让这个物体看起来是透明的。许多初学者没有开启混合功能,导致无法得到透明的效果。开启混合后,GPU取源颜色和目标颜色,通过一个混合函数将两个颜色混合。源颜色指片元着色器得到的颜色值,目标颜色则是已经存在颜色缓冲区中的颜色,这个混合函数通常与透明通道息息相关(类似Photoshop中对图层的操作)。

需注意:测试顺序并不唯一,为了充分提高性能和效率,GPU希望更早的舍弃不需要的片元,节省更多的资源。如Untiy给出的流水线中,我们发现深度测试是在片元着色器之前,这种将深度测试提前的技术通常也被称作Early-Z技术。但是如果将这些测试提前,其检验结果可能会与片元着色器产生一些冲突。因此,现代GPU会判断片元着色器中的操作是否和提前测试发生冲突,如果有冲突就会禁用提前测试。但是,这样也会造成性能下降。

当模型的图元经过了上面层层的计算和测试后,就会显示到我们的屏幕上。

                                                                                                             

posted @ 2017-06-19 17:24  20世纪少年  阅读(448)  评论(0)    收藏  举报