Metal入門(使用Metal畫一個三角形)

學(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渲染流程圖.png

以下是使用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錄像

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末羡疗,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子别洪,更是在濱河造成了極大的恐慌叨恨,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挖垛,死亡現(xiàn)場離奇詭異痒钝,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)痢毒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進(jìn)店門送矩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人哪替,你說我怎么就攤上這事栋荸。” “怎么了凭舶?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵蒸其,是天一觀的道長。 經(jīng)常有香客問我库快,道長摸袁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任义屏,我火速辦了婚禮靠汁,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘闽铐。我一直安慰自己蝶怔,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布兄墅。 她就那樣靜靜地躺著踢星,像睡著了一般。 火紅的嫁衣襯著肌膚如雪隙咸。 梳的紋絲不亂的頭發(fā)上沐悦,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天成洗,我揣著相機(jī)與錄音,去河邊找鬼藏否。 笑死瓶殃,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的副签。 我是一名探鬼主播遥椿,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼淆储!你這毒婦竟也來了冠场?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤本砰,失蹤者是張志新(化名)和其女友劉穎碴裙,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體灌具,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年譬巫,在試婚紗的時候發(fā)現(xiàn)自己被綠了咖楣。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡芦昔,死狀恐怖诱贿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情咕缎,我是刑警寧澤珠十,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站凭豪,受9級特大地震影響焙蹭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜嫂伞,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一孔厉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧帖努,春花似錦撰豺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至匙监,卻和暖如春凡橱,著一層夾襖步出監(jiān)牢的瞬間小作,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工梭纹, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留躲惰,地道東北人。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓变抽,卻偏偏與公主長得像础拨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子绍载,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內(nèi)容