マテリアルエディタのCustomノードあれこれ


Unreal Engine 4のマテリアルノードあれこれ

 UE4には強力なマテリアルエディタが搭載されています。ユーザーが自由に使える多数のマテリアルノードが用意されており、シェーダーの知識が無くても、それらをつなげることで多彩なシェーダー表現が可能になります。
 とは言え、シェーダーでできる事がすべてマテリアルノードでできるわけではないですし、シェーダーの知識を全く持たずにマテリアルを作成するよりも、多少の知識を持っていたほうが効率よく、パフォーマンスの良いマテリアルを作成できます。
 マテリアルノードにはCustomという、直にシェーダーコードを記述することのできるノードが用意されています。今回の記事では、Customノードでこんな事ができるという例を挙げながらマテリアルノードのTips的なものを披露していきたいと思います。

この記事は、Unreal Engine 4.18.1を元にしています。

Customノードとは

 シェーダーコードを直接書けるマテリアルノードです。入力を複数設定できますが、出力はひとつです。基本的にはHLSLに準拠したシェーダーコードを使用することができます。
公式ドキュメント:Custom 表現式

Customノードの使用例

 ではCustomノードでこんなことができるという例を紹介します。

Light Sourceとして置かれている、シーンのディレクショナルライトの方向を取得する。

 のっけからわりと大技です。シェーダー内部でシェーディングに使用されるライトの方向を取得したい場合がありますが、そういった機能のノードがありません。LightVectorというノードはあるのですが、使おうとすると

エラー SM5 LightVector can only be used in LightFunction or DeferredDecal materials

と表示されてしまいます。
そこで、Customノードに以下のコードを書きます。

return ResolvedView.DirectionalLightDirection;

すると、シーンを代表する平行光源への方向を正規化されたベクトルで返してくれます。通常はシーンに置かれたStationayな平行ライトです。
例として、UnlitなマテリアルでLambertシェーディングをしてみます。

 光源方向のベクトルと法線の内積を取ると、そのピクセルの明るさを得ることができます。-1~1の値になるのですが、0以下は光が当たっていないので、saturateで0~1にします。
 光源ベクトルが取得できると、NPRっぽいマテリアルを作成したり、フレネルっぽい計算をしたりできます。
 0より大きい数字をすべて1にすれば、簡単なセルシェーダーにもなります。
 0より大きい場合に1にするには、ceilを使うと良いでしょう。


 0以上じゃなくて、-0.1以上なら1にしたいといった場合は、stepを使うのが良いのですが、マテリアルノードに無いので、Customノードでつくります。

セルシェーディングの話はよろしかったらこちらも参考にしてください
https://www.slideshare.net/TatsuhiroTanaka1/unreal-engine-npr-80721783

GBufferの内容を取得する

 ディファードライティングのポストプロセス用のマテリアルの話になりますが、ポストプロセスマテリアルではSceneTextureノードでGBufferの内容を取得できます。
 しかし、GBuffer情報の全部の項目が用意されているわけではありません。例えば、シェーディングモデルを選択すると、シェーディングモデルごとに設定されている表示用の色が取得できますが、実際のShadingModelIDを取得することができません。そんな時はCustomノードを使うことで全ての項目を取得できます。
 例えばShading Model IDを数値のまま取得したい場合、コードでは

FScreenSpaceData ScreenSpaceData = GetScreenSpaceData(inUV, false);
return ScreenSpaceData.GBuffer.ShadingModelID;

 でShadingModeIDを直接取り出すことができます。
 FScreenSpaceData構造体の中には、GBufferの情報や、アンビエントオクルージョンなど、様々な情報が格納されています。
 ではやってみましょう。

と思ったら、コンパイルエラーが出てしまいます。

MaterialTemplate.usfを見ると、GBufferにアクセスする場合は、

MaterialTemplate.usf
#if NEEDS_SCENE_TEXTURES
#include "DeferredShadingCommon.usf"        // GetGBufferData()

 このNEEDS_SCENE_TEXTURESのdefineが1になっている必要があります。これを1にするには、Customノードになんでも良いので、SceneTextureノードのColor出力を繋いでやります。別に実際にその値を使う必要はありません。ただ繋ぐだけでコンパイルが通るようになります。

 ShadingModelIDを評価して、Unlit以外の部分にポストエフェクトをかけたいといった用途に使用できます。
 SceneTextureで同時に複数の値を使用したい場合は、CustomノードでFScreenSpaceData構造体ごと取得することで一度に取得できるので、パフォーマンスの向上が図れる場合もあります。

 と、ここまでは4.20以前のお話。4.20ではポストプロセスのコンパイル時にBasePassShaderも作成しようとして、

 と、怒られてしまいます。これを回避するには、Customノード内のFScreenSpaceDataを使用する部分を

#if SCENE_TEXTURES_DISABLED
// FScreenSpaceDataを使わない。BasePassShaderとしてコンパイルされる場合。
    return 0;
#else
// FScreenSpaceDataを使う。PostProceeMaterial時
    FScreenSpaceData ScreenSpaceData = GetScreenSpaceData(inUV, false);
    return ScreenSpaceData.GBuffer.ShadingModelID;
#endif

といった感じで分岐してあげましょう。
「ポストプロセスマテリアルなのに、なんでBasePassShaderなんか作るんだよ」と思いますが、プレビュー用のマテリアルも作成するので、その際にコンパイルエラーが出て、マテリアル全体が無効になってしまうようです。

 余談ですが、Unlitのピクセルだけ検出するには他にも方法があります。Unlitなマテリアルは法線情報を持たないので、GBufferの法線情報は(0,0,0)になっています。
 GBufferから法線を取り出す際にはDecodeNormalという関数で、{ return N * 2 - 1; }という変換が行われます。GBufferには0~1の範囲の数値しか格納できないため、-1~1の範囲の数値を持つ法線ベクトルを{ return N * 0.5 + 0.5; }という変換で0~1の範囲にして格納されたものを元の範囲に戻す計算です。
 ところが、Unlitの場合は(0,0,0)のままなので、{ N * 2 - 1; }で計算すると結果は(-1, -1, -1)になります。なので、法線ベクトルを取得してxyz各要素を足して-3になった場合は、そのピクセルはUnlitなマテリアルで描画されたとみなすことができます。Customコードで書くなら

return inVec.x+inVec.y+inVec.z == -3;

 でLitなピクセルは0、UnLitなピクセルは1が返ります。
 将来的にずっとGBufferを0で初期化するかはわからないので確実な方法ではありませんが。

 話を戻して、Unlitを区別して何ができるかというと、天球のシェーダーはUnlitなので、空にはポストプロセスをかけたくない場合にUnlit部分を判別してポストプロセスをスキップするといった使い方ができます。他にもUnlitなピクセルがあると、それも拾っちゃいますけど。

テクスチャオブジェクト

 Customノードでテクスチャを直接扱いたい場合は、Texture Objectを入力につなげば良いのですが、テクスチャを読み出すには、テクスチャサンプラーも必要です。どうすれば良いかというと、実は入力テクスチャ変数名に、"Sampler"をくっつけたものが自動的に引数としてセットされます。
 読み出すミップレベルを指定できるCustomノードを作ってみました。

return Texture2DSampleLevel(inTexture, inTextureSampler, inUV, inMipLevel).rgb;


 このように、既存のマテリアルノードではできない特殊なテクスチャアクセスも記述することができます。

Customノード作成のコツ

 Customノードの仕組みを見るには、Customノードを含んだマテリアルをマテリアルエディタで開いて、ウィンドウ->HLSLコードを選択し、そのマテリアルのシェーダーのソースを見てみると良い。

// Uniform material expressions.
MaterialFloat3 CustomExpression0(FMaterialPixelParameters Parameters)
{
return ResolvedView.DirectionalLightDirection;
}

 こんな風にCustomノードの中身を持った関数が作成されています。
そして、このHLSLコード全体がコンパイルされてマテリアルシェーダーになります。
 と、言うことはこのHLSLコードでincludeしている他のShaderコードも利用可能という事になります。
 頭の方を見ていくと、
#include "Random.usf"
 お、乱数欲しかったんだよね。Radom.usfの中を見ると何種類もの乱数生成関数があります。このうちのRandFastを使ってみましょう。
 ピクセルの座標を与えてやると、0~1の乱数値を返してくれる関数のようです。

Random.usf
// [0, 1[
// ~10 ALU operations (2 frac, 5 *, 3 mad)
float RandFast( uint2 PixelPos, float Magic = 3571.0 )
{
    float2 Random2 = ( 1.0 / 4320.0 ) * PixelPos + float2( 0.25, 0.0 );
    float Random = frac( dot( Random2 * Random2, Magic ) );
    Random = frac( Random * Random * (2 * Magic) );
    return Random;
}

 では呼び出してみましょう。

 画面全体にノイズを出力するポストプロセスが出来ました。
 ちょっと改良してみましょう。

 ランダムシードを時間で変化させれば、テレビの砂嵐っぽい効果が簡単につくれます。RandFast関数のデフォルトのランダムシードが3571なので、それに時間変化で少し数字を足してやります。あまり大きな数字を足すときれいな乱数にならないので注意。

 こんな感じです。

 もちろん、includeされているものだけでなく、このコード内で定義されている関数や構造体などはCustomノードで使うことができるものが色々あります。

その他シェーダー命令

 他にもシェーダーノードとしては用意されていないけど、HLSLというシェーダー言語で定義されているシェーダー命令をCustomに入れることで使うことができます。例えば次のような命令。

  • step
     先程のセルシェーダーでも出てきましたが、入力値が指定の値より大きければ1、小さければ0を返します
  • smoothstep
     特定の範囲の数値を0~1の範囲に変換します。マテリアルノードには存在するのですが、Material Functionで作られていて、scalarしか使えません。vectorのsmoothstepが必要な場合はCustomノードで作りましょう。
  • rcp
     シェーダーで割り算は比較的重い処理になりますが、逆数を必要とする場合が多々あります。rcpは逆数を高速に計算しますが、近似値なので正確な値が必要な場合には使わない方が良い場合があります。

 などなど、マテリアルノードになっていないHLSL命令がたくさんあります。

Customノードの注意

 自由にシェーダーコードが書けて無敵感のあるCustomノードですが、注意しないといけない点がいくつかあります。

  • {}には注意
     シェーダーコードなので、{や}が閉じていないと後ろの関数につながってしまったりして、コンパイルエラーになります。これを逆手に取った家弓メソッドと言われる裏技もあったりします。詳しくはこちらにもんしょさんがまとめています。
    http://monsho.blog63.fc2.com/blog-entry-194.html

  • エンジンバージョンによって使えない場合がある
     エンジンの内部仕様に依存するコードを書いた場合は、当然エンジンの仕様変更でコンパイルできなくなる可能性があります。

  • マテリアルの種類に注意
     エンジンの関数や構造体を使う場合、通常のマテリアルとポストプロセスマテリアルで使えるもの、使えないものがあります。SceneTextureにアクセスすることができるのは、ポストプロセスマテリアルのみです。
     また、ライティングモードがディファードかフォワードかでも多少変わってきます。フォワードではGBufferを持たないので、ポストプロセスではカラーとデプスくらいしか使えません。

  • 入力変数名に注意
     Customノードの入力変数名はそのままシェーダーコード内の関数の引数として使用されます。文字コードによってはコンパイルエラーになることがあります。またエンジンやプラットフォームで予約されている単語を使った場合にもコンパイルエラーになることがあります。なので、僕は変数名には必ず"in"を付けています。

  • 最適化を阻害する?
     公式ドキュメントには次のような注意書きがあります。

    カスタム ノードは定数畳み込みを防止します。また、ノードでビルドされた同等バージョンと比べて、かなり多くのインストラクションを使用する場合があります。

 実際のところどうなのか、検証しきれていないですが、実はマテリアルファンクションもCustomノード同様に、別関数に展開されます。なので、この注意書きはCustomノードだけでなく、マテリアルファンクションを使用した場合も同様と思われます。
 分岐などをifノードで組むよりはCustomノード内で処理したほうが良さそうな気もしますし、細かくCustomノードをつなげるなら、ひとつのCustomノードにまとめた方が最適化されやすい気もします。
 UnrealFestでも触れましたが、巨大なポストプロセスをひとつのCustomノードにまとめたところ、かなりのパフォーマンス向上になりました。
 Customノードを使うことでインストラクションが増えるというのもケースバイケースなんじゃないかなと思っています。

まとめ

 以上、マテリアルエディタでCustomノードを使う話でした。Customノードを効果的に使うためにもUE4のシェーダーコードを覗いてみると新しい世界が開けるかもしれません。詳しい実装が理解できなくても関数名やコメントを見るだけで、なんとなく少しは理解できたり、知識が広がったりします。

明日はみんなのアイドルおかずさん登場です。サイリウム用意して待ちましょう。