#AppleのMetalサンプルをSwiftで書き直す:第二弾
前回( https://qiita.com/shunga/items/6b24c6f928038ab90c08 )に引き続きまさかの第二弾。今回もObjective-Cの奔放さとMetalのややこやしさに翻弄されドハマリしたものの、基本的な設計は変えずに移植に成功したので記事をまとめることにした。Metal初学者の助けになれれば幸い。
###環境
- macOS 10.14
- Xcode10.0
##"Hello Triangle"の概要
https://developer.apple.com/documentation/metal/hello_triangle
起動するとこのような三角形がウィンドウ中央に表示される。今回は特にアニメーション要素は無く、三角形が常に真ん中のいい感じの位置に表示される(その位置計算はシェーダー内で行っている)。どうやらこのような三角形を描画するのはシェーダー界隈の"Hello, World!"らしい。それにしてはやることが多い。本当に多い。
###構成
- 表示を担当するMTKView
- ViewController
- ビューに描画コマンドを送るレンダラー(デリゲート)
という構成は前回と変わらないが、今回はシェーダーというやつがいよいよ登場する。しかし全くノータッチで移植可能だったのでこの記事では触れない(実際はバグ解決のために1日かけてシェーダーをいじっていたが全く無関係であった)。
###共有ヘッダファイル
さて、このサンプルではレンダラー側(CPUプログラム:Swift)とシェーダー側(GPUプログラム:MetalShadingLanguage)とで一部の構造体と列挙型を共有するという設計になっている。AAPLRendererとAAPLShaderの両方がAAPLShaderTypes.hをincludeしており、同じ定義の構造体を使って頂点や色の情報をやりとりするというわけだ。小洒落ている。おかげで大層悩まされたが、この構造もそのまま移植して再現できた。
typedef enum AAPLVertexInputIndex
{
AAPLVertexInputIndexVertices = 0,
AAPLVertexInputIndexViewportSize = 1,
} AAPLVertexInputIndex;
///中略///
typedef struct
{
// Positions in pixel space
// (e.g. a value of 100 indicates 100 pixels from the center)
vector_float2 position;
// Floating-point RGBA colors
vector_float4 color;
} AAPLVertex;
###C/Objective-Cファイルを使うための下準備
AAPLShaderTypes.hはご覧の通り、そのままではSwiftでは扱えない。AAPLVer...と書いてもコード補完は働かないし、未定義だと怒ってくる。というわけで当初は同名の構造体定義を.swiftファイルに書いていた。しかし『詳解Swift』によれば、こういうときにブリッジング・ヘッダーというのが自動的に作られてC/Objective-Cのファイルが使えるようになると書いてある。しかし何度やっても作られないので調べたら自分で作る方法が見つかった。
ライブラリ利用時によく使う、ブリッジヘッダーの作り方【Swift低空飛行ガイド】 | Hayashi No Oto
https://hayashi-rin.net/post-178
これでめでたくSwift側でもAAPLVertex構造体が使えるようになった。
###ViewControllerの実装
ここは前回とほとんど変わらない。ただ、前回はStoryBoardで新しいCustom Viewを追加して使っていたが、最初からあるviewのclassをMTKViewと書き換えればそのまま使えることがわかったので、今回はそうすることにした。
###レンダラーの実装
var _device:MTLDevice!
var _pipelineState:MTLRenderPipelineState!
var _commandQueue:MTLCommandQueue!
var _viewportSize:vector_uint2 = vector_uint2(0,0)
_pipelineState
と_viewportSize
というプロパティが増えた。前者はレンダーパイプラインというやつに使い、後者は頂点座標の計算に使う。_viewportSizeは宣言と同時に初期値を入れてしまっているが、Objective-C版では単に宣言されただけでイニシャライザ内でも初期化している様子は全くなかった。んじゃどこで値をセットしていたのかというと、ウィンドウサイズが変わったときに呼び出されるデリゲートメソッド- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size
の中でやっていた。
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size
{
// Save the size of the drawable as we'll pass these
// values to our vertex shader when we draw
_viewportSize.x = size.width;
_viewportSize.y = size.height;
}
Objective-Cならnilが入ってるものをいじろうとしても普通に許されるけど、こういうことはSwiftだと許してくれずに実行時エラーをくらう。だもんでレンダラーのinit()の中で初期化しておこうかと思ったが、最初にnil以外の値さえ入っていれば問題ないので(0,0)とベタ打ちした。仮に_viewportSize
をOptionalで宣言したとしても、初期化前に入っているのは結局nilなのでnil.x
に値を入れようとして実行時エラーになる。
ちなみに、礼儀正しくイニシャライザを使うとこういう感じになる。
init(by mtkView:MTKView){
//中略
_viewportSize = vector_uint2(UInt32(mtkView.drawableSize.width), UInt32(mtkView.drawableSize.height))
//ViewControllerから貰ったmtkViewからビューのサイズを取得してwidthとheightをUint32に変換してからvector_uint2に変換して代入
}
変換につぐ変換で面倒すぎる。やってられるか。どうせすぐに現在のビューサイズから適切な値が割り当てられるんだから最初は(0,0)でいいだろもう。
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
// Save the size of the drawable as we'll pass these
// values to our vertex shader when we draw
_viewportSize.x = UInt32(size.width)
_viewportSize.y = UInt32(size.height)
}
この_viewportSize
、上でちょいと登場したがvector_uint2型というよくわからん型になっている。どうやらSIMDというものらしい。Metalとは直接関係ないものの、"Hello Triangle"のドキュメントでは「早いからオススメだよ」と書かれている。しかし型安全に厳しいSwiftだと、やはり代入するにもこのように一手間かかってしまう。本当に得なのだろうか。
init(by mtkView:MTKView){
_device = mtkView.device
let defaultLibrary = _device.makeDefaultLibrary()
let vertexFunction = defaultLibrary!.makeFunction(name: "vertexShader")
let fragmentFunction = defaultLibrary!.makeFunction(name: "fragmentShader")
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.label = "Simple Pipeline"
pipelineStateDescriptor.vertexFunction = vertexFunction
pipelineStateDescriptor.fragmentFunction = fragmentFunction
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat
do {
try _pipelineState = _device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
} catch { print("パイプラインステートの作成に失敗") }
_commandQueue = _device!.makeCommandQueue()
}
レンダラーの初期セットアップである。シェーダー関係のことは増えているが、デバイスの取得とコマンドキューの生成といった基本的な部分は変わらない。使う関数こそ違うものの、全く同じ機能のものがSwiftにもあるので、ほぼ単純置き換えといって差し支えない。エラー処理構文とか初めて使ったよ。
###AAPLVertex構造体
- (void)drawInMTKView:(nonnull MTKView *)view
{
static const AAPLVertex triangleVertices[] =
{
// 2D positions, RGBA colors
{ { 250, -250 }, { 1, 0, 0, 1 } },
{ { -250, -250 }, { 0, 1, 0, 1 } },
{ { 0, 250 }, { 0, 0, 1, 1 } },
};
またdrawIn関数の中でstatic const を使ってやがるので、この構造体はレンダリングループが一周終わっても消えず、インスタンス変数のように永続する。んでもってこれは何をやっているかというと、**AAPLVertex構造体が3つ入った配列triandleVertecies
**を作っている。
いろいろやってみたが、Swiftでこのように書くのはどうやら無理っぽいらしい。というわけでこの部分は多少不恰好だが以下のようになった。SwiftRenderer.swift
の冒頭でプロパティとして宣言している。こういうのの表記を揃えるかどうかは性格で分かれるんだろうなとは思う。
let triangleVertices = [AAPLVertex(position: [ 250, -250], color: [1,0,0,1]),
AAPLVertex(position: [-250, -250], color: [0,1,0,1]),
AAPLVertex(position: [ 0, 250], color: [0,0,1,1])]
後日、この部分を構造体でなくタプルで渡せないかと色々実験してみたが結局ダメだった。シェーダー側の型指定でタプルを割り当てるのはどうやら不可能のようだった。というわけでおとなしくAAPLVertex構造体を使うことにする。この構造体をレンダリングループの中でShaderに渡してやることで三角形を描画する。
###レンダリングループの実装
[renderEncoder setVertexBytes:triangleVertices
length:sizeof(triangleVertices)
atIndex:AAPLVertexInputIndexVertices];
今回かなりハマったのがこの箇所である。さきほど作った配列をrenderEncoderに渡しているが、このlength:
で頂点処理に必要なバイト数(整数値)も一緒に渡してやらなければならない。ここではsizeof(triangleVertices)
してそれを実現しているが、しかしSwiftにはsizeof()がない。「詳解Swift』によればMemoryLayout.size(of:)
という関数がそれっぽかったものの、どうしても「長さが足りない」といってビルドが通らない。どうしたものかといろいろ調べていたが、正解は、
renderEncoder!.setVertexBytes(triangleVertices,length: triangleVertices.count * MemoryLayout<AAPLVertex>.size,index: Int(AAPLVertexInputIndexVertices.rawValue))
配列の要素数を数えてMemoryLayout<型名>.sizeで掛け算する でした。こういうところSwift君バカっぽいよな。
さてAAPLVertexInputIndexVertices
というのは共有ヘッダで宣言された列挙型であるが、ここでは普通に整数値として使わねばならんため、Int()で囲って.rawValue
までつけてやらなくてはならない。スタイリッシュな設計が台無しである。
##できたーーーーーー!
こうしてまとめると簡単な流れのように見えるが、どうやっても三角形が表示されずに真っ黒画面になってしまうという現象に陥って、丸二日悩んで悩んでいろいろ手を尽くした。renderEncoderに頂点を渡すsetVertexBytes
をsetVertexBuffer
で実装しようとしたり(巷の作例ではこちらの方をよく見かけるので)、setVertexBytes
の引数に"UnsafeRawPointer"と書いてあったからtriangleVertecies配列のポインタを渡そうとして結局どうにもならなかったり、とにかく散々であった。
###Metalのデバッグ
なにせシェーダーの中ではprintとかを使って中間の値を参照できないものだから(俺が知らんだけかもしれんけど)、何が起こって何が起こってないのか全くといっていいほどわからない。そこで転機になったのがこの記事である。
Metalのデバッグまとめ(随時更新) - Qiita
https://qiita.com/shu223/items/1e88d19fbb31298146ca
これによると、開発中のMetalアプリが動いているときにカメラマークを押すと、GPUのフレームがキャプチャされてこのような情報画面を見ることができるらしい。"Geometry"をクリックしてやると、なんだかソレっぽい画面になる。
let triangleVertices = [AAPLVertex(position: [ 250, -250], color: [1,0,0,1]),
AAPLVertex(position: [-250, -250], color: [0,1,0,1]),
AAPLVertex(position: [ 0, -250], color: [0,0,1,1])]
....あーーーーーッッ!!配列の最後が250じゃなくて-250になってる!!!プラスとマイナスを間違えた!!!
#教訓
- 大事なデータはちゃんとコピペしよう
- 構造を疑うのは自分の入力を疑ってから
次回 「"Hello Triangle"をアニメーションさせたい」https://qiita.com/shunga/items/40c9785fa8d92ba409d3 に続く
##今回のソースコード
import Cocoa
import MetalKit
import simd
class SwiftRenderer: NSObject, MTKViewDelegate{
var _device:MTLDevice!
var _pipelineState:MTLRenderPipelineState!
var _commandQueue:MTLCommandQueue!
var _viewportSize:vector_uint2 = vector_uint2(0,0)
let triangleVertices = [AAPLVertex(position: [ 250, -250], color: [1,0,0,1]),
AAPLVertex(position: [-250, -250], color: [0,1,0,1]),
AAPLVertex(position: [ 0, 250], color: [0,0,1,1])]
init(by mtkView:MTKView){
_device = mtkView.device
let defaultLibrary = _device.makeDefaultLibrary()
let vertexFunction = defaultLibrary!.makeFunction(name: "vertexShader")
let fragmentFunction = defaultLibrary!.makeFunction(name: "fragmentShader")
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.label = "Simple Pipeline"
pipelineStateDescriptor.vertexFunction = vertexFunction
pipelineStateDescriptor.fragmentFunction = fragmentFunction
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat
do {
try _pipelineState = _device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
} catch { print("パイプラインステートの作成に失敗") }
_commandQueue = _device!.makeCommandQueue()
}
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
_viewportSize.x = UInt32(size.width)
_viewportSize.y = UInt32(size.height)
}
func draw(in view: MTKView) {
let commandBuffer = _commandQueue.makeCommandBuffer()
commandBuffer?.label = "MyCommand"
if let renderPassDescriptor = view.currentRenderPassDescriptor {
let renderEncoder = commandBuffer!.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
renderEncoder!.label = "MyRenderEncoder"
renderEncoder!.setViewport(MTLViewport(originX: 0.0,
originY: 0.0,
width: Double(_viewportSize.x),
height:Double(_viewportSize.y),
znear: -1.0,
zfar: 1.0 ))
renderEncoder!.setRenderPipelineState(_pipelineState)
renderEncoder!.setVertexBytes(triangleVertices,length: triangleVertices.count * MemoryLayout<AAPLVertex>.size, index:Int(AAPLVertexInputIndexVertices.rawValue))
renderEncoder!.setVertexBytes( &_viewportSize,
length: MemoryLayout<vector_uint2>.size,
index: Int(AAPLVertexInputIndexViewportSize.rawValue))
renderEncoder?.drawPrimitives(type: MTLPrimitiveType.triangle, vertexStart: 0, vertexCount: 3)
renderEncoder?.endEncoding()
commandBuffer!.present(view.currentDrawable!)
}
commandBuffer!.commit()
}
}