標籤:
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.建立一個MTLDevice2.建立一個CAMetalLayer3.建立一個Vertex Buffer4.建立一個Vertex Shader5.建立一個Fragment Shader6.建立一個Render Pipeline7.建立一個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.你建立了一個CAMetalLayerb.你必須明確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 是一個在GPU上啟動並執行小程式,它由像c++的一門語言編寫,那門語言叫做Metal Shading Language。 一個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
- const device packed_float3* vertex_array [[ buffer(0) ]], // 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[0].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 Descriptor3.建立一個Command Buffer4.建立一個Render Command Encoder5.提交你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[0].texture = drawable.texture
- renderPassDescriptor.colorAttachments[0].loadAction = .Clear
- renderPassDescriptor.colorAttachments[0].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: 0, atIndex: 0)
- renderEncoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
- 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,紋理,光照,以及匯入模型。如果你感到有興趣、並想看到更多教程的話,請留下你的評論。同時,確定查看蘋果一些很好的資源: 蘋果Metal開發人員文檔,有很多文檔、錄影、範例代碼的連結。蘋果的Metal編程指導蘋果的Metal Shading Language 指導WWDC2014 Metal錄影
iOS開發——Metal教程