Metalでグラフィック処理を行うにしろ並列演算を行うにしろ、GPUに処理をさせるためのシェーダーを書かないといけないわけですが、これがまだ情報が少なくて、「こういうシェーダを書きたいんだけど、誰かもう書いてないかな・・・」というときに参考になる近いものとかはそうそう都合よく出てこないわけです。
ただ、WebGL/GLSLの情報はググると山ほどあって、GLSL Sandbox という、Web上で編集できてプレビューできてシェアできるサイトもあり、何がどうなってそうなるのか理解できない難しそうなものから、ただの円といったシンプルなものまで、既に偉大な先人たちのサンプルがたくさんアップされています
Metalのシェーダというのは正しくは Metal Shading Language といいまして、C++をベースとする独自言語なのですが、やはり同じGPUに実行させるプログラムだけあって、概ねGLSLと一緒です。
実際にやってみたところ、GLSL -> Metal Shader の移植はほとんど単純置き換えで済み、Swift 2 を Swift 3 に直すのよりも簡単という感覚でした。
いずれも画像等のリソースは使用しておらず、iOSデバイス上で、GPUによってリアルタイム計算されたものです。
実際のところ自分はゲーム開発やVJをやったりしているわけではないので、こういう派手なエフェクトではなく、線とか円とかグラデーションとかのもっと単純なものをMetalで動的描画したいだけだったりするのですが 1、移植が簡単に行えることがわかっていれば、GLSLを参考にMetalシェーダを書けるというのはもちろんのこと、GLSL Sandboxで動作確認しつつシェーダを書いて、できたものをiOSに移植する、ということもできるので、個人的にはMetalシェーダを書く敷居がグッと下がりました。
というわけで、以下GLSLのコードをMetalに移植する方法です。
雛形となるプロジェクトを作成する
プロジェクトテンプレートに "OpenGL ES" はあっても "Metal" というのはないのですが、"Game" というのがあり、次の画面で [Game Technology] の欄で Metal を選択すると、シンプルなMetalのプロジェクトが生成されます。
これをベースに、GLSLのコードを移植して来やすいように次のように手を入れました。(現時点ではGLSL SandboxのコードをiOSで動かしてみるべく、フラグメントシェーダだけに手を入れています)
1.「画面の解像度」をフラグメントシェーダに渡す
多くのGLSLのサンプルでは、xy平面における座標を 0.0〜1.0 に正規化した状態で取り扱っています。ピクセルベースの座標値をシェーダ側で正規化できるよう、画面の解像度をシェーダに渡すよう修正します。
まずはシェーダ側。下記のように引数に float2型の "resolution" を追加します。
fragment float4 practiceFragment(VertexInOut inFrag [[stage_in]],
constant float2 &resolution [[buffer(0)]])
次にSwift側。下記のようにバッファを用意して、
var resolutionBuffer: MTLBuffer! = nil
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: [])
フラグメントシェーダ関数の引数にバッファをセットします。
renderEncoder.setFragmentBuffer(resolutionBuffer, offset: 0, at: 0)
こんな感じで正規化した座標値を算出します。
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" を追加します。
fragment float4 practiceFragment(VertexInOut inFrag [[stage_in]],
constant float2 &resolution [[buffer(0)]],
constant float &time [[buffer(1)]])
Swift側。下記のようにバッファを用意して、
var timeBuffer: MTLBuffer! = nil
timeBuffer = device.makeBuffer(length: MemoryLayout<Float>.size, options: [])
timeBuffer.label = "time"
フラグメントシェーダ関数の引数にバッファをセットします。インデックスが変わる点に注意。
renderEncoder.setFragmentBuffer(timeBuffer, offset: 0, at: 1)
時刻の更新時にバッファを更新します。
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の場合
gl_FragColor = vec4(color, 1.0 );
Metalの場合
return float4(color, 1.0);
-
関数はプロトタイプ宣言が必要
- これがないと、ビルド時に "No previous prototype for function 〜" という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の差異
- この置換でコンパイルは通るようになるが、少数の扱いに差異があるため結果が同じにならない。厳密に同じにするためには、
-
uintBitsToFloat
,floatBitsToUint
->as_type<type-id>()
- たとえば変換先の型が
uint2
ならas_type<uint2>
- 参考: What's the equivalent of GLSL's uintBitsToFloat and floatBitsToUint in Metal shading language?
- たとえば変換先の型が
また出てきたら追記します。
GLSL Sandboxから移植してみる
GLSL Sandboxでいくつかピックアップして上記手順でMetalに移植し、iOSで動かしてみました。それぞれの移植にかかった時間は5分ぐらいです。ほとんど単純置き換えで済みました。
ソースコード
http://glslsandbox.com/e#36694.0
http://glslsandbox.com/e#36538.3
※デフォルトのITERATIONS 128では3fpsぐらいしか出なかったので、ITERATIONS 64に変更
http://glslsandbox.com/e#36614.0
-
線とか円とかの単純なものでも、カメラプレビューで動的かつリアルタイムに、かつ他の重い画像処理と一緒に、といった場合、そして描画数が多かったり毎フレームの更新が必要な場合、やはりUIKitやCoreGraphicsでは厳しい場面が出てきます。 ↩