您的位置:首页 > 其它

使用Metal打造令人惊叹的游戏效果

2015-12-09 09:31 423 查看


苹果最新推出的Metal框架支持GPU硬件加速、高级3D图形渲染以及大数据并行运算。且提供了先进而精简的API来确保框架的细粒度(fine-grain),并且在组织架构、程序处理、图形呈现、运算指令以及指令相关数据资源的管理上都支持底层控制。其核心目的是尽可能的减少CPU开销,而将运行时产生的大部分负载交由GPU承担。

——Metal编程指南

Metal是一套用于在iPhone和iPad上GPU编程的高效框架。而Metal这个名称的来源是想说明这个图形框架的的确确是非常底层的——底层到已经非常接近金属板了(Metal)。

该框架的设计主要为了两个目的:3D图形渲染和并行运算。这也是平时图形学上最常涉及的两个东西,它们都是基于特殊的代码在GPU上对大量的数据并行计算。

Metal适用于谁

在谈论API和Metal本身的着色语言之前我们需要来谈论一下Metal能为开发者们带来什么。正如前面所言,Metal提供了两大功能:3D图形渲染和并行运算。

对于正在需求游戏引擎的人来说,Metal并非不二之选。光是苹果自己家的Scene Kit(3D)和Sprite
Kit(2D)就已足矣,这些API提供的是高层游戏引擎,其中还包括了物理仿真。另外还有功能更为全面的3D引擎,比如Epic家的Unreal Engine,还有Unity,而且这些引擎还不仅局限于苹果平台。然而对于上述这些引擎,也都可以使用Metal来代替使用它们,并会(或者可能会)从中受益。

再论及使用底层图形API的渲染引擎,也有可替代Metal的OpenGL和OpenGL ES,OpenGL的优势不仅是支持包括OS X,Windows,Linux和Android多种平台,还因为其拥有大量的教程、书籍以及实践指南。相较而言Metal的资料少得可怜,而且仅仅局限于搭载了64位处理器的iPhone或iPad上。不过从另一方面说,OpenGL在运行性能上比起Metal来说就逊色不少,在编写的时候还会碰见各种各样的问题。

而当需要在iOS平台上寻求高效的并行运算解决方案的时候,使用哪种引擎显而易见,Metal毋庸置疑是首选。由于OpenCL在iOS上是私有框架(private framework),使用了OpenCL的Core Image在这种需求下显得既低效又笨拙。

使用Metal的好处

最大的好处就是相比OpenGL ES而言可以大大的减少资源开销。当使用OpenGL来创建一段缓存或者纹理,往往会产生一份拷贝以防GPU在使用这段数据时有其他的访问(译者按:比如来自CPU的访问)。如果想要在确保数据安全的情况下拷贝缓存区或是纹理这样的大资源,那么产生的消耗会是相当大的。而反观Metal,就会发现它并无需这样的处理方式,开发者可以在CPU和GPU之间同步使用这些数据。为此苹果还提供了一个更为优秀的接口使得这种同步访问的情况变得更为容易:Grand
Cnetral Dispatch(GCD技术)。不过使用Metal同样能做到这点,所谓先进的引擎就是能避免产生拷贝的情况下高效渲染那些需要加载或撤掉的资源。

另一大好处就是Metal能预判GPU的状态从而避免那些多余的校验和编辑。在使用OpenGL的时候,习惯上我们会挨个儿设置GPU的状态,然后每次进行绘制调用之前必须要校验一道GPU的状态。在最糟糕的情况下你甚至需要为了一个新的GPU状态重编译一遍阴影效果,当然,在这里采用预判GPU状态就显得相当必要了。不过Metal另辟蹊径,在初始化渲染引擎的时候,GPU的状态会被打包进一个预估的渲染通道,(render pass),此状态下渲染通道会与多种资源一起被使用,而其他的状态不会有任何影响。Metal使用的渲染通道不需要多余的校验,因而最大限度的减少了API负载,且对于每一帧的渲染都有质的提升。

Metal API

虽然很多API都通过具体类来实现平台支持,不过Metal使用的方法是基于协议的。因为Metal中具体的类型是由运行的设备所决定的。这很好的鼓励了程序员选择面向接口编程而非面向实现,以降低程序的耦合。当然也意味着需要冒着风险大量的在Objective C运行时来对Metal的类型添加继承和扩展类型。

为了提高效率Metal必然也牺牲了一部分安全性,在对待出错情况的时候,苹果的其他框架都会显得更为健壮和安全。而到了Metal这里就完全不一样了,在某些情况下你甚至会得到一个指向内部缓冲区的裸指针,所以在执行同步的时候必须多加小心。在用OpenGL出错的时候,撑死了就是黑屏,但是在Metal的情况下就是一切皆有可能(蓝屏?),可能会发生闪屏或者间歇性死机之类的情况,所有这些都归咎于Metal框架所针对的特殊处理器恰巧在CPU旁。

值得一提的是苹果尚未针对虚拟机上运行Metal提供任何辅助软件,所以在测试的时候仅支持真机。

一个基本的Metal程序

在本节中,我们会为介绍关于第一个Metal程序所必要知识。这一简单程序绘制了一个自旋转的正方形,你可以在GitHub上下载示例程序源代码。

尽管我们不能详述所有细节,但是会尽量提及大部分相关内容,如果你想了解得更多的话可以自行参阅源码,或是查阅网上的相关资料。

使用UIKit创建设备(Device)和接口

在Metal中,设备(Device)被作为GPU抽象概念,被用于去创建其他的对象,包括缓存、材质以及函数库。使用MTLCreateSystemDefaultDevice()方法可以获取默认设备:

注意这里的device并不是一个特殊的具体类,不过它遵循的是MTLDebice协议。

下面的这一段代码演示的是如何创建一个Metal层并将其作为UIView背景层的子层。

CAMetalLayer是CALayer的子类,能够表现一个Metal framebuffer中的内容。我们需要告知图层metalLayer我们使用了那个device(就是刚刚创建的那个),并设置其显示格式,在这里设置的是8bit通道BGRA的格式,也就是说每个像素包括蓝、绿、红以及Alpha(透明度)四个值,每个值的取值范围为0-255。

函数和库

Metal程序中大多数功能是依照顶点和片段功能来写的,通俗点说就是着色器。Metal的着色器是Metal Shading language写的,下面我们会详细说说这个东西。Metal的一大优势就在于着色功能是在应用生成中间代码时才编译,并在应用启动时保存有效时间值。

Metal库就是对于各种功能集合。你在自己的项目中所写的所有着色器功能都会编译进这个默认的库里面,它们可以从你的device里面提取恢复:

我们在下面构建渲染管线状态的时候需要用到这个库。

指令序列

所有指令会被集结为指令序列之后提交到Metal device,指令序列允许指令在线程安全的情况下改变或者序列化他们的执行。你可以直截了当的创建一个指令序列:

创建管线

Metal中的管线编程,也就意味着顶点数据在被渲染的时候会发生不同寻常的变化。顶点着色器和片段着色器都是可编程渲染管线,另外也还有一些其他的必要的操作(比如剪切、光栅扫描、视口变换)则不需要我们直接去控制。后面这些不需要去控制的特性则组成了固定渲染管线。

在Mrtal中创建管线的时候,我们需要分别对每个顶点和像素指定使用哪种顶点和片段函数。同样还需要告知管线在frambuffer中的像素格式。在这种情况下,Metal要求像素格式必须与图层的格式匹配,因为这样才能将其绘制到屏幕上。

我们可以从库中使用其名字调用这些函数:

接下来创建一个管线描述符并使用其方法和像素格式来配置。

最后我们利用描述符来创建管线态(pipeline state),这样在程序运行的时候它就会根据硬件将中间代码优化之后编译着色器功能。

加载数据到Buffer中

现在我们已经成功创建了管线,当然还需要将其填满数据。在本例中,我们只需要绘制一个简单的几何图形:旋转的正方形。它由两个共边的直角三角形组成。

每一行的前四个值分别表示每个顶点的x,y,z,w值(译者按:即四元数),后面四个数代表的是红、绿、蓝和Alpha值。

你也许会好奇为什么在3D空间中表示顶点的位置信息要用四个数,第四个数w其实是为了我们在对顶点进行变换(旋转、平移和缩放)的时候在数学上更加方便。这一细节在本例中不作为重点。

要使用Metal来绘制顶点数据,我们需要先把数据放进一个Buffer中,这个Buffer就是存在于内存中能被CPU和GPU共享的非结构化二进制串。

我们还需要另一个Buffer来存储旋转矩阵以便让正方形旋转,仅需创建一个指定长度的Buffer为其提供空间就行了,不需要预先初始化数据:

动画

为了使正方形旋转,需要将顶点作为顶点着色器的一部分进行变换。也就是说要为每帧画面更新统一的数据缓存区。为此,我们利用三角函数从当前的旋转角生成旋转矩阵并将其复制到统一的缓存区中。

下面的Uniform结构体仅有一个4×4旋转矩阵作为成员,类型是matrix_float4x4,这是苹果SIMD库中的数据结构,这个库中包含了多种适用于数据并行处理的类型:

为了成功复制旋转矩阵的数据到缓存中,我们需要得到指向其数据的指针然后使用memcpy来拷贝数据:

准备绘制

在绘制之前先要从图层中获取drawable,该对象管理了一组用于渲染的纹理:

然后创建一个MTLRenderPassDescriptor,用于描述Metal在渲染先后需要完成的操作。下面我们将使用它来清除framebuffer然后用白色填充,然后执行绘制调用,最后将结果存储在frambuffer中显示出来。

发布绘制调用

使用指令序列需要现将它们编码进指令缓存中,指令缓存就是将已经按照GPU能理解的方式编码的指令集合。:

为了真正的对指令序列进行编码,需要一个对象来将所有的绘制调用转换为GPU能够读取的语言。这一对象类型叫做MTLRenderCommandEncoder,可以通过向指令缓存区请求编码以及之前创建的描述符来创建

在调用之前,需要用前面预编译好的管线态(pipeline state)来配置渲染指令的编码器,然后设置这段缓存,这段缓存将作为我们顶点着色器的参数:

为了能渲染出几何形状,我们要告诉Metal我们希望绘制哪一种图形(三角形),以及需要从缓存区中解析出多少个顶点(在本例中是六个):

最后告知编码器我们已经完成了绘制调用的分发,调用endEncoding:

Framebuffer的呈现

现在我们的绘制调用已经准备妥当,于是需要告知指令缓存区将结果显示到屏幕上。在此调用presentDrawable方法并以drawable为参数:

通知缓存区准备执行,使用commit方法:

一切完成~

Metal 着色语言(Shading Language)

虽然Metal是跟着Swift一起在WWDC2014上作为重磅内容发布的,不过它的着色语言还是基于C++11标准,并添加了少部分特性以及一些关键字。

Metal着色语言练习

为了能在着色器中使用顶点数据,我们要定义一个结构体类型,相当于将顶点的布局用Objective-C来表示。

我们还另需要一个相似的的结构体来描述从顶点着色器到片段着色器传递的顶点类型。不过,在这种情况下,需要确定结构中的哪个成员应该被认为是顶点的位置信息(通过使用[[position]])。

顶点函数会对每个顶点都执行一次,它接受的一个指向全部顶点、统一数据的相关信息以及旋转矩阵的指针。第三个索引参数则告诉函数当前的顶点的操作。

注意顶点函数的的参数是根据函数用途不同安排的。在本例中的所用的buffer参数中,参数的索引相当于我们在设置渲染指令编码器的时候指定的索引。这也是Metal分辨这些参数谁是谁的方法。

在顶点函数中,我们将顶点位置与旋转矩阵相乘。我们之前定的旋转矩阵能够使正方形围绕它的中心旋转,我们将变换后的数据存储在输出顶点中,顶点颜色则完全拷贝输入顶点:

片段函数则是对每个像素执行一次,所需参数则是Metal在光栅化过程中生成的,这一过程还会对每个顶点中指定的颜色和位置信息进行插值。在这个简单的片段函数中,我们传出的插值颜色是Metal已经算好的,就是最终显示在屏幕上的像素的颜色值。

为啥就不拓展OpenGL

苹果也是OpenGL体系架构审查委员会的一员,其在iOS上支持OpenGL的扩展也有历史了。不过从内部更改OpenGL显然是很难的,毕竟设计的目的就不是全心全意支持iOS设备,更何况OpenGL是要支持如此杂乱繁多的硬件设备的。虽然OpenGL也在不断的提升之中,但是其变化会将缓慢而有待商榷。

Metal,从另一方面看,在苹果的设计下是独占型的。协议型的API从一开始就显得与众不同,而且和苹果其他框架又如此兼容。Metal的开发语言是Objective-C,又基于Foundation框架,且在GPU于CPU同时工作时使用了GCD技术。在GPU管线抽象上比起OpenGL来说更加先进,完全无需重写。

Mac上的Metal

Metal移植到OS X,这只是一个时间问题。它的API并不只局限于iPhone和iPad上的ARM处理器,Metal的大多数优点在当前的很多GPU上来说都是可移植的。另外在iPhone和iPad上的GPU和CPU是共享RAM的,也就是会说它们无需复制来交换数据。而现在的Mac本并不支持内存的共用,不过这也只是一个时间问题。也许Metal的API会慢慢更改以支持独立RAM的架构,或者说Metal只应用于未来那些共享RAM的Mac本。

总结

我们希望通过本文对Metal框架进行一个有用而客观的介绍。

当然,大多数游戏开发者可能对此不会有什么接触,不过不少主流引擎已经开始青睐Metal,而开发者也将得益于在开发中不需要自己去创建API。此外,对那些设备发烧友来说,Metal的出现,会让开发者们营造更加令人惊叹的游戏画面效果,同时运行也会更为流畅,对他们而言这无疑是一大利好。

资源

Metal编程指南

Metal着色语言指南

Metal实例

本站其他资源

iOS 8 Metal Swift教程 :开始学习

iOS 8 Metal Swift 教程(二):3D图形

Metal Framework基础使用教程

Metal基本图像处理实例

(本文由CocoaChina翻译自objc.io转载请注明出处)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: