iOS开发——Metal教程

Metal Swift教程

学习使用苹果GPU加速3D绘图的新API:Metal!

在iOS 8里,苹果发布了一个新的接口叫做Metal,它是一个支持GPU加速3D绘图的API。

Metal和OpenGL ES相似,它也是一个底层API,负责和3D绘图硬件交互。它们之间的不同在于,Metal不是跨平台的。与之相反的,它设计的在苹果硬件上运行得极其高效,与OpenGL ES相比,它提供了更快的速度和更低的开销。

在这篇教程里,你将会获得亲身的经历,使用Metal和Swift来创建一个有基本脉络的应用:画一个简单的三角形。在这个过程中,你将会学习一些Metal里最重要的类,比如devices、command queues,等等。

这篇教程是设计为任何人可以阅读明白,无论你是否学习过3D绘图。但是,我们会过得很快。如果你之前有过3D编程或者是OpenGL编程的经历,你会发现它非常简单,因为里面的很多概念你已经很熟悉了。

这篇教程假设你已经熟悉Swift了。如果你还是个Swift新手,先学习这些教程吧,苹果Swift站点、一些Swift教程。

注意:Metal应用不能跑在iOS模拟器上,它们需要一个设备,设备上装载着苹果A7芯片或者更新的芯片。所以要学习这篇教程,你需要一台这样的设备(iPhone 5S,iPad Air,iPad mini2)来完成代码的测试。

Metal vs. Sprite Kit, Scene Kit, or Unity

在我们开始之前,我想要讨论怎样比较Metal和一些没那么底层的框架,比如:Sprite Kit,Scene Kit或者Unity。

Metal是一个底层3D绘图API,和OpenGL类似,但是它的开销更低。它是一个GPU上一个简单的封装,所以能够完成几乎所有事情,像 在屏幕上渲染一个精灵(sprite)或者是一个3D模型。但你要编写完成这些事情的所有代码。这样麻烦的代价是,你拥有了GPU的力量和控制。

没那么底层的游戏框架,像Sprite Kit、Scene Kit或者Unity都是在底层3D绘图API(像是Metal或是OpenGL ES)的基础上构建的。它们提供大部分你需要在游戏中编写的底层封装代码,比如在屏幕上渲染一个精灵(sprite)或者一个3D模型。

如果你所想要做的就是制作一个游戏,大多数情况下我会推荐你使用一个没那么底层的库,像Sprite Kit、Scene Kit或者Unity,因为它会让你的工作更轻松。如果你喜欢这样,我们有很多教程来帮助你学习这些框架。

但是,还是有两个很好的原因来学习Metal:

1.使硬件达到运行效率的峰值:因为Metal非常底层,它允许你使硬件达到运行效率的峰值,对你的游戏如何运行有着完全的控制。

2.这是一个很好的学习经历:学习Metal教导你很多关于3D绘图编程的概念,编写你自己的游戏引擎,以及高层(higher level)游戏框架如何运作。

如果以上任何一点对你来说是个好的理由,继续读下去!

Metal vs OpenGL ES

下面让我们来对比一下Metal和OpenGL ES的不同之处。

OpenGL ES被设计成跨平台的。那意味着你可以用C++OpenGL ES的代码,在大部分情况下只要作少许改动就能让它在另一个平台上运行,比如Android。

苹果意识到尽管OpenGL ES对跨平台的支持很赞,但是它缺少了一些苹果设计产品的基本理念:苹果把操作系统、硬件、软件整合在了一起。

所以苹果认真考虑了如果他们设计一套特定基于他们硬件的绘图API,会是怎样呢?它的目标是极速运行、低开销以及支持最新最好的特性。

于是Metal诞生了。它对比OpenGL ES,能为你的应用单位时间内提高最高10倍的绘图调用次数。这能够产生超赞的特效,就像WWDC 2014 keynote上zen花园样例。

让我们开始看看一些Metal代码吧!

开头

Xcode的iOS游戏模板有一个Metal选项,但是你不要在这里选择。这是因为我想要向你一步步展示如何编写一个Metal应用,所以你能够理解这过程中的每一步骤。

打开Xcode 6通过iOS\Application\Single View Application template创建一个新的项目。使用HelloMetal作为项目名称,设置开发语言为Swift,设置设备为通用设备(Universal)。点击 Next,选择一个目录,点击Create。

有七个步骤来设置metal:

1.创建一个MTLDevice

2.创建一个CAMetalLayer

3.创建一个Vertex Buffer

4.创建一个Vertex Shader

5.创建一个Fragment Shader

6.创建一个Render Pipeline

7.创建一个Command Queue

让我们一个个看它们。

1)创建一个MTLDevice

使用Metal你要做的第一件事就是获取一个MTLDevice的引用。

你可以把一个MTLDevice想象成是你和CPU的直接连接。你将通过使用MTLDevice创建所有其他你需要的Metal对象(像是command queues,buffers,textures)。

为了完成这点,打开ViewController.swift 并添加下面的import语句到文件最上方:

  1. import Metal

这导入了Metal框架,所以你能够使用Metal的类(像这文件中的MTLDevice)。接着,在ViewController类中添加以下属性:

  1. var device: MTLDevice! = nil

你将要在viewDidLoad函数内初始化这个属性,而不是在一个init函数里,所以它不得不是一个optional。既然你知道你一定会 在使用它前初始化它,你为了方便,把它标记为一个隐式不包裹的optional。最后,添加这一行到viewDidLoad函数的最后。

  1. device = MTLCreateSystemDefaultDevice()

这个函数返回一个默认MTLDevice引用,你的代码将会用到它。

2)创建一个CAMetalLayer

在iOS里,你在屏幕上看见的所有东西,被一个CALayer所承载。存在不同特效的CALayer的子类,比如:渐变层(gradient layers)、形状层(shape layers)、重复层(replicator layers) 等等。

好的,如果你想要用Metal在屏幕上画一些东西,你需要使用一个特别的CALayer子类,CAMetalLayer。所以在你的viewcontroller中添加一个。

首先在这个文件的上方添加import语句。

  1. import QuartzCore

你需要它因为CAMetalLayer是QuartzCore框架的部分,而不是Metal框架里的。

然后把新属性添加到类中:

  1. var metalLayer: CAMetalLayer! = nil

这将会存储你新layer的引用。

最后,把这行代码添加到viewDidLoad方法最后。

  1. metalLayer = CAMetalLayer()          // 1
  2. metalLayer.device = device           // 2
  3. metalLayer.pixelFormat = .BGRA8Unorm // 3
  4. metalLayer.framebufferOnly = true    // 4
  5. metalLayer.frame = view.layer.frame  // 5
  6. view.layer.addSublayer(metalLayer)   // 6

让我们一行行来看:

a.你创建了一个CAMetalLayer

b.你必须明确layer使用的MTLDevice,你简单地设置你早前获取的device。

c.你把像素格式(pixel format)设置为BGRA8Unorm,它代表”8字节代表蓝色、绿色、红色和透明度,通过在0到1之间单位化的值来表示”。这次两种用在CAMetalLayer的像素格式之一,一般情况下你这样写就可以了。

d.苹果鼓励你设置framebufferOnly为true,来增强表现效率。除非你需要对从layer生成的纹理(textures)取 样,或者你需要在layer绘图纹理(drawable textures)激活一些计算内核,否则你不需要设置。(大部分情况下你不用设置)

e.你把layer的frame设置为view的frame。

f.你把layer作为view.layer下的子layer添加。

3)创建一个Vertex Buffer

在Metal里每一个东西都是三角形。在这个应用里,你只需要画一个三角形,不过即使是极其复杂的3D形状也能被解构为一系列的三角形。

在Metal里,默认的坐标系是向量坐标系,这意味着默认的时候,一个2x2x1的立方体,中心点是(0,0,0.5)。

如果你认为z=0是平面,那么(-1,-1,0)就是左下角,(0,0,0)就是中心,(1,1,0)是右上角。在这篇教程中,你想要在这些点上画三角形:

让我们创建一个缓冲区。在你的类中添加下列的常量属性:

  1. let vertexData:[Float] = [
  2. 0.0, 1.0, 0.0,
  3. -1.0, -1.0, 0.0,
  4. 1.0, -1.0, 0.0]

这在CPU创建一个浮点数数组——你需要通过把它移动到一个叫MTLBuffer的东西,来发送这些数据到GPU。

添加另一个新的属性:

  1. var vertexBuffer: MTLBuffer! = nil

然后在 viewDidLoad 方法的最后添加以下代码:

  1. let dataSize = vertexData.count * sizeofValue(vertexData[0]) // 1
  2. vertexBuffer = device.newBufferWithBytes(vertexData, length: dataSize, options: nil) // 2

让我们一行行来看:

a.你需要获取vertex data的字节大小。你通过把第一个元素的大小和数组元素个数相乘来得到。

b.你在MTLDevice上调用newBufferWithBytes(length:options:) ,在GPU创建一个新的buffer,从CPU里输送data。你传递nil来接受默认的选项。

4)创建一个Vertex Shader

你之前创建的顶点将成为你接下来写的一个叫vertext shader的小程序的输入。

一个vertex shader 是一个在GPU上运行的小程序,它由像c++的一门语言编写,那门语言叫做Metal Shading Language

一个vertex shader被每个顶点调用,它的工作是接受顶点的信息(如:位置和颜色、纹理坐标),返回一个潜在的修正位置(可能还有别的相关信息)。

为了把事情保持简单,你的vertex shader将会返回一个和传递位置相同的位置。

最简单的了解 vertex shader 的方法是,自己体验。点击File\New\File,选择iOS\Source\Metal File,然后点击Next。输入Shader.metal作为文件名上按回车,然后点击Create。

注意:在Metal里,你能够在一个Metal文件里包含多个shaders。你也能把你的shader 分散在多个Metal文件中。Metal会从任意Metal文件中加载你项目包含的shaders。

在Shaders.metal底部添加下列代码:

  1. vertex float4 basic_vertex(                           // 1
  2. const device packed_float3* vertex_array [[ buffer(0) ]], // 2
  3. unsigned int vid [[ vertex_id ]]) {                 // 3
  4. return float4(vertex_array[vid], 1.0);              // 4
  5. }

让我们一行行来看:

a.所有的vertex shaders必须以关键字vertex开头。函数必须至少返回顶点的最终位置——你通过指定float4(一个元素为4个浮点数的向量)。然后你给一个名字给vetex shader,以后你将用这个名字来访问这个vertex shader。

b.第一个参数是一个指向一个元素为packed_float3(一个向量包含3个浮点数)的数组的指针,如:每个顶点的位置。这个 [[ ... ]] 语法被用在声明那些能被用作特定额外信息的属性,像是资源位置,shader输入,内建变量。这里你把这个参数用 [[ buffer(0) ]] 标记,来指明这个参数将会被在你代码中你发送到你的vertex shader的第一块buffer data所遍历。

c.vertex shader会接受一个名叫vertex_id的属性的特定参数,它意味着它会被vertex数组里特定的顶点所装入。

d.现在你基于vertex id来检索vertex数组中对应位置的vertex并把它返回。同时你把这个向量转换为一个float4类型,最后的value设置为1.0(简单的来说,这是3D数学要求的)。

5)创建一个Fragment Shader

完成我们的vertex shader后,另一个shader,它被每个在屏幕上的fragment(think pixel)调用,它就是fragment shader。

fragment shader通过内插(interpolating)vertex shader的输出还获得自己的输入。比如:思考在三角形两个底顶点之间的fragment:

fragment的输入值将会由50%的左下角顶点和50%的右下角顶点组成。

fragment shader的工作是给每个fragment返回最后的颜色。为了简便,你将会把每个fragment返回白色。

在Shader.metal的底部添加下列代码:

  1. fragment half4 basic_fragment() { // 1
  2. return half4(1.0);              // 2
  3. }

让我们一行行来看:

a. 所有fragment shaders必须以fragment关键字开始。这个函数必须至少返回fragment的最终颜色——你通过指定half4(一个颜色的RGBA值)来 完成这个任务。注意,half4比float4在内存上更有效率,因为,你写入了更少的GPU内存。

b. 这里你返回(1,1,1,1)的颜色,也就是白色。

6)创建一个Render Pipeline

现在你已经创建了一个vertex shader和一个fragment shader,你需要组合它们(加上一些配置数据)到一个特殊的对象,它名叫render pipeline。Metal一个很酷的地方是,渲染器(shaders)是预编译的,render pipeline 配置会在你第一次设置它的时候被编译,所以所有事情都极其高效。

首先在ViewController.swift里添加一个属性:

  1. var pipelineState: MTLRenderPipelineState! = nil

这会对你即将要创建的render pipeline ,在它被编译后进行跟踪。

接着,在 viewDidLoad 方法最后添加如下代码:

  1. // 1
  2. let defaultLibrary = device.newDefaultLibrary()
  3. let fragmentProgram = defaultLibrary.newFunctionWithName("basic_fragment")
  4. let vertexProgram = defaultLibrary.newFunctionWithName("basic_vertex")
  5. // 2
  6. let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
  7. pipelineStateDescriptor.vertexFunction = vertexProgram
  8. pipelineStateDescriptor.fragmentFunction = fragmentProgram
  9. pipelineStateDescriptor.colorAttachments[0].pixelFormat = .BGRA8Unorm
  10. // 3
  11. var pipelineError : NSError?
  12. pipelineState = device.newRenderPipelineStateWithDescriptor(pipelineStateDescriptor, error: &pipelineError)
  13. if !pipelineState {
  14. println("Failed to create pipeline state, error \(pipelineError)")
  15. }

让我们分部分看这些代码:

a.你可以通过调用device.newDefaultLibrary方法获得的MTLibrary对象访问到你项目中的预编译shaders。然后你能够通过名字检索每个shader。

b.你在这里设置你的render pipeline。它包含你想要使用的shaders、颜色附件(color attachment)的像素格式(pixel format)。(例如:你渲染到的输入缓冲区,也就是CAMetalLayer)。

c.最后,你把这个pipeline 配置编译到一个pipeline 状态(state)中,让它使用起来有效率。

7)创建一个Command Queue

你需要做的最终的一次性设置步骤,是创建一个MTLCommandQueue。

把这个想象成是一个列表装载着你告诉GPU一次要执行的命令。

要创建一个command queue,简单地添加一个属性:

  1. var commandQueue: MTLCommandQueue! = nil

把下面这行添加到 viewDidLoad 的最后:

  1. commandQueue = device.newCommandQueue()

恭喜,你的预设置的代码完成了。

渲染三角形

现在,是时候学习每帧执行的代码,来渲染这个三角形!

它将在五个步骤中被完成:

1.创建一个Display link。

2.创建一个Render Pass Descriptor

3.创建一个Command Buffer

4.创建一个Render Command Encoder

5.提交你Command Buffer的内容。

让我们深入来看!

注意:理论上这个应用实际上不需要每帧渲染,因为三角形被绘制之后不会动。但是,大部分应用会有物体的移动,所以我们会那样做。同时也为将来的教程打下基础。

1)创建一个Display Link

你想要一个函数,在每次设备屏幕刷新的时候被调用,这样你就可以重绘屏幕。

在iOS平台上,你通过CADisplayLink 类来实现。

为了使用它,在类里添加一个新的属性:

  1. var timer: CADisplayLink! = nil

然后在 viewDidLoad 方法的末尾像这样初始化它:

  1. timer = CADisplayLink(target: self, selector: Selector("gameloop"))
  2. timer.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)

这会设置你的代码,让它每次刷新屏幕的时候调用一个名叫gameloop的方法。

  1. func render() {
  2. // TODO
  3. }
  4. func gameloop() {
  5. autoreleasepool {
  6. self.render()
  7. }
  8. }

这里 gameloop 函数简单地调用 render 函数,这时 render 函数只有一个空实现。让我们来实现它!

2)创建一个Render Pass Descriptor

下一步是创建一个MTLRenderPassDescriptor,它能配置什么纹理会被渲染到、什么是clear color,以及其他的配置。

简单地在 render 函数里添加以下行:

  1. var drawable = metalLayer.nextDrawable()
  2. let renderPassDescriptor = MTLRenderPassDescriptor()
  3. renderPassDescriptor.colorAttachments[0].texture = drawable.texture
  4. renderPassDescriptor.colorAttachments[0].loadAction = .Clear
  5. renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 104.0/255.0, blue: 5.0/255.0, alpha: 1.0)

首先你在之前的metal layer上调用nextDrawable() ,它会返回你需要绘制到屏幕上的纹理(texture)。接下来,你配置你的render pass descriptor 来使用它。你设置load action为clear,也就是说在绘制之前,把纹理清空。然后你把绘制的背景颜色设置为绿色。

3)创建一个Command Buffer

下一步是创建一个command buffer。你可以把它想象为一系列这一帧想要执行的渲染命令。酷的是在你提交command buffer之前,没有事情会真正发生,这样给你对事物在何时发生有一个很好的控制。创建一个command buffer很简单,只要在render函数末尾加上这行代码:

  1. let commandBuffer = commandQueue.commandBuffer()

一个command buffer包含一个或多个渲染指令(render commands)。让我们下面创建一个。

4)创建一个渲染命令编码器(Render Command Encoder)

为了创建一个渲染命令(render command),你使用一个名叫render command encoder的对象。在render函数的最后添加以下代码:

  1. let renderEncoder = commandBuffer.renderCommandEncoderWithDescriptor(renderPassDescriptor)
  2. renderEncoder.setRenderPipelineState(pipelineState)
  3. renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, atIndex: 0)
  4. renderEncoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
  5. renderEncoder.endEncoding()

这里你创建一个command encoder,并指定你之前创建的pipeline和顶点。最重要的部分是,调用drawPrimitives(vertexStart:vertexCount:instanceCount:)。

这里你你告诉GPU,让它基于vertex buffer画一系列的三角形。每个三角形由三个顶点组成,从vertex buffer 下标为0的顶点开始,总共有一个三角形。

当你完成后,你只要调用 endEncoding()。

5)提交你的Command Buffer

最后一步是提交command buffer。在render函数最后添加这些代码:

  1. commandBuffer.presentDrawable(drawable)
  2. commandBuffer.commit()

第一行需要保证新纹理会在绘制完成后立即出现。然后你把事务(transaction)提交,把任务交给GPU。过去我们敲了不少代码,不过现在终于结束了。编译并运行这个应用:

我见过最赞的三角形!

注意:如果你的应用崩溃了,请确定你在一台拥有A7芯片真机(iPhone 5S,iPad Air,iPad mini2 ,非模拟器)运行。


最后

这是我们教程最终的项目

恭喜你,你学到了很多关于Metal API的知识!你现在对Metal的一些重要的概念有了了解,比如:shaders、devices、command buffers,pipeline等等。

我可能会写更多这系列的教程,覆盖uniforms,3D,纹理,光照,以及导入模型。如果你感到有兴趣、并想看到更多教程的话,请留下你的评论。同时,确定查看苹果一些很好的资源:

苹果Metal开发者文档,有很多文档、录像、样例代码的链接。

苹果的Metal编程指导

苹果的Metal Shading Language 指导

WWDC2014 Metal录像

时间: 2024-08-28 15:45:34

iOS开发——Metal教程的相关文章

iOS开发入门教程_iOS开发视频教程

iOS开发入门教程 (Object-C.网络编程.多线程.蓝牙.二维码.Cocos2D.OpenGL)适合人群:初级课时数量:34课时用到技术:IOS,Object-C,OpenGL,XCode,Cocos 2D涉及项目:Cocos+2D.Game Kit蓝牙数据处理等咨询QQ:1840215592 iOS开发入门教程详细查看:http://www.ibeifeng.com/goods-471.html1.1.课程目标iOS开发入门教程内容的目标是初学者入门,让入门者提高,让所有人符合企业招聘的

iOS开发入门教程

iOS开发入门教程 (Object-C.网络编程.多线程.蓝牙.二维码.Cocos2D.OpenGL)http://www.ibeifeng.com/goods-471.html咨询QQ2110053820课程讲师:群雄 课程分类:IOS适合人群:初级课时数量:34课时更新程度:完成用到技术:IOS,Object-C,OpenGL,XCode,Cocos 2D涉及项目:Cocos+2D.Game Kit蓝牙数据处理等 1.1.课程背景随着移动互联网时代的到来,iPhone作为智能手机旗舰版,iO

IOS开发入门教程-总结篇-写给狂热的编程爱好者们

程序发轻狂,代码阑珊,苹果开发安卓狂!——写给狂热的编程爱好者们 写在前面的话 学习iOS应用程序开发已有一段时间,最近稍微闲下来了,正好也想记录一下前阶段的整个学习过程.索性就从最基础的开始,一步一步记录一个最简单的 iOS应用从创建到运行的全过程,其中会穿插很多相关知识或是遇到过的问题.其实算不上教程,但希望能够帮助更多的开发爱好者更快.更好的入门. iOS开发环境简述 最友好,最现实,也是最方便的开发环境就是在Mac系统中安装Xcode进行开发.原因在于:第一,开发工具(Xcode)限制.

ios开发的教程以及视频在如何下载?

从基础入手才能根本上解决一些不必要的问题,所以学好C语言十分有必要,再者,从基础开发到各类APP再到iOS开发,C语言基础必须保证扎实,才能接收更新更前沿的开发技能. 很多人选择零基础学习iOS开发,但不知道如何学习更加省力,要知道,C语言是iOS的编程基础,打牢C语言基础至关重要,传智播客iOS培训机构作为行业最具权威的iOS开发和人才培训中心,是教育部教育信息管理中心认证的全国移动互联网开发人才实训基地,专业的教学培养大批高技能iOS开发者,传智播客注重基础教学,从C语言开发学习到进入下一阶

iOS开发终极教程索引

做iOS开发有3年之久了,各种项目,大小项目,独立抑或是合作,等等都有涉及,我比较懒,没有写很多高质量的文章. 简单总结下iOS开发涉及的知识和各种工具,帮助文档. iOS开发终极索引地址: https://github.com/Aufree/trip-to-iOS http://dev.swiftguide.cn/ http://objccn.io/ http://www.cocoachina.com/ https://github.com/ipader/SwiftGuide https://

iOS开发异常处理教程

以下是两篇xcode开发如何处理异常的教程,建议一读 part 1 part 2 梗概如下: 基本上你能碰到两种崩溃的情况:SIGABRT (也叫EXC_CRASH),和EXC_BAD_ACCESS (也可能叫SIGBUS或SIGSEGV). SIGABRT是程序异常,容易定位,EXC_BAD_ACCESS通常直接就把断点断在了main处,有一定的技巧来寻找原因(比如启用Zombie Objects). 文章介绍了如何读控制台的错误日志,介绍了异常断点(Exception Breakpoint)

IOS开发入门教程(一)

OC语法入门(一) 数据类型和运算符 1.1注释 注释和其他语言一样,允许单行 ,多行注释,一份规范的代码里面需要有一些正式的注释,如下注释: /* 这是多行 注释 */ //这是多行注释 OC语言暂时不支持嵌套注释.(swift里面支持这一特性) 1.2标示符和变量 1.2.1分隔符 1.分号 每一个句子以分号结尾,一行可以多个语句,一个语句也可以跨多行,但是变量名不能分开 合法的: NSString *name  = @"你好"; int age  = 20; NSString *

ArcGIS Runtime SDK for iOS开发系列教程(5)——要素信息的绘制

在客户端绘制点.线.面要素是GIS应用的基本功能,这一讲我将向大家介绍在iOS中如何来实现这一功能.大家都知道在Flex.Silverlight.js中对于要素的绘制都有一个叫GraphicsLayer的图层,那么在ArcGIS for iOS中也不例外,具体的功能和用法也比较类似.同时,在ArcGIS for iOS中还有一个实用的图层叫AGSSketchGraphicsLayer(草图编辑图层),它不仅可以方便客户要素的绘制保存,也是实现离线在线编辑同步的利器. 另外,在Objective-

分享一些iOS开发的教程

OS教程链接: http://pan.baidu.com/s/1hqBzR9M 密码: dbg3 swift教程链接: http://pan.baidu.com/s/1DEaFc 密码: ucys