Metalシェーダーことはじめ - WebGL/GLSLの豊富なサンプルを参考にMSLを書く

Metalでグラフィック処理を行うにしろ並列演算を行うにしろ、**GPUに処理をさせるためのシェーダー**を書かないといけないわけですが、これがまだ情報が少なくて、「こういうシェーダを書きたいんだけど、誰かもう書いてないかな・・・」というときに**参考になる近いものとかはそうそう都合よく出てこない**わけです。


ただ、WebGL/GLSLの情報はググると山ほどあって、[GLSL Sandbox](http://glslsandbox.com/) という、<b>Web上で編集できてプレビューできてシェアできる</b>サイトもあり、何がどうなってそうなるのか理解できない難しそうなものから、ただの円といったシンプルなものまで、既に偉大な先人たちのサンプルがたくさんアップされています

<img width="922" alt="glsl.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/3180/012e6a30-d709-02f3-8269-53102a5ca47e.jpeg">


Metalのシェーダというのは正しくは <b>Metal Shading Language</b> といいまして、C++をベースとする独自言語なのですが、やはり同じGPUに実行させるプログラムだけあって、**概ねGLSLと一緒**です。


実際にやってみたところ、GLSL -> Metal Shader の移植はほとんど単純置き換えで済み、<b>Swift 2 を Swift 3 に直すのよりも簡単</b>という感覚でした。

![ss.jpg](https://qiita-image-store.s3.amazonaws.com/0/3180/8b420fb6-f134-7da3-4e4e-f08fb2bd868e.jpeg)



いずれも画像等のリソースは使用しておらず、<b>iOSデバイス上で、GPUによってリアルタイム計算</b>されたものです。


実際のところ自分はゲーム開発やVJをやったりしているわけではないので、こういう派手なエフェクトではなく、線とか円とかグラデーションとかのもっと単純なものをMetalで動的描画したいだけだったりするのですが [^1]、移植が簡単に行えることがわかっていれば、**GLSLを参考にMetalシェーダを書ける**というのはもちろんのこと、**GLSL Sandboxで動作確認しつつシェーダを書いて、できたものをiOSに移植**する、ということもできるので、個人的にはMetalシェーダを書く敷居がグッと下がりました。


[^1]: 線とか円とかの単純なものでも、**カメラプレビューで動的かつリアルタイムに、かつ他の重い画像処理と一緒に**、といった場合、そして描画数が多かったり毎フレームの更新が必要な場合、やはりUIKitやCoreGraphicsでは厳しい場面が出てきます。


というわけで、以下GLSLのコードをMetalに移植する方法です。


## 雛形となるプロジェクトを作成する

プロジェクトテンプレートに "OpenGL ES" はあっても "Metal" というのはないのですが、"Game" というのがあり、次の画面で [Game Technology] の欄で Metal を選択すると、シンプルなMetalのプロジェクトが生成されます。


<img src="https://qiita-image-store.s3.amazonaws.com/0/3180/03ef7572-ce19-e2b0-55a9-57bba813a5e1.jpeg" width="500">


これをベースに、<b>GLSLのコードを移植して来やすいように</b>次のように手を入れました。（現時点ではGLSL SandboxのコードをiOSで動かしてみるべく、フラグメントシェーダだけに手を入れています）


### 1.「画面の解像度」をフラグメントシェーダに渡す

多くのGLSLのサンプルでは、xy平面における座標を <b>0.0〜1.0 に正規化</b>した状態で取り扱っています。ピクセルベースの座標値をシェーダ側で正規化できるよう、画面の解像度をシェーダに渡すよう修正します。


まずはシェーダ側。下記のように引数に float2型の "resolution" を追加します。

```cpp
fragment float4 practiceFragment(VertexInOut     inFrag      [[stage_in]],
                                   constant float2 &resolution [[buffer(0)]])
```


次にSwift側。下記のようにバッファを用意して、

```swift
var resolutionBuffer: MTLBuffer! = nil
```

```swift
let screenSize = UIScreen.main.nativeBounds.size
let resolutionData = [Float(screenSize.width), Float(screenSize.height)]
let resolutionSize = resolutionData.count * MemoryLayout<Float>.size
resolutionBuffer = device.makeBuffer(bytes: resolutionData, length: resolutionSize, options: [])
```


フラグメントシェーダ関数の引数にバッファをセットします。

```swift
renderEncoder.setFragmentBuffer(resolutionBuffer, offset: 0, at: 0)
```


こんな感じで正規化した座標値を算出します。

```cpp
float p_x = inFrag.position.x / resolution.x;
float p_y = inFrag.position.y / resolution.x;
float2 p = float2(p_x, p_y);
```

GLSL Sandboxはスクリーンが必ず正方形なのですが、iOSデバイスはそうではないので、比率が変わらないようどちらもx方向の解像度（つまり幅）で割っています。


### 2. 「経過時間」をフラグメントシェーダに渡す

ほとんど同様です。シェーダ側では、引数に float型の "time" を追加します。

```cpp
fragment float4 practiceFragment(VertexInOut     inFrag      [[stage_in]],
                                   constant float2 &resolution [[buffer(0)]],
                                   constant float  &time       [[buffer(1)]])
```


Swift側。下記のようにバッファを用意して、

```swift
var timeBuffer: MTLBuffer! = nil
```

```swift
timeBuffer = device.makeBuffer(length: MemoryLayout<Float>.size, options: [])
timeBuffer.label = "time"
```


フラグメントシェーダ関数の引数にバッファをセットします。インデックスが変わる点に注意。

```swift
renderEncoder.setFragmentBuffer(timeBuffer, offset: 0, at: 1)
```


時刻の更新時にバッファを更新します。

```swift
let pTimeData = timeBuffer.contents()
let vTimeData = pTimeData.bindMemory(to: Float.self, capacity: 1 / MemoryLayout<Float>.stride)
vTimeData[0] = Float(Date().timeIntervalSince(startDate))
```


## GLSLを移植する際の改変点

GLSL を Metal に移植してくる準備が整いました。ほとんど同じ、と書きましたが細部はやはり違います。以下、大まかな移行ポイントです。

- GLSLのフラグメントシェーダでは、最後に gl_FragColor にvec4値をセットすることで出力とするが、return で float4 なり half4 なりを返す

（例）GLSLの場合

```cpp
gl_FragColor = vec4(color, 1.0 );
```

Metalの場合

```cpp
return float4(color, 1.0);
```

- 関数はプロトタイプ宣言が必要
  - これがないと、ビルド時に "No previous prototype for function &#12316;" というwarningが出る

- `vec2`, `vec3`, `vec4` -> `float2`, `float3`, `float4`
- `ivec2`, `ivec3`, `ivec4` -> `int2`, `int3`, `int4`
- `uvec2`, `uvec3`, `uvec4` -> `uint2`, `uint3`, `uint4`
- `mat2`, `mat3`, `mat4` -> `float2x2`, `float3x3`, `float4x4`
  - コンストラクタも微妙に違う（`float2x2` なら `float2` を2つ渡す。公式ドキュメントp15あたり）
- `const` -> `constant`
- `mouse.x`, `mouse.y` -> 適当に `0.5` とか
- `mod` -> `fmod`
  - この置換でコンパイルは通るようになるが、少数の扱いに差異があるため結果が同じにならない。厳密に同じにするためには、 `mod` を定義する。参考： [modとfmodの差異](https://zenn.dev/ikeh1024/articles/cc51846dfad295#mod%E3%81%A8fmod%E3%81%AE%E5%B7%AE%E7%95%B0)
- `uintBitsToFloat`, `floatBitsToUint` -> `as_type<type-id>()`
  - たとえば変換先の型が `uint2` なら `as_type<uint2>`
  - 参考： [What's the equivalent of GLSL's uintBitsToFloat and floatBitsToUint in Metal shading language?](https://stackoverflow.com/questions/39665827/whats-the-equivalent-of-glsls-uintbitstofloat-and-floatbitstouint-in-metal-sha)

また出てきたら追記します。


## GLSL Sandboxから移植してみる

GLSL Sandboxでいくつかピックアップして上記手順でMetalに移植し、iOSで動かしてみました。<b>それぞれの移植にかかった時間は5分ぐらい</b>です。ほとんど単純置き換えで済みました。

### ソースコード

https://github.com/shu223/iOS-Experiments-Contents#msl-from-glsl

### http://glslsandbox.com/e#36694.0

![sample1.gif](https://qiita-image-store.s3.amazonaws.com/0/3180/efdff0eb-3f66-24ef-8778-0d7608f87f3f.gif)

### http://glslsandbox.com/e#36538.3

![sample2_64.gif](https://qiita-image-store.s3.amazonaws.com/0/3180/3c0b5de5-3c57-c2e4-5a00-e9d26fed4101.gif)

※デフォルトのITERATIONS 128では3fpsぐらいしか出なかったので、ITERATIONS 64に変更


### http://glslsandbox.com/e#36614.0

![sample3.gif](https://qiita-image-store.s3.amazonaws.com/0/3180/d844c1d7-5c3c-1ee2-c0d9-8fe9058e1e20.gif)