前言
metal是iOS底層圖形渲染技術(shù),它是利用GPU進(jìn)行渲染,它允許我們程序員直接操作GPU繪制焕梅,所以相比UIKit層面,它更底層卦洽,效率更高贞言。它跟OpenGL是一個層面的,OpenGL是跨平臺的阀蒂,metal雖說只支持iOS平臺该窗,但是它的效率確是OpenGL的十倍,作為iOS開發(fā)者蚤霞,有必要學(xué)習(xí)一下酗失。
metal、OpenGL昧绣、UiKit规肴、CGGraphites之間的關(guān)系如圖:
[圖片上傳中...(image.png-bc22ca-1573183714631-0)]
讓我們來看一下metal框架結(jié)構(gòu)圖
[圖片上傳中...(image.png-f20d8b-1573183280420-0)]
理論基礎(chǔ)學(xué)習(xí)
我們先來了解一下metal中幾個屬性:
- MTLDevice:獲取渲染的GPU硬件設(shè)備,這個是metal渲染的基礎(chǔ)夜畴,必須要有GPU拖刃,所以該屬性不能為空,模擬器獲取不到該設(shè)備贪绘,所以metal不支持模擬器(iOS13開始兑牡,metal開始支持模擬器)。
iOS 可以通過下面方法獲取GPU:
MTLCreateSystemDefaultDevice()
- MTLCommandQueue:命令隊列兔簇,負(fù)責(zé)管理所有提交給GPU渲染的Buffer的順序发绢,該對象有MTLDevice生成硬耍,是個單例,可以通過以下方式獲得:
device?.makeCommandQueue()
- MTLBuffer:每一個由程序員提交的每一個指令塊边酒,他是metal編程的基本單元经柴,每一個指令塊應(yīng)該明確告知二進(jìn)制數(shù)據(jù)bytes,內(nèi)存長度墩朦。創(chuàng)建API:
device?.makeBuffer(bytes: vertex_data, length: data_size, options: [])
- MTLRenderPassDescriptor:渲染描述符坯认。渲染描述符是描述本次GPU要渲染的頂點函數(shù)和片段函數(shù)。
什么是頂點函數(shù)氓涣?
比如說我們用metal繪制一個三角形牛哺,那么三角形的三個頂點的坐標(biāo)就是頂點函數(shù)所要描述的。
片段函數(shù):
三角形3個頂點之間如何過渡劳吠,過渡的顏色引润,由片段函數(shù)負(fù)責(zé)。
需要了解的是:頂點函數(shù)和片段函數(shù)都是由Shader語言編寫
Shader語言
Shader語言是metal的著色器語言痒玩,著色器語言不同于一般的編程語言淳附,它不擅長邏輯運算,它只擅長數(shù)學(xué)計算蠢古,比如矩陣操作奴曙。因為Shader語言是面向GPU的,GPU內(nèi)存很小草讶,所以Shader語言不支持邏輯運算符語法洽糟,比如if for循環(huán)統(tǒng)統(tǒng)不支持,支持矩陣運算堕战,基本內(nèi)置函數(shù)等坤溃。
了解了Shader后,我們繼續(xù)
我們剛才說了践啄,渲染描述符對象怎么有兩個重要屬性必須要賦值浇雹,還有一個屬性也要賦值:colorAttachments沉御,這個是設(shè)置過渡顏色的屿讽。所以渲染描述符生成和重要屬性賦值的代碼如下:
let library = device?.makeDefaultLibrary()
let vertext_func = library?.makeFunction(name: "vertex_func")
let frag_func = library?.makeFunction(name: "fragment_func")
let rpld = MTLRenderPipelineDescriptor()
rpld.vertexFunction = vertext_func
rpld.fragmentFunction = frag_func
rpld.colorAttachments[0].pixelFormat = .bgra8Unorm
- MTLRenderPipelineState:渲染管道狀態(tài)。這個即是上一個渲染描述符得到吠裆,代碼如下:
try device?.makeRenderPipelineState(descriptor: rpld)
該對象伐谈,應(yīng)該全局保存,因為它需要每次draw函數(shù)獲取渲染數(shù)據(jù)就需要該對象试疙,可以看出诵棵,該對象里面包含了所以GPU要渲染的東西了。
- MTLRenderCommandEncoder:渲染命令編碼器祝旷。這個是將渲染管道狀態(tài)和頂點函數(shù)提交給draw函數(shù)履澳,是最后階段嘶窄。
commandEncoder?.setRenderPipelineState(rps!)
commandEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
commandEncoder?.setVertexBuffer(uniform_buffer, offset: 0, index: 1)
實戰(zhàn)演練
我們要繪制一個顏色漸變的三角形,最后讓這個三角形沿著z軸旋轉(zhuǎn)距贷。好了柄冲,不多說了,直接看演示效果圖:
[圖片上傳中...(image.png-9ce581-1573196630965-0)]
我們新建一個view,繼承MTKView忠蝗。
首先现横,我們獲取device對象:
required init(coder: NSCoder) {
super.init(coder: coder)
device = MTLCreateSystemDefaultDevice()
render()
}
init(frame : CGRect) {
super.init(frame: frame, device: MTLCreateSystemDefaultDevice())
render()
}
然后,我們要繪制一個三角形阁最,頂點函數(shù)應(yīng)該是這樣:
func createBuffer() {
let vertex_data = [Vertex(position: [-1.0, -1.0, 0, 1.0], color: [1, 0, 0, 1]),
Vertex(position: [ 1.0, -1.0, 0, 1.0], color: [0, 1, 0, 1]),
Vertex(position: [ 0, 0.5, 0, 1.0], color: [0, 0, 1, 1])]
let data_size = vertex_data.count * MemoryLayout<Vertex>.size
vertexBuffer = device?.makeBuffer(bytes: vertex_data, length: data_size, options: [])
}
頂點規(guī)則是戒祠,建立以屏幕中心為左邊原定,這里我們每個點是4維結(jié)構(gòu)速种,便于和3D模型數(shù)據(jù)統(tǒng)一哈姜盈,當(dāng)然,我們這個例子是二維的配阵,所以只要設(shè)置前2個數(shù)據(jù)贩据,后面兩個可以固定為0和1.
color,也是4維,分別對應(yīng)著r闸餐、g饱亮、b、alpha
data_size標(biāo)識每個頂點數(shù)據(jù)結(jié)構(gòu)的內(nèi)存大小舍沙。
這樣近上,我們就創(chuàng)建了三角形的三個頂點buffer了。
然后我們看完整的render函數(shù):
func render() {
commandQueue = device?.makeCommandQueue()
// vertexData = [-1.0, -1.0, 0, 1.0,
// 1.0, -1.0, 0, 1.0,
// 0, 0.5, 0, 1.0]
// let data_size = vertexData!.count * MemoryLayout<Float>.size
// vertexBuffer = device?.makeBuffer(bytes: vertexData!, length: data_size, options: [])
createBuffer()
let library = device?.makeDefaultLibrary()
let vertext_func = library?.makeFunction(name: "vertex_func")
let frag_func = library?.makeFunction(name: "fragment_func")
let rpld = MTLRenderPipelineDescriptor()
rpld.vertexFunction = vertext_func
rpld.fragmentFunction = frag_func
rpld.colorAttachments[0].pixelFormat = .bgra8Unorm
do{
rps = try device?.makeRenderPipelineState(descriptor: rpld)
}catch let error {
fatalError("\(error)")
}
}
我們首先得到bufferQueue,然后創(chuàng)建buffer數(shù)據(jù)拂铡,
然后設(shè)置頂點著色器和片段著色器壹无,合并得到渲染管道描述符,最后包裝成渲染管道狀態(tài)對象rps感帅。
我們保持了這個rps, 給誰用呢斗锭?
對了,就是失球,都是給draw函數(shù)做準(zhǔn)備的
我們知道岖是,draw()每一幀運行一次,每次運行实苞,我們把rps傳進(jìn)去豺撑,這樣cpu就會把渲染狀態(tài)對象提交到GPU去渲染。
所以黔牵,下面聪轿,我們看draw()函數(shù):
override func draw(_ rect: CGRect) {
if let drawable = currentDrawable ,
let rpd = currentRenderPassDescriptor {
rpd.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0.5, blue: 0.5, alpha: 1)
rpd.colorAttachments[0].loadAction = .clear
let commandBuffer = commandQueue?.makeCommandBuffer()
let commandEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: rpd)
commandEncoder?.setRenderPipelineState(rps!)
commandEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
commandEncoder?.setVertexBuffer(uniform_buffer, offset: 0, index: 1)
update()
commandEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
commandEncoder?.endEncoding()
commandBuffer?.present(drawable)
commandBuffer?.commit()
}
}
這里主要是將上一步包裝的rps交給encoder, 然后encoder提交給GPU。這樣猾浦,一個簡單的metal編程過程結(jié)束了陆错。
還有個問題灯抛,如何進(jìn)行縮放、平移呢音瓷?
這就要了解數(shù)學(xué)矩陣基礎(chǔ)知識了牧愁。在數(shù)學(xué)矩陣中,平移矩陣是這樣的:
[圖片上傳中...(image.png-398fc6-1573198339721-0)]
縮放矩陣:
[圖片上傳中...(image.png-a44394-1573198366436-0)]
旋轉(zhuǎn)矩陣:
[圖片上傳中...(image.png-4511e8-1573198487096-0)]
所以外莲,我們新建一個Matrix結(jié)構(gòu)體:
struct Matrix {
var m: [Float]
init() {
m = [1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]
}
那么猪半,上面三種變換,用矩陣表示:
func translationMatrix(_ matrix: Matrix, _ position: SIMD3<Float>) -> Matrix {
var mutate = matrix
mutate.m[12] = position.x
mutate.m[13] = position.y
mutate.m[14] = position.z
return mutate
}
func scalingMatrix(_ matrix: Matrix, _ scale: Float) -> Matrix {
var mutate = matrix
mutate.m[0] = scale
mutate.m[5] = scale
mutate.m[10] = scale
mutate.m[15] = 1.0
return mutate
}
func rotationMatrix(_ matrix: Matrix, _ rot: SIMD3<Float>) -> Matrix {
var mutate = matrix
mutate.m[0] = cos(rot.y) * cos(rot.z)
mutate.m[4] = cos(rot.z) * sin(rot.x) * sin(rot.y) - cos(rot.x) * sin(rot.z)
mutate.m[8] = cos(rot.x) * cos(rot.z) * sin(rot.y) + sin(rot.x) * sin(rot.z)
mutate.m[1] = cos(rot.y) * sin(rot.z)
mutate.m[5] = cos(rot.x) * cos(rot.z) + sin(rot.x) * sin(rot.y) * sin(rot.z)
mutate.m[9] = -cos(rot.z) * sin(rot.x) + cos(rot.x) * sin(rot.y) * sin(rot.z)
mutate.m[2] = -sin(rot.y)
mutate.m[6] = cos(rot.y) * sin(rot.x)
mutate.m[10] = cos(rot.x) * cos(rot.y)
mutate.m[15] = 1.0
return mutate
}
好偷线,現(xiàn)在矩陣變換寫好了磨确,如何在metal中使用呢?
那么声邦,我們上面矩陣是4x4的矩陣乏奥,所以,我們要創(chuàng)建buffer ,大小應(yīng)該是16個頂點的大泻ゲ堋:
func createUniformBuffer() {
let data_length = 16 * MemoryLayout<Float>.size
uniform_buffer = device?.makeBuffer(length: data_length, options: [])
let bufferPointer = uniform_buffer?.contents()
memcpy(bufferPointer, Matrix().modelMatrix(Matrix()).m, data_length)
}
我們應(yīng)該把這個方法插入到上面的render()中邓了。
最后我們在draw()方法中,設(shè)置變化buffer:
commandEncoder?.setVertexBuffer(uniform_buffer, offset: 0, index: 1)
這里index為1媳瞪,因為之前0的位置是三角形頂點buffer.
最后骗炉,為了讓圖形轉(zhuǎn)起來,我們只要設(shè)置一個定時器蛇受,每一幀增加0.01的角度:
func update() {
time += 0.01
let length = 16 * MemoryLayout<Float>.size
let bufferPointer = uniform_buffer?.contents()
memcpy(bufferPointer, Matrix().rotateMatrixDlta(Matrix(), time).m, length)
}
最后句葵,應(yīng)該把這個update()函數(shù)放到draw()方法里面,因為draw()是沒一幀執(zhí)行一次兢仰,這樣圖形就轉(zhuǎn)起來了乍丈。
以上,就是我這兩天metal學(xué)習(xí)過程和體會心得把将,還有很多理解不透徹的地方轻专,希望多多加油!