啥是饅頭(Metal)
What's Metal
The Metal framework supports GPU-accelerated advanced 3D graphics rendering and data-parallel computation workloads.
- Metal 框架是一套專門給圖形處理器(GPU)定制的API。它可以盡可能發(fā)揮GPU的3D圖形渲染以及并行數(shù)據計算能力此蜈。Metal 給開發(fā)者提供的是非常底層的可以操作到GPU的接口蜕便,并且,Metal 對數(shù)據的并行計算能力以及對資源的預編譯能力可以極大的減少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 構建的應用可以繼續(xù)在 macOS 10.14 中運行,但這些遺留技術在 macOS 10.14 中不推薦使用∽酱椋現(xiàn)在使用 OpenGL 的游戲和應用應轉向 Metal「竟福總體來說巾遭,蘋果爸爸已經表示要棄用 OpenGL/CL,并且推薦使用 Metal 作為替代闯估。
Where Metal
Metal 作為一個能夠高效地利用 GPU 對數(shù)據的并行處理能力以及對數(shù)據的圖形化接口灼舍,它可以解決很多由于高計算量帶來的問題。在機器學習涨薪、圖像視頻處理以及圖形渲染領域骑素,Metal 都能發(fā)揮出它的優(yōu)勢。
當你遇到以下的情況時刚夺,Metal 也許是你最好的選擇:
- 你想要盡可能高效的渲染3D模型
- 你想要在處理圖像或者視頻的時候砂豌,類似對每一幀每一個像素進行數(shù)據集中處理的情況。
- 你碰到一些數(shù)據量很大的計算問題時光督,可以運用 Metal 的高并發(fā)處理能力阳距,將數(shù)據量分解為很多子數(shù)據集進行處理。
- 你想要在自己的游戲中制作一些獨特的效果结借,比如自定義 shading 和 lighting筐摘。
Hello Metal
在我們學習一門編程語言的時候,往往第一句代碼就是打印 "Hello world" 字符串船老。那么作為渲染框架的入門第一課咖熟,學會在界面上渲染出第一個三角形是最合適不過的了。
首先我們來介紹一下使用 Metal 來渲染一個模型的大致流程:
Initialize Metal -> Load Model -> Set up pipeline -> Render
直接上手柳畔,我們先從創(chuàng)建一個新的項目 HelloMetal 開始馍管,選擇iOS開發(fā)平臺,語言用 swift薪韩。
Initialize Metal
在 ViewController 中將 MetalKit 框架導入
import MetalKit
聲明 MTLDevice 屬性 device确沸,在 viewdidload 中初始化device。
var device: MTLDevice!
device = MTLCreateSystemDefaultDevice()
你可以理解 MTLDevice 為你和GPU的直接連接的一個抽象俘陷。你將通過使用 MTLDevice 創(chuàng)建所有其他你需要的Metal對象(像是command queues罗捎,buffers,textures)拉盾。
PS:注意如果是在 iOS 的模擬器環(huán)境下桨菜,是取不到 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
由于現(xiàn)在要繪制的是一個平面三角形,所以這里簡單地 hardcode 三角形的頂點數(shù)據作為數(shù)據源霞掺,后續(xù)會介紹如何通過 Model I/O 框架來 load 基本 3D 模型谊路,以及加載 obj 模型。
首先添加聲明一個頂點的常量數(shù)組以及聲明 一個 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 的解說會在后續(xù)教程中給出挤巡。
pipeline 就是渲染管線,是在渲染處理過程中順序執(zhí)行的一系列操作酷麦。這一套渲染流程在理論層面上都是統(tǒng)一的矿卑,所以不論是 OpenGL ES 的渲染管線還是 Metal 的渲染管線,在理解上都是相同的沃饶。pipeline 來源于生產車間的流水線作業(yè)母廷,在渲染過程中,一個操作接一個操作進行糊肤,就如同流水線一樣琴昆,這樣的實現(xiàn)可以極大地提高渲染效率。整個渲染管線如同下圖所示:
渲染管線的大致流程為:頂點數(shù)據來源 -> 頂點著色器 -> 圖元裝配 ->
光柵化 -> 片元著色器 -> 拿到FrameBuffer
圖中標紅的 Vertex Proccesing 和 Fragment Proccessing 是可編程管線馆揉,一般是通過寫著色器語言(Shader Language)腳本實現(xiàn)业舍。在 Metal 中使用的 Metal Shading Language,同樣也是 C++ 的一個子集升酣。
queues,buffer and encoders
GPU 渲染出來的每一幀都是通過你發(fā)送給 GPU 的指令來生成的舷暮。在 Metal 中,每一幀的渲染我們都將用一個 render command encoder 包裹這些相關的指令噩茄。而 command buffer 是用于管理這些 encoders下面,再上一層, command queue 用于管理這些 command buffers绩聘。
在整個渲染過程中沥割,只需要創(chuàng)建一個 command queue 來管理 command buffers,以及上文提到過的 device凿菩、vertex buffer 也只需要創(chuàng)建一次驯遇。還有頂點著色器、片元著色器蓄髓、pipelineState 都是叉庐。需要多次創(chuàng)建的是那些和幀的變化具備強關聯(lián)的東西,比如 command buffer会喝,command encoder陡叠。每一幀的渲染都需要 encoder 去設置pipelineState玩郊,去設置 vertex buffer 以及繪制指令。
shader
shader 是運行在GPU上的腳本枉阵,它是 C++ 的一種子集語言译红。一般來說我們可以在 xcode 中創(chuàng)建 .metal 格式的 shader 腳本文件,但是其實直接在主文件中將 shader 以 string 的形式賦值保存也可以兴溜。以下就是兩個最簡單的 shader 函數(shù)侦厚,頂點處理器 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傳輸過來的頂點數(shù)據做處理拙徽,當然也可以什么都不做刨沦,直接返回,就和這里的 vertex_main 一樣膘怕。而片元處理器是用來確定一個像素的著色想诅,它決定了像素的顏色表現(xiàn)。
然后我們通過這個 shader 的 string 或者 .metal 文件來初始化兩個函數(shù)岛心,并將它們設置給一個渲染管道描述器(renderPipelineDescriptor)来破,用于后續(xù)初始化 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 設置渲染管線狀態(tài)徘禁,以此告訴 GPU 在 pipeline state 發(fā)生改變之前,其他的都不會有變化髓堪,從而使 GPU 的工作更加高效晌坤。pipelineState 包含了所有 GPU 需要知道的信息,包括像素格式以及剛剛創(chuàng)建的 shader 函數(shù)等旦袋。pipeline state 是通過一個 pipeline descriptor 創(chuàng)建的骤菠,我們可以通過設置 descriptor 的相關屬性來改變 pipeline state。
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor)
這里需要注意的一點是疤孕,創(chuàng)建一個 pipelineState 是耗時的操作商乎,所以我們應該一次性創(chuàng)建 pipelineState。在實際項目中祭阀,或許我們需要一次性創(chuàng)建多個 pipelineState 以調用不同的 shader 函數(shù)鹉戚,或者使用不同的頂點布局等等。
Render
終于到了渲染這步专控,從這一步開始抹凳,我們所寫的代碼針對的是每一幀的渲染,也就是每一幀都要調用這部分的代碼伦腐。
MTKView 的一個代理方法 public func draw(in view: MTKView) 會在每一幀繪制的時候進行調用赢底,所以一般來說,我們可以在這個代理中去繪制每一幀的內容。但是本節(jié)的需求只是繪制一個不會動的三角形幸冻,所以沒有必要每幀渲染粹庞,直接在 viewdidload 中接著往下寫。
guard let commandBuffer = commandQueue.makeCommandBuffer(),
let descriptor = view.currentRenderPassDescriptor,
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else {
fatalError()
}
在這里洽损,我們通過 commandQueue 創(chuàng)建 commandBuffer庞溜。commandBuffer 中保存著這一幀中所有你需要讓 GPU 給你渲染的指令。
同時碑定,我們創(chuàng)建了一個 renderPassDescriptor流码,用于 commandEncoder 的創(chuàng)建。
接下來延刘,我們需要給 commandEncoder 設置當前的 pipelineState漫试,告訴 GPU 有關像素格式以及 shader 函數(shù)等信息已經包含在這個 pipelinestate 中了,在 state 發(fā)生改變之前访娶,以上的信息都不會有任何變化商虐,你放心地去處理渲染觉阅。
renderEncoder.setRenderPipelineState(pipelineState)
然后給 commandEncoder 設置頂點數(shù)據崖疤,這里的頂點數(shù)據就是上文創(chuàng)建的 vertexbuffer,告訴它需要處理的頂點數(shù)據來自哪里典勇。
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
最后是要 draw 的部分了
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
在這一步告訴 GPU 的是劫哼,去將那些頂點數(shù)據按照給出的頂點順序數(shù)目渲染成一個三角形。當然割笙,這一步也不是真正的渲染权烧,在 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 上面板祝。這一步觸發(fā)了真正的渲染,編譯運行代碼可以看到在屏幕上出現(xiàn)了一個全屏的綠色三角形走净,而背景部分則是被 clearColor 覆蓋的米黃色券时。
如圖所示:
通過繪制一個簡單的三角形我們熟悉了 Metal 渲染的整體流程,這也是學習 Metal 的第一步而已伏伯,后續(xù)會繼續(xù)介紹更多有關 Metal橘洞、圖形學以及線代方面的東西。下一章主要介紹 3D 模型的渲染以及詳細的 render pipeline 渲染管線工作流程说搅。