MetalシェーダでSceneKitのマテリアルを描画する

SCNProgramというクラスを使って、**SceneKitのマテリアルとかジオメトリの描画をMetalシェーダで行う**ことができます。

```swift
let program = SCNProgram()
program.fragmentFunctionName = "myFragment"
program.vertexFunctionName = "myVertex"
material.program = program
```

この `program` というプロパティは SCNShadable というプロトコルに用意されていて、そのプロトコルにSCNMaterialやSCNGeometryが適合しています。

```swift:SCNMaterial
open class SCNMaterial : NSObject, SCNAnimatable, SCNShadable, NSCopying, NSSecureCoding {
```

このへんはなかなか情報がないのですが、以下の記事が参考になりました。

- [Metal Shader with SceneKit SCNProgram - Stack Overflow](http://stackoverflow.com/questions/37697939/metal-shader-with-scenekit-scnprogram)
- [SceneKitでMetalのシェーダを利用する(SCNProgram) · M.Ike](https://mike-neko.github.io/blog/metalshader-scenekit/)

最終的にはAppleの公式ドキュメントが頼りになります。

- [SCNShadable - SceneKit | Apple Developer Documentation](https://developer.apple.com/reference/scenekit/scnshadable)
- [SCNProgram - SceneKit | Apple Developer Documentation](https://developer.apple.com/reference/scenekit/scnprogram)

本記事では主にマテリアルの描画をMetalシェーダで行う場合のアレコレについて書きます。


##基本的な実装：テクスチャ描画をMetalで行う

画像ファイルから読み込んだテクスチャをMetalで描画する場合、Metalシェーダ、SceneKitコードを以下のように実装します。

###Metalシェーダ側

```cpp:Shaders.metal
#include <metal_stdlib>
using namespace metal;

#include <SceneKit/scn_metal>

struct MyNodeBuffer {
    float4x4 modelViewProjectionTransform;
};

struct MyVertexInput {
    float3 position [[attribute(SCNVertexSemanticPosition)]];
    float2 texCoords [[attribute(SCNVertexSemanticTexcoord0)]];
};

struct SimpleVertex
{
    float4 position [[position]];
    float2 texCoords;
};

vertex SimpleVertex myVertex(MyVertexInput in [[stage_in]],
                             constant SCNSceneBuffer& scn_frame [[buffer(0)]],
                             constant MyNodeBuffer& scn_node [[buffer(1)]])
{
    SimpleVertex vert;
    vert.position = scn_node.modelViewProjectionTransform * float4(in.position, 1.0);
    vert.texCoords = in.texCoords;
    
    return vert;
}


fragment half4 myFragment(SimpleVertex in [[stage_in]],
                          texture2d<float, access::sample> diffuseTexture [[texture(0)]])
{
    constexpr sampler sampler2d(coord::normalized, filter::linear, address::repeat);
    float4 color = diffuseTexture.sample(sampler2d, in.texCoords);
    return half4(color);
}
```

通常のMetalシェーダ実装との違いとしては、

- `#include <SceneKit/scn_metal>` が必要
- 頂点の属性は、`[[attribute(SCNVertexSemanticPosition)]]` や `[[attribute(SCNVertexSemanticTexcoord0)]]` 等、必要なものを構造体で持っておく
- `modelViewProjectionTransform`とかの変換行列は、あらかじめ用意されたものがあるので、必要なものをピックアップして構造体を定義する。このデータは `[[buffer(1)]]` に渡されてくる

```cpp:Available_Fields_for_Per-Node_Shader_Data
float4x4 modelTransform;
float4x4 inverseModelTransform;
float4x4 modelViewTransform;
float4x4 inverseModelViewTransform;
float4x4 normalTransform; // Inverse transpose of modelViewTransform
float4x4 modelViewProjectionTransform;
float4x4 inverseModelViewProjectionTransform;
float2x3 boundingBox;
float2x3 worldBoundingBox;
```

といったあたりです。

参考記事に示したStackOverflowの回答ほぼそのまんまですが、これでSCNMaterialのテクスチャ描画がMetalによって行われるようになります。個人的な用途としてはフラグメントシェーダをカスタムしたかったので、頂点シェーダの実装はこれをそのまま使いまわしています。


###SceneKitコード側

```swift:SomeClass.swift
let program = SCNProgram()
program.fragmentFunctionName = "myFragment"
program.vertexFunctionName = "myVertex"
material.program = program

let image = UIImage(named: filename)!
let imageProperty = SCNMaterialProperty(contents: image)
material.setValue(imageProperty, forKey: "diffuseTexture")
```

こちらはものすごくシンプルです。フラグメントシェーダにテクスチャを渡すために、

- SCNMaterialPropertyオブジェクトとして用意
- SCNMaterialの"diffuseTexture"にセット

しています。こうすることで、シェーダ側の引数 `diffuseTexture [[texture(0)]]` にテクスチャデータが渡されます。**キー名／引数名は一致していれば何でもok**です。


##SceneKitコードからMetalシェーダに任意の値を渡す

###浮動小数点数を渡す

たとえばフラグメントシェーダの引数に`Float`型の「経過時間」を渡したい場合、次のようにします。

####Metalシェーダ側

```cpp:Shaders.metal
fragment half4 myFragment(SimpleVertex in [[stage_in]],
                          constant float &time [[buffer(0)]])
```

####SceneKitコード側

```swift:SomeClass.swift
private lazy var startTime = Date()
```

```swift:SomeClass.swift
Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true, block: { (timer) in
    self.updateTime()
})
```

```swift:SomeClass.swift
private func updateTime() {
    var time = Float(Date().timeIntervalSince(startTime))
    let timeData = Data(bytes: &time, count: MemoryLayout<Float>.size)
    node.geometry?.firstMaterial?.setValue(timeData, forKey: "time")
}
```

テクスチャを渡したときとほぼ一緒ですが、今回は `Float` の値を渡したいので、以下の点が違います。

- `SCNMaterialProperty` ではなく `Data` で渡す
- `[[texture(0)]]` ではなく `[[buffer(0)]]` で受け取る


###構造体を渡す

上の例では `Float` 型の値をMetalシェーダに渡したわけですが、たとえば `Uniforms` という構造体を次のように定義して、これをシェーダに渡したいとします。

```swift
struct Uniforms {
    var resolution: float2
}
```

やることはほぼ同じです。

```swift:SomeClasss.swift
let screenSize = UIScreen.main.nativeBounds.size
var uniforms = Uniforms(resolution: float2(Float(screenSize.width), Float(screenSize.height)))
let uniformsData = Data(bytes: &uniforms, count: MemoryLayout<Uniforms>.size)
node.geometry?.firstMaterial?.setValue(uniformsData, forKey: "uniforms")
```

Metalシェーダ側では `Uniforms &uniforms [[buffer(0)]]` という引数で値を受け取れます。



###配列を渡す

例えばLineという線分の描画情報（始点・終点etc）の入った構造体を配列でMetalシェーダに渡したいとします。


####SceneKitコード側

ここでのポイントは、バッファサイズを `handleBindingOfBufferNamed:frequency:usingBlock:` で渡してやること。こうしないと、GPUで処理するときにバッファサイズが合わないっていう実行時エラーになります。

```objc:SomeClass.mm
std::vector<Line> lines = // 略;
NSData *linesData = [Utils dataFromLines:lines];
NSUInteger numLines = lines.count;
NSData *numLinesData = [NSData dataWithBytes:&numLines length:sizeof(NSUInteger)];
[program handleBindingOfBufferNamed:@"lines" frequency:SCNBufferFrequencyPerFrame usingBlock:^(id<SCNBufferStream>  _Nonnull buffer, SCNNode * _Nonnull node, id<SCNShadable>  _Nonnull shadable, SCNRenderer * _Nonnull renderer) {
    [buffer writeBytes:(void *)linesData.bytes length:linesData.length];
}];
```

```objc:Utils.mm
+ (NSData *)dataFromLines:(std::vector<Line>)lines
{
    NSMutableData *data = [NSMutableData data];
    for (int i=0; i<lines.size(); i++) {
        Line line = lines[i];
        NSData *lineData = [NSData dataWithBytes:&line length:sizeof(Line)];
        [data appendData:lineData];
    }
    return data;
}
```

この実装をしていたときC++のコードとの連携の関係で、ここだけObjective-C++ですが、まぁポイントはSwiftでも同様です。

このAPIはググってもAppleのリファレンスしか出てこなくて、本当に情報がなかったです...


####Metalシェーダ側
    
```cpp:Shaders.metal
fragment half4 myFragment(SimpleVertex in [[stage_in]],
                                 const device Line *lines [[buffer(0)]],
                                 constant uint &numLines [[buffer(1)]])
```

**要素数も別引数で受け取る必要がある**、という点に注意。

- 参考：
  - http://stackoverflow.com/questions/38164634/compute-sum-of-array-values-in-parallel-with-metal-swift

##透過を有効にする

シェーダでアルファを指定したり、`discard_fragment` しても**デフォルトでは透過にならない**ので、

```
program.isOpaque = false;
```

をやっておきます。


##作例

SceneKitでSCNBoxをジオメトリとするノードを置いたサンプルをベースに、

![none.gif](https://qiita-image-store.s3.amazonaws.com/0/3180/e5055dd7-d67c-ae9a-82c7-856854520ee9.gif)

GLSL Sandboxを参考にMetalシェーダを書いてみます。

参考： [[iOS] Metalシェーダことはじめ - WebGL/GLSLの豊富なサンプルを参考にする - Qiita](http://qiita.com/shu223/items/dd5a53b8c291abe30fc2)


- http://glslsandbox.com/e#36858.0

![color.gif](https://qiita-image-store.s3.amazonaws.com/0/3180/4aead202-00e0-20e7-ffad-3208c31ab041.gif)


- http://glslsandbox.com/e#37017.0

![voronoi.gif](https://qiita-image-store.s3.amazonaws.com/0/3180/d16268ae-628e-9e32-7183-39e75912d68f.gif)


※実際の開発でこういうシェーダを利用したかったのではなく、円とか線を3D空間上に描画するのにSceneKit+Metalを利用しました。