Search Unity

最近シェーダーグラフが追加されたことで、Unity でのカスタムシェーダー作成が今までになく簡単になりました。しかし、デフォルトでどんなに種類豊富なノードが提供されたとしても、皆様の制作し得る全てのケースに対応することは不可能です。そこで、C# で新しいノードを作成できるカスタムノード API を開発しました。これを使用すれば、必要に応じてシェーダーグラフを拡張することができます。

本記事では、Unity 2018.1 ベータ版でこれを行う方法のひとつをご紹介します。シェーダー関数を作成するカスタムノードを最も簡単に作成するには、Code Function Node を使用します。この方法で新しいノードを作成する方法を具体的に見ていきましょう。

まず、C# スクリプトを新規作成します。この例では、スクリプトを MyCustomNode と名付けました。Code Function Node API を使用するには、名前空間 UnityEditor.ShaderGraph を含め(あるいはクラスをこの名前空間に追加し)、ベースクラス CodeFunctionNode から継承する必要があります。

ここでまず、MyCustomNode がエラーで強調表示されていることにお気付きでしょう。このメッセージの上にカーソルを乗せると、GetFunctionToConvert という名前の継承メンバーを実装する必要があることが確認できます。このノードの処理方法をシェーダーグラフに指示するために必要な処理の大部分はベースクラス CodeFunctionNode によって行われますが、結果の関数の実装については別途、記述する必要があります。

メソッド GetFunctionToConvert はリフレクションによって別のメソッドを MethodInfo のインスタンスに変換します。これが CodeFunctionNode によって変換され、シェーダーグラフ内で使用できるようになります。これにより、必要なシェーダー関数をより直感的に記述することが可能となります。

リフレクションに関する詳細は Microsoft のプログラミングガイドのリフレクション(C#)に関するページをご覧ください。

名前空間 System.Reflection とオーバーライド関数 GetFunctionToConvert を、以下の例のように追加してください。MyCustomFunction という文字列にご注目ください。これが、最終的なシェーダー内に書き込まれる関数の名前になります。これは記述する関数の内容に合わせて(頭文字を数字にしない限り)自由に命名可能です。本記事ではこれを MyCustomFunction という名前にします。

これでスクリプトのエラーが解決されたので、新しいノードの機能に取り掛かれます!まずは、名前を付ける必要があります。このクラス用の公開コンストラクターを引数なしで追加してください。その中で、変数 name に、ノードのタイトルを含む文字列を設定してください。これは、このノードがグラフ内に表示された時に、そのタイトルバー内に表示されます。この例では My Custom Node という名前にしています。

次に、このノードの関数自体を定義します。リフレクションに関する知識のある方は、メソッド GetFunctionToConvertMyCustomFunction というクラス内のメソッドにアクセスしようとしていることにお気付きでしょう。これが、シェーダー関数自体を定義するメソッドです。

戻り値の型が string の新しい静的メソッドを、メソッド GetFunctionToConvert 内の文字列と同じ名前で作成しましょう。この例では、MyCustomFunction がこれに当たります。このメソッドの引数内で、ノードに持たせたいポートを定義できます。これは最終的なシェーダー関数内で引数に直接マッピングされます。これを行うには、シェーダーグラフが対応しているタイプの引数を Slot 属性を付けて追加します。ここでは、A および B という名前の、タイプ DynamicDimensionVector の 2 つの引数を追加し、これに加えて Out という名前の、タイプ DynamicDimensionVectorout 引数を 1 つ追加しましょう。次に上記それぞれの引数にデフォルトの Slot 属性を追加します。それぞれの Slot 属性に固有のインデックスとバインディングが必要です。これは None に設定します。

使用可能なタイプとバインディングの一覧は GitHub の CodeFunctionNode API に関するドキュメンテーション(英語)をご覧ください。

以下のメソッド内で、シェーダー関数の内容を、戻り値の文字列内で定義します。これには、シェーダー関数の中括弧と、含めたい HLSL コードが含まれている必要があります。この例では、Out = A + B; と定義しましょう。今作成したメソッドは以下のようになります。

これは、シェーダーグラフに搭載の Add Node の中で使用されている C# コードと全く同じです。

ノードを機能させるには、もうひとつ最後に必要な作業があります。このノードは Create Node Menu 内のどこに表示されるべきか指示しなければなりません。これを行うには、該当クラスの上に Title 属性 を追加します。これは、メニュー階層内における表示位置を表す文字列の配列を定義します。この配列内の最後の文字列が、Create Node Menu 内でこのノードが表示される名前を定義します。この例では、ノード名は My Custom Node とし、Custom というフォルダー内に配置します。

これで、機能するノードが出来ました!Unity に戻ってスクリプトにコンパイルさせ、シェーダーグラフを開くと、この新しいノードがノード作成メニューに表示されます。

シェーダーグラフ内でこのノードのインスタンスを 1 つ作成してみてください。このインスタンスは、先に MyCustomFunction クラスの引数と同じ名前およびタイプで定義したポートを持っていることが確認できます。

これで、各種のポートタイプやバインディングを使って様々なノードが作成できます。メソッドの戻り値の文字列は、通常の Unity のシェーダー内で有効な HLSL であればどれでも含むことができます。以下は、3 つの入力値のうち最も小さいものを返すノードです。

そしてこれは、Boolean の入力値に基づいて法線を反転させるノードです。この例の中で、ポート NormalWorldSpaceNormal 用のバインディングを持っていることにご注目ください。このポートに接続されたエッジがない場合は、メッシュのワールド空間法線ベクトルがデフォルトで使用されます。詳細は GitHub の Port Binding に関するドキュメンテーション(英語)をご覧ください。また、Vector3 などの具体的な出力タイプを使用する場合には、シェーダー関数を戻す前にその定義を行う必要があります。この例ではこの値は使用されていません。

以上、シェーダーグラフ内で Code Function Node を使用してノードを作成する方法をご紹介しました。しかし、これはほんの始まりに過ぎません。シェーダーグラフは、システムをカスタマイズする上で、これ以外にも実に様々な方法で活用することができます。

今後もぜひ本ブログにご注目ください。また、フォーラムで皆様のご意見やご質問をお待ちしております!

20 コメント

コメントの配信登録

コメント受付を終了しました。

  1. Jonney Shih

    4月 23, 2018 1:04 am

    Instead of all this boilerplate you could’ve made a hybrid solution and let us write HLSL directly in the ShaderGraph, or atleast provide some interop between SG and .shader files.
    This is way overcsharping things!

  2. Alan Mattano

    3月 30, 2018 2:05 am

    WHERE IS THE BEST PLACE TO PUT THIS SCRIPT?

    1. Brandon Rivera-Melo

      4月 2, 2018 5:25 am

      Looks like this uses the UnityEditor namespace (using UnityEditor.ShaderGraphs;), so I’ve put it “Assets>[ProjectName]>Shaders>Editor”. Regardless if your overall hierarchy, I believe the namespace part requires it live in a folder titled “Editor” so this code can be left out from builds.

  3. Isaac Surfraz

    3月 28, 2018 12:56 pm

    has anyone moaning about the sting part here actually even bothered to look at the examples posted by andy touch and similar postings?

    I really think you are expecting it to be far more unusable than it is.

    Also you are defining the bindings using a string, not the entire shader. Calm down.

    1. It has little to do with how “usable” it is and everything to do with how this is an alpha/proof-of-concept approach being passed off as a production-ready system. If you *ever* have the developer writing anything script-related as a plain string (and embedded in another script file, no less), then someone, somewhere, royally screwed up. That is because writing script as a string within another script is highly-coupled, virtually-untestable, and difficult to maintain – the three most common signs of bad code design.

      And it’s not like this is the only way to do this, either. Myself and others have posted numerous alternative approaches in other comments. This system is nothing short of lazy and sloppy, and it will be addressed within a month (if not a week) of this feature’s official release by an editor add-on on the Asset Store. And while the problem will be effectively solved at that point, the issue is that the Unity Dev Team created the problem in the first place seemingly without even recognizing that it *is* a problem.

  4. Is it still possible to write shaders the old boring way? I don’t see how nodes really help with writing anything requiring custom algebra. I don’t mind the node system, as long as it doesn’t get in the way of my boring old HLSL approuch. I’m not to fond of creating code in a huge string like I have brain damage or something.

    1. Peter Bay Bastian

      3月 28, 2018 11:59 am

      Yes, absolutely. Shader Graph is a system on top of the existing shader system in Unity. You won’t be able to use your shaders written the old way in Shader Graph, but it also won’t prevent you from doing what you’ve always been doing.

      1. Awesome

  5. Interesting! But indeed seems like the wrong way around – you should concentrate on the hlsl first – have an hlsl file full of functions and nodes are created from that – if u need a little C# to create the nodes then ok but can you not parse the hlsl to get function names, input/output to a degree?

  6. It’s not so much that you released an API based on writing HLSL in a string literal that bothers me. We’ve all done worse things when necessary.

    It’s the fact that the announcement of this aspect of the API wasn’t wrapped in shameful apologies and embarrassed justifications. It concerns me – not that you’ve done it like this, but that you don’t seem to think it’s anything to be bothered about.

  7. Could you expand more on the “why” to this approach? I echo the other comments here, I feel this is long winded and prone to error writing the HLSL as a string… beyond simple stuff like in this blog, in production (using proprietary engines that have shader graphs with custom nodes), these custom nodes can get quiet elaborate, like, it might be a lengthy “Draw Object Outline” node…

    So – hearing the why (and how you arrived at this approach compared to all these others suggested/attempted) would be great as it would help put some context to it all. Cheers!

  8. This is the worst workflow I’ve ever seen in Unity.

  9. CorruptScanline

    3月 27, 2018 8:55 pm

    Why not just define the inputs and outputs as class members and have a virtual GenerateCode function? In any case the awkward GetFunctionToConvert reflection stuff could be hidden by having a ShaderFunction attribute that you put on whatever function you want.

  10. So. I am liking. strings’s not a problem for me. I’m used to writing shaders without autocomplitionDD
    I’ve been eagerly awaiting for 2018 release.

  11. writing shader function body in a string ? seriously ? this is what you released ?
    did you ever think what this might look like exposed for the user when you were designing shader graph ?
    that’s laughable

  12. Michal Piatek

    3月 27, 2018 5:35 pm

    It’s cool and simple but I dislike the fact that I will be forced to either use C# code highlighting&autocompletion or HLSL. Unless I missed something and there are options to do that, in let’s just say, VS Code.

  13. Binding the method via a reflection reference? Returning the entire shader function code as a string? This feels super clunky, not gonna lie. Why can’t you pass a reference to the method itself instead of reflecting it, for example? Why can’t you just supply a shader file instead of going through the hassle of converting it into a string literal?

    I’m sure there must be technical reasons why it was implemented this way, but for the life of me I can’t imagine what they could be other than the Shader Graph team having designed themselves into a corner on this.

    1. We could do this via a function reference, but it would just tidy the user facing API a bit. Well look into it but its not a huge priority for us. We use reflection to access the function arguments and convert them to ports, all this would do is hide that.

      As for the string literals we do this for simplicity, we current ship ~150 nodes, with the vast majority using this abstraction. Splitting the HLSL into separate files isn’t optimal for us. However, message received. You can still do what you want to do via this API though. Something along the lines of return File.Read("otherfile.hlsl"). This just isnt mentioned in this blog post (maybe it should have been).

      1. Arthur Brussee

        3月 28, 2018 12:17 am

        I feel like the string is mostly fine – it’s tightly coupled with the names and such of the surrounding function, so it’s easier to have it inline. However, I feel there’s 2 paths forward:

        ->Most ambitious: Generate HLSL from a C# subset, Xenko style (http://doc.xenko.com/latest/en/manual/graphics/effects-and-shaders/shading-language/shader-classes-mixins-and-inheritance.html). It seems unity is doing _some_ of that in their new pipelines already, and the new math library even makes more syntax match. The idea of being able to build shaders out of reusable C# snippets would be an incredible dev experience.

        -> Little more reasonable: Work with the UnityVS team to recognize these snippets as HLSL, and use the HLSL coloring / autocomplete on them.

      2. Andrew Ackerman

        3月 28, 2018 2:01 am

        I don’t really dispute that the existence of this class may or may not be necessary (the mapping needs to be done somewhere) but this strikes me as a very odd way of going about it. Using this approach, the C# class is almost entirely arbitrary boilerplate while the embedded HLSL code is represented as a string literal, which means no syntax coloring, compile-time type safety, Intellisense, or any of that other goodness.

        IMHO, the C# code should be entirely abstracted away, auto-generated for any but the people who have specific reasons to do it manually (whatever those reasons might be). Maybe when the developer chooses the option to create a custom shader node, there is a properties window for the file that displays the ins and outs similarly to how properties for shaders are currently handled, and that is where you can specify what properties the node will have. Or, when they get defined in the shader file, the C# file gets updated with the properties when the shader is “compiled”.

        As far as the HLSL code itself, while it might technically be the most “optimal” to have the shader code and the C# code in a single script file, that doesn’t make it the best option. This approach is extremely unfriendly to anyone who doesn’t know HLSL by heart and can code out a shader file in their sleep, and even for those people, the ability to use regular HLSL development tools and not having to essentially write their shader using vanilla Notepad is still a plus. (And yes, while you could write the code in an .hlsl file and load the string with a File.Read, that is an extremely arbitrary step that is just avoiding the problem rather than actually solving it.)