ARKitやSceneKitの図形をMetalシェーダーで変形させる方法

ARKitやSceneKitの図形を自由に変形させる方法を紹介します。

なお、この手法を使って最終的にはARで空中に水を浮かべるといった処理も作りましたので、こちらの記事もご覧下さい。

[ARKit + Metalで空中に水を浮かべる方法](https://note.com/tokyoyoshida/n/n76df3be1373a)
![](https://assets.st-note.com/production/uploads/images/36387744/rectangle_large_type_2_2333b675a63d18071aeb1c194983ab19.png?fit=bounds&quality=60&width=1280)

### やり方

今回はこんな感じの球体を変形させていきます。
※下の画像は球体にライトを当てていないのでのっぺりしていますが球体です。
<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/2b91302c-00ab-f710-ea38-b567d7ab669d.png">

### SceneKitのマテリアルをMetalシェーダーで描画させる方法

球体のノードを作り、SCNProgramを使ってMetalのシェーダーの関数を設定すると、シェーダーに描画させることができます。

```swift:GameViewController.swift
// 球体をノードに追加する
let sphereNode = SCNNode()
sphereNode.geometry = SCNSphere(radius: 2)
sphereNode.position.y += Float(0.05)
sphereNode.name = "my_node"

// Metalのシェーダーを指定する
let program = SCNProgram()
program.vertexFunctionName = "vertexShader"
program.fragmentFunctionName = "fragmentShader"
sphereNode.geometry?.firstMaterial?.program = program
// 経過時間の情報をシェーダーに渡す
let time = Float(Date().timeIntervalSince(startDate))
globalData.time = time
let uniformsData = Data(bytes: &globalData, count: MemoryLayout<GlobalData2>.size)
sphereNode.geometry?.firstMaterial?.setValue(uniformsData, forKey: "globalData")
```

このあたりを詳しく知りたい方はこちらを読むと良いと思います。

[Metal入門](https://booth.pm/ja/items/826318)
<img width="100px" src="https://booth.pximg.net/74c0c65b-cd71-4b0d-9263-f3095f3e17a0/i/826318/381e8253-f80e-44a4-bf13-254e1af1fdf4_base_resized.jpg">

### Vertexシェーダーで変形してみる

まずは、簡単な変形をしてみます。
x座標にy座標分を足してみます。

```c++:Shader.metal
vertex ColorInOut vertexShader(VertexInput2          in       [[ stage_in ]],
                               constant SCNSceneBuffer& scn_frame [[buffer(0)]],
                               constant NodeBuffer& scn_node [[ buffer(1) ]],
                               device GlobalData2 &globalData [[buffer(2)]])
{
    // 出力用変数
    ColorInOut out;
    // 頂点の座標情報
    float3 pos = in.position;
    // 頂点のxにyを足している。ここが変形処理の実体
    pos.x += pos.y;
    // SceneKit用のMVP変換をする
    float4 transformed = scn_node.modelViewProjectionTransform * float4(pos, 1.0);
    // 変換後の座標を出力変数に入れる
    out.position = transformed;
    // テクスチャー座標は何もせずに出力変数に入れる
    out.texCoords = in.texCoords;
    
    // 出力する
    return out;
}
```

このようになります。
<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/898f977d-9a6f-5bfe-fa6f-d0fa532993ea.png">

y座標の値が大きくなるほど（y座標は下から上に大きくなる）、x座標が大きくなっている（左から右に大きくなる）のがわかります。

なお、シェーダーの宣言の方法や、`modelViewProjectionTransform`については、先程紹介した[Metal入門](https://booth.pm/ja/items/826318)に詳しくありますので、そちらをご覧ください。


### cosを与えて変形させてみる。

さきほど、`pos.x += pos.y;`としていたところを、次のように変更してみます。

```c++:Shader.metal
    pos.x += cos(pos.y);
```

このようになります。
<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/44683602-15e1-0567-c922-b41ec99ac93e.png">

面白いですね。cosを与えると-1〜1の範囲で波打つようになります。

こうした数式の考え方はこちらの本にわかりやすく書いてありました。

[Unityでわかる！ゲーム数学](https://www.amazon.co.jp/dp/B07D3M9Q68/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)
<img width="100px" src="https://m.media-amazon.com/images/I/51n7dPdci+L.jpg">

### 経過時間を与えてアニメーションさせてみる

さきほど、`pos.x += cos(pos.y);`としていたところを、次のように変更してみます。

```c++:Shader.metal
    pos.x += cos(pos.y + globalData.time);
```

こんな感じで面白い動きをします。
![qiita用動画.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/f81402c4-3c5e-47c7-536c-096ec9313167.gif)

最初に紹介した空中に水を浮かべる方法では、x, y, zのそれぞれについてこのような処理をすることで水のような感じを出しています。

NoteではiOS開発について定期的に発信していますので、フォローしていただけますと幸いです。
https://note.com/tokyoyoshida

Twitterでは簡単なtipsを発信しています。
https://twitter.com/jugemjugemjugem