学习使用苹果GPU加速3D绘图的新API:Metal
Metal和OpenGL ES相似,它也是一个底层API,负责和3D绘图硬件交互。它们之间的不同在于,Metal不是跨平台的, Metal 是用 Objective-C 编 写的,基于 Foundation,使用 GCD 在 CPU 和 GPU 之间保持同步
。与之相反的,它设计的在苹果硬件上运行得极其高效,与OpenGL ES相比,它提供了更快的速度和更低的开销。它是一个GPU上一个简单的封装,所以能够完成几乎所有事情,像在屏幕上渲染一个精灵(sprite)或者是一个3D模型。但你要编写完成这些事情的所有代码。这样麻烦的代价是,你拥有了GPU的力量和控制。
优点:
1、使硬件达到运行效率的峰值:因为Metal非常底层,它允许你使硬件达到运行效率的峰值,对你的游戏如何运行有着完全的控制。
2、这是一个很好的学习经历:学习Metal教导你很多关于3D绘图编程的概念,编写你自己的游戏引擎,以及高层(higher level)游戏框架如何运作。
关于metal详细的介绍可参考:Metal
以下是使用Metal和Swift来创建一个有基本脉络的应用:画一个简单的三角形。
注意:Metal应用不能跑在iOS模拟器上,它们需要一个设备,设备上装载着苹果A7芯片或者更新的芯片。所以需要一台这样的设备(iPhone 5S,iPad Air,iPad mini2)来完成代码的测试。
打开Xcode 通过iOS\Application\Single View Application template创建一个新的项目。使用TriangleSwift作为项目名称,设置开发语言为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的引用。
为了完成这点,打开ViewController.swift 并添加下面的import语句
import Metal
导入了Metal框架,所以你能够使用Metal的类(像这文件中的MTLDevice)。接着,在ViewController类中添加以下属性:
在viewDidLoad函数内初始化这个属性
// 1、创建一个MTLDevice, 你可以把一个MTLDevice想象成是你和CPU的直接连接。你将通过使用MTLDevice创建所有其他你需要的Metal对象(像是command queues,buffers,textures)。
var device: MTLDevice! = nil
2 创建一个CAMetalLayer
在iOS里,你在屏幕上看见的所有东西,被一个CALayer所承载。存在不同特效的CALayer的子类,比如:渐变层(gradient layers)、形状层(shapelayers)、重复层(replicator layers) 等等。如果你想要用Metal在屏幕上画一些东西,你需要使用一个特别的CALayer子类,CAMetalLayer。
因为CAMetalLayer是QuartzCore框架的部分,而不是Metal框架里的,首先在这个文件的上方添加import语句
import QuartzCore
把新属性添加到类中:
// 2、创建一个CAMetalLayer
var metalLayer: CAMetalLayer! = nil
设置metalLayer
// 2.1 创建CAMetalLayer
metalLayer = CAMetalLayer()
// 2.2 必须明确layer使用的MTLDevice,简单地设置早前获取的device
metalLayer.device = device
// 2.3 把像素格式(pixel format)设置为BGRA8Unorm,它代表"8字节代表蓝色、绿色、红色和透明度,通过在0到1之间单位化的值来表示"。这次两种用在CAMetalLayer的像素格式之一,一般情况下你这样写就可以了。
metalLayer.pixelFormat = .bgra8Unorm
// 2.4 苹果鼓励将framebufferOnly设置为true,来增强表现效率。除非你需要对从layer生成的纹理(textures)取样,或者你需要在layer绘图纹理(drawable textures)激活一些计算内核,否则你不需要设置。(大部分情况下你不用设置)
metalLayer.framebufferOnly = true
// 2.5 把layer的frame设置为view的frame
metalLayer.frame = view.layer.frame
var drawableSize = self.view.bounds.size
drawableSize.width *= self.view.contentScaleFactor
drawableSize.height *= self.view.contentScaleFactor
metalLayer.drawableSize = drawableSize
view.layer.addSublayer(metalLayer)
3 创建一个Vertex Buffer
创建一个缓冲区。在你的类中添加下列的常量属性
// 3、创建一个Vertex Buffer
var vertexBuffer: MTLBuffer! = nil
// 3.1 在CPU创建一个浮点数数组,需要通过把它移动到一个MTLBuffer,来发送这些数据到GPU。
let vertexData:[Float] = [
0.0, 1.0, 0.0,
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0
]
在MTLDevice上调用makeBuffer(bytes:, length:, options:),在GPU创建一个新的buffer,从CPU里输送data。options不能为空。
// 3.2 获取vertex data的字节大小。你通过把元素的大小和数组元素个数相乘来得到
let dataSize = vertexData.count * 4
// 3.3 在GPU创建一个新的buffer,从CPU里输送data
vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: MTLResourceOptions(rawValue: UInt(0)))
4 创建一个Vertex Shader
你之前创建的顶点将成为接下来写的一个叫vertext shader的小程序的输入。
一个vertex shader 是一个在GPU上运行的小程序,它由像c++的一门语言编写,那门语言叫做Metal Shading Language。
一个vertex shader被每个顶点调用,它的工作是接受顶点的信息(如:位置和颜色、纹理坐标),返回一个潜在的修正位置(可能还有别的相关信息)
点击File\New\File,选择iOS\Source\Metal File,然后点击Next。输入Shader.metal作为文件名,然后点击Create。
// 一个vertex shader被每个顶点调用,它的工作是接受顶点的信息(如:位置和颜色、纹理坐标),返回一个潜在的修正位置(可能还有别的相关信息)
#include <metal_stdlib>
using namespace metal;
/**
* 1、所有的vertex shaders必须以关键字vertex开头。函数必须至少返回顶点的最终位置——你通过指定float4(一个元素为4个浮点数的向量)。然后你给一个名字给vetex shader,以后你将用这个名字来访问这个vertex shader。
* 2、vertex shader会接受一个名叫vertex_id的属性的特定参数,它意味着它会被vertex数组里特定的顶点所装入。
* 3、一个指向一个元素为packed_float4(一个向量包含4个浮点数)的数组的指针,如:每个顶点的位置。这个 [[ ... ]] 语法被用在声明那些能被用作特定额外信息的属性,像是资源位置,shader输入,内建变量。这里你把这个参数用 [[ buffer(0) ]] 标记,来指明这个参数将会被在你代码中你发送到你的vertex shader的第一块buffer data所遍历。
* 4、基于vertex id来检索vertex数组中对应位置的vertex并把它返回。向量必须为一个float4类型
vertex float4 basic_vertex (
constant packed_float3* vertex_array[[buffer(0)]],
unsigned int vid[[vertex_id]]){
return float4(vertex_array[vid], 1.0);
}
*/
5 创建一个Fragment Shader
完成vertex shader后,另一个shader,它被每个在屏幕上的fragment(think pixel)调用,它就是fragment shader。
fragment shader通过内插(interpolating)vertex shader的输出来获得自己的输入。
/*
1. 所有fragment shaders必须以fragment关键字开始。这个函数必须至少返回fragment的最终颜色——你通过指定half4(一个颜色的RGBA值)来完成这个任务。注意,half4比float4在内存上更有效率,因为,你写入了更少的GPU内存。
2. 这里你返回(0.6,0.6,0.6,0.6)的颜色,也就是灰色。
*/
fragment half4 basic_fragment() {
return half4(0.6);
}
6 创建一个Render Pipeline
现在你已经创建了一个vertex shader和一个fragment shader,你需要组合它们(加上一些配置数据)到一个特殊的对象,它名叫render pipeline。Metal的渲染器(shaders)是预编译的,render pipeline 配置会在你第一次设置它的时候被编译,所以所有事情都极其高效。
首先在ViewController.swift里添加一个属性:
// 6、创建一个Render Pipeline
var pipelineState: MTLRenderPipelineState! = nil
在viewDidLoad方法最后添加如下代码:
// 6.1 通过调用device.newDefaultLibrary方法获得的MTLibrary对象访问到你项目中的预编译shaders,然后通过名字检索每个shader
let defaultLibrary = device.newDefaultLibrary()
let fragmentProgram = defaultLibrary?.makeFunction(name: "basic_fragment")
let vertextProgram = defaultLibrary?.makeFunction(name: "basic_vertex")
// 6.2 这里设置你的render pipeline。它包含你想要使用的shaders、颜色附件(color attachment)的像素格式(pixel format)。(例如:你渲染到的输入缓冲区,也就是CAMetalLayer)
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.vertexFunction = vertextProgram
pipelineStateDescriptor.fragmentFunction = fragmentProgram pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
7 创建一个Command Queue
你需要做的最终的设置步骤,是创建一个MTLCommandQueue。
把这个想象成是一个列表装载着你告诉GPU一次要执行的命令。
要创建一个command queue,简单地添加一个属性:
// 7、创建一个Command Queue
var commandQueue: MTLCommandQueue! = nil
把下面这行添加到viewDidLoad中:
// 7.1 初始化commandQueue
commandQueue = device.makeCommandQueue()
预设置的代码到这里完成了。
接下来就是渲染三角形了,它将需要在五个步骤来完成:
1 创建一个Display link。
2 创建一个Render Pass Descriptor
3 创建一个Command Buffer
4 创建一个Render Command Encoder
5 提交Command Buffer的内容
注意:理论上这个应用实际上不需要每帧渲染,因为三角形被绘制之后不会动。但是,大部分应用会有物体的移动,所以我们会那样做。
1 创建一个Display link
在iOS平台上,通过CADisplayLink 类,可以创建一个函数在每次设备屏幕刷新的时候被调用,这样你就可以重绘屏幕。
为了使用它,在类里添加一个新的属性:
// 8、创建一个Display Link
var timer: CADisplayLink! = nil
初始化timer
// 8.1 初始化 timer,设置timer,让它每次刷新屏幕的时候调用一个名叫drawloop的方法
timer = CADisplayLink(target: self, selector: #selector(ViewController.drawloop))
timer.add(to: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)
渲染的代码在render()中实现
func render() {
//TODO
}
func drawloop() {
self.render()
}
2 创建一个Render Pass Descriptor
// metal layer上调用nextDrawable() ,它会返回你需要绘制到屏幕上的纹理(texture)
let drawable = metalLayer.nextDrawable()
// 8、创建一个Render Pass Descriptor,配置什么纹理会被渲染到、clear color,以及其他的配置
let renderPassDesciptor = MTLRenderPassDescriptor()
renderPassDesciptor.colorAttachments[0].texture = drawable?.texture
// 设置load action为clear,也就是说在绘制之前,把纹理清空
renderPassDesciptor.colorAttachments[0].loadAction = .clear
// 绘制的背景颜色设置为绿色
renderPassDesciptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.8, 0.5, 1.0)
3 创建一个Command Buffer
一个command buffer包含一个或多个渲染指令(render commands)。
// 9、创建一个Command Buffer
// 你可以把它想象为一系列这一帧想要执行的渲染命令。注意在你提交command buffer之前,没有事情会真正发生,这样给你对事物在何时发生有一个很好的控制。
let commandBuffer = commandQueue.makeCommandBuffer()
4 创建一个渲染命令编码器(Render Command Encoder)
// 10、创建一个渲染命令编码器(Render Command Encoder)
// 创建一个command encoder,并指定你之前创建的pipeline和顶点
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDesciptor)
renderEncoder.setRenderPipelineState(pipelineState)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, at: 0)
/**
绘制图形
- parameter type: 画三角形
- parameter vertexStart: 从vertex buffer 下标为0的顶点开始
- parameter vertexCount: 顶点数
- parameter instanceCount: 总共有1个三角形
*/
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
// 完成后,调用endEncoding()
renderEncoder.endEncoding()
5 提交Command Buffer
// 保证新纹理会在绘制完成后立即出现
commandBuffer.present(drawable!)
// 提交事务(transaction), 把任务交给GPU
commandBuffer.commit()
学习资料:
• 苹果Metal开发者文档,有很多文档、录像、样例代码的链接。
• 苹果的Metal编程指导
• 苹果的Metal Shading Language 指导
• WWDC2014 Metal录像