Metal
Swift
swift4

Metal入門: プロジェクトはmacOSから始めるのがオススメ

クロスプラットフォームでもあるOpenGLも存在するなかで、プロプライエタリ🔒でAppleの端末でしか動作しないMetal🔩を学習しなければならないことに意欲を持てなかった人も多かったことでしょう。

iOS 12📱とmacOS Mojave🖥でOpenGL非推奨を発表したことで、Metal🔩に対する注目度が高まりました。OpenGLが非推奨化されたことで、将来的にMetalに移行していかねばならない・・・と不安に思っている人もいるのではないでしょうか😩。

そこでMetal入門と題して、超簡単な説明をしてみます。

※Swift言語の入門には記事「10分で試せる! Swiftを使った初めてのiOSアプリ開発入門 - ICS MEDIA」を参照ください。
※この記事はmacOS 10.13.5 High Sierra, Xcode 9.4で解説しています。

Metalの入門はmacOSから始めるのがオススメ

初心者殺しのMetalサンプル

世の中のMetalのサンプルはiOS向けに提供されているものが多いです。これがまず初心者殺し💀。

iOS端末におけるMetalの利点は、CPUとGPUが同じプロセッサ内に存在するという機構の利点を活かせること。具体的には、CPUとGPUでメモリの共有ができること🤝。

しかし、デメリットとしてiOSシミュレーターではMetalが動作しないため、実機に転送しないと試せません🚫。開発中のちょっとした確認でも、iPhone実機に転送しないと駄目🙅‍♀️

XcodeからiPhone実機に転送しようとすると、開発設定をすませてプロビジョニングうんたらを用意しなけばなりません。これは少し試したい人を振るいにかける儀式💃🏿。Apple Developer Centerの手続きを頑張ってしなければなりません。ここでやる気をなくす人が大半だと思います🧟‍♂️

macOSプロジェクトだと面倒な設定いらずで、macですぐに試せる

Xcodeでコードを書くのはmacOSだと思いますが、やはり自身のmacでそのまま実行結果を確認できるのが最適。macOSプロジェクトだとそのままMetalが動作します💡

Xcodeでの始め方

新規プロジェクトでは、[macOS]→[Cocoa App]を選択します。

image.png

使いもしない[Unit Tests]と[UI Tests]のチェックは外しておきます。

image.png

コードを書く

Metalで三角形を描いてみましょう。ViewController.swift ファイルに次のコードを書きます。ただの三角形とはいえ、めちゃくちゃコードは長くなります。

ViewController.swift
import Cocoa
import MetalKit

class ViewController: NSViewController {

    private let device = MTLCreateSystemDefaultDevice()!

    private let positionData: [Float] = [
        +0.00, +0.75, 0, +1,
        +0.75, -0.75, 0, +1,
        -0.75, -0.75, 0, +1
    ]

    private let colorData: [Float] = [
        1, 1, 1, 1,
        0, 1, 0, 1,
        0, 1, 1, 1,
    ]

    private var commandQueue: MTLCommandQueue!
    private var renderPassDescriptor: MTLRenderPassDescriptor!
    private var bufferPosition: MTLBuffer!
    private var bufferColor: MTLBuffer!
    private var renderPipelineState: MTLRenderPipelineState!
    private var metalLayer: CAMetalLayer!;

    override func loadView() {
        view = NSView(frame: NSRect(x: 0, y: 0, width: 960, height: 540))
        view.layer = CALayer()
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // ビューを初期化
        initLayer();

        // Metalのセットアップ
        setupMetal()
        // バッファーを作成
        makeBuffers()
        // パイプラインを作成
        makePipeline()
        // 描画
        draw();
    }

    private func initLayer(){
        // レイヤーを作成
        metalLayer = CAMetalLayer()
        metalLayer.device = device
        metalLayer.pixelFormat = .bgra8Unorm
        metalLayer.framebufferOnly = true
        metalLayer.frame = view.layer!.frame
        view.layer!.addSublayer(metalLayer)
    }

    private func setupMetal() {
        // MTLCommandQueueを初期化
        commandQueue = device.makeCommandQueue()

        renderPassDescriptor = MTLRenderPassDescriptor()
        // このRender Passが実行されるときの挙動を設定
        renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadAction.clear
        renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreAction.store
        // 背景色は黒にする
        renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0)
    }

    private func makeBuffers() {
        let size = positionData.count * MemoryLayout<Float>.size
        // 位置情報のバッファーを作成
        bufferPosition = device.makeBuffer(bytes: positionData, length: size)
        // 色情報のバッファーを作成
        bufferColor = device.makeBuffer(bytes: colorData, length: size)
    }

    private func makePipeline() {
        guard let library = device.makeDefaultLibrary() else {fatalError()}
        let descriptor = MTLRenderPipelineDescriptor()
        descriptor.vertexFunction = library.makeFunction(name: "myVertexShader")
        descriptor.fragmentFunction = library.makeFunction(name: "myFragmentShader")
        descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
        // レンダーパイプラインステートを作成
        renderPipelineState = try! device.makeRenderPipelineState(descriptor: descriptor)
    }

    func draw() {
        // ドローアブルを取得
        guard let drawable = metalLayer.nextDrawable() else {fatalError()}
        renderPassDescriptor.colorAttachments[0].texture = drawable.texture
        // コマンドバッファを作成
        guard let cBuffer = commandQueue.makeCommandBuffer() else {fatalError()}
        // エンコーダ生成
        let encoder = cBuffer.makeRenderCommandEncoder(
            descriptor: renderPassDescriptor
        )!
        encoder.setRenderPipelineState(renderPipelineState)
        // バッファーを頂点シェーダーに送る
        encoder.setVertexBuffer(bufferPosition, offset: 0, index: 0)
        encoder.setVertexBuffer(bufferColor, offset: 0, index:1)
        // 三角形を作成
        encoder.drawPrimitives(type: MTLPrimitiveType.triangle,
                               vertexStart: 0,
                               vertexCount: 3)
        // エンコード完了
        encoder.endEncoding()
        // 表示するドローアブルを登録
        cBuffer.present(drawable)
        // コマンドバッファをコミット(エンキュー)
        cBuffer.commit()
    }
}

次にMetalファイルを用意します。名前は適当でいいのですが、Heavy.metal🎸とでもしておきましょうか。適当につけたファイル名でも自動的にバンドルされます。

Heavy.metal
#include <metal_stdlib>

using namespace metal;

// 構造体を定義
struct MyVertex {
    // 座標
    float4 position [[position]];
    // 色
    float4 color;
};

// 頂点シェーダー
vertex MyVertex myVertexShader(device float4 *position [[ buffer(0) ]],
                               device float4 *color [[ buffer(1) ]],
                               uint vid [[vertex_id]]) {
    MyVertex v;
    // 0番目のバッファーから頂点座標を設定
    v.position = position[vid];
    // 1番目のバッファーから頂点に色を設定
    v.color = color[vid];
    return v;
}

// 断片シェーダー
fragment float4 myFragmentShader(MyVertex vertexIn [[stage_in]]) {
    // 塗りの色を指定
    return vertexIn.color;
}

いざ実行

再生ボタン▶️をクリックします。

image.png

すると、Windowがたちあがり、画面内に三角形🔺が描画されます。
(ここまで実現するのに苦労しました…😹)

image.png

初回の入門はここまで。

余談

MetalはWebGPUの仕様のベースとして提案されているみたいです。