SceneKitでMetalのシェーダを利用する(SCNProgram)
Jul 10, 2016 · oldmetalswift2
Metal
を使いたい場合にネックとなるのが、シーンの構築とかモデル・テクスチャの管理。
なので、その面倒な部分をSceneKit
に任せたいという時の話。
今回は描画周りにMetal
のシェーダを使うパターン。
主にSceneKit
でカスタムシェーダを使いたい場合は、
SCNProgram
SCNShadable
SCNTechnique
といった辺りがあるみたい。
SCNTechnique
はマルチパスのレンダリングに使うのがメインっぽい。
(これも試したけどシェーダへカスタム変数を渡す辺りでつまずいて放置)
SCNShadable
はMetal
での使えそうなサンプルがなかったので断念。
という訳で、WWDCのセッションの資料にあったSCNProgram
を使って実装。
下準備
プロジェクトはXcodeのデフォルトのテンプレートのGame
を流用している。
作成時のGame Technology
ではSceneKit
を選択する。
SceneKitの設定
Main.storeyboard
を開き、Game View Controller
のSceneKit View
のRendering API
をMetal
にするGameViewController
のviewDidLoad
の中のライト周りのコードを削除
(今回のシェーダはライトを使わないもので不必要なので消す)
SCNProgramの作成
以下のようにしてSCNProgram
を生成してシェーダの関数名を設定する
let program = SCNProgram()
program.vertexFunctionName = "textureVertex"
program.fragmentFunctionName = "textureFragment"
生成したSCNProgram
を適用させたいマテリアルに設定する
let material = ship.childNodes.first?.geometry?.firstMaterial!
material.program = program
シェーダの準備
通常どおりMetal
のファイルを追加した後に、
#include <SceneKit/scn_metal>
とする。
これは後述のSceneKit
とデータのやりとりに必要。
データの渡し方
SceneKit
から描画に必要なデータ(座標や変換行列、テクスチャなど)を
シェーダへ渡す方法
VertexShader側
頂点属性(位置とか法線とかuv座標とか)
まずは、
struct VertexInput {
float3 position [[ attribute(SCNVertexSemanticPosition) ]];
float2 texcoord [[ attribute(SCNVertexSemanticTexcoord0) ]];
};
という感じで、頂点属性の中で必要なものを構造体で定義する。
すると、変数名の後ろのAttribute Qualifier
(”[[]]“で囲まれた部分)で指定したものが
[[stage_in]]
にバインドされて自動で渡されてくる。
指定できるものはドキュメントの
Table 1 SceneKit Vertex Attribute Qualifiers for Metal Shaders
を参照
フレーム定数(ビューの変換行列など)
あらかじめSCNSceneBuffer
という構造体が用意されており、これらはその中に入っている。
このデータは[[buffer(0)]]
にバインドされて渡されてくる。
構造体の定義はドキュメントの
Frame-Constant Data
の項目を参照
ノード毎のデータ(モデルの変換行列など)
これは、あらかじめ用意された構造体がなく、代わりに必要なものをピックアップして 以下のように自分で構造体を定義する。
struct NodeBuffer {
float4x4 modelViewProjectionTransform;
};
すると、そのデータが[[buffer(1)]]
にバインドされてシェーダに渡される。
ピックアップできるものはドキュメントの
Listing 1 Available Fields for Per-Node Shader Data
を参照
カスタム変数
上記以外の変数は構造体として定義が必要。定義自体は通常通りに行う。 ただし、バインドされるバッファは2以降になる。
実装
まとめると、VertexShader
の宣言部分は以下の通り
vertex output myVertex(input in [[ stage_in ]],
constant SCNSceneBuffer& scn_frame [[ buffer(0) ]],
constant NodeBuffer& scn_node [[ buffer(1) ]],
constant CustomBuffer& custom [[ buffer(2) ]]) {
ここでの重要なポイントは引数名。
- scn_frame
とscn_node
は固定
(違う名前にすると正しくバインドされない)
- カスタム変数の引数名custom
はSceneKit
からデータを渡す時に使う
中での処理は必要な計算をして、それをFragmentShader
に渡すという、
通常のMetal
のシェーダと同じ実装を行う。
FragmentShader側
テクスチャ
テクスチャを利用したい場合は、特に事前の定義などは不要で通常通り宣言する
fragment half4 textureFragment(VertexOut in [[ stage_in ]],
texture2d<float> texture [[ texture(0) ]]) {
ただしここでも重要なポイントは引数名(詳細は後述)。
SceneKit側
カスタム変数とテクスチャ以外は自動でバインドされる
(=SceneKit側の処理は特にない)
カスタム変数
シェーダ側と同じ構造体のデータを準備する辺りは通常通り。
そのデータをシェーダ側にバインドするのは以下のようにsetValue
を利用する
var custom = CustomBuffer(color: float4(0, 0, 0, 1))
material.setValue(NSData(bytes: &custom, length:sizeof(CustomBuffer)),
forKey: "custom")
setValue
はSCNProgram
をセットしたのと同じ対象(今回はmaterial
)に行う。
また、value
はNSData
としてバイナリで渡し、key
はシェーダでの宣言と同じ名前にする。
テクスチャ
今回のモデルではマテリアルのdiffuse
にテクスチャが設定されているので、まずそれを取得する。
その後、SCNMaterialProperty
の形式にしてから変数と同様にsetValue
する
guard let contents = material.diffuse.contents else { return }
material.setValue(SCNMaterialProperty(contents: contents),
forKey: "texture")
ポイントは、SCNMaterialProperty
を生成しなおしてからセットすること。
直接diffuse
の中のデータをsetValue
すると正しくデータが渡されない。
命名の注意点
シェーダの引数名とSceneKit
側でsetValue
のkey
は一致させる必要がある。
さらに大事な点として、setValue
はKVO
を利用しているので、
命名時にはオブジェクトのプロパティとかぶる様な名前をつけてはいけない。
(例えばcolor
などは実行時にエラーログが出て連携ができない)
感想
今回はサンプルもなくとても苦戦した。。。特にシェーダとSceneKit間のデータのやりとり辺りは、
ドキュメントにも細かく書いてなくて苦労した。
妙に親切に頂点属性などをバインドしてくれると思ったら、
引数名固定だったり、テクスチャの再生成が必要だったりと落とし穴もいっぱい・・・
あと、引数名がスネークケースなのもいただけない。 他がキャメルケースなのでここは統一して欲しかった。
ただ、Metalのバインドの辺りの仕組みはどうなっているのか興味深いので、 もっといろいろ触ってみたい。
参考リンク
- Apple公式ドキュメント(SCNProgram)
開発環境
- OS X 10.11.5
- Xcode 7.3.1
- iOS 9.3.2
- iPhone 6+