【Unity Shader】渲染流水线

主要参考《Unity Shader入门精要》一书,外加自己的一些总结


前言


过去不管是沉迷于游戏也罢,疯狂追剧也罢,都已经过去了,时间不会为谁而停留,岁月终会在每个人脸上留下痕迹。很感激身边还有人会让我对自己的行为感到深深的自责,从而拥有前进的动力。上个月,身边的人推荐给我一本Shader方面的书,让我了解一下,我通过博客来记录自己从零开始所学的知识点,希望自己能坚持的记录下去,fighting!


什么是流水线


举个例子,隔壁老王有个生产洋娃娃的工厂,一个洋娃娃的生产流程分为4个步骤:第1步,制作洋娃娃的躯干;第2步,添加眼睛和嘴巴;第3步,添加头发;第4步,给洋娃娃进行包装。在流水线出现之前,只有把这四个步骤都做完才能进行下一个娃娃的制作,如果每个步骤需要一个小时,那么每4个小时才能生产一个洋娃娃。当老王把流水线引入工厂之后,每个步骤由专人在做,所有步骤同时进行,这样每1个小时就能生产一个洋娃娃了。

采用流水线的好处在于大大提高了单位时间的额生产量。


什么是渲染流水线


在学习Shader(着色器)之前,先了解一下渲染流水线,因为Shader是渲染流水线中的一个环节。

渲染流水线分为两种,一种是可编程渲染流水线,另一种是固定渲染流水线。现在的渲染流水线基本都是可编程的,以实现不同的效果。

渲染流水线分为三个阶段:应用阶段、几何阶段、光栅化阶段。


图1 渲染流水线中的三个概念阶段 


a.应用阶段(CPU 处理)

这个阶段是由我们开发者主导的,主要有3个任务:

1), 准备好场景数据,比如摄像机位置、视锥体、模型、光源等;

2),粗粒度剔除工作,把不可见的物体剔除出去;

3),设置好每个模型的渲染状态(使用的材质、使用的纹理、使用的Shader等)。

这一阶段最重要的输出是渲染所需的几何信息,即渲染图元,传递给下一个阶段,几何阶段。图元:实际上就是点、线、面。

b.几何阶段(GPU 处理)

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

通过对渲染图元进行处理后,将输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等信息,并传递给下一阶段。该阶段可以分为更小的流水线阶段。

c.光栅化阶段(GPU处理)

使用几何阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。该阶段可以分为更小的流水线阶段。

光栅化的任务主要是决定每个渲染图元中的哪些像素应该被绘制在屏幕上。


CPU与GPU之间的通信


渲染流水线的起点是CPU,即应用阶段。应用阶段大致可分为下面 3 个阶段:
(1)把数据加载到显存中

所有渲染所需的数据都需要从硬盘(HDD)中加载到系统内存(RAM)中,然后网格和纹理等数据又被加载到显卡上的存储空间,即显存(VRAM)中。这是因为显卡对显存的访问速度更快,而且大多数显卡对RAM没有直接的访问权利。

数据加载到显存中之后,系统内存RAM中的数据就可以移除了,但如果CPU需要这些数据来进行碰撞检测等,则系统内存里的数据就会保留。


图2  渲染所需的数据(两张纹理以及3个网格)从硬盘最终加载到显存中。在渲染时,GPU可以快速访问这些数据 


(2)设置渲染状态:

渲染状态是指场景中的网格是怎样被渲染的,使用的哪个Vertex Shader 、Fragment Shader、光源属性、材质等。

(3)调用Draw Call:

实际上,Draw Call 就是一个命令,它的发起者是CPU,接收者是GPU。一个Draw Call指向本次需要被渲染的图元列表。给定一个Draw Call后,GPU会根据渲染状态和所有的顶点数据来进行计算,最终输出成屏幕上显示的像素,这个计算过程,就是GPU流水线。


图3   CPU通过调用Draw Call来告诉GPU开始进行一个渲染过程。一个Draw Call会指向本次调用需要渲染的图元列表 


GPU流水线


当GPU从CPU那里得到渲染命令后,就会进行一系列的流水线操作,最终把图元渲染到屏幕上。几何阶段和光栅化阶段的实现载体是GPU,开发者是无法完全控制这两个阶段的,但GPU向开发者开发了很多控制权。
这个两个阶段可以分成很多小的流水线阶段,这些流水线阶段由GPU来实现,每个阶段GPU提供了不同的可配置性或可编程性。

图4   GPU的渲染流水线实现。颜色表示了不同阶段的可配置性或可编程性:绿色表示该流水线阶段是完全可编程控制的,黄色表示该流水线阶段可以配置但不是可编程的,蓝色表示该流水线阶段是由GPU固定实现的,开发者没有任何控制权。实线表示该shader必须由开发者编程实现,虚线表示该Shader是可选的 

几何阶段
(1)顶点着色器:

从上图可以看出,GPU的渲染流水线接受顶点数据作为输入,这些顶点数据是由应用阶段加载到显存中的,再由Draw Call指定的。这些数据随后被传递到顶点着色器。顶点着色器是完全可编程的,主要用于实现顶点的空间变换、顶点着色等。

(2)曲面细分着色器:

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

(3)几何着色器:

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

(4)剪裁:

这一阶段是可配置的。这一流水阶段的目的是把哪些不在视野内的顶点剪掉,并剔除某些三角形图元的面片。

一个图元与摄像机视野的关系有 3 种:完全在视野内、部分在视野内、完全在视野外。 

完全在视野内的图元会继续传递给下一个流水线阶段,完全在视野外得图元不会继续向下传递,部分在视野内的图元需要做剪裁处理。

(5)屏幕映射:

这一阶段是不可配置和编程的,主要负责把每个图元的坐标(三维坐标系)转换成屏幕坐标(二维坐标系)。


光栅化阶段


(1)三角形设置:

上一阶段输出的信息是屏幕坐标系下的顶点位置以及相关的额外信息,如深度值(z坐标)、法线方向、视角方向等。

光栅化阶段的重要目标是:计算每个图元覆盖了哪些像素 和 为这些像素计算它们的颜色。

三角形设置这一阶段,会计算光栅化一个三角网格所需的信息。上一阶段输出的是三角网格的顶点,如果我们想得到整个三角网格的覆盖情况,就必须计算每条边上的像素坐标而得到三角形边界的表达方式。这样一个得到三角形边界表示方式的过程就是三角形设置。

(2)三角形遍历:

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

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

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


图5   三角形遍历的过程。根据几何阶段输出的顶点信息,最终得到该三角网格覆盖的像素位置。对应像素会生成一个片元,而片元中的状态是对三个顶点的信息进行插值得到的。例如,对图2.12中三个顶点的深度进行插值得到其重心位置对应的片元的深度值为-10.0 

(3)片元着色器:

片元着色器的输入是上一个阶段对顶点信息插值得到的结果,输出是一个或者多个颜色值。片元着色器是一个非常重要的可编程着色器阶段。这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。片元着色器可以完成很多重要的效果,但它仅可以影响单个片元。


图6 根据上一步插值后的片元信息,片元着色器计算该片元的输出颜色 

(4)逐片元操作:

主要任务有两个:

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

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


图7 逐片元操作阶段所做的操作。只有通过了所有的测试后,新生成的片元才能和颜色缓冲区中已经存在的像素颜色进行混合,最后再写入颜色缓冲区中


什么是OpenGL/DirectX


OpenGL和DirectX 就是图形应用编程接口,这些接口用于渲染二维或三维图形,这些接口架起了上层应用程序和底层GPU的沟通桥梁。概括来说,我们的应用程序运行在CPU上,应用程序通过调用OpenGL或DirectX的图形接口将渲染所需的数据,如顶点数据、纹理数据、材质参数等数据存储在显存中的特定区域。随后,开发者可以通过图形编程接口发出渲染命令(Draw Call),它们将会被显卡驱动翻译成GPU能够理解的代码,进行真正的绘制。

图8 CPU、OpenGL/DirectX、显卡驱动和GPU之间的关系



什么是HLSL、GLSL、CG


上面讲到很多可编程的着色器阶段,如顶点着色器、片元着色器等。这些着色器的可编程性在于,我们可以使用一种特定的语言来编写程序,即着色器语言。常见的着色器语言有:

HLSL:High Level Shadeing Language,DirectX的着色器语言,由微软控制着色器的编译,就算使用不同的硬件,同一个着色器的编译结果也是一样的,但也因此支持HLSL的平台相对比较有限,几乎都是微软自己的产品,如Windows、Xbox 360、PS3等,这是因为在其他平台上没有可以编译HLSL的编译器。

GLSL:OpenGL Shading Language,OpenGL的着色器语言,优点在于跨平台性,可以在Windows、Linux、Mac甚至移动平台等多平台上工作,但这种跨平台性是由于OpenGL没有提供着色编译器,而是由显卡驱动来完成着色器的编译工作。也就是说,只要显卡驱动支持对GLSL的编译它就可以运行。

CG:NVIDIA的着色器语言,真正意义上的跨平台,它会根据平台的不同,编译成相应的中间语言。


什么是Draw Call


Draw Call本身的含义很简单,就是CPU调用图像编程接口,如OpenGL中的glDrawElements命令或者DirectX中的DrawIndexedPrimitive命令,以命令GPU进行渲染的操作。

深入了解Draw Call之前,先了解一下CPU和GPU是如何实现并行工作的。如果没有流水线,那么CPU需要等到GPU完成上一个渲染任务才能再次发送渲染命令。这种方法显然会造成效率低下。因此,要像隔壁老王的洋娃娃厂一样,我们需要让CPU和GPU可以并行工作。解决方法就是使用一个命令缓冲区。

命令缓冲区包含了一个命令队列,由CPU向其中添加命令,而由GPU从中读取命令,添加和读取的过程是相互独立的。命令缓冲区中的命令由很多种类,而Draw Call就是其中一种,如下图:

图9  命令缓冲区。CPU通过图像编程接口向命令缓冲区中添加命令,而GPU从中读取命令并执行。黄色方框内的命令就是Draw Call,而红色方框内的命令用于改变渲染状态。我们使用红色方框来表示改变渲染状态的命令,
是因为这些命令往往更加耗时 

因为GPU的渲染能力是很强的,渲染200个还是2000个三角网格通常没什么区别,因此渲染速度往往快于CPU提交命令的速度。如果Draw Call的数量太多,CPU就会把大量时间花费在提交DrawCall上,造成CPU的过载。因此要减少Draw Call,就要把很多小的DrawCall合并成一个大的DrawCall,这就是批处理的思想。批处理技术更适合那些静态的物体,比如不会移动的大地、石头等,对于这些静态物体我们只需要合并一次即可。当然,对动态物体也可以进行批处理,但是,由于这些物体是不断运动的,因此每一帧都需要重新进行合并并再发送给GPU,这对空间和时间都会造成一定的影响。

游戏开发过程中,为了减少Draw Call的开销,需要注意下面两点:

1), 避免使用大量很小的网格。如果需要使用很小的网格,考虑是否可以合并它们。

2), 避免使用过多的材质。尽量在不同的网格之间公用同一个材质。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值