Metal 练习:第五篇-MetalKit


Metal 练习:第五篇-MetalKit

此篇练习是基于前一篇 的拓展

此篇练习完成后,将会学到如何利用MetalKit框架,同时也要使用3D数学计算相关的smid框架

第一步:MetalKit

打开前一篇练习的工程,此篇练习还要另一个文件float4x4-Extension.swift

在WWDC2015上Apple提供了MetalKit作为使用Metal一个通道。这个框架提供工具减少了在Metal上运行应用程序编写大量的模板代码,主要提供3个功能

  1. 纹理加载:使用MTKTextureLoader轻松加载图片资源到Metal纹理
  2. 视图管理:通过MTKview减少大量代码使你的Metal渲染到屏幕上
  3. 模型I/O集成:高效加载模型资源进Metal缓存和用内置的容器管理大量数据

SIMD

SMID框架提供很多公共数据类型和函数来帮助处理向量和矩阵数学运算。前面我们用的是Objective-C的数据类型,本篇练习用Swift写,因此转到用本框架。现在删掉Matrix4.mMatrix4.hHelloMetalStart-Bridging-Header.h文件以及修改Build Settings中头文件的设置。
然后工程中全局搜索Matrix4float4x4代替。

Cmd + B一下,有很多错误,首先将import smid加入以下几个文件中BufferProvider.swiftviewController.swiftNode.swift

//  BufferProvider中nextUniformsBuffer(_:modelViewMatrix:light:)方法找到下面代码
memcpy(bufferPointer, modelViewMatrix.raw(), MemoryLayout.size * float4x4.numberOfElements())
memcpy(bufferPointer + MemoryLayout.size*float4x4.numberOfElements(), projectionMatrix.raw(), MemoryLayout.size * float4x4.numberOfElements())
memcpy(bufferPointer + 2 * MemoryLayout.size * float4x4.numberOfElements(), light.raw(), Light.size())
// 替换为
// 1. 现在矩阵是Swift的结构体,需要将它们改为可变的,然后通过引用传递
var projectionMatrix = projectionMatrix
var modelViewMatrix = modelViewMatrix  
// 2. 通达引用传递
memcpy(bufferPointer, &modelViewMatrix, MemoryLayout.size*float4x4.numberOfElements())
memcpy(bufferPointer + MemoryLayout.size*float4x4.numberOfElements(), &projectionMatrix, MemoryLayout.size*float4x4.numberOfElements())
memcpy(bufferPointer + 2*MemoryLayout.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)

  1. 从一个文件中加载图片
  2. 从图片中获取像素数据转为原始字节
  3. 要求MTLDevice创建一个空的纹理
  4. 拷贝字节数据到空的纹理
    很幸运,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

MTKViewUIView的子类,允许你快速连接到一个视图到一个渲染通道的输出,帮忙我实现了:

  1. 配置视图的layerCAMetalLayer
  2. 控制绘制调用的时间
  3. 快速管理一个 MTLRenderPassDescriptor
  4. 轻松处理大小调整

使用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)
  }
  
}