啥是饅頭(Metal)

啥是饅頭(Metal)

What's Metal

The Metal framework supports GPU-accelerated advanced 3D graphics rendering and data-parallel computation workloads.

  • Metal 框架是一套專門給圖形處理器(GPU)定制的API。它可以盡可能發揮GPU的3D圖形渲染以及并行數據計算能力。Metal 給開發者提供的是非常底層的可以操作到GPU的接口,并且,Metal 對數據的并行計算能力以及對資源的預編譯能力可以極大的減少CPU的負擔。所以 Metal 同時具備了 low-level 和 low-overhead 的特點。

Why Metal

Deprecation of OpenGL and OpenCL
Apps built using OpenGL and OpenCL will continue to run in macOS 10.14, but these legacy technologies are deprecated in macOS 10.14. Games and graphics-intensive apps that use OpenGL should now adopt Metal. Similarly, apps that use OpenCL for computational tasks should now adopt Metal and Metal Performance Shaders.

  • 在 MacOS 10.14 的更新文檔中,蘋果表示使用 OpenGL 和 OpenCL 構建的應用可以繼續在 macOS 10.14 中運行,但這些遺留技術在 macOS 10.14 中不推薦使用。現在使用 OpenGL 的游戲和應用應轉向 Metal。總體來說,蘋果爸爸已經表示要棄用 OpenGL/CL,并且推薦使用 Metal 作為替代。

Where Metal

  • Metal 作為一個能夠高效地利用 GPU 對數據的并行處理能力以及對數據的圖形化接口,它可以解決很多由于高計算量帶來的問題。在機器學習、圖像視頻處理以及圖形渲染領域,Metal 都能發揮出它的優勢。

  • 當你遇到以下的情況時,Metal 也許是你最好的選擇:

  1. 你想要盡可能高效的渲染3D模型
  2. 你想要在處理圖像或者視頻的時候,類似對每一幀每一個像素進行數據集中處理的情況。
  3. 你碰到一些數據量很大的計算問題時,可以運用 Metal 的高并發處理能力,將數據量分解為很多子數據集進行處理。
  4. 你想要在自己的游戲中制作一些獨特的效果,比如自定義 shading 和 lighting。

Hello Metal

在我們學習一門編程語言的時候,往往第一句代碼就是打印 "Hello world" 字符串。那么作為渲染框架的入門第一課,學會在界面上渲染出第一個三角形是最合適不過的了。

首先我們來介紹一下使用 Metal 來渲染一個模型的大致流程:
Initialize Metal -> Load Model -> Set up pipeline -> Render

直接上手,我們先從創建一個新的項目 HelloMetal 開始,選擇iOS開發平臺,語言用 swift。

Initialize Metal

在 ViewController 中將 MetalKit 框架導入

import MetalKit

聲明 MTLDevice 屬性 device,在 viewdidload 中初始化device。

var device: MTLDevice!
device = MTLCreateSystemDefaultDevice()

你可以理解 MTLDevice 為你和GPU的直接連接的一個抽象。你將通過使用 MTLDevice 創建所有其他你需要的Metal對象(像是command queues,buffers,textures)。

PS:注意如果是在 iOS 的模擬器環境下,是取不到 device 的

隨后初始化 MTKView 供顯示渲染后的圖像

let frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width , height:self.view.frame.size.height)
let view = MTKView(frame: frame, device: device)
view.clearColor = MTLClearColor(red: 1, green: 1, blue: 0.8, alpha: 1)
self.view.addSubview(view)

MTKView 是 UIView 的一個子類,用于在 Metal 中展示渲染結果,同時提供一些方便的屬性和代理。

設置 clearColor 使得 view 的默認背景被 clearcolor 填充。

Load Model

由于現在要繪制的是一個平面三角形,所以這里簡單地 hardcode 三角形的頂點數據作為數據源,后續會介紹如何通過 Model I/O 框架來 load 基本 3D 模型,以及加載 obj 模型。

首先添加聲明一個頂點的常量數組以及聲明 一個 MTLBuffer 變量
vertexBuffer。

let vertexData: [Float] = [
        0.0, 1.0, 0.0,
        -1.0, -1.0, 0.0,
        1.0, -1.0, 0.0
    ]
    
var vertexBuffer: MTLBuffer!

然后在 viewdidload 中接著初始化 vertexBuffer

let dataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0])
vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options:[])

Set up pipeline

pipeline 渲染管線

在上手寫 pipeline 相關的代碼之前,我們先來簡單了解一下什么是 pipeline,更加詳細的針對 pipeline 的解說會在后續教程中給出。

pipeline 就是渲染管線,是在渲染處理過程中順序執行的一系列操作。這一套渲染流程在理論層面上都是統一的,所以不論是 OpenGL ES 的渲染管線還是 Metal 的渲染管線,在理解上都是相同的。pipeline 來源于生產車間的流水線作業,在渲染過程中,一個操作接一個操作進行,就如同流水線一樣,這樣的實現可以極大地提高渲染效率。整個渲染管線如同下圖所示:

pipeline.png

渲染管線的大致流程為:頂點數據來源 -> 頂點著色器 -> 圖元裝配 ->
光柵化 -> 片元著色器 -> 拿到FrameBuffer

圖中標紅的 Vertex Proccesing 和 Fragment Proccessing 是可編程管線,一般是通過寫著色器語言(Shader Language)腳本實現。在 Metal 中使用的 Metal Shading Language,同樣也是 C++ 的一個子集。

queues,buffer and encoders

GPU 渲染出來的每一幀都是通過你發送給 GPU 的指令來生成的。在 Metal 中,每一幀的渲染我們都將用一個 render command encoder 包裹這些相關的指令。而 command buffer 是用于管理這些 encoders,再上一層, command queue 用于管理這些 command buffers。

在整個渲染過程中,只需要創建一個 command queue 來管理 command buffers,以及上文提到過的 device、vertex buffer 也只需要創建一次。還有頂點著色器、片元著色器、pipelineState 都是。需要多次創建的是那些和幀的變化具備強關聯的東西,比如 command buffer,command encoder。每一幀的渲染都需要 encoder 去設置pipelineState,去設置 vertex buffer 以及繪制指令。

pipeline2.png

shader

shader 是運行在GPU上的腳本,它是 C++ 的一種子集語言。一般來說我們可以在 xcode 中創建 .metal 格式的 shader 腳本文件,但是其實直接在主文件中將 shader 以 string 的形式賦值保存也可以。以下就是兩個最簡單的 shader 函數,頂點處理器 vertex_main 以及片元處理器 fragment_main:

        let shader = """
#include <metal_stdlib>
using namespace metal;

vertex float4 vertex_main(constant packed_float3* vertex_array[[buffer(0)]],
                      unsigned int vid[[vertex_id]]) {
   return float4(vertex_array[vid], 1.0);
}

fragment float4 fragment_main() {
  return float4(0, 1, 0, 1);
}
"""

簡單來講,頂點處理器顧名思義就是對CPU傳輸過來的頂點數據做處理,當然也可以什么都不做,直接返回,就和這里的 vertex_main 一樣。而片元處理器是用來確定一個像素的著色,它決定了像素的顏色表現。

然后我們通過這個 shader 的 string 或者 .metal 文件來初始化兩個函數,并將它們設置給一個渲染管道描述器(renderPipelineDescriptor),用于后續初始化 pipelineState。

 let library = try! device.makeLibrary(source: shader, options: nil)
        let vertexFunction = library.makeFunction(name: "vertex_main")
        let fragmentFunction = library.makeFunction(name: "fragment_main")
        

pipeline state

在 Metal 中,我們需要給 GPU 設置渲染管線狀態,以此告訴 GPU 在 pipeline state 發生改變之前,其他的都不會有變化,從而使 GPU 的工作更加高效。pipelineState 包含了所有 GPU 需要知道的信息,包括像素格式以及剛剛創建的 shader 函數等。pipeline state 是通過一個 pipeline descriptor 創建的,我們可以通過設置 descriptor 的相關屬性來改變 pipeline state。

 let pipelineDescriptor = MTLRenderPipelineDescriptor()
 pipelineDescriptor.vertexFunction = vertexFunction
 pipelineDescriptor.fragmentFunction = fragmentFunction
 pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm

 pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor)

這里需要注意的一點是,創建一個 pipelineState 是耗時的操作,所以我們應該一次性創建 pipelineState。在實際項目中,或許我們需要一次性創建多個 pipelineState 以調用不同的 shader 函數,或者使用不同的頂點布局等等。

Render

終于到了渲染這步,從這一步開始,我們所寫的代碼針對的是每一幀的渲染,也就是每一幀都要調用這部分的代碼。

MTKView 的一個代理方法 public func draw(in view: MTKView) 會在每一幀繪制的時候進行調用,所以一般來說,我們可以在這個代理中去繪制每一幀的內容。但是本節的需求只是繪制一個不會動的三角形,所以沒有必要每幀渲染,直接在 viewdidload 中接著往下寫。

guard let commandBuffer = commandQueue.makeCommandBuffer(),
let descriptor = view.currentRenderPassDescriptor,
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { 
             fatalError() 
             }
            

在這里,我們通過 commandQueue 創建 commandBuffer。commandBuffer 中保存著這一幀中所有你需要讓 GPU 給你渲染的指令。
同時,我們創建了一個 renderPassDescriptor,用于 commandEncoder 的創建。

接下來,我們需要給 commandEncoder 設置當前的 pipelineState,告訴 GPU 有關像素格式以及 shader 函數等信息已經包含在這個 pipelinestate 中了,在 state 發生改變之前,以上的信息都不會有任何變化,你放心地去處理渲染。

renderEncoder.setRenderPipelineState(pipelineState)

然后給 commandEncoder 設置頂點數據,這里的頂點數據就是上文創建的 vertexbuffer,告訴它需要處理的頂點數據來自哪里。

renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)

最后是要 draw 的部分了

renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)

在這一步告訴 GPU 的是,去將那些頂點數據按照給出的頂點順序數目渲染成一個三角形。當然,這一步也不是真正的渲染,在 GPU 接收到所有的 commandbuffer 的指令之后,它才會去做真正的渲染過程。

//1
renderEncoder.endEncoding() 
//2
guard let drawable = view.currentDrawable else {
            fatalError()
        } 
// 3    
commandBuffer.present(drawable)
commandBuffer.commit()

步驟1 告訴 renderEncoder 已經沒有更多的指令了,步驟2 是從 MTKView 中拿到一個 CAMetalDrawable 類實例,這個 drawable 持有著一個可供 Metal 讀寫的可繪制 texture。步驟3 就是要求 commandBuffer 將指令提交給 GPU 并且將結果渲染展示到 drawable 上面。這一步觸發了真正的渲染,編譯運行代碼可以看到在屏幕上出現了一個全屏的綠色三角形,而背景部分則是被 clearColor 覆蓋的米黃色。
如圖所示:

result.png

通過繪制一個簡單的三角形我們熟悉了 Metal 渲染的整體流程,這也是學習 Metal 的第一步而已,后續會繼續介紹更多有關 Metal、圖形學以及線代方面的東西。下一章主要介紹 3D 模型的渲染以及詳細的 render pipeline 渲染管線工作流程。

Demo地址

點擊查看 Whats Metal 第一節Demo

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容