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

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



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

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

<img width="699" alt="about_metal.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270214/e550abe8-47d9-6565-93e0-b2911d21977e.png">

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



# 描画情報の設定

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

<img width="699" alt="about_metal_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270214/dcec57ae-6bc4-59ab-0001-5260b3ca8762.png">

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

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

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


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

詳しくは[こちら](https://developer.apple.com/documentation/arkit/effecting_people_occlusion_in_custom_renderers?language=objc)の公式で紹介されているソースコードを参照.


### MTLRenderPathDescriptor

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

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

``` swift
let renderPassDescriptor = mtkView.currentRenderPassDescriptor
```





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

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

<img width="699" alt="about_metal_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270214/ba2aa849-3f21-8ba5-8ede-a946137901e4.png">

``` swift
commandQueue = device.makeCommandQueue()
let commandBuffer = commandQueue.makeCommandBuffer()
```
このcommandBufferに、必要な画像等のデータをセットしてGPU描画が実行されます.

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

``` swift
let filter = MPSImageGaussianBlur(device: device!, sigma: 5)
            
            filter.encode(commandBuffer: commandBuffer,
                          sourceTexture: secTex, destinationTexture: dstTex)
```




# Shader

最後はShaderです.

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

<img width="699" alt="about_metal_3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270214/56b79773-cb8e-c1ff-d232-212e63fbd8cd.png">

``` swift
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)

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

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

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


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


# 参考
Metalの各種Tipsについて、[こちら](https://qiita.com/Susumu0417/items/df8fac84051bde8c2b30)の記事にまとめていますので併せてご参考にいただけますと幸いです。


# 終わりに
改善点やご意見などあれば、どしどしコメント下さい！