學(xué)習(xí)使用蘋果GPU加速3D繪圖的新API:Metal
Metal和OpenGL ES相似倦逐,它也是一個底層API捅彻,負(fù)責(zé)和3D繪圖硬件交互域蜗。它們之間的不同在于透乾,Metal不是跨平臺的, Metal 是用 Objective-C 編 寫的,基于 Foundation熔恢,使用 GCD 在 CPU 和 GPU 之間保持同步
脐湾。與之相反的,它設(shè)計的在蘋果硬件上運(yùn)行得極其高效叙淌,與OpenGL ES相比秤掌,它提供了更快的速度和更低的開銷。它是一個GPU上一個簡單的封裝鹰霍,所以能夠完成幾乎所有事情闻鉴,像在屏幕上渲染一個精靈(sprite)或者是一個3D模型。但你要編寫完成這些事情的所有代碼茂洒。這樣麻煩的代價是孟岛,你擁有了GPU的力量和控制。
優(yōu)點:
1、使硬件達(dá)到運(yùn)行效率的峰值:因為Metal非常底層渠羞,它允許你使硬件達(dá)到運(yùn)行效率的峰值斤贰,對你的游戲如何運(yùn)行有著完全的控制。
2次询、這是一個很好的學(xué)習(xí)經(jīng)歷:學(xué)習(xí)Metal教導(dǎo)你很多關(guān)于3D繪圖編程的概念荧恍,編寫你自己的游戲引擎,以及高層(higher level)游戲框架如何運(yùn)作屯吊。
關(guān)于metal詳細(xì)的介紹可參考:Metal
以下是使用Metal和Swift來創(chuàng)建一個有基本脈絡(luò)的應(yīng)用:畫一個簡單的三角形送巡。
注意:Metal應(yīng)用不能跑在iOS模擬器上,它們需要一個設(shè)備雌芽,設(shè)備上裝載著蘋果A7芯片或者更新的芯片授艰。所以需要一臺這樣的設(shè)備(iPhone 5S,iPad Air,iPad mini2)來完成代碼的測試。
打開Xcode 通過iOS\Application\Single View Application template創(chuàng)建一個新的項目世落。使用TriangleSwift作為項目名稱淮腾,設(shè)置開發(fā)語言為Swift,設(shè)置設(shè)備為通用設(shè)備(Universal)屉佳。點擊Next谷朝,選擇一個目錄,點擊Create武花。
有七個步驟來設(shè)置metal:
1 創(chuàng)建一個MTLDevice
2 創(chuàng)建一個CAMetalLayer
3 創(chuàng)建一個Vertex Buffer
4 創(chuàng)建一個Vertex Shader
5 創(chuàng)建一個Fragment Shader
6 創(chuàng)建一個Render Pipeline
7 創(chuàng)建一個Command Queue
1 創(chuàng)建一個MTLDevice
使用Metal你要做的第一件事就是獲取一個MTLDevice的引用圆凰。
為了完成這點,打開ViewController.swift 并添加下面的import語句
import Metal
導(dǎo)入了Metal框架体箕,所以你能夠使用Metal的類(像這文件中的MTLDevice)专钉。接著,在ViewController類中添加以下屬性:
在viewDidLoad函數(shù)內(nèi)初始化這個屬性
// 1累铅、創(chuàng)建一個MTLDevice, 你可以把一個MTLDevice想象成是你和CPU的直接連接跃须。你將通過使用MTLDevice創(chuàng)建所有其他你需要的Metal對象(像是command queues,buffers娃兽,textures)菇民。
var device: MTLDevice! = nil
2 創(chuàng)建一個CAMetalLayer
在iOS里,你在屏幕上看見的所有東西投储,被一個CALayer所承載第练。存在不同特效的CALayer的子類,比如:漸變層(gradient layers)玛荞、形狀層(shapelayers)娇掏、重復(fù)層(replicator layers) 等等。如果你想要用Metal在屏幕上畫一些東西勋眯,你需要使用一個特別的CALayer子類婴梧,CAMetalLayer壁涎。
因為CAMetalLayer是QuartzCore框架的部分,而不是Metal框架里的志秃,首先在這個文件的上方添加import語句
import QuartzCore
把新屬性添加到類中:
// 2、創(chuàng)建一個CAMetalLayer
var metalLayer: CAMetalLayer! = nil
設(shè)置metalLayer
// 2.1 創(chuàng)建CAMetalLayer
metalLayer = CAMetalLayer()
// 2.2 必須明確layer使用的MTLDevice嚼酝,簡單地設(shè)置早前獲取的device
metalLayer.device = device
// 2.3 把像素格式(pixel format)設(shè)置為BGRA8Unorm浮还,它代表"8字節(jié)代表藍(lán)色、綠色闽巩、紅色和透明度钧舌,通過在0到1之間單位化的值來表示"。這次兩種用在CAMetalLayer的像素格式之一涎跨,一般情況下你這樣寫就可以了洼冻。
metalLayer.pixelFormat = .bgra8Unorm
// 2.4 蘋果鼓勵將framebufferOnly設(shè)置為true,來增強(qiáng)表現(xiàn)效率隅很。除非你需要對從layer生成的紋理(textures)取樣撞牢,或者你需要在layer繪圖紋理(drawable textures)激活一些計算內(nèi)核,否則你不需要設(shè)置叔营。(大部分情況下你不用設(shè)置)
metalLayer.framebufferOnly = true
// 2.5 把layer的frame設(shè)置為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 創(chuàng)建一個Vertex Buffer
創(chuàng)建一個緩沖區(qū)。在你的類中添加下列的常量屬性
// 3、創(chuàng)建一個Vertex Buffer
var vertexBuffer: MTLBuffer! = nil
// 3.1 在CPU創(chuàng)建一個浮點數(shù)數(shù)組望门,需要通過把它移動到一個MTLBuffer甫菠,來發(fā)送這些數(shù)據(jù)到GPU。
let vertexData:[Float] = [
0.0, 1.0, 0.0,
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0
]
在MTLDevice上調(diào)用makeBuffer(bytes:, length:, options:)婴谱,在GPU創(chuàng)建一個新的buffer蟹但,從CPU里輸送data。options不能為空谭羔。
// 3.2 獲取vertex data的字節(jié)大小华糖。你通過把元素的大小和數(shù)組元素個數(shù)相乘來得到
let dataSize = vertexData.count * 4
// 3.3 在GPU創(chuàng)建一個新的buffer,從CPU里輸送data
vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: MTLResourceOptions(rawValue: UInt(0)))
4 創(chuàng)建一個Vertex Shader
你之前創(chuàng)建的頂點將成為接下來寫的一個叫vertext shader的小程序的輸入口糕。
一個vertex shader 是一個在GPU上運(yùn)行的小程序缅阳,它由像c++的一門語言編寫,那門語言叫做Metal Shading Language景描。
一個vertex shader被每個頂點調(diào)用十办,它的工作是接受頂點的信息(如:位置和顏色、紋理坐標(biāo))超棺,返回一個潛在的修正位置(可能還有別的相關(guān)信息)
點擊File\New\File向族,選擇iOS\Source\Metal File,然后點擊Next棠绘。輸入Shader.metal作為文件名件相,然后點擊Create再扭。
// 一個vertex shader被每個頂點調(diào)用,它的工作是接受頂點的信息(如:位置和顏色夜矗、紋理坐標(biāo))泛范,返回一個潛在的修正位置(可能還有別的相關(guān)信息)
#include <metal_stdlib>
using namespace metal;
/**
* 1、所有的vertex shaders必須以關(guān)鍵字vertex開頭紊撕。函數(shù)必須至少返回頂點的最終位置——你通過指定float4(一個元素為4個浮點數(shù)的向量)罢荡。然后你給一個名字給vetex shader,以后你將用這個名字來訪問這個vertex shader对扶。
* 2区赵、vertex shader會接受一個名叫vertex_id的屬性的特定參數(shù),它意味著它會被vertex數(shù)組里特定的頂點所裝入浪南。
* 3笼才、一個指向一個元素為packed_float4(一個向量包含4個浮點數(shù))的數(shù)組的指針,如:每個頂點的位置络凿。這個 [[ ... ]] 語法被用在聲明那些能被用作特定額外信息的屬性骡送,像是資源位置,shader輸入絮记,內(nèi)建變量各谚。這里你把這個參數(shù)用 [[ buffer(0) ]] 標(biāo)記,來指明這個參數(shù)將會被在你代碼中你發(fā)送到你的vertex shader的第一塊buffer data所遍歷到千。
* 4昌渤、基于vertex id來檢索vertex數(shù)組中對應(yīng)位置的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 創(chuàng)建一個Fragment Shader
完成vertex shader后憔四,另一個shader膀息,它被每個在屏幕上的fragment(think pixel)調(diào)用,它就是fragment shader了赵。
fragment shader通過內(nèi)插(interpolating)vertex shader的輸出來獲得自己的輸入潜支。
/*
1. 所有fragment shaders必須以fragment關(guān)鍵字開始。這個函數(shù)必須至少返回fragment的最終顏色——你通過指定half4(一個顏色的RGBA值)來完成這個任務(wù)柿汛。注意冗酿,half4比float4在內(nèi)存上更有效率,因為络断,你寫入了更少的GPU內(nèi)存裁替。
2. 這里你返回(0.6,0.6,0.6,0.6)的顏色,也就是灰色貌笨。
*/
fragment half4 basic_fragment() {
return half4(0.6);
}
6 創(chuàng)建一個Render Pipeline
現(xiàn)在你已經(jīng)創(chuàng)建了一個vertex shader和一個fragment shader弱判,你需要組合它們(加上一些配置數(shù)據(jù))到一個特殊的對象,它名叫render pipeline锥惋。Metal的渲染器(shaders)是預(yù)編譯的昌腰,render pipeline 配置會在你第一次設(shè)置它的時候被編譯开伏,所以所有事情都極其高效。
首先在ViewController.swift里添加一個屬性:
// 6遭商、創(chuàng)建一個Render Pipeline
var pipelineState: MTLRenderPipelineState! = nil
在viewDidLoad方法最后添加如下代碼:
// 6.1 通過調(diào)用device.newDefaultLibrary方法獲得的MTLibrary對象訪問到你項目中的預(yù)編譯shaders,然后通過名字檢索每個shader
let defaultLibrary = device.newDefaultLibrary()
let fragmentProgram = defaultLibrary?.makeFunction(name: "basic_fragment")
let vertextProgram = defaultLibrary?.makeFunction(name: "basic_vertex")
// 6.2 這里設(shè)置你的render pipeline固灵。它包含你想要使用的shaders、顏色附件(color attachment)的像素格式(pixel format)劫流。(例如:你渲染到的輸入緩沖區(qū)怎虫,也就是CAMetalLayer)
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.vertexFunction = vertextProgram
pipelineStateDescriptor.fragmentFunction = fragmentProgram pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
7 創(chuàng)建一個Command Queue
你需要做的最終的設(shè)置步驟,是創(chuàng)建一個MTLCommandQueue困介。
把這個想象成是一個列表裝載著你告訴GPU一次要執(zhí)行的命令。
要創(chuàng)建一個command queue蘸际,簡單地添加一個屬性:
// 7座哩、創(chuàng)建一個Command Queue
var commandQueue: MTLCommandQueue! = nil
把下面這行添加到viewDidLoad中:
// 7.1 初始化commandQueue
commandQueue = device.makeCommandQueue()
預(yù)設(shè)置的代碼到這里完成了。
接下來就是渲染三角形了粮彤,它將需要在五個步驟來完成:
1 創(chuàng)建一個Display link根穷。
2 創(chuàng)建一個Render Pass Descriptor
3 創(chuàng)建一個Command Buffer
4 創(chuàng)建一個Render Command Encoder
5 提交Command Buffer的內(nèi)容
注意:理論上這個應(yīng)用實際上不需要每幀渲染,因為三角形被繪制之后不會動导坟。但是屿良,大部分應(yīng)用會有物體的移動,所以我們會那樣做惫周。
1 創(chuàng)建一個Display link
在iOS平臺上尘惧,通過CADisplayLink 類,可以創(chuàng)建一個函數(shù)在每次設(shè)備屏幕刷新的時候被調(diào)用递递,這樣你就可以重繪屏幕喷橙。
為了使用它,在類里添加一個新的屬性:
// 8登舞、創(chuàng)建一個Display Link
var timer: CADisplayLink! = nil
初始化timer
// 8.1 初始化 timer贰逾,設(shè)置timer,讓它每次刷新屏幕的時候調(diào)用一個名叫drawloop的方法
timer = CADisplayLink(target: self, selector: #selector(ViewController.drawloop))
timer.add(to: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)
渲染的代碼在render()中實現(xiàn)
func render() {
//TODO
}
func drawloop() {
self.render()
}
2 創(chuàng)建一個Render Pass Descriptor
// metal layer上調(diào)用nextDrawable() 菠秒,它會返回你需要繪制到屏幕上的紋理(texture)
let drawable = metalLayer.nextDrawable()
// 8疙剑、創(chuàng)建一個Render Pass Descriptor,配置什么紋理會被渲染到践叠、clear color言缤,以及其他的配置
let renderPassDesciptor = MTLRenderPassDescriptor()
renderPassDesciptor.colorAttachments[0].texture = drawable?.texture
// 設(shè)置load action為clear,也就是說在繪制之前禁灼,把紋理清空
renderPassDesciptor.colorAttachments[0].loadAction = .clear
// 繪制的背景顏色設(shè)置為綠色
renderPassDesciptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.8, 0.5, 1.0)
3 創(chuàng)建一個Command Buffer
一個command buffer包含一個或多個渲染指令(render commands)轧简。
// 9、創(chuàng)建一個Command Buffer
// 你可以把它想象為一系列這一幀想要執(zhí)行的渲染命令匾二。注意在你提交command buffer之前哮独,沒有事情會真正發(fā)生拳芙,這樣給你對事物在何時發(fā)生有一個很好的控制。
let commandBuffer = commandQueue.makeCommandBuffer()
4 創(chuàng)建一個渲染命令編碼器(Render Command Encoder)
// 10皮璧、創(chuàng)建一個渲染命令編碼器(Render Command Encoder)
// 創(chuàng)建一個command encoder舟扎,并指定你之前創(chuàng)建的pipeline和頂點
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDesciptor)
renderEncoder.setRenderPipelineState(pipelineState)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, at: 0)
/**
繪制圖形
- parameter type: 畫三角形
- parameter vertexStart: 從vertex buffer 下標(biāo)為0的頂點開始
- parameter vertexCount: 頂點數(shù)
- parameter instanceCount: 總共有1個三角形
*/
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
// 完成后,調(diào)用endEncoding()
renderEncoder.endEncoding()
5 提交Command Buffer
// 保證新紋理會在繪制完成后立即出現(xiàn)
commandBuffer.present(drawable!)
// 提交事務(wù)(transaction), 把任務(wù)交給GPU
commandBuffer.commit()
學(xué)習(xí)資料:
? 蘋果Metal開發(fā)者文檔悴务,有很多文檔睹限、錄像、樣例代碼的鏈接讯檐。
? 蘋果的Metal編程指導(dǎo)
? 蘋果的Metal Shading Language 指導(dǎo)
? WWDC2014 Metal錄像