新しいグラフィックスAPI「Metal」の入門的内容です。
XcodeでMetalを試す場合Gameプロジェクトで始めることができますが、いきなり大量のコードが目に入るのためそっとファイルを閉じたくなりますよね。
今回はSingle View Applicationから、順番に設定しMetalを使えるところまで持っていきます。
このガイドはLearning iOS 8 Game Development Using Swiftを参考に解説しています。
この本はGame開発だけでなく、iOSでのグラフィックスの扱い方を解説しているおすすめの良書です!
参考サイト
はじめる前に下記のページもおすすめです。
-
DSAS開発者の部屋
詳細な解説あります。わからない用語はこちらで調べながら進めると良いでしょう。 -
iOSの新グラフィックAPI - Metal入門してみる
こちらもおすすめです。
ざっくり解説なので、興味が出てきたら細かいところを調べてみるとより理解が進むと思います。
実装時の注意点
Metalの実装は必ず各デバイスで行ってください。Macのシュミレーターでは動きません。
(最新のOS10.11では未確認です)
新規プロジェクトを作成
まずは、新規プロジェクトを作ります。「Single View Application」を選択してください。
ライブラリを設定
Builde PhasesでMetalのライブラリ一式設定します。
QuartzCoreはCAMetalLayerを使うため設定します。
(CoreAnimation系はQuartzCoreフレームワークが必要になるようです。)
下準備
グラフィックスライブラリ系はOpenGL然り、いろいろな下準備が必要です。
なるべく必要最小限の設定で進めたいと思います。
まずはライブラリの読み込み設定をします。
import UIKit
import Metal
import QuartzCore
つぎにMetalのコードを記述します。
今回のコードはすべてViewControllerのviewDidLoad()メソッド内に記述します。
まずは、ViewControllerを開きます。
Metalを利用するにあたって、必ず必要となるコードを準備します。
MTLDevice
MTLCreateSystemDefaultDevice()でデバイスを取得します。これで直接デバイスに対してアクセスできます。
let device: MTLDevice? = MTLCreateSystemDefaultDevice()
MTLCommandQueue
レンダリングのコマンドをGPUに伝えるキューです。キューが保持するコマンドを順番に処理しレンダリングを進めていきます。
let commandQueue: MTLCommandQueue! = device.newCommandQueue()
リソースの準備
次にリソースの準備をします。描画系でリソースと言われたら頂点座標リストかテクスチャです。
ここでは頂点座標を準備します。今回は2Dの三角形を描画します。
let vertexArray: [Float] = [
0.0, 0.1,
-0.1, -0.1,
0.1, -0.1
]
VertexArrayはViewControllerクラスの外側で定義しておきます。
頂点座標を元に画面上に図形を描画します。
頂点Bufferを作る
頂点座標は準備できましたが、これをグラフィックスライブラリで扱うにはVertex Bufferに格納します。
MetalではMTLBUfferです。頂点数と頂点の型(今回はFloat)から格納するメモリを準備しています。
このあたりは、いったんこういったルールという認識で大丈夫です。
var vertexBuffer: MTLBuffer! = device.newBufferWithBytes( vertexArray,
length: vertexArray.count * sizeofValue(vertexArray[0]),
options: MTLResourceOptions.OptionCPUCacheModeDefault
)
Shaderを作る
Shaderとは頂点の変換やピクセルの色付けなど、グラフィックスに関する計算処理をまとめたものです。
もちろんShaderを使わないくてもグラフィックス処理を進めることはできます。使わない場合はCPUに計算させるのですがピクセルの処理などは膨大な計算量になるので、すぐにパフォーマンスの問題にぶつかります。
(画面サイズ100x100に描画する場合は、ピクセル処理は10000にも及びます)
そこでShaderはGPUを利用して計算させます。これは非常に高速です。Shaderを利用するのはここが理由です。
シェーダーは2種類あります。"Vertex shader"と"Fragment shader"です。
ここでは、以下の理解で進みます。とりあえずはこんなものでも大丈夫です。
- Vertex shader: 頂点の移動、回転の変換処理
- Fragment shader: 各ピクセルの処理をする(1pxごとに処理を実行)
さっそくShaderを作ってみます。
一般的にShaderのファイルは2種類用意しますが(VertexとFragment)、Metalでは一つのファイルにまとめることができます。もちろん2ファイルに分けてもよいです。今回は1ファイルにまとめます。Shader.metalを作ります。
// スタンダードライブラリを読み込む
#include <metal_stdlib>
// 識別子metalを省略できる設定
using namespace metal;
// VertexShader
// 頂点の計算処理 入力の座標に対して特に何もしません。
vertex float4 myVertexShader(const device float2 * vertex_array [[ buffer(0) ]], uint vid [[ vertex_id ]]) {
return float4(vertex_array[vid],0,1);
}
// Fragment Shader
// 各ピクセルの処理 黒でぬりつぶします。別名Pixel Shaderとも呼ばれます。
fragment float4 myFragmentShader() {
return float4(0.0, 0.0, 0.0, 1.0); // 図形ないの各ピクセルの色を指定します。
}
Shader.metalはC++11の記述のようです。
二つの関数がありますが、それらがShader処理です。
2つの関数の先頭にどのShaderであるか表す"vertex"、"fragment"という識別子がつきます。
関数の記述方法は
(Shader識別子) (戻り値) 関数名(引数){
// 処理内容
}
といった形になります。
今回はShaderの処理内容は細かく解説しません。
Shaderではいくつか特殊な型を持ちます。
- float, float2, flaot3, float4
- int, int2, int3, int4
- vert1, vert2...
vertは変数です。
float4は4つのfloat値要素をもった構造値と考えれば問題ないと思います。
たとえば、x,y,z,w(四元数)やr,g,b,a(色)などです。
このあたりは初見では難しいとおもうので、とりあえず次へ移動します。
このShaderファイルはコンパイル時にShader Libraryに追加されます。これによってShaderの再コンパイルを防ぎ、効率化を図ります。Shader Libraryにはdeviceからアクセスできます。
let defaultLibrary = device.newDefaultLibrary()
let newVertexFunction = defaultLibrary!.newFunctionWithName("myVertexShader")
let newFragmentFunction = defaultLibrary?.newFunctionWithName("myFragmentShader")
Shader関数名も取得していますが、これは後ほど使います。
次に描画パイプラインを作ります。pipeline descriptorを作ります。これはFile記述子と同じように、
これを経由して描画処理を進めます。
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
// 頂点処理を設定
pipelineStateDescriptor.vertexFunction = newVertexFunction
// ピクセル処理を設定
pipelineStateDescriptor.fragmentFunction = newFragmentFunction
// 色成分の順序を設定
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .BGRA8Unorm
// RenderPipelineの状態を取得
var pipelineState: MTLRenderPipelineState!
do {
pipelineState = try device.newRenderPipelineStateWithDescriptor(pipelineStateDescriptor)
} catch {
print("error with device.newRenderPipelineStateWithDescriptor")
}
MetalのPixelFormatについて詳しく知りたい人は、こちらをご確認ください。
CAMetalLayerを作る
描画結果を出力するために、ViewにCAMetalLayerを追加します。
こいつでエラーが出る場合は、そのデバイスではMetalが使えない場合があります。
(Apple A7チップ以降であれば使えます。)
let metalLayer = CAMetalLayer()
metalLayer.device = device
metalLayer.pixelFormat = .BGRA8Unorm // pipeline descriptorに設定したものと同じものを設定する。
metalLayer.frame = view.layer.frame // フレームサイズを設定
view.layer.addSublayer(metalLayer)
これで描画の準備ができました。
描画処理
ここから描画に入ります。
まず最初にCAMetalLayerから描画エリアの参照を取得します。
let drawable = metalLayer.nextDrawable()
次にRender Descriptorを作ります。
let renderPassDescriptor = MTLRenderPassDescriptor()
描画するテクスチャを設定します。
renderPassDescriptor.colorAttachments[0].texture = drawable?.texture
フレームがloadされた時の処理を設定します。
renderPassDescriptor.colorAttachments[0].loadAction = .Clear // 毎フレームクリアされる
クリアカラーを設定、この色でlayerが塗りつぶされ、その上に再度描画されます。
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(
red: 1.0,
green: 1.0,
blue: 1.0,
alpha: 1.0
)
次に、layerに三角形を描画します。
commandQueueからcommandBufferを取得します。
let commandBuffer = commandQueue.commandBuffer()
全てのcommandはマシン語に変換しなければなりません。
これはMTLRenderCommandEncoderを利用します。
let renderEncoder: MTLRenderCommandEncoder = commandBuffer.renderCommandEncoderWithDescriptor(renderPassDescriptor
RenderPipelineStateを設定し、頂点Bufferをセットします。
renderEncoder.setRenderPipelineState(pipelineState)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, atIndex: 0)
これで三角形を描画できる準備が整いました。長かった...
renderEncoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1); // 描画
Render Encodingを終了します。
renderEncoder.endEncoding()
まだ画面には表示されてないので、表示するようにメソッドを実行します。
commit()でテクスチャが反映されます。
commandBuffer.presentDrawable(drawable!)
commandBuffer.commit()
これで完了です。今回作成したコードはこちらでも確認できます。
最後はちょっと駆け足になりましたが、OpenGLと比べるとMetalの方が入門しやすい気がしました。
情報自体は、まだまだWeb上にも十分なものが少なく、コード自体はObjective-cのものが多いです。
今後を考えればSwiftでの学習がおすすめです。
OpenGLを試してみるとグラフィックスライブラリとはどうゆうものか、より理解もできます。
グラフィックスライブラリは決してGameだけのものではないので、多くの開発者が試してくれることを期待しています。
この記事の続きはこちらのアドベントカレンダーでも続けます。