背景
在 WWDC 2014 上捺檬,Apple為游戲開(kāi)發(fā)者推出了新的平臺(tái)技術(shù) Metal,該技術(shù)能夠?yàn)?D圖像提高 10 倍的渲染性能贸铜,充分利用GPU的運(yùn)算能力堡纬。
在現(xiàn)階段,AVFoundation蒿秦、?臉識(shí)別等大量需要顯示計(jì)算的時(shí)候烤镐,蘋(píng)果采用了硬件加速器驅(qū)動(dòng)GPU工作;在音視頻方面棍鳖,?頻編碼/解碼 / 視頻編碼/解碼 ->壓縮任務(wù)都與硬件加速器分不開(kāi)炮叶,蘋(píng)果提供的Metal,能發(fā)揮GPU/CPU的最大性能渡处,并且管理我們的資源镜悉,蘋(píng)果想用metal替代opengl作為底層繪制框架。metal常見(jiàn)應(yīng)用于一些游戲医瘫、濾鏡侣肄、相機(jī)類的app。
設(shè)備支持:iOS 8以上醇份,A7處理器以上稼锅,因此只有iphone5以上機(jī)型才支持metal,并且不支持模擬器運(yùn)行僚纷,只支持真機(jī)矩距。
基本概念
坐標(biāo)系
這里的坐標(biāo)系先不講坐標(biāo)空間,只是最基礎(chǔ)的頂點(diǎn)坐標(biāo)和紋理坐標(biāo)畔濒。
跟平時(shí)我們開(kāi)發(fā)寫(xiě)UI以左上角為原點(diǎn)不一樣剩晴,Metal的頂點(diǎn)坐標(biāo)系跟openGL一樣,是以屏幕中心為原點(diǎn),歸一化的坐標(biāo)系赞弥。
四維均勻向量 ( x毅整,y,z绽左,w) 指定一個(gè)三維點(diǎn)剪輯空間坐標(biāo)悼嫉。頂點(diǎn)著色器在剪輯空間坐標(biāo)中生成位置。Metal分 x 拼窥,y戏蔑,和z值由w將剪輯空間坐標(biāo)轉(zhuǎn)換為標(biāo)準(zhǔn)化設(shè)備坐標(biāo)左下角位于( x,y)的坐標(biāo)(- 1.0鲁纠,-1.0) 而上角在(1.0,1.0) 总棵。
如果只是繪制單色形狀的話,只用頂點(diǎn)坐標(biāo)然后填充顏色就行改含,但如果要繪制圖片情龄,或者給形狀貼上紋理,就需要用到紋理坐標(biāo)系捍壤。在metal中骤视,紋理的原點(diǎn)坐標(biāo)在左上角,這和openGL是不同的(OpenGL的紋理原點(diǎn)坐標(biāo)在左下角)
著色器
metal的著色器有主要頂點(diǎn)著色器鹃觉、片元著色器专酗、內(nèi)核計(jì)算函數(shù)
vertex: 表示該函數(shù)是一個(gè)頂點(diǎn)著色函數(shù),它將為頂點(diǎn)數(shù)據(jù)流中的每一個(gè)頂點(diǎn)執(zhí)行一次然后為每一個(gè)頂點(diǎn)生成數(shù)據(jù)輸出到繪制管線盗扇。
示例代碼:
vertex Vertex vertex_func(constant Vertex *vertices [[buffer(0)]],uint vid [[vertex_id]]){
return vertices[vid];
}
fragment: 表示該函數(shù)是一個(gè)片元函數(shù)祷肯,它將為片元數(shù)據(jù)流中的每一個(gè)片元和其關(guān)聯(lián)執(zhí)行一次然后將每一個(gè)片元的顏色數(shù)據(jù)輸出到繪制管線中。
示例代碼:
fragment float4 fragment_func(Vertex vert [[stage_in]]){
return float4(1.0, 1.0, 0.0, 1.0);
}
kernel:表示該函數(shù)是一個(gè)并行計(jì)算著色函數(shù)粱玲,它可以被分配在一維/二維/三維線程中去執(zhí)行躬柬。
示例代碼:
kernel void blend(texture2d<float, access::read> imageTexture [[texture(0)]],
texture2d<float, access::read> faceTexture [[texture(1)]],
texture2d<float, access::write> blendTexture [[texture(2)]],
uint2 gid [[thread_position_in_grid]]) {
float width = faceTexture.get_width();
float height = faceTexture.get_height();
if ((gid.x >= width) || (gid.y >= height)) {
return;
}
float4 face = faceTexture.read(gid);
if(face.a > 0.0){
blendTexture.write(face, gid);
}
else {
uint2 pos = uint2(gid.x, gid.y);
float4 image = imageTexture.read(pos);
blendTexture.write(image, gid);
}
}
著色器的基礎(chǔ)語(yǔ)法規(guī)范可以參考蘋(píng)果官方文檔拜轨,文檔比較復(fù)雜抽减,還是全英文的,中文翻譯可以參考這個(gè)專欄
Metal工作流程
Metal渲染管道流程
在OpenGLES中橄碾,圖元裝配有9種卵沉,在Metal中,圖元裝配只有五種法牲,他們分別是:
MTLPrimitiveTypePoint = 0, 點(diǎn)
MTLPrimitiveTypeLine = 1, 線段
MTLPrimitiveTypeLineStrip = 2, 線環(huán)
MTLPrimitiveTypeTriangle = 3, 三角形
MTLPrimitiveTypeTriangleStrip = 4, 三角型扇
metal的驅(qū)動(dòng)GPU進(jìn)行繪制的流程如圖所示史汗,下面進(jìn)行一些參數(shù)名詞的解釋
基礎(chǔ)名詞解釋
MTLDevice
可以理解為GPU對(duì)象,可以用如下方法獲得:
let device = MTLCreateSystemDefaultDevice()
guard device != nil else {
print("Metal is not supported on this device")
return
}
上面說(shuō)過(guò)只有iOS8以及A7芯片以上才支持Metal拒垃,所以MTLDevice可以為空停撞,需要判斷
MTLCommandQueue
有了GPU之后,需要?jiǎng)?chuàng)建一個(gè)渲染隊(duì)列MTLCommandQueue,隊(duì)列是單一隊(duì)列戈毒,確保了指令能夠按順序執(zhí)行艰猬,里面的對(duì)象是需要渲染的指令MTLCommandBuffer,可以支持多個(gè)CommandBuffer同時(shí)編碼埋市。通過(guò)MTLDevice可以獲取MTLCommandQueue:
let queue:MTLCommandQueue = device?.makeCommandQueue()
MTKView
承接Metal繪制的視圖冠桃,初始化方法為let viewiew = MTKView.init(frame: self.bounds, device: MTLCreateSystemDefaultDevice)
MTKView有兩個(gè)delegate:
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size;
- (void)drawInMTKView:(nonnull MTKView *)view;
drawInMTKView:方法是MetalKit每幀的渲染回調(diào),可以在內(nèi)部做渲染的處理
drawableSizeWillChange:方法是繪制區(qū)域發(fā)生改變的方法道宅,在里面可以給繪圖區(qū)域大小重新賦值
在demo中沒(méi)有使用這兩個(gè)delegate食听,而是直接重寫(xiě)了MTKView的draw方法,也能實(shí)現(xiàn)同樣的效果
不用MTKView也可以使用CAMetalLayer污茵,添加到當(dāng)前view的layer上樱报。
剩下的一些參數(shù)會(huì)通過(guò)一個(gè)畫(huà)五角星例子具體說(shuō)明。
Metal繪制步驟
1. 新建MTKView
可以用let viewiew = MTKView.init(frame: self.bounds, device: MTLCreateSystemDefaultDevice)初始化MTKView泞当,demo中是使用storyBoard拖入使用的
2. 設(shè)置頂點(diǎn)數(shù)據(jù)
demo是畫(huà)一個(gè)三角形肃弟、五角星,三角形的話需要提供三個(gè)頂點(diǎn)的坐標(biāo)零蓉,五角星的話笤受,由于Metal繪制都是通過(guò)畫(huà)三角形去繪制,所以敌蜂,需要繪制下圖所示的十個(gè)三角形箩兽,加上中心點(diǎn)總計(jì)10個(gè)頂點(diǎn)數(shù)據(jù)
func setFiveAngleData() {
vertexData = [0.0, 0.0, 0.0, 1.0,
0.0, BIG_R*Y_SCALE, 0.0, 1.0,
SMALL_R*cos(54*RAD), SMALL_R*sin(54*RAD)*Y_SCALE, 0.0, 1.0,
BIG_R*cos(18*RAD), BIG_R*sin(18*RAD)*Y_SCALE, 0.0, 1.0,
SMALL_R*cos(18*RAD), -SMALL_R*sin(18*RAD)*Y_SCALE, 0.0, 1.0,
BIG_R*cos(54*RAD), -BIG_R*sin(54*RAD)*Y_SCALE, 0.0, 1.0,
0.0, -SMALL_R*Y_SCALE, 0.0, 1.0,
-BIG_R*cos(54*RAD), -BIG_R*sin(54*RAD)*Y_SCALE, 0.0, 1.0,
-SMALL_R*cos(18*RAD), -SMALL_R*sin(18*RAD)*Y_SCALE, 0.0, 1.0,
-BIG_R*cos(18*RAD), BIG_R*sin(18*RAD)*Y_SCALE, 0.0, 1.0,
-SMALL_R*cos(54*RAD), SMALL_R*sin(54*RAD)*Y_SCALE, 0.0, 1.0]
indexData = [0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4 ,5, 0, 5, 6, 0, 6, 7, 0, 7, 8, 0, 8, 9, 0, 9 , 10, 0, 10, 1]
}
其中,vertexData是頂點(diǎn)數(shù)據(jù)章喉,四個(gè)代表一個(gè)點(diǎn)(x,y,z,w)汗贫;indexData是索引數(shù)據(jù),三個(gè)為一組秸脱,代表頂點(diǎn)組成三角形的順序
3. 設(shè)置紋理數(shù)據(jù)
將一張UIImage渲染到MKTView上落包,需要用到紋理數(shù)據(jù)MTLTexture,可以由以下方法獲忍健:
func getTexture() {
do {
self.imageTexture = try MTKTextureLoader(device: self.device).newTexture(cgImage: image.cgImage!, options: [MTKTextureLoader.Option.SRGB:false])
} catch {
assertionFailure("Could not create Texture - \(error) ")
}
render(texture: imageTexture)
}
然后需要將紋理坐標(biāo)(二維)加入到頂點(diǎn)坐標(biāo)中
4. 設(shè)置渲染管道
func render() {
let library = device?.makeDefaultLibrary()!
let vertex_func = library?.makeFunction(name: "vertex_func")
let frag_func = library?.makeFunction(name: "fragment_func")
let rpld = MTLRenderPipelineDescriptor()
rpld.vertexFunction = vertex_func
rpld.fragmentFunction = frag_func
rpld.colorAttachments[0].pixelFormat = .bgra8Unorm
do{
try rps = device?.makeRenderPipelineState(descriptor: rpld)
}catch let error{
fatalError("\(error)")
}
}
其中咐蝇,MTLRenderPipelineDescriptor是渲染管道的描述符,可以設(shè)置頂點(diǎn)處理函數(shù)巷查、片元處理函數(shù)有序、輸出顏色格式等;
5. 具體渲染過(guò)程
override func draw(_ rect: CGRect) {
if let drawable = currentDrawable, let rpd = currentRenderPassDescriptor {
let dataSize = vertexData!.count * MemoryLayout<Float>.size
// 設(shè)置頂點(diǎn)buffer
vertexBuffer = device?.makeBuffer(bytes: vertexData!, length: dataSize, options: [])
// 設(shè)置索引buffer
indexBuffer = device?.makeBuffer(bytes: indexData!, length: MemoryLayout<UInt16>.size * indexData!.count , options: [])
// 設(shè)置背景色為紅色
rpd.colorAttachments[0].clearColor = MTLClearColorMake(1.0, 0.0, 0.0, 1.0)
// 創(chuàng)建commandBuffer
let commandBuffer = commandQueue!.makeCommandBuffer()
let commandEncode = commandBuffer?.makeRenderCommandEncoder(descriptor: rpd)
commandEncode?.setRenderPipelineState(rps!)
// 設(shè)置頂點(diǎn)緩存
commandEncode?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
// 繪制
commandEncode?.drawIndexedPrimitives(type: .triangle, indexCount: indexBuffer!.length / MemoryLayout<UInt16>.size, indexType: MTLIndexType.uint16, indexBuffer: indexBuffer!, indexBufferOffset: 0)
// 結(jié)束設(shè)置
commandEncode?.endEncoding()
// 顯示繪制內(nèi)容
commandBuffer?.present(drawable)
// 提交命令編碼器
commandBuffer?.commit()
}
}
繪制的第一步是從commandQueue里面創(chuàng)建commandBuffer岛请,commandQueue是整個(gè)app繪制的隊(duì)列旭寿,commandBuffer存放每次渲染的指令,commandQueue內(nèi)部存在著多個(gè)commandBuffer崇败,CommandEncoder是命令編碼器
6. Shader處理
#include <metal_stdlib>
using namespace metal;
struct Vertex {
float4 position [[position]];
};
vertex Vertex vertex_func(constant Vertex *vertices [[buffer(0)]],uint vid [[vertex_id]]){
return vertices[vid];
}
fragment float4 fragment_func(Vertex vert [[stage_in]]){
return float4(1.0, 1.0, 0.0, 1.0);
}
Shader的語(yǔ)法與C++類似盅称,參數(shù)名前面的是類型,后面的[[ ]]是描述符。
其中vertex函數(shù)是讀取頂點(diǎn)信息缩膝,fragment函數(shù)是進(jìn)行顏色填充處理搭幻,這里填充的是黃色。
運(yùn)行出來(lái)的效果:
demo地址
Metal圖片處理
上面第一個(gè)例子通過(guò)一個(gè)五角星講了最基礎(chǔ)的Metal用法逞盆,接下來(lái)會(huì)用另一個(gè)例子來(lái)講一下圖片的一些處理效果大咱,是將一張平面圖片轉(zhuǎn)化為三維圖片歌溉。
實(shí)現(xiàn)思路
二維圖片頂點(diǎn)坐標(biāo)z軸的值都為0凿叠,要變成三維程腹,只需要z軸不為0即可,因此可以在將圖片顯示到MTKView上后舅逸,處理頂點(diǎn)著色器桌肴,給每個(gè)頂點(diǎn)的z軸都賦上相應(yīng)的值,這里可以用每個(gè)像素點(diǎn)的RGB轉(zhuǎn)YUV的算法琉历,取每個(gè)像素的亮度Y的值坠七,作為z方向上的深度值,具體轉(zhuǎn)化為:
inVertex.position.z = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b
如果只給z軸不為0的值旗笔,圖片雖然是三維的彪置,但是依舊只能看到正面的部分,z軸的深度部分是看不到的蝇恶,所以還需要給圖片加入相應(yīng)的空間矩陣變換,會(huì)用到三種矩陣MVP:分別是模型矩陣(model)拳魁、觀察矩陣(view)、投影矩陣(Projection):
投影矩陣:GLKMatrix4MakePerspective
透視投影是模仿人眼觀察物體撮弧,有遠(yuǎn)小近大的效果潘懊,所以這種投影更加真實(shí)
由于Metal并沒(méi)有對(duì)應(yīng)的矩陣運(yùn)算的框架(不知道到底有沒(méi)有,我沒(méi)找到)贿衍,所以這里采用的是GLKit框架里面的矩陣授舟,下同理。
來(lái)看一下這個(gè)投影矩陣的參數(shù)贸辈,是個(gè)4X4矩陣
public func GLKMatrix4MakePerspective(_ fovyRadians: Float, _ aspect: Float, _ nearZ: Float, _ farZ: Float) -> GLKMatrix4
其中參數(shù)fovyRadians定義視野在Y-Z平面的角度释树,范圍是[0.0,180.0];參數(shù)aspect是投影平面寬度與高度的比率裙椭;參數(shù)nearZ和farZ分別是近遠(yuǎn)裁剪面到視點(diǎn)(沿Z負(fù)軸)的距離躏哩,它們總為正值。
觀察矩陣:GLKMatrix4MakeLookAt
來(lái)看一下這個(gè)觀察矩陣的參數(shù)揉燃,是個(gè)4X4矩陣
public func GLKMatrix4MakeLookAt(_ eyeX: Float, _ eyeY: Float, _ eyeZ: Float, _ centerX: Float, _ centerY: Float, _ centerZ: Float, _ upX: Float, _ upY: Float, _ upZ: Float) -> GLKMatrix4
這個(gè)矩陣模擬了人眼或者攝像機(jī)在空間的一些位置參數(shù),設(shè)置這9個(gè)參數(shù)以控制攝像機(jī)從不同的角度觀察物體:
eyeX, eyeY, eyeZ定義攝像機(jī)的位置筋栋;
centerX, centerY, centerZ攝像機(jī)看向的點(diǎn)炊汤;
相機(jī)還可以旋轉(zhuǎn)360,upX, upY, upZ三個(gè)參數(shù)確定相機(jī)向上的朝向。
模型矩陣
模型矩陣就設(shè)為單位矩陣GLKMatrix4Identity抢腐,這里可以給他加上旋轉(zhuǎn)變換矩陣GLKMatrix4Rotate姑曙,給模型矩陣加上旋轉(zhuǎn)矩陣是讓物體自己動(dòng),如果修改上面觀察矩陣的一些參數(shù)迈倍,就是攝像機(jī)或人眼圍繞著物體在動(dòng)伤靠,無(wú)論哪一種方法都能模擬出物體旋轉(zhuǎn)的效果,這里選擇的是物體自己動(dòng)啼染,也就是在模型矩陣上加入旋轉(zhuǎn)變化宴合。
然后將mvp直接相乘,結(jié)果再與頂點(diǎn)坐標(biāo)相乘迹鹅。注意相乘的順序先進(jìn)行模型矩陣變換卦洽,再是觀察矩陣,最后是投影矩陣變換斜棚,所以應(yīng)為
P * V * M * vertex.position阀蒂。
繪制
基本流程跟上面說(shuō)的大體一致,但是需要額外設(shè)置一些東西
func draw(renderEncoder: MTLRenderCommandEncoder, texture: MTLTexture, type: MTLPrimitiveType) {
self.uniforms = Uniforms.init()
self.vertexData = buildPointData()
self.indexData = buildIndexData()
// 設(shè)置頂點(diǎn)和索引buffer
let vertexBufferSize = MemoryLayout<Float>.stride * self.vertexData.count
let indexBufferSize = MemoryLayout<UInt32>.stride * self.indexData.count
let vertexBuffer = device.makeBuffer(bytes: self.vertexData, length: vertexBufferSize, options: MTLResourceOptions.cpuCacheModeWriteCombined)
let indexBuffer = device.makeBuffer(bytes: self.indexData, length: indexBufferSize , options: MTLResourceOptions.cpuCacheModeWriteCombined)
// 設(shè)置MVP矩陣及其buffer
let aspect = self.bounds.width / self.bounds.height
// 這里將pinch手勢(shì)的縮放參數(shù)傳入投影矩陣弟蚀,就能進(jìn)行縮放
var GLKPerspective = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(degree), Float(aspect), 0.1, 10.0)
var GLKView = GLKMatrix4MakeLookAt(0.0, 0.0, 2.0, 0, 0, 0.0, 0.0, 1.0, 0.0)
var GLKModel = GLKMatrix4Identity
// 這里將pan手勢(shì)的滑動(dòng)距離參數(shù)經(jīng)過(guò)調(diào)整后傳到旋轉(zhuǎn)矩陣中蚤霞,就能旋轉(zhuǎn)滑動(dòng)了
GLKModel = GLKMatrix4Rotate(GLKModel, centerX, 1, 0, 0)
GLKModel = GLKMatrix4Rotate(GLKModel, centerY, 0, 1, 0)
let perspectiveBuffer = device.makeBuffer(bytes: &GLKPerspective, length: MemoryLayout<float4x4>.size, options: .cpuCacheModeWriteCombined)
let viewBuffer = device.makeBuffer(bytes: &GLKView, length: MemoryLayout<float4x4>.size, options: .cpuCacheModeWriteCombined)
let modelBuffer = device.makeBuffer(bytes: &GLKModel, length: MemoryLayout<float4x4>.size, options: .cpuCacheModeWriteCombined)
// 設(shè)置頂點(diǎn)著色器的緩沖區(qū),index要對(duì)應(yīng)shader
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder.setVertexBuffer(modelBuffer, offset: 0, index: 1)
renderEncoder.setVertexBuffer(viewBuffer, offset: 0, index: 2)
renderEncoder.setVertexBuffer(perspectiveBuffer, offset: 0, index: 3)
let uniformBuffer = device.makeBuffer(bytes: self.uniforms.data(), length: Uniforms.sizeInBytes(), options: MTLResourceOptions.cpuCacheModeWriteCombined)
renderEncoder.setVertexBuffer(uniformBuffer, offset: 0, index: 4)
// 設(shè)置紋理緩沖區(qū)
renderEncoder.setFragmentTexture(texture, index: 0)
// 設(shè)置頂點(diǎn)紋理义钉,因?yàn)轫旤c(diǎn)的z軸數(shù)據(jù)需要獲取亮度值争便,所以需要把紋理傳到頂點(diǎn)著色器中
renderEncoder.setVertexTexture(texture, index: 0)
// 圖元裝配
if type == .point {
renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: self.vertexData.count / 5)
} else if type == .triangle {
renderEncoder.drawIndexedPrimitives(type: .triangle, indexCount: self.indexData.count, indexType: MTLIndexType.uint32, indexBuffer: indexBuffer!, indexBufferOffset: 0)
} else {
renderEncoder.drawPrimitives(type: .lineStrip, vertexStart: 0, vertexCount: self.vertexData.count / 5)
}
}
著色器shader:
vertex VertexOut vertex_func(uint vid [[vertex_id]],
texture2d<float> diffuse [[texture(0)]],
// 對(duì)應(yīng)緩沖區(qū)的下標(biāo)
const device VertexIn* vertexIn [[buffer(0)]],
const device float4x4& model [[buffer(1)]],
const device float4x4& view [[buffer(2)]],
const device float4x4& perspective [[buffer(3)]],
const device Uniforms& uniforms [[buffer(4)]])
{
VertexOut outVertex;
VertexIn inVertex = vertexIn[vid];
float4 color = diffuse.sample(s, inVertex.uv);
// 亮度作為z軸深度值
inVertex.position.z = 0.3 * (0.299 * color.r + 0.587 * color.g + 0.114 * color.b);
outVertex.uv = inVertex.uv;
// MVP矩陣相乘
outVertex.position = perspective * view * model * float4(inVertex.position);
outVertex.pointSize = uniforms.pointSizeInPixel;
return outVertex;
};
fragment float4 fragment_func(VertexOut infrag [[stage_in]], texture2d<float> diffuse [[texture(0)]]) {
// 紋理采樣
float4 imageColor = diffuse.sample(s, infrag.uv);
return imageColor;
};
運(yùn)行效果