Unity Shader 入门精要---渲染流水线

渲染流水线的最终目的在于生成或者说是渲染一张二位纹理,即我们在电脑屏幕上看到的所有效果。它的输人是一个虚拟摄像机、一些光源、一些Shader以及纹理等。

《Render-Time Rendering, Third Edition》一书中将一个渲染流程分成3阶段:应用阶段(Application Stage)、几何阶段(Geometry Stage)、光栅化阶段(Rasterizer Stage)。

应用阶段:由我们的应用主导的,通常由CPU负责实现,开发者具有这个阶段的绝对控制权。

     准备场景数据,如摄像机的位置、视锥体、场景中的模型、光源等,其次,为提高渲染性能,需要一个粗粒度剔除(culling)工作,用以剔除那些不可见的物体。最后,设置每个模型的渲染状态。渲染状态包括但不限于它使用的材质(漫反射颜色、高光反射颜色)、使用的纹理、使用的Shader等。

     应用阶段最重要的输出是渲染所需的几何信息,即渲染图元(rendering primitives)。渲染图元将被传递给下一阶段---几何阶段

几何阶段:处理所有和我们要绘制的几何相关的事情。输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息。

     几何阶段负责和每个渲染图元打交道,进行逐顶点、逐多边形的操作。其一个重要的任务是把顶点坐标变换到屏幕空间中,再交给光栅器进行处理。

光栅化阶段:将使用几何阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。

     该阶段是在GPU上运行。光栅化的任务主要是决定每个渲染图元中的那些像素应该被绘制在屏幕上。它需要对从几何阶段得到的逐顶点数据(例如纹理坐标、顶点颜色等)进行插值,然后再进行逐像素处理。

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

   1.把数据加载到显存中。

   2.设置渲染状态。

   3.调用Draw Call。

所有渲染所需的数据都需要从硬盘中加载到系统内存中,然后网格和纹理等数据又被加载到显卡上的存储空间--显存中。当数据加载到显存中后,RAM系统内存中的数据就可以移除了,但对于一些数据不能移除,CPU仍然需哟访问。例如CPU可以访问网格数据来进行碰撞检测等。当数据加载到显存后,开发者还需要通过CPU来设置渲染状态,从而"指导"GPU如何进行渲染工作。

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

Draw Call就是一个命令,发起方是CPU,接收方是GPU。DrawCall指向一个需要被渲染的图元列表,该列表不包含任何材质信息。GPU会根据渲染状态(例如材质、纹理、着色器等)和所有输入的顶点数据进行计算,并最终输出成屏幕上显示的像素。

几何阶段和光栅化阶段,开发者无法拥有绝对控制权,其实现的载体是GPU,但GPU向开发者开放了很多的控制权。GPU通过实现流水线化,加快渲染速度。

几何阶段细分为:

     接受顶点数据输入,顶点着色器=〉曲面细分着色器=〉几何着色器=〉裁剪=〉屏幕映射

光栅化阶段细分为:

    接受屏幕映射数据输入,三角形设置=〉三角形遍历=〉片元着色器=〉逐片元操作=〉屏幕图像

顶点着色器(Vertex Shader)是完全可编程的。通常用于实现顶点的空间变换、顶点着色等功能。

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

几何着色器(Geometry Shader)可选着色器。用于执行逐图元的着色操作,或者用于产生更多的图元。

裁剪(Clipping)目的在于将那些不在摄像机视野内的顶点裁剪掉,并剔除某些三角图元的面片。

屏幕映射(Screen Mapping)是不可配置和编程的,负责把每个图元的坐标转换到屏幕坐标系中。

三角形设置(Triangle Setup)、三角形遍历(Triangle Traversal)阶段是固定函数的阶段。

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

逐片元操作(Per-Fragment Operations)阶段负责执行那个很多重要的操作,例如修改颜色、深度缓冲、进行混合等,它是不可编程的,但具有很高的可配置性。

顶点着色器:

     是渲染流水线的第一个阶段,它的输入来自于CPU。输入进来的每个顶点都会调用一次顶点着色器。顶点着色器本身不可以创建或者销毁任何顶点,也无法得到顶点与顶点之间的关系。由于这样的相互独立性,GPU可以利用本身的特性并行化处理每一个顶点。

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

     坐标变换。对顶点的坐标进行某种变换。可以通过改变顶点位置来模拟水面、布料等。一个最基本的顶点着色器必须完成的一个工作是:把顶点坐标从模型空间转换到齐次裁剪空间。

     顶点坐标转换到齐次裁剪坐标系下,接着通常再由硬件做透视除法后,最终得到归一化的设备坐标

     从观察空间到裁剪空间,也称为齐次裁剪空间。用于变换的矩阵叫做裁剪矩阵,也称为投影矩阵。裁剪空间的目的是方便对渲染图元进行裁剪,位于该空间外的被裁剪。该空间由视锥体决定。视锥体涉及到两种投影类型:正交投影和透视投影。

裁剪:

     当一个图元完全在摄像机视野内,会继续传递给下一个流水线阶段,完全在视野外的图元不会继续向下传递。部分在视野内的图元,在视野外的顶点会被新顶点代替,将其转化成完全在视野内。我们无法通过编程来控制裁剪的过程,这个是硬件上的固定操作。

屏幕映射:

     这一阶段输入的坐标是三维坐标系下的坐标,该阶段的任务是把每一个图元的x和y坐标转换到屏幕坐标系下。屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。每个图元被转化后的屏幕坐标系加上自身的z坐标构成了窗口坐标系。这些值会一起被传递到光栅化阶段。

     屏幕坐标系在OpenGL和DirectX之间存在差异问题。OpenGL把屏幕的左下角当成最小的窗口坐标值,而DirectX则定义屏幕的左上角为最小的窗口坐标值。

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

三角形设置:

    这是光栅化阶段的第一个流程,上一阶段输出的信息是屏幕坐标系下的顶点位置以及它们的相关的额外信息(如深度值z坐标、法线方向、视角方向等)。  上一阶段输出的都是三角网格的顶点,即我们得到的是三角网格每条边的两个端点。为了得到整个三角网格对像素的覆盖情况,我们必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,就需要得到三角形边界的表示方式。总结三角形设置就是一个计算三角网格表示数据的过程,该输出是为下一阶段做准备。

三角形遍历:

    该阶段会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话,就会生成一个片元(fragment)。该阶段也被称为扫描变换(Scan Conversion)。三角遍历阶段会根据上一阶段的计算结果来判断三角网格覆盖了那些像素,并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值。

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

片元着色器(Fragment Shader):

     这是另一个非常重要的可编程着色器阶段。在DirectX中,片元着色器被称为像素着色器(Pixel Shader)。该阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。

    为了在片元着色器中进行纹理采样,通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。片元着色器的局限在于,它仅可影响单个片元。只有一个情况例外,就是片元着色器可以访问到导数信息(gradient或者说是derivative)。

逐片元操作:

    这是渲染流水线的最后一步,也是真正对像素产生影响的阶段。逐片元操作(Per-Fragement Operations)是OpenGL中的说话,在DirectX中,这一阶段被称为输出合并阶段(Output-Merger)。

    主要任务:

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

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

模板测试(Stencil Test),与之相关的是模板缓冲(Stencil Buffer)。模板缓冲和我们经常听到的颜色缓冲、深度缓冲几乎是一类东西。如果开启了模板测试,GPU会首先读取(使用读取掩码)模板缓冲区中该片元位置的模板值,然后将该值和读取(使用读取掩码)到的参考值(reference value)进行比较,比较函数是由开发者指定的。不管一个片元有没有通过模板测试,都可以根据模板测试和下面的深度测试结果来修改模板缓冲区。该操作也是开发者指定的。模板测试还有一些更高级的用法,如渲染阴影、轮廓渲染等。模板测试可用于控制遮罩问题。

深度测试(Depth Test),可高度配置的。如果开启了深度测试,GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较。比较函数由开发者设置。通常该片元的深度值大于等于当前深度缓冲区中的值,就舍弃它。这是因为我们总想只显示出离摄像机最近的物体。如果一个片元没有通过这个深度测试,就没有权利更改深度缓冲区的值。若通过了测试,开发者可以通过开启/关闭深度写入来指定是否覆盖原来的深度值。

混合(Blend),渲染过程是一个物体接着一个物体画到屏幕上的。而每个像素的颜色信息被存储在一个名为颜色缓冲的地方。如果开启了混合,GPU会取出源颜色和目标颜色,将两种颜色进行混合。源颜色指片元着色器得到的颜色值,而目标颜色是已经存在于颜色缓冲区中的颜色值。之后使用一个混合函数来进行混合操作。混合函数通常和透明通道息息相关,例如根据透明通道的值进行相加、想减、相乘等。

在Unity给出的渲染流水线中,可以发现给出的深度测试时在片元着色器之前。这样避免了在片元着色器阶段计算片元颜色,而该片元根本没有通过测试而丢弃。这种将深度测试提前执行的技术通常被称为Early-Z技术。若将这些测试提前的话,其检验结果可能会与片元着色器中的一些操作冲突。例如我们在片元着色器进行了透明测试,而这个片元没有通过透明度测试,我们会在着色器中调用API来手动将其丢弃掉。这就导致GPU无法提前执行各种测试。因此,现代的GPU会判断片元着色器中的操作是否和提前测试发生冲突,如果有冲突,就会禁用提前测试。但是这样也会造成性能上的下降,因为有更多片元需要被处理了。这也是透明度测试会导致性能下降的原因。

当模型的图元经过了上面层层计算和测试后,就会显示到我们的屏幕上。我们的屏幕显示的是颜色缓冲区种的颜色值。为了避免我们看到正在进行光栅化的图元,GPU会使用双重缓冲(Double Buffering)的策略。对场景的渲染是存储在后置缓冲中,一旦场景渲染完,GPU就会交换后置缓冲区和前置缓冲中的内容,而前置缓冲区是显示在屏幕上的图像。由此保证了我们看到的图像总是连续的。

重点总结:

渲染状态:定义场景中的网格是怎样被渲染的。如使用哪个顶点着色器/片元着色器(其他shader脚本)、光源属性、材质等。

Drall Call:指向一个需要被渲染的图元(primitives)列表,用于CPU通知GPU数据准备好了,可以开始渲染了。

CPU和GPU是通过一个命令缓冲区(Command Buffer)来实现并行工作的。CPU向其中添加命令,GPU从中取命令执行。命令缓冲区中的命令种类很多,Draw Call只是其中一种,其他命令还有改变渲染状态等。其中主要的是Draw Call指明要渲染的图元列表,以及改变渲染状态命令(如改变使用的着色器,使用不同的纹理等)。

Draw Call造成性能问题的元凶是CPU。GPU的渲染能力是很强的,渲染200个还是2000个三角网格通常没有什么区别,而每次调用Draw Call之前,CPU需要向GPU发送很多内容,包括数据、状态、命令等。同时这一阶段,CPU还需要完成很多工作,例如检查渲染状态、改变渲染状态等,这些命令往往更加耗时。因此GPU渲染速度往往快于CPU提交命令的速度。

减少Draw Call的主要方法:批处理(Batching)。提交大量很小的Draw Call会造成CPU的性能瓶颈,即CPU把时间都花费在准备工作上了。批处理的优化想法是把很多小的Draw Call合并成一个大的Draw Call。由于需要在CPU的内存中合并网格,而合并的过程是需要消耗时间的,所以批处理更适合静态的物体,这些静态物体只需合并一次即可。对于动态物体,由于其运动性,所以每一帧都需要重新进行合并然后再发送给GPU。

非涅耳反射:根据视角方向控制反射程度。描述了一种光学现象,即当光线照射到物体表面上时,一部分发生发射,一部分进入物体内部,发生折射或散射。被反射的光和入射光之间存在一定的比率关系,比率关系可以通过菲涅尔等式进行计算。例子,直接低头看水面,发现水面几乎透明能看到水底,抬头看远处,几乎看不到水下的情景,只能看到水面反射的环境。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值