[iOS, GPU] Metalの描画パイプラインを図解してみる


Apple製のGPUシェーダー言語Metalにて、初学者には中々掴みにくい描画の仕組みについて図示しながら概説してみたいと思います.

はじめに

ざっくりと、Metalでは以下のような手順で描画が行われます。

  • 描画情報の設定 (頂点や色の設定など)
  • 描画コマンドの生成と送信
  • Shader (.Metal拡張子に書かれた描画関数) の通り描画を実行.

以下、これらそれぞれの概説を図とともに見ていきたいと思います.

描画情報の設定

まずは図に記載の
- MTLRenderPipelineDescriptor
- MTLRenderPassDescriptor
の2つのオブジェクトに描画情報をセットし、これを描画コマンドへと渡すようにします.

let renderPipelineDescriptor = MTLRenderPipelineDescriptor()
let depthStencilDescriptor = MTLDepthStencilDescriptor()

MTLRenderPipelineDescriptor

主に、
・頂点描画用の関数 (VertexFunction)
・頂点描画設定 (VertexDescriptor)
・頂点の色情報決めるための関数 (FragmentFunction)
等を、MTTLRenderPipelineDescriptorオブジェクトのattributesに設定します.

その後、MTLPipelineStateオブジェクトがMTTLRenderPipelineDescriptorから生成されます.
このように、MTLPipelineState (描画設定を保持し、情報をGPUへのコマンド受け渡しに使用する) を作ります.

またDepth情報については、MTLDepthStencilDescriptorオブジェクトで設定します. 設定手法は、パラメータは違えど上記と同様なので割愛します.

詳しくはこちらの公式で紹介されているソースコードを参照.

MTLRenderPathDescriptor

主に出力先の設定が行われます. 出力としてMTLTextureをセットすることで、GPU計算をしてCPUへ結果のMTLTexureを返すこともできます.

以下のように、ベースとなる mtkView から毎フレーム取得して設定も実施します. MTLRenderPathDescriptorは毎フレームで初期化されるため絵、毎フレーム設定しなければなりません.

let renderPassDescriptor = mtkView.currentRenderPassDescriptor

描画コマンドの生成と送信

基本の流れ

先ほどの描画設定は下図の通り、MTLCommandBufferに渡され、これを用いてMTLRenderCommandEncoderクラスオブジェクトを生成します.
この...CommandEncoderは、GPUが解釈可能なデータになっているものと思われます.

commandQueue = device.makeCommandQueue()
let commandBuffer = commandQueue.makeCommandBuffer()

このcommandBufferに、必要な画像等のデータをセットしてGPU描画が実行されます.

他の流れ

描画をカスタムして実行する場合は、上記のMTLCommandBufferの設定が必要ですが、カスタムの描画でなければMTLCommandBufferを飛ばす場合もあります. (上図中の紫の矢印)
例えばビルトインの画像フィルター郡は、基本この仕様になっています.

let filter = MPSImageGaussianBlur(device: device!, sigma: 5)

            filter.encode(commandBuffer: commandBuffer,
                          sourceTexture: secTex, destinationTexture: dstTex)

Shader

最後はShaderです.

Shader関数は先述のMTLRenderPipelineDescriptorの時点で文字列にて設定された関数を用いることになります.

let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)

// (MTLTextureを順にセットする等、必要な処理を実行)

renderEncoder.endEncoding()
commandBuffer.present(currentDrawable) // 描画
commandBuffer.commit() // 描画完了

まず、MTLComanndBuffeerからMTLRenderEncoderが生成されます.
このMTLRenderEncoderには、先述のMTLRenderPipelineStateオブジェクトも入力されます. (コードは割愛)

(Shaderの仕組みは本稿に追記する、もしくは別記事にて形で後日説明します)

参考

Metalの各種Tipsについて、こちらの記事にまとめていますので併せてご参考にいただけますと幸いです。

終わりに

改善点やご意見などあれば、どしどしコメント下さい!