この記事はユニークビジョン株式会社 Advent Calendar 2018の23日目の記事です。
はじめに
最近Metalの勉強をはじめました。
Metalを使っていろいろとやろうと思うことはじめです。
Metalって響きがなんかかっこいい。。。
Metal入門・・・の前の基礎知識を一読するとこの記事は読みやすいと思います。
Metalとは
MetalはグラフィックスAPIのことでGPUにアクセスできるAPIです。
有名なものだとOpenGL、OpenCLなどがありますがそれらは汎用的に設計されているため、Apple製品本来のパフォーマンスが出せないってことでMetalがつくられました。
グラフィックスAPIと聞くと、ゲーム作るわけでもないから関係ないとか思う人も多いと思います(私はそうでした)。ですがCPUが不得手なこと、つまり並列演算をGPUに任せられるんですね。Metalは機械学習(CNN)向けのAPIもありますしメインはそこになってくると思います。(CoreMLも内部ではMetalを使用しています)
Metalを使う準備をする
まずはMetalを使うためにいろいろ初期化します。
import MetalKit
import UIKit
var mtkView: MTKView!
let device = MTLCreateSystemDefaultDevice()!
var commandQueue: MTLCommandQueue!
func setupMetal() {
commandQueue = device.makeCommandQueue()
mtkView.device = self.device
mtkView.delegate = self
}
extension ViewController: MTKViewDelegate {
func draw(in view: MTKView) {}
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}
}
draw(in view: MTKView)
は毎フレームごと呼ばれます。
今回は60fpsに設定しています
mtkView.preferredFramesPerSecond = 60
CIFilterでやってみる
CIFilterは優秀な子です。こちらによると201種類ものフィルターが公式で用意されていて、普通に画像処理をしたいだけなら十分だと思われます。
今回はブラーをかけてぼかしたいのでCIGaussianBlur
を使ってみます。
let filter = CIFilter(name: "CIGaussianBlur")!
filter.setValue(ciImage, forKey: kCIInputImageKey)
func processImage(_ image: CIImage, scale: Double) {
filter.setValue(scale, forKey: kCIInputScaleKey)
}
ブラーをかける準備が整いました。
これをMetalで描画します。
let context = CIContext()
let colorSpace = CGColorSpaceCreateDeviceRGB()
extension ViewController: MTKViewDelegate {
func draw(in view: MTKView) {
guard let drawable = view.currentDrawable else { return }
guard let commandBuffer = commandQueue.makeCommandBuffer() else { return }
guard let outputImage = self.filter.outputImage else { return }
defer {
commandBuffer.present(drawable)
commandBuffer.commit()
commandBuffer.waitUntilCompleted()
}
self.processImage(image, scale: scale)
self.context.render(
outputImage,
to: drawable.texture,
commandBuffer: commandBuffer,
bounds: outputImage.extent,
colorSpace: self.colorSpace
)
}
}
scaleは時間経過で増えていくように設定してあります
let startTime = Date().timeIntervalSinceReferenceDate
var scale: Double {
return Date().timeIntervalSinceReferenceDate - self.startTime
}
これだと実行時に
failed assertion `frameBufferOnly texture not supported for compute.'
とかいわれて死んだので以下を設定しておきます。(readonlyなのに何書き込もうとしてるんだよ的な理由だと思います)
mtkView.framebufferOnly = false
結果
ピクセルが散らばるので縁が黒くなっていきます。
トリミングすれば問題にはなりませんが手間がかかります。
(右下にずれていくのはリサイズ処理が甘いせいです...)
MPSでもっと簡単にできる
はい。MPSというとても便利なものがありました。
MetalPerformanceShadersの略です。
Optimize graphics and compute performance with kernels that are fine-tuned for the unique characteristics of each Metal GPU family.
とApple様はおっしゃっていて、各GPUに最適化され簡単かつ効率的に優れたパフォーマンスを発揮するそうです。
画像フィルター、ニューラルネットワーク、行列とベクトル、光線追跡といった機能があります。詳しくはこちら
MPS対応デバイス
MPSに対応しているデバイスはドキュメントによるとGPUファミリー2以降(iPhone6以降)をサポートしています。
GPUファミリーってなんぞやなのですがこちらの記事がわかりやすいです。
MPSSupportsMTLDevice(_ device: MTLDevice?) -> Bool
という関数も用意されているので実行時に判定することもできます。
MPSでやってみる
画像フィルターの中からMPSImageGaussianBlur
を使います。
まずは画像を読み込んでtextureを作成します。
func loadImage() {
let textureLoader = MTKTextureLoader(device: self.device)
self.texture = try! textureLoader.newTexture(
name: "imageName",
scaleFactor: view.contentScaleFactor,
bundle: nil
)
mtkView.colorPixelFormat = texture.pixelFormat
}
CIFilterでレンダリングしていた箇所を以下に書き換えます
import MetalPerformanceShaders
extension ImageProcessViewController: MTKViewDelegate {
func draw(in view: MTKView) {
// 中略
let blur = MPSImageGaussianBlur(device: self.device, sigma: Float(scale))
blur.encode(
commandBuffer: commandBuffer,
sourceTexture: self.texture,
destinationTexture: drawable.texture
)
}
}
これだけでOKです。
結果
とてもいい感じです(gifなので荒く見えてしまってます...)
まとめ
今回はMetalことはじめということで簡単な画像処理をやってみました。
本格的にやるのであればシェーダを書く必要があるのでC++もといMetalShadingLanguageを使いこなす必要があります。
日本語の記事はあまり多くないのですが、ドキュメントはけっこう豊富なので基本の動きを抑えておけば理解はしやすいです。
アプリにうまくMetalを組み込めたらもっと幅が広がりそうですね。
次は計算処理をやってみようと思います。
参考
Metal入門・・・の前の基礎知識
Metal入門 ←とてもわかりやすいです
MetalKitでGPUを使いこなす