最近シェーダーグラフが追加されたことで、Unity でのカスタムシェーダー作成が今までになく簡単になりました。しかし、デフォルトでどんなに種類豊富なノードが提供されたとしても、皆様の制作し得る全てのケースに対応することは不可能です。そこで、C# で新しいノードを作成できるカスタムノード API を開発しました。これを使用すれば、必要に応じてシェーダーグラフを拡張することができます。
本記事では、Unity 2018.1 ベータ版でこれを行う方法のひとつをご紹介します。シェーダー関数を作成するカスタムノードを最も簡単に作成するには、Code Function Node を使用します。この方法で新しいノードを作成する方法を具体的に見ていきましょう。
まず、C# スクリプトを新規作成します。この例では、スクリプトを MyCustomNode と名付けました。Code Function Node API を使用するには、名前空間 UnityEditor.ShaderGraph を含め(あるいはクラスをこの名前空間に追加し)、ベースクラス CodeFunctionNode から継承する必要があります。
using UnityEngine; using UnityEditor.ShaderGraph; public class MyCustomNode : CodeFunctionNode { }
ここでまず、MyCustomNode がエラーで強調表示されていることにお気付きでしょう。このメッセージの上にカーソルを乗せると、GetFunctionToConvert という名前の継承メンバーを実装する必要があることが確認できます。このノードの処理方法をシェーダーグラフに指示するために必要な処理の大部分はベースクラス CodeFunctionNode によって行われますが、結果の関数の実装については別途、記述する必要があります。
メソッド GetFunctionToConvert はリフレクションによって別のメソッドを MethodInfo のインスタンスに変換します。これが CodeFunctionNode によって変換され、シェーダーグラフ内で使用できるようになります。これにより、必要なシェーダー関数をより直感的に記述することが可能となります。
リフレクションに関する詳細は Microsoft のプログラミングガイドのリフレクション(C#)に関するページをご覧ください。
名前空間 System.Reflection とオーバーライド関数 GetFunctionToConvert を、以下の例のように追加してください。MyCustomFunction という文字列にご注目ください。これが、最終的なシェーダー内に書き込まれる関数の名前になります。これは記述する関数の内容に合わせて(頭文字を数字にしない限り)自由に命名可能です。本記事ではこれを MyCustomFunction という名前にします。
using UnityEngine; using UnityEditor.ShaderGraph; using System.Reflection; public class MyCustomNode : CodeFunctionNode { protected override MethodInfo GetFunctionToConvert() { return GetType().GetMethod("MyCustomFunction", BindingFlags.Static | BindingFlags.NonPublic); } }
これでスクリプトのエラーが解決されたので、新しいノードの機能に取り掛かれます!まずは、名前を付ける必要があります。このクラス用の公開コンストラクターを引数なしで追加してください。その中で、変数 name に、ノードのタイトルを含む文字列を設定してください。これは、このノードがグラフ内に表示された時に、そのタイトルバー内に表示されます。この例では My Custom Node という名前にしています。
using UnityEngine; using UnityEditor.ShaderGraph; using System.Reflection; public class MyCustomNode : CodeFunctionNode { public MyCustomNode() { name = "My Custom Node"; } protected override MethodInfo GetFunctionToConvert() { return GetType().GetMethod("MyCustomFunction", BindingFlags.Static | BindingFlags.NonPublic); } }
次に、このノードの関数自体を定義します。リフレクションに関する知識のある方は、メソッド GetFunctionToConvert が MyCustomFunction というクラス内のメソッドにアクセスしようとしていることにお気付きでしょう。これが、シェーダー関数自体を定義するメソッドです。
戻り値の型が string の新しい静的メソッドを、メソッド GetFunctionToConvert 内の文字列と同じ名前で作成しましょう。この例では、MyCustomFunction がこれに当たります。このメソッドの引数内で、ノードに持たせたいポートを定義できます。これは最終的なシェーダー関数内で引数に直接マッピングされます。これを行うには、シェーダーグラフが対応しているタイプの引数を Slot 属性を付けて追加します。ここでは、A および B という名前の、タイプ DynamicDimensionVector の 2 つの引数を追加し、これに加えて Out という名前の、タイプ DynamicDimensionVector の out 引数を 1 つ追加しましょう。次に上記それぞれの引数にデフォルトの Slot 属性を追加します。それぞれの Slot 属性に固有のインデックスとバインディングが必要です。これは None に設定します。
static string MyCustomFunction( [Slot(0, Binding.None)] DynamicDimensionVector A, [Slot(1, Binding.None)] DynamicDimensionVector B, [Slot(2, Binding.None)] out DynamicDimensionVector Out) { }
使用可能なタイプとバインディングの一覧は GitHub の CodeFunctionNode API に関するドキュメンテーション(英語)をご覧ください。
以下のメソッド内で、シェーダー関数の内容を、戻り値の文字列内で定義します。これには、シェーダー関数の中括弧と、含めたい HLSL コードが含まれている必要があります。この例では、Out = A + B; と定義しましょう。今作成したメソッドは以下のようになります。
static string MyCustomFunction( [Slot(0, Binding.None)] DynamicDimensionVector A, [Slot(1, Binding.None)] DynamicDimensionVector B, [Slot(2, Binding.None)] out DynamicDimensionVector Out) { return @" { Out = A + B; } "; } }
これは、シェーダーグラフに搭載の Add Node の中で使用されている C# コードと全く同じです。
ノードを機能させるには、もうひとつ最後に必要な作業があります。このノードは Create Node Menu 内のどこに表示されるべきか指示しなければなりません。これを行うには、該当クラスの上に Title 属性 を追加します。これは、メニュー階層内における表示位置を表す文字列の配列を定義します。この配列内の最後の文字列が、Create Node Menu 内でこのノードが表示される名前を定義します。この例では、ノード名は My Custom Node とし、Custom というフォルダー内に配置します。
[Title("Custom", "My Custom Node")] public class MyCustomNode : CodeFunctionNode {
これで、機能するノードが出来ました!Unity に戻ってスクリプトにコンパイルさせ、シェーダーグラフを開くと、この新しいノードがノード作成メニューに表示されます。
シェーダーグラフ内でこのノードのインスタンスを 1 つ作成してみてください。このインスタンスは、先に MyCustomFunction クラスの引数と同じ名前およびタイプで定義したポートを持っていることが確認できます。
これで、各種のポートタイプやバインディングを使って様々なノードが作成できます。メソッドの戻り値の文字列は、通常の Unity のシェーダー内で有効な HLSL であればどれでも含むことができます。以下は、3 つの入力値のうち最も小さいものを返すノードです。
static string Min3( [Slot(0, Binding.None)] DynamicDimensionVector A, [Slot(1, Binding.None)] DynamicDimensionVector B, [Slot(2, Binding.None)] DynamicDimensionVector C, [Slot(3, Binding.None)] out DynamicDimensionVector Out) { return @" { Out = min(min(A, B), C); } "; } }
そしてこれは、Boolean の入力値に基づいて法線を反転させるノードです。この例の中で、ポート Normal が WorldSpaceNormal 用のバインディングを持っていることにご注目ください。このポートに接続されたエッジがない場合は、メッシュのワールド空間法線ベクトルがデフォルトで使用されます。詳細は GitHub の Port Binding に関するドキュメンテーション(英語)をご覧ください。また、Vector3 などの具体的な出力タイプを使用する場合には、シェーダー関数を戻す前にその定義を行う必要があります。この例ではこの値は使用されていません。
static string FlipNormal( [Slot(0, Binding.WorldSpaceNormal)] Vector3 Normal, [Slot(1, Binding.None)] Boolean Predicate, [Slot(2, Binding.None)] out Vector3 Out) { Out = Vector3.zero; return @" { Out = Predicate == 1 ? -1 * Normal : Normal; } "; } }
以上、シェーダーグラフ内で Code Function Node を使用してノードを作成する方法をご紹介しました。しかし、これはほんの始まりに過ぎません。シェーダーグラフは、システムをカスタマイズする上で、これ以外にも実に様々な方法で活用することができます。
今後もぜひ本ブログにご注目ください。また、フォーラムで皆様のご意見やご質問をお待ちしております!!