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

OrangesChen發表於2018-01-02

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

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

相關文章