Metal超入門!!背景色を反映させる

はじめに

最近Metalについて勉強をしているんですが、シェーダのHello Worldは三角形を描画することだといろんなところで言われているのを目にします。

ですが三角形を描画するのはかなり大変です。MTKViewに背景色を反映させるのにもそれなりに手間がかかるので今回はMTKViewの背景色を変更する手順についてまとめていきます。

本来であれば以下の画像のように処理を進めていく必要があります。

この記事では以下の物を実装します。

赤く塗りつぶされている部分は本記事では実装しません。

Metalの処理を内包するCustomViewを作成する

MTKViewの背景色はMTLClearColorで設定することができます。ですが、このままMTLClearColorを設定しただとMetalViewをStoryBoardに配置しても色は反映されず白いままです。

import UIKit
import MetalKit

class MetalView: UIView {

    @IBOutlet weak var metalView: MTKView! {
        didSet {
            metalView.delegate = self
            metalView.clearColor = MTLClearColor(red: 0.0, green: 1.0, blue: 1.0, alpha: 1.0)
        }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        loadNib()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        loadNib()
    }
    
    private func loadNib() {
        if let view = Bundle.main.loadNibNamed(String(describing: type(of: self)), owner: self)?.first as? UIView {
            view.frame = self.bounds
            self.addSubview(view)
        }
    }

}

extension MetalView: MTKViewDelegate {
    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
        
    }
    
    func draw(in view: MTKView) {
        
    }    
}

デフォルトデバイスの取得

まず最初にデバイスを取得する必要があります。(どのGPUを使うか決めるため)Macだといろんなデバイスがつながっていることがあるのでデバイスリストの中から選んだりするようなのですが、iPhone, iPadを対象としているのでデフォルトのデバイスをMetalから取得するだけでOKです。

デフォルトのデバイスはMTLCreateSystemDefaultDevice()取得することができます。

metalView.device = MTLCreateSystemDefaultDevice()

コマンドキューを作成する

まずコマンドキューとは何者かという話なんですが、GPU上で実行する命令を積んでいく命令の器です。積まれているキューから順次処理されていきます。

GPUが持っているものなのでデバイスのmakeCommandQueue()メソッドから作成します。

if let device = metalView.device {
   commandQueue = device.makeCommandQueue()
}

コマンドバッファを作成する

次に描画処理に必要なコマンドバッファを作成します。コマンドバッファは送信するコマンドの内容や情報を格納するバッファです。

描画するたびに作って送信するため描画処理を行っているところで作成します

func draw(in view: MTKView) {
    guard let commandBuffer = commandQueue?.makeCommandBuffer() else {
        return
    }
}

描画処理のコマンドエンコーダを作成する

GPUに送信された描画コマンドの実行結果はテクスチャと呼ばれるメモリバッファに書き込まれます。テクスチャはGPUから読み書き可能なメモリバッファでイメージデータを格納します。

またGPUに送信される一連の描画コマンドのことをレンダーパス呼びます。

guard let renderPassDescriptor = metalView.currentRenderPassDescriptor else {
    return
}
guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
    return
}

encoder.endEncoding()

renderPassDescriptorという謎のキャラクターが出てきていますが、こいつはMTKViewの持っている描画コマンドを参照するための目印みたいなやつです。デスクリプタに関しては以下の記事がわかりやすかったです。

ファイルディスクリプタ (FD)

そしてrenderPassDescriptorからエンコーダを作成してアプリ側が発行する描画コマンドをエンコードしてGPUに送ります。

私はエンコードに関してもイメージがつかなかったのですが以下の画像がわかりやすかったです。

エンコードとは?動画を扱うなら知っておきたいエンコードの基本について。

テクスチャをビューに表示する

テクスチャに書き込まれたイメージは自動的にビューに表示されるわけではないのでpresent()メソッドを実行する必要があります。

このメソッドを呼び出す必要があるのはコマンドバッファ実行完了直後です。

if let drawable = view.currentDrawable {
    commandBuffer.present(drawable)
}

コマンドバッファを実行する

最後にコマンドバッファをGPUに送信してコマンドバッファの内容を実行します。

コマンドバッファの内容を寿司んするにはMTLCommandBufferの次のメソッドを実行します。

commandBuffer.commit()

コード一覧

import UIKit
import MetalKit

class MetalView: UIView {
    
    @IBOutlet weak var metalView: MTKView! {
        didSet {
            metalView.delegate = self
            metalView.clearColor = MTLClearColor(red: 0.0, green: 1.0, blue: 1.0, alpha: 1.0)
            metalView.device = MTLCreateSystemDefaultDevice()
        }
    }
    
    private var commandQueue: MTLCommandQueue?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        loadNib()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        loadNib()
    }
    
    private func loadNib() {
        if let view = Bundle.main.loadNibNamed(String(describing: type(of: self)), owner: self)?.first as? UIView {
            view.frame = self.bounds
            self.addSubview(view)
        }
    }
    
    func setup(){
        if let device = metalView.device {
            commandQueue = device.makeCommandQueue()
        }
    }

}

extension MetalView: MTKViewDelegate {
    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
        
    }
    
    func draw(in view: MTKView) {
        guard let commandBuffer = commandQueue?.makeCommandBuffer() else {
            return
        }
        
        guard let renderPassDescriptor = metalView.currentRenderPassDescriptor else {
            return
        }
        
        guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
            return
        }
        
        encoder.endEncoding()
        
        if let drawable = view.currentDrawable {
            commandBuffer.present(drawable)
        }
        
        commandBuffer.commit()
    }
}

参考文献

ファイルディスクリプタ (FD)

基礎から学ぶ Metal〜MetalによるGPUプログラミング入門