Metal 練習:第五篇-MetalKit
此篇練習是基於前一篇 Metal 練習:第四篇-Lighting 的拓展
此篇練習完成后,將會學到如何利用
MetalKit框架,同時也要使用3D數學計算相關的smid框架
第一步:MetalKit
打開前一篇練習的工程Metal 練習:第四篇-Lighting,此篇練習還要另一個文件
float4x4-Extension.swift。
在WWDC2015上Apple提供了
MetalKit作為使用Metal一個通道。這個框架提供工具減少了在Metal上運行應用程序編寫大量的模板代碼,主要提供3個功能
- 紋理加載:使用
MTKTextureLoader輕松加載圖片資源到Metal紋理- 視圖管理:通過
MTKview減少大量代碼使你的Metal渲染到屏幕上- 模型I/O集成:高效加載模型資源進
Metal緩存和用內置的容器管理大量數據
SIMD
SMID框架提供很多公共數據類型和函數來幫助處理向量和矩陣數學運算。前面我們用的是Objective-C的數據類型,本篇練習用Swift寫,因此轉到用本框架。現在刪掉Matrix4.m、Matrix4.h、HelloMetalStart-Bridging-Header.h文件以及修改Build Settings中頭文件的設置。
然后工程中全局搜索Matrix4用float4x4代替。
Cmd + B一下,有很多錯誤,首先將import smid加入以下幾個文件中BufferProvider.swift、viewController.swift、Node.swift
// BufferProvider中nextUniformsBuffer(_:modelViewMatrix:light:)方法找到下面代碼
memcpy(bufferPointer, modelViewMatrix.raw(), MemoryLayout<Float>.size * float4x4.numberOfElements())
memcpy(bufferPointer + MemoryLayout<Float>.size*float4x4.numberOfElements(), projectionMatrix.raw(), MemoryLayout<Float>.size * float4x4.numberOfElements())
memcpy(bufferPointer + 2 * MemoryLayout<Float>.size * float4x4.numberOfElements(), light.raw(), Light.size())
// 替換為
// 1. 現在矩陣是Swift的結構體,需要將它們改為可變的,然后通過引用傳遞
var projectionMatrix = projectionMatrix
var modelViewMatrix = modelViewMatrix
// 2. 通達引用傳遞
memcpy(bufferPointer, &modelViewMatrix, MemoryLayout<Float>.size*float4x4.numberOfElements())
memcpy(bufferPointer + MemoryLayout<Float>.size*float4x4.numberOfElements(), &projectionMatrix, MemoryLayout<Float>.size*float4x4.numberOfElements())
memcpy(bufferPointer + 2*MemoryLayout<Float>.size*float4x4.numberOfElements(), light.raw(), Light.size())
// 在Node.swift中的render方法,將
let nodeModelMatrix = self.modelMatrix()
// 替換為
var nodeModelMatrix = self.modelMatrix()
// 在Node.swift中的modelMatrix方法,將
let matrix = float4x4()
// 替換為
var matrix = float4x4()
Run一下看看效果吧!!!
MetalKit 紋理加載
在查看
MetalKit提供的功能前,我們回顧下MetalTexture.swift文件中加載紋理的方法loadTexture(_ device: MTLDevice, commandQ: MTLCommandQueue, flip: bool)
- 從一個文件中加載圖片
- 從圖片中獲取像素數據轉為原始字節
- 要求
MTLDevice創建一個空的紋理- 拷貝字節數據到空的紋理
很幸運,MetalKit提供了強大的API幫助我們去加載紋理,這時我們主要使用的是MTKTextureLoader類。在我們轉向MetalKit時要將之前的MetalTexture.swift刪掉。
// 在Cubic.swift中 將 `import Metal` --> `import MetalKit`
// 將初始化方法
init(device: MTLDevice, commandQ: MTLCommandQueue) {
// 改成如下
init(device: MTLDevice, commandQ: MTLCommandQueue, textureLoader :MTKTextureLoader) {
// 將如下創建紋理的方法
let texture = MetalTexture(resourceName: "cube", ext: "png", mipmaped: true)
texture.loadTexture(device, commandQ: commandQ, flip: true)
super.init(name: "Cube", vertices: verticesArray, device: device, texture: texture.texture)
// 改成如下代碼
let path = Bundle.main.path(forResource: "cube", ofType: "png")!
let data = NSData(contentsOfFile: path) as! Data
let texture = try! textureLoader.newTexture(with: data, options: [MTKTextureLoaderOptionSRGB : (false as NSNumber)]
super.init(name: "Cube", vertices: verticesArray, device: device, texture: texture)
// 在ViewController.swift中 將 `import Metal` --> `import MetalKit`
// 在 MetalViewController 中添加屬性
var textureLoader: MTKTextureLoader!
// 然后在 viewDidLoad 方法中創建 device 的代碼下面加上
textureLoader = MTKTextureLoader(device: device)
// 在 ViewController類中創建 objectToDraw
objectToDraw = Cube(device: device, commandQ: commandQueue)
// 改成如下代碼
objectToDraw = Cube(device: device, commandQ: commandQueue, textureLoader: textureLoader)
MTKView
MTKView是UIView的子類,允許你快速連接到一個視圖到一個渲染通道的輸出,幫忙我實現了:
- 配置視圖的
layer為CAMetalLayer- 控制繪制調用的時間
- 快速管理一個
MTLRenderPassDescriptor- 輕松處理大小調整
使用
MTKView時,你可以實現代理或者子類化為視圖提供繪制更新,這里選擇第一種。首先要將視圖改成MTKView,在Main.Storyboard選擇控制器將視圖的類切換為MTKView。
將
MetalViewController類中,以下代碼可以刪掉
timer = CADisplayLink(target: self, selector: #selector(MetalViewController.newFrame(_:)))
timer.add(to: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)
metalLayer = CAMetalLayer()
metalLayer.device = device
metalLayer.pixelFormat = .bgra8Unorm
metalLayer.framebufferOnly = true
view.layer.addSublayer(metalLayer)
// 以及 newFrame(_:)、gameloop(_:)方法全部刪掉
添加MTKViewDelegate協議
// MARK: - MTKViewDelegate
extension MetalViewController: MTKViewDelegate {
// 1. 當MTKView大小調整時調用(這是是重置projectionMatrix時)
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
projectionMatrix = float4x4.makePerspectiveViewAngle(float4x4.degrees(toRad: 85.0),
aspectRatio: Float(self.view.bounds.size.width / self.view.bounds.size.height),
nearZ: 0.01, farZ: 100.0)
}
// 2. 當在view上繪制新的幀時
func draw(in view: MTKView) {
render(view.currentDrawable)
}
}
// 此處更改了render方法,將原先的render方法
func render() {
if let drawable = metalLayer.nextDrawable() {
self.metalViewControllerDelegate?.renderObjects(drawable)
}
}
// 替換為
func render(_ drawable: CAMetalDrawable?) {
guard let drawable = drawable else { return }
self.metalViewControllerDelegate?.renderObjects(drawable)
}
我們這里通過代理來響應大小的改變,因此可以將
viewDidLayoutSubviews方法刪掉
為了連接視圖的代理到控制器,在MetalViewController類屬性最下面添加如下代碼
@IBOutlet weak var mtkView: MTKView! {
didSet {
mtkView.delegate = self
mtkView.preferredFramesPerSecond = 60
mtkView.clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
}
}
添加完代碼后,要將
Main.Storyboard中控制的視圖與上面的代碼相連接,這樣才真正的擁有。
現在就是要給
MTKView的屬性device設置值
// 在 viewDidLoad 方法中
textureLoader = MTKTextureLoader(device: device)
// 下面添加
mtkView.device = device
最后,刪掉沒用的屬性
var metalLayer: CAMetalLayer! = nil
var timer: CADisplayLink! = nil
var lastFrameTimestamp: CFTimeInterval = 0.0
Well Done!
參考及更多資料
- 原文:iOS Metal Tutorial with Swift Part 5: Switching to MetalKit
- Apple’s Metal For Developers Page
- Apple’s Metal Programming Guide
- Apple’s Metal Shading Language Guide
- WWDC2014 For Metal
- WWDC2015 For Metal
- WWDC2016 For Metal
float4x4-Extension.swift代碼
import Foundation
import simd
import GLKit
extension float4x4 {
init() {
self = unsafeBitCast(GLKMatrix4Identity, to: float4x4.self)
}
static func makeScale(_ x: Float, _ y: Float, _ z: Float) -> float4x4 {
return unsafeBitCast(GLKMatrix4MakeScale(x, y, z), to: float4x4.self)
}
static func makeRotate(_ radians: Float, _ x: Float, _ y: Float, _ z: Float) -> float4x4 {
return unsafeBitCast(GLKMatrix4MakeRotation(radians, x, y, z), to: float4x4.self)
}
static func makeTranslation(_ x: Float, _ y: Float, _ z: Float) -> float4x4 {
return unsafeBitCast(GLKMatrix4MakeTranslation(x, y, z), to: float4x4.self)
}
static func makePerspectiveViewAngle(_ fovyRadians: Float, aspectRatio: Float, nearZ: Float, farZ: Float) -> float4x4 {
var q = unsafeBitCast(GLKMatrix4MakePerspective(fovyRadians, aspectRatio, nearZ, farZ), to: float4x4.self)
let zs = farZ / (nearZ - farZ)
q[2][2] = zs
q[3][2] = zs * nearZ
return q
}
static func makeFrustum(_ left: Float, _ right: Float, _ bottom: Float, _ top: Float, _ nearZ: Float, _ farZ: Float) -> float4x4 {
return unsafeBitCast(GLKMatrix4MakeFrustum(left, right, bottom, top, nearZ, farZ), to: float4x4.self)
}
static func makeOrtho(_ left: Float, _ right: Float, _ bottom: Float, _ top: Float, _ nearZ: Float, _ farZ: Float) -> float4x4 {
return unsafeBitCast(GLKMatrix4MakeOrtho(left, right, bottom, top, nearZ, farZ), to: float4x4.self)
}
static func makeLookAt(_ eyeX: Float, _ eyeY: Float, _ eyeZ: Float, _ centerX: Float, _ centerY: Float, _ centerZ: Float, _ upX: Float, _ upY: Float, _ upZ: Float) -> float4x4 {
return unsafeBitCast(GLKMatrix4MakeLookAt(eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ), to: float4x4.self)
}
mutating func scale(_ x: Float, y: Float, z: Float) {
self = self * float4x4.makeScale(x, y, z)
}
mutating func rotate(_ radians: Float, x: Float, y: Float, z: Float) {
self = float4x4.makeRotate(radians, x, y, z) * self
}
mutating func rotateAroundX(_ x: Float, y: Float, z: Float) {
var rotationM = float4x4.makeRotate(x, 1, 0, 0)
rotationM = rotationM * float4x4.makeRotate(y, 0, 1, 0)
rotationM = rotationM * float4x4.makeRotate(z, 0, 0, 1)
self = self * rotationM
}
mutating func translate(_ x: Float, y: Float, z: Float) {
self = self * float4x4.makeTranslation(x, y, z)
}
static func numberOfElements() -> Int {
return 16
}
static func degrees(toRad angle: Float) -> Float {
return Float(Double(angle) * Double.pi / 180)
}
mutating func multiplyLeft(_ matrix: float4x4) {
let glMatrix1 = unsafeBitCast(matrix, to: GLKMatrix4.self)
let glMatrix2 = unsafeBitCast(self, to: GLKMatrix4.self)
let result = GLKMatrix4Multiply(glMatrix1, glMatrix2)
self = unsafeBitCast(result, to: float4x4.self)
}
}
