凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity で Shader Graph の Custom Function Node を試してみた

はじめに

以下の公式エントリで UnityShader Graph における Custom Function Node の存在を知ったので試してみた記事です。

blogs.unity3d.com

環境

  • Unity 2019.3.1f1
  • Shader Graph 7.1.8

デモ

github.com

従来の問題点

Shader Graph はグラフィカルにシェーダを構築出来てとても便利ですが、「あ、これコードで書いたら簡単なのにノードで表現すると複雑...」みたいなケースに当たることが良くあると思います。たとえばアルファ付きのテクスチャやスプライトのアウトラインが欲しいケースを考えてみます。テクスチャにアウトラインを付ける場合、UV を上下左右にちょっとずつずらして足し合わせて上げるとつけることが出来ます。ノードで表現すると次のような感じです:

f:id:hecomi:20200224114504p:plain

前半の囲った部分がアウトラインの生成部です。コードで書いた場合は UV をずらして足し合わせるのは数行で事足りますが、ノードにすると複雑になってしまいますね。

この要求に対して、C# で自作ノードを生成する方法が以下の記事で紹介されています。

blogs.unity3d.com

ただ、毎回いちいちスクリプトを作るのは面倒です。そこで、Custom Function Node の出番です。

Custom Function Node

docs.unity3d.com

このノードではノード内にコードを記述することが出来ます。出力と入力変数を定義するとノードの入出力にそのスロットが追加され、入力変数を使って出力変数に値を詰めるようにすると動作します。では具体的に先程のアウトラインの例がどうなるか見てみましょう。

f:id:hecomi:20200224120744p:plain

ロジックはほとんど Custom Function Node に入ります。ノードの設定は右上の歯車アイコンから行うことが出来、次のように設定しました:

f:id:hecomi:20200224121230p:plain

Alpha = 0.0;
float r = Outline;

Alpha += SAMPLE_TEXTURE2D(Texture, Sampler, UV + float2(+r,  0)).a;
Alpha += SAMPLE_TEXTURE2D(Texture, Sampler, UV + float2(-r,  0)).a;
Alpha += SAMPLE_TEXTURE2D(Texture, Sampler, UV + float2( 0, +r)).a;
Alpha += SAMPLE_TEXTURE2D(Texture, Sampler, UV + float2( 0, -r)).a;
Alpha = min(Alpha, 1.0);

Alpha -= SAMPLE_TEXTURE2D(Texture, Sampler, UV).a;
Alpha = max(Alpha, 0.0);

今回は一つの関数内で簡単に記述できるロジックでしたが、uniform 変数が欲しいときや、複数の関数呼び出しを使って何かするようなより複雑なロジックが必要なときは、TypeString から File に変更すれば、Body が関数展開されずにより柔軟な記述が可能です。コードは TextAsset で与えられ、.hlsl で保持する形になります。ドキュメントにも書いてあるように幾つかのルールを守る必要があるのでちょっと大変になります。以下、上記コードをファイルで書いた例です:

#ifndef SAMPLE_OUTLINE_INCLUDED
#define SAMPLE_OUTLINE_INCLUDED

void SampleOutline_float(
    Texture2D Texture,
    float2 UV,
    SamplerState Sampler,
    float Outline,
    out float Alpha)
{
    Alpha = 0.0;
    float r = Outline;

    Alpha += SAMPLE_TEXTURE2D(Texture, Sampler, UV + float2(+r,  0)).a;
    Alpha += SAMPLE_TEXTURE2D(Texture, Sampler, UV + float2(-r,  0)).a;
    Alpha += SAMPLE_TEXTURE2D(Texture, Sampler, UV + float2( 0, +r)).a;
    Alpha += SAMPLE_TEXTURE2D(Texture, Sampler, UV + float2( 0, -r)).a;
    Alpha = min(Alpha, 1.0);

    Alpha -= SAMPLE_TEXTURE2D(Texture, Sampler, UV).a;
    Alpha = max(Alpha, 0.0);
}

#endif

precision をサフィックスにつけたり、返す変数は out で指定したりといった Shader Graph のルールを守る必要があります。そしてこのファイルを次のように Source に設定します。

f:id:hecomi:20200224124938p:plain

なお、指定できるファイルは TextAsset ですが .hlsl か .cginc だけのようです。そうしない場合は、variable '_CustomFunction_A36DA959_Alpha_1' used without having been completely initialized といった文言でエラーが表示されます(おそらく Shader Graph のバグで、本来は .hlsl か .cginc だけだよ、といった文言が出るのが設計っぽいです)。

f:id:hecomi:20200224124737p:plain

コードの再利用

複数のグラフにまたがって使う場合は Sub Graph 化をします。不要な変数も省けてシンプルになるのでそういった用途にも使えると思います。

f:id:hecomi:20200224125804p:plain

f:id:hecomi:20200224134433p:plain

ただ、Sub Graph にするとプレビューが Quad でなく Sphere になってしまうのが不便で...、どなたか修正方法ご存じの方いらっしゃいましたら教えて下さい。

発展

その他の用途としては、冒頭の公式ブログにもかかれていたように、シェーダグラフでは公開されていない変数にアクセスするためのノードを作ったりも出来ます。

より進んだテクニックについては Youtube に幾つかあるので見てみてください:

おわりに

あとは Tags や Blend、デプス出力などが拡充されれば uRaymarching もシェーダグラフ対応ができるかも。。