(二)unity shader基础之——————shader一些专业术语的解释(OpenGL/DirectX、HLSL/GLSL/Cg、Draw Call、固定管线渲染等)
一、什么是OpenGL/DirectX
我们直接访问GPU是一件非常麻烦的事情,可能需要各自寄存器、显存打交道而图像编程接口在这些硬件的基础上实现了一层抽象。
OpenGL和DirectX就是这些图像应用编程接口,这些接口用于渲染二维或三维图形。这些接口架起了上层应用程序和底层GPU的沟通桥梁。一个应用程序向这些接口发送渲染接口发送渲染命令,而这些接口会依次向显卡驱动发送渲染命令,这些显卡驱动真正知道如何和GPU通讯的角色,正是他们OpenGL或DirectX的函数调用翻译成了GPU能够听懂的语言,同时它们也负责把纹理等数据转换成GPU所支持的格式,一个比喻就是,显卡驱动就是显卡的操作系统。下图显示了这样的关系:
概括来说我们的应用程序在CPU上,应用程序可以通过调用OpenGL或DirectX的图像接口将渲染所需的数据,如顶点数据、纹理数据、材质参数等数据存储在显存中的特定区域。随后开发者可以通过图形编程接口发出渲染命令,这些渲染命令也被称为Draw Call,它们将会被显卡驱动翻译成GPU能够理解的代码,进行真正的绘制。
由上图可以看出,一个显卡除了有图像处理单元GPU外,还拥有自己的内存,这个内存通常被称为显存,GPU可以在显存中存储任何数据,但对于渲染来说一些数据类型是必需的,例如用于屏幕显示的图像缓冲、深度缓冲等。
因为显卡驱动的存在,几乎所有的GPU都既可以和OpenGL合作,也可以和DirectX一起工作。从显卡角度出发,实际上它只需要和显卡驱动打交道就可以了。而显卡驱动就好像一个中介者,负责和两方(图像编程接口和GPU)打交道。因此一个显卡制作商为了让他们的显卡可以同时和OpenGL、DirectX合作,就必须支持OpenGL和DirectX接口的显卡驱动。
二、什么是HLSL、GLSL、CG
我们之前降到了很多可编程的着色器阶段,如顶点着色器、片元着色器等。这些着色器可编程行在于,可以使用一种特定的语言来编写程序,就跟我们可以用C#来写游戏逻辑一样。
在可编程管线出现之前,为了编写着色器代码,我们要学习汇编语言。为了给开发者们提供方便,就出现了更高级的着色语言。着色语言是专门用于编写着色器的,常见的着色语言有DirectX的HLSL、OpenGL的GLSL以及NVIDIA的CG。HLSL、GLSL、Cg都是“高级”语言,这些语言会被编译成与机器无关的汇编语言,也被称为中间语言。这些中间语言再交给显卡驱动翻译成真正的机器语言,即GPU可以理解的语言。
那么初学者该选择哪种语言呢?
1.GLSL的优点在于跨平台性,它可以在Windows、Linux、Mac甚至移动平台等多种平台上工作,但这种跨平台性是由于OpenGL没有提供着色器编译器,而是由显卡驱动来完成着色器的编译工作。也就是说只要显卡驱动支持对GLSL的编译它就可以运行。这种做法好处在于,由于供应商完全了解自己硬件构造,他们知道怎么做可以发挥最大作用。
2.而对于HLSL,由微软控制着色器的编译,就算使用了不同的硬件,同一个着色器的编译结果也是一样的。但也因此支持HLSL的平台相对比较有限,几乎完全是微软自己的产品,如windows,Xbox 360等。这是因为其他平台没有可以编译HLSL的编译器。
3.Cg则是真正意义上的跨平台。会根据平台的不同编译成相应中间语言。Cg语言的跨平台性很大原因取决于和微软合作。也导致Cg语言和HLSL非常像,Cg语言可以无缝移植成HLSL代码,但缺点可能无法完全发挥出OpenGL的最新特性。
对于unity平台,我们同样可以选择使用哪种语言。在untiy shader中,可以选择使用Cg/HLSL或者GLSL。尽管这些着色语言并不是真正意义上的对应的着色语言, 尽管它们语法几乎一样,以unity Cg为例,有时也会发现有些Cg语法在unity shader中是不支持的。
三、什么是Draw Call
Draw Call本身含义很简单,就是CPU调用图像编程接口,以命令GPU进行渲染操作。
一个常见的误区是,Draw Call中造成性能问题的元凶是GPU,认为GPU上的状态切换是耗时的,真正拖后腿的其实是CPU。下面看几个问题:
(1)CPU和GPU是如何实现并行工作的?
如果没有流水线化。那么CPU需要等到GPU完成上一个渲染任务才能再次发送渲染命令。但这种方法会造成效率低下。我们要让CPU和GPU并行工作,解决方法就是使用一个命令缓冲区。
命令缓冲区包含了一个命令队列,由CPU向其中添加命令,而由GPU从中读取命令,添加和读取的过程是互相独立的。命令缓冲区使得CPU和GPU可以相互独立工作,当CPU需要渲染一些对象时,可以向命令缓冲区添加命令,而当GPU完成上一次渲染任务后,就可以从命令队列再取出一个命令执行它。
命令缓冲区的命令有很多种类,而Draw Call是其中一种,其他命令还有改变渲染状态等(例如改变使用的着色器,使用不同纹理等)。如下图:
(2) 为什么Draw Call多了会影响帧率?
举一个例子:创建10000个小文件,每个文件大小1kb,然后把他们从一个文件夹复制到另一个文件夹。尽管空间总和不超过10m,但要花很长时间。现在创建一个单独的10m文件,复制时间少很多。原因在于每一个复制动作需要很多额外操作,例如分配内存、创建各种元数据等。这些操作将造成很多额外的性能开销,如果我们复制很多小文件,那么这个开销将会很大。
渲染过程虽然和上面实现很大不同,但从某些角度上是类似的。在每次调用Draw Call之前,CPU都需要向GPU发送很多内容,包括数据、状态和命令等。在这一阶段CPU需要完成很多工作,例如检测渲染状态等。而一旦CPU完成了这些准备工作,GPU就可以开始本次的渲染。GPU渲染能力很强,渲染200个还是2000个三角形网格通常区别不大,因此渲染速度往往快于CPU提交命令的速度,如果Draw Call数量太多,CPU就会把大量时间花费在提交Draw Call上,造成CPU过载,如下图:
(3)如何减少Draw Call?
在这里仅讨论使用批处理的方法。
提交大量很小的Draw Call会造成CPU性能瓶颈,即CPU把时间都花在准备Draw Call的工作上了。那么一个很显然的优化想法就是把很多小的Draw Call合并成一个大的Draw Call,也就是批处理的思想,如下图是批处理所做的工作:
需要注意的是我们需要在CPU内存中合并网格,而合并过程是需要消耗时间的。因此批处理技术更适用于那些静态物体,例如不会移动的大地、石头等。对于这些静态物体我们只需要合并一次即可。当然我们也可以对动态物体进行批处理。但是这些物体是不断运动的,因此每一帧都需要重新进行合并然后再发给GPU,这对空间和时间都会造成一定影响。
在游戏开发过程中,为了减少Draw Call的开销,有两点需要注意:
1.避免使用大量很小的网格,当不可避免的需要使用很小的网格结构时,考虑是否可用合并它们。
2.避免使用过多的材质,进了在不同的网格之间共用同一个材质。
四、什么是固定渲染管线
固定函数的流水线也称为固定管线,通常是指较旧的GPU上实现的渲染流水线。这种流水线只给开发者提供一些配置操作。但开发者没有对流水线阶段的完全控制权。
固定管线通常提供了一系列接口,这些接口包含了一个函数入口点集合,这些函数入口点会匹配GPU上的一个特定的逻辑功能,开发者们通过这些接口来控制渲染流水线。换句话说,固定渲染管线是只可配置的管线,一个形象的比喻是,我们在使用固定渲染管线进行渲染时,就好像在控制电路上的多个开关,我们可以选择打开或者关闭一个开关,但永远无法控制整个电路的排布。
GPU流水线越来越朝着更高的灵活性和可控性方向发展,可编程渲染管线应运而生。我们在上面看到了许多可编程的流水线阶段,如顶点着色器,片元着色器,这些可编程的着色器阶段可以说是GPU进化最重要的贡献,下表给出了3种常见的图像接口从固定管线向可编程管线进化的版本:
在GPU发展的过程中,为了继续提供固定管线的接口抽象,一些显卡驱动的开发者们使用了更加通用的着色架构,即使用可编程的管线来模拟固定管线,这是为了在提供可编程渲染管线的同时,可以让那些已经熟悉了固定管线的开发者们继续使用固定管线进行渲染,如果不是为了对比较旧的设备进行兼容,不建议使用固定管线的渲染方式。
五、什么是shader
之所以讲GPU的渲染流水线,是因为shader所在的阶段就是渲染流水线的一部分,具体来说 shader就是:
1.GPU流水线上一些可高度编程的阶段,而由着色器编译出来的最终代码是会在GPU上运行的(对于固定管线的渲染来说,着色器有时等同于一些特定的渲染设置)。
2.有一些特定类型的着色器,如顶点着色器、片元着色器等。
3.依靠着着色器我们可以控制流水线中的渲染细节,例如用顶点着色器进行顶点变换以及传递数据,用片元着色器来进行逐像素的渲染。
但同时也要明白,要得到出色的游戏画面是需要包括shader在内的所有渲染流水线阶段的共同参与才可完全,设置适当的渲染状态,使用合适的混合函数,开启还是关闭深度写入等。