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语句到文件最上方:
- import Metal
这导入了Metal框架,所以你能够使用Metal的类(像这文件中的MTLDevice)。接着,在ViewController类中添加以下属性:
- var device: MTLDevice! = nil
你将要在viewDidLoad函数内初始化这个属性,而不是在一个init函数里,所以它不得不是一个optional。既然你知道你一定会 在使用它前初始化它,你为了方便,把它标记为一个隐式不包裹的optional。最后,添加这一行到viewDidLoad函数的最后。
- device = MTLCreateSystemDefaultDevice()
这个函数返回一个默认MTLDevice引用,你的代码将会用到它。
2)创建一个CAMetalLayer
在iOS里,你在屏幕上看见的所有东西,被一个CALayer所承载。存在不同特效的CALayer的子类,比如:渐变层(gradient layers)、形状层(shape layers)、重复层(replicator layers) 等等。
好的,如果你想要用Metal在屏幕上画一些东西,你需要使用一个特别的CALayer子类,CAMetalLayer。所以在你的viewcontroller中添加一个。
首先在这个文件的上方添加import语句。
- import QuartzCore
你需要它因为CAMetalLayer是QuartzCore框架的部分,而不是Metal框架里的。
然后把新属性添加到类中:
- var metalLayer: CAMetalLayer! = nil
这将会存储你新layer的引用。
最后,把这行代码添加到viewDidLoad方法最后。
metalLayer = CAMetalLayer() // 1
metalLayer.device = device // 2
metalLayer.pixelFormat = .BGRA8Unorm // 3
metalLayer.framebufferOnly = true // 4
metalLayer.frame = view.layer.frame // 5
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)是右上角。在这篇教程中,你想要在这些点上画三角形:
让我们创建一个缓冲区。在你的类中添加下列的常量属性:
- let vertexData:[Float] = [
- 0.0, 1.0, 0.0,
- -1.0, -1.0, 0.0,
- 1.0, -1.0, 0.0]
这在CPU创建一个浮点数数组——你需要通过把它移动到一个叫MTLBuffer的东西,来发送这些数据到GPU。
添加另一个新的属性:
- var vertexBuffer: MTLBuffer! = nil
然后在 viewDidLoad 方法的最后添加以下代码:
- let dataSize = vertexData.count * sizeofValue(vertexData[0]) // 1
- 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被每个顶点调用,它的工作是接受顶点的信息(如:位置和颜色、纹理坐标),返回一个潜在的修正位置(可能还有别的相关信息)。
为了把事情保持简单,你的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底部添加下列代码:
vertex float4 basic_vertex( // 1
) ]], // 2
unsigned int vid [[ vertex_id ]]) { // 3
return float4(vertex_array[vid], 1.0); // 4
}
让我们一行行来看:
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的底部添加下列代码:
fragment half4 basic_fragment() { // 1
return half4(1.0); // 2
}
让我们一行行来看:
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里添加一个属性:
- var pipelineState: MTLRenderPipelineState! = nil
这会对你即将要创建的render pipeline ,在它被编译后进行跟踪。
接着,在 viewDidLoad 方法最后添加如下代码:
// 1
let defaultLibrary = device.newDefaultLibrary()
let fragmentProgram = defaultLibrary.newFunctionWithName("basic_fragment")
let vertexProgram = defaultLibrary.newFunctionWithName("basic_vertex")
// 2
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.vertexFunction = vertexProgram
pipelineStateDescriptor.fragmentFunction = fragmentProgram
pipelineStateDescriptor.colorAttachments[].pixelFormat = .BGRA8Unorm
// 3
var pipelineError : NSError?
pipelineState = device.newRenderPipelineStateWithDescriptor(pipelineStateDescriptor, error: &pipelineError)
if !pipelineState {
println("Failed to create pipeline state, error \(pipelineError)")
}
让我们分部分看这些代码:
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,简单地添加一个属性:
- var commandQueue: MTLCommandQueue! = nil
把下面这行添加到 viewDidLoad 的最后:
- 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 类来实现。
为了使用它,在类里添加一个新的属性: var timer: CADisplayLink! = nil
然后在 viewDidLoad 方法的末尾像这样初始化它:
timer = CADisplayLink(target: self, selector: Selector("gameloop"))
timer.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
这会设置你的代码,让它每次刷新屏幕的时候调用一个名叫gameloop的方法。
func render() {
// TODO
}
func gameloop() {
autoreleasepool {
self.render()
}
}
这里 gameloop 函数简单地调用 render 函数,这时 render 函数只有一个空实现。让我们来实现它!
2)创建一个Render Pass Descriptor
下一步是创建一个MTLRenderPassDescriptor,它能配置什么纹理会被渲染到、什么是clear color,以及其他的配置。
简单地在 render 函数里添加以下行:
var drawable = metalLayer.nextDrawable()
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[].texture = drawable.texture
renderPassDescriptor.colorAttachments[].loadAction = .Clear
renderPassDescriptor.colorAttachments[].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函数末尾加上这行代码:
let commandBuffer = commandQueue.commandBuffer()
一个command buffer包含一个或多个渲染指令(render commands)。让我们下面创建一个。
4)创建一个渲染命令编码器(Render Command Encoder)
为了创建一个渲染命令(render command),你使用一个名叫render command encoder的对象。在render函数的最后添加以下代码:
let renderEncoder = commandBuffer.renderCommandEncoderWithDescriptor(renderPassDescriptor)
renderEncoder.setRenderPipelineState(pipelineState)
renderEncoder.setVertexBuffer(vertexBuffer, offset: , atIndex: )
renderEncoder.drawPrimitives(.Triangle, vertexStart: , vertexCount: , instanceCount: )
renderEncoder.endEncoding()
这里你创建一个command encoder,并指定你之前创建的pipeline和顶点。最重要的部分是,调用drawPrimitives(vertexStart:vertexCount:instanceCount:)。
这里你你告诉GPU,让它基于vertex buffer画一系列的三角形。每个三角形由三个顶点组成,从vertex buffer 下标为0的顶点开始,总共有一个三角形。
当你完成后,你只要调用 endEncoding()。
5)提交你的Command Buffer
最后一步是提交command buffer。在render函数最后添加这些代码:
commandBuffer.presentDrawable(drawable)
commandBuffer.commit()
第一行需要保证新纹理会在绘制完成后立即出现。然后你把事务(transaction)提交,把任务交给GPU。过去我们敲了不少代码,不过现在终于结束了。编译并运行这个应用:
我见过最赞的三角形!
注意:如果你的应用崩溃了,请确定你在一台拥有A7芯片真机(iPhone 5S,iPad Air,iPad mini2 ,非模拟器)运行。
最后
恭喜你,你学到了很多关于Metal API的知识!你现在对Metal的一些重要的概念有了了解,比如:shaders、devices、command buffers,pipeline等等。
我可能会写更多这系列的教程,覆盖uniforms,3D,纹理,光照,以及导入模型。如果你感到有兴趣、并想看到更多教程的话,请留下你的评论。同时,确定查看苹果一些很好的资源:
- iOS开发入门教程
iOS开发入门教程 http://my.oschina.net/mailzwj/blog/133273 摘要 iOS开发入门教程,从创建项目到运行项目,包括OC基础,调试,模拟器设置等相关知识. iO ...
- Apple官方IOS开发入门教程[v0.2]
今天,又跑去找IOS开发入门教程了,结果发现没什么好的PDF. 后来发现,原来苹果官方有开发入门教程,而且写的很好.所以整理出来了,给大家分享一下. 我就不在这里贴pdf的内容了,下面有苹果官方教程的 ...
- IOS开发入门教程-总结篇-写给狂热的编程爱好者们
程序发轻狂,代码阑珊,苹果开发安卓狂!--写给狂热的编程爱好者们 写在前面的话 学习iOS应用程序开发已有一段时间,最近稍微闲下来了,正好也想记录一下前阶段的整个学习过程.索性就从最基础的开始,一步一 ...
- iOS开发--绘图教程
本文是<Programming iOS5>中Drawing一章的翻译,考虑到主题完整性,翻译版本中加入了一些书中未涉及到的内容.希望本文能够对你有所帮助. 本文由海水的味道翻译整理,转载请 ...
- iOS开发异常处理教程
以下是两篇xcode开发如何处理异常的教程,建议一读 part 1 part 2 梗概如下: 基本上你能碰到两种崩溃的情况:SIGABRT (也叫EXC_CRASH),和EXC_BAD_ACCESS ...
- IOS开发新手教程(一)-数据类型和运算符
OC语法入门(一) 数据类型和运算符 1.1凝视 凝视和其它语言一样,同意单行 ,多行凝视,一份规范的代码里面须要有一些正式的凝视,例如以下凝视: /* 这是多行 凝视 */ //这是多行凝视 OC语 ...
- ArcGIS Runtime SDK for iOS开发系列教程(5)——要素信息的绘制
在客户端绘制点.线.面要素是GIS应用的基本功能,这一讲我将向大家介绍在iOS中如何来实现这一功能.大家都知道在Flex.Silverlight.js中对于要素的绘制都有一个叫GraphicsLaye ...
- 新手必看,史上最全的iOS开发教程集锦,没有之一!
最近大火的iPhone XS Max和iPhone XS,不知道有没有同学已经下手了呢?一万三的价位确实让很多人望而却步啊.据说为了赢得中国的用户,专门出了双卡双待的,可想而知中国市场这块“肥肉”人人 ...
- iOS开发系列--Swift语言
概述 Swift是苹果2014年推出的全新的编程语言,它继承了C语言.ObjC的特性,且克服了C语言的兼容性问题.Swift发展过程中不仅保留了ObjC很多语法特性,它也借鉴了多种现代化语言的特点,在 ...
随机推荐
- Docker管理面板Crane开源了!
导读 数人云容器管理面板 Crane 开源啦!Crane 包含着数人云工程师对 Docker 最新技术的热爱和实践.希望借助开源社区的力量,让 Crane 完善自身,更好地成长起来,让更多的国内用户体 ...
- E asy Boo t 6.51 启动易 制作启动光盘的软件(附注册码)
内建ISO文件生成器,可直接生成可启动ISO文件,并支持N合1优化. -------中文版注册码------- 用户名:中华人民共和国 注册码:2898-5448-5603-BB2D -------英 ...
- 【LeetCode 208】Implement Trie (Prefix Tree)
Implement a trie with insert, search, and startsWith methods. Note:You may assume that all inputs ar ...
- 谈谈作为一个菜B的培训感受
培训的目的是为了让新员工更快的适应当前的工作,尽快的跟上前辈的步伐,从而能全身心的投入到当前的工作当中.感觉在培训的时候需要注意以下的几个问题: 1. 新员工必须在意识上认同当前的工作 如今的项目组也 ...
- Python的列表排序
Python的列表排序 本文为转载,源地址为:http://blog.csdn.net/horin153/article/details/7076321 在 Python 中, 当需要对一个 list ...
- 利用redis分布式锁的功能来实现定时器的分布式
文章来源于我的 iteye blog http://ak478288.iteye.com/blog/1898190 以前为部门内部开发过一个定时器程序,这个定时器很简单,就是配置quartz,来实现定 ...
- js滑动门及对像的使用
function scrollDoor() { } scrollDoor.prototype = { sd: function (menus, divs, openClass, closeClass) ...
- 最全面的 MySQL 索引详解
什么是索引? 1.索引 索引是表的目录,在查找内容之前可以先在目录中查找索引位置,以此快速定位查询数据.对于索引,会保存在额外的文件中. 2.索引,是数据库中专门用于帮助用户快速查询数据的一种数据结构 ...
- 第二百八十四天 how can I 坚持
又是一个周一.今天感觉过得好艰辛啊,幸好晚上程秀通过生日请客,吃了顿大餐,还拿回了一瓶酒.哈哈. 其他也没什么了.晚上玩的挺好.不过,回来,老是渴,一直想喝水,现在是又困,又累啊,睡觉了.
- 转】Maven学习总结(二)——Maven项目构建过程练习
原博文出自于:http://www.cnblogs.com/xdp-gacl/p/4051690.html 感谢! 上一篇只是简单介绍了一下maven入门的一些相关知识,这一篇主要是体验一下Maven ...