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