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錄影