Customノード3分ハッキング

UE4 Advent Calendar その2、12日目の割り込み記事です。

代わりに書くボタンが出来ていたので、予定のものとは全く異なる記事ですが割り込ませていただきました。

元担当者の方、問題があるようならコメンとなりTwitterなりで呼びかけていただけると助かります。

10月のUnreal Fest後の飲み会にて、ギルティギアなどでお馴染みのアークシステムワークス家弓さんからマテリアルエディタの [Custom] ノードについて面白い話を教えてもらいました。

試してみたところ実際にうまくいき、そのことをTwitterで呟いたら家弓さんが簡単にまとめてくださいました。

プログラマであればこの情報だけで色々とできるようになるかと思いますが、[Custom] ノードはこの方法を使ったちょっと特殊な手法なんかもありますので、ここでまとめておこうと思います。

なお、最新のUE4.11 Preview1にて動作を確認しています。

今後のアップデートで使用できなくなったり、使用できるけどやり方変えないとダメという場合もあるかもしれませんのでご注意ください。

元々、Epic Games的には、[Custom] ノードはノードベースで作成するには面倒だけどコードで書いたらすごく簡単、といったものを簡単に実装できるように用意したものではないかと思います。

わかりやすい例を挙げるならベクトルの要素のスウィズルですね。

スウィズルは主にシェーダで用いられますが、要素の入れ替え処理です。

使い道はともかくとして、例えばfloat3のカラーの値があったとして、これのrgb値をgbrの順番に入れ替えたい、という場合です。

これをマテリアルノードで表現するとこうなります。

ue392.jpg

[Texture Sample] ノードの場合は [Component Mask] を使う必要は本来ないのですが、定数でも同じように処理できるようにするために使用しています。

この程度のノードの組み方は別に難しくはありませんが、同じような処理が散見されるようになってしまうと可読性が悪くなってしまいます。

この組み方がマテリアルエディタのいたるところで使用されている、という状況を想像するだけで処理を追いたくなくなるんじゃないでしょうか?

では、これを [Custom] ノードで表現すると、このようになります。

ue393.jpg

結果は同じですが、マテリアルノードは[Custom]1つだけです。

処理もたったの1行で、極めて簡単です。

ベクトル要素のスウィズル機能は現在のプログラマブルシェーダを組み込んでいるハードであればこのように簡単に実装できるのですが、ノードで表現するとどうしても冗長になってしまいますね。

このように冗長なノードを簡単にまとめられるという利点が [Custom] ノードには存在しますが、いくつか弱点が存在しています。

プログラムコードを書かなければならないためシェーダプログラムに対する知識が必要だという点が1つ、プラットフォームごとにコンパイルが通るコードを書かなければいけないという点が1つ、そしてなにより、マテリアル関数として作成されている関数を呼び出すことが出来ないという点です。

1つ目の問題は、まあ [Custom] ノード使う人はシェーダコードの書けるプログラマかTAくらいでしょうからそれほど問題にならないでしょう。

2つ目の問題も当たり障りのない処理を書く分には処理系に依存することはほぼありません。四則演算やlerp, powといった、どのプラットフォームにも存在する命令だけを使っておけば問題になりません。

3つ目の問題は現状では対応できません。が、やはり使いたいと思うことはあるのではないかと思います。

実は今回の [Custom] ノードの家弓メソッド(適当に命名w)を使うと、この問題にも限定的ではありますが対応できるのです。

というわけで、続きからで家弓メソッドの詳細と、様々な使い方について書いていこうと思います。

ただ、先に注意しておくと、この手法はハック的な手法であるため、いつか使用できなくなる可能性もあります。

家弓さんの話ではUE3の頃から使えた手法らしいのでそんなに簡単に潰されることはないとは思いますが、使用の際には十分注意してください。

まずはおさらいですが、普通に [Custom] ノードを使った場合に生成されるコードを見てみましょう。

マテリアルエディタの [ウインドウ] → [HLSLコード] で先のマテリアルで生成されるコードをチェックします。

... 略

MaterialFloat3 CustomExpression0(FMaterialPixelParameters Parameters,MaterialFloat3 In)

{

return In.gbr;

}

... 略

void CalcPixelMaterialInputs(in out FMaterialPixelParameters Parameters, in out FPixelMaterialInputs PixelMaterialInputs)

{

... 略

// Now the rest of the inputs

MaterialFloat4 Local0 = ProcessMaterialColorTextureLookup(Texture2DSample(Material.Texture2D_0,Material.Texture2D_0Sampler,Parameters.TexCoords[0].xy));

MaterialFloat3 Local1 = CustomExpression0(Parameters,Local0.rgb);

MaterialFloat3 Local2 = (Local1 + Material.VectorExpressions[1].rgb);

PixelMaterialInputs.EmissiveColor = Local2;

... 略

[Custom] ノードは CustomExpression0 という関数名で登録されていることがわかります。

[Custom] ノードの [Code] の部分に書いたのは4行目の命令のみですので、2,3,5行目はUE4が自動的に追加した部分である、ということがわかるでしょう。

さて、もしも別の [Custom] ノードでこの CustomExpression0 を使いたいという要件が出てきたとしましょう。

この場合、CustomExpression0という関数名で別の [Custom] ノードから呼び出すことは出来るでしょうか?

結論から言うと、可能です。しかし、不確実性が高いです。

実際にうまくいった例を見てみましょう。

ue394.jpg

これは正常にシェーダコンパイルが通りました。

しかし、[Add] ノードに入れる順番を上下切り替えると今度はコンパイルが通らなくなります。

[Custom] ノードはシェーダコードに変換される際に CustomExpression0, CustomExpression1, ...という具合に数字が割り振られていきますが、この数字は [Custom] ノードが評価された順番で付けられていきます。

上の例では [Emissive Color] へ入力されるノードを辿ると、まず最初に [Add] ノードが出てきて、次に [Add] ノードの上のピンが評価されます。

すると出てくるのは "RGB to GBR" と名付けられた [Custom] ノードなので、こちらが先に評価され、CustomExpression0 の称号を獲得します。

"RGB to GBR 2" と名付けられた [Custom] ノードは2番めに評価されるので、こちはら CsutomExpression1 となるわけです。

この程度の単純なものであればどれが先に評価されるかわかるでしょうけど、複雑なノードネットワーク内でお目当ての [Custom] ノードが何番に評価されるかを検出するのは難しいでしょう。

そこで出てくる家弓メソッド。こいつを使えば確実な関数名でアクセスすることが出来ます。

ue395.jpg

"SwizzleRGBtoGBR" と名付けられた [Custom] ノードはダミーの0を返すだけの関数を作成し、これを中括弧で閉じておきます。

UE4はCustomExpressionX という関数名と中括弧の開閉を自動挿入するわけですが、すでに開かれている中括弧をここで一度閉じてしまうわけです。

すると、次の SwizzleRGBtoGBR という関数が有効になり、これを今度は [Custom] ノードのコード内で中括弧を開きます。

最終的にはUE4が自動的に中括弧を閉じてくれるわけですが、UE4的には CustomExpressionX の中括弧を閉じているつもりなのに実はユーザが騙して SwizzleRGBtoGBR という関数の中括弧を閉じさせているというわけです。

完全にハック的な考え方で、正規の動作とは言えません。使用を逆手にとっているわけです。

ダミーの処理を用意し、これを [Add] ノードで加算しているわけですが、もちろんこれにも意味があります。

前述したとおり、UE4のシェーダコード生成は有効なピンに入力されているノードを順番に辿っていって、最初に出てきた"有効な" [Custom] ノードをコードとして展開します。

"SwizzleRGBtoGBR" ノードをどこにも繋いでいないとこのノードは評価されず、コードは展開されないことになってしまいます。

そのため、ダミー処理を用意し、最後に加算するなどで、数値的には意味が無いけどコード的には意味があるという状態を作り出す必要があるわけです。

もう1つのルールとして、やはり評価順序を考えなければならないという点があります。

上の例では最後の [Add] ノードの入力を逆にするとコンパイルが通らなくなります。

これは、"RGB to GBR" ノードが先に評価されてしまい、評価された段階では "SwizzleRGBtoGBR" という関数が存在せず、それ故にコンパイルできないと怒られてしまうからです。

実際に生成されるコードはこのようになります。

MaterialFloat3 CustomExpression0(FMaterialPixelParameters Parameters,MaterialFloat3 In)

{

return SwizzleRGBtoGBR(In);

}

MaterialFloat3 CustomExpression1(FMaterialPixelParameters Parameters)

{

return float3(0, 0, 0);

}

float3 SwizzleRGBtoGBR(float3 In)

{

return In.gbr;

}

下には存在するのですが、プログラムコードは基本的に、使用する前にその関数が存在することを示す必要があります。

残念ながらこの場合では使う前に関数が存在しているかわからないのでコンパイルが通りません。

しかしこの問題は解決が簡単です。最初に存在していなければならないノードを最後に [Add] ノードで加算、入力ピンは必ず上のピン、というルールを徹底すればOKです。

このルールを徹底すればどのように、いくつ使っても問題はないのです。

ue396.jpg

こうしても最後の [Add] ノードのルールが守られていればコンパイルは通ります。

さて、マテリアル関数を [Custom] ノードから呼び出したい、という需要もあるでしょう。

この手法を用いることで、ちょっと面倒ではあるものの、呼び出しが可能です。

つまり、マテリアル関数内で家弓メソッドを使っておけば呼び出せるわけです!

ue397.jpg

こちらはマテリアル関数内の実装です。各 [Custom] ノードの中身は前述の例と同じです。

入力値は [Use Preview Value as Default] のチェックが入っているので、何も入力しなければ結果として返ってくるのは黒色(つまり全て0)です。

マテリアル側の使い方も前述の手法とほぼ一緒です。

ue398.jpg

違いは [Custom] ノードの代わりにマテリアル関数が呼び出されている部分です。

このマテリアル関数は普通にマテリアル関数としても使えますし、[Custom] ノードからの呼び出しにも対応しているという扱いやすいものになっています。

しかしこうしなければならないというわけでもなく、例えば、[MF_OreOreFunctions] みたいなマテリアル関数を作成、出力される結果は常に0で、しかし中身の [Custom] ノードには俺々関数が山のように定義されている、という使い方もあるのです。

このような使い方は複数人がマテリアルをいじるような大規模開発ではあまりオススメできませんが、数人のプログラマとTAのみがマテリアルやマテリアル関数をいじる環境ではコンセンサスをちゃんと取っておけば使い勝手はいいかもしれません。

何にしても、使う場合はきちんとルールを制定し、用法用量を守って正しくお使いください。

さて、最後に、これまた家弓メソッドを使ったかなり恐ろしいけどプログラマにとっては福音になるかもしれない手法を紹介します。

それはインクルードファイルを用いる方法です。

この手法はHLSLコードでは問題なく通る手法ですが、OpenGL環境のGLSLでは通るかどうか不明です。また、PS4のシェーダコードでも通るかは不明です。

なので、PCとXboxONE専用になるかもしれませんのでご注意ください。

C/C++言語ではインクルードファイルは馴染み深いものではありますが、HLSLコードでもインクルードファイルは使用できます。

UE4のシェーダコードでももちろん使えて、基本的に Engine/Shaders フォルダにあるファイルはインクルード可能です。

なので、Engine/Shaders フォルダに自分のシェーダコードをコピーすることでインクルードすることが可能になるのです。

しかもHLSLでのインクルードファイルは本当にただコードを展開するだけなので、実はこのような書き方が許されています。

float3 Foo(float3 In)

{

#include "Foo.usf"

}

Foo.usf ファイルの中身はこんな感じ。

// Foo.usfの中身

return In.gbr;

これを利用すれば [Custom] ノードからファイルインクルードが出来るのでは?

普通にやると出来ないのですが、最近、こうすれば出来るという手法が見つかりました。

ue399.jpg

"Foo.usf"は予め Engine/Shaders フォルダに入れてあります。

[Custom] ノードは最初の行の先頭に # が入っていると不正とみなしてエラーを返しますが、最初の行に当たり障りのない命令を書いておけばOKだったりしますw

ちなみに、コメント行だと怒られますのでご注意ください。

そして最後に "return 0;" という行を追加します。

[Custom] ノードはコード中に return 命令が存在しない場合、1行目に自動的に挿入する仕様になっているようですので、これを入れないと1行目が "return float a = 0;" なる命令になってエラーとなります。

Foo.usf 内で return は行われているので、最後の行には命令が到達しません。なので0を返しておけば問題ないです。

さて、家弓メソッドを使うことでインクルードファイルの中身はもっと充実させることが出来ます。

例えば、FooFoo.usf というファイルを用意し、この中身をこう書きましょう。

float3 SwizzleRGBtoGBR(float3 In)

{

return In.gbr;

}

float3 SwizzleRGBtoBRG(float3 In)

{

return In.brg;

}

float3 SwizzleRGBtoRBG(float3 In)

{

return In.rbg;

}

3つの関数がこれで定義できます。

実際の使い方はこんな感じ。

ue400.jpg

最後の中括弧閉じのため、#include の後にやはりダミーの関数を用意しました。

この関数は最終的には使用しないので、何の意味もない、必ず他とはかぶらない暗号化された名前をつけてもいいかもしれません。

画像の "RGB to GBR" 等の [Custom] ノードはそれぞれ名前に沿った関数を呼び出しています。

あとはこのインクルードファイルに好きな関数を詰め込んでいけばOKです。[Custom] ノードのコード内に書くよりテキストファイルで編集できる方がプログラマにとっては嬉しいでしょう。

もしもこのインクルードファイルと俺々マテリアル関数を配布したいという場合はプラグインを書くことをオススメします。

このプラグインは有効になった際に Engine/Shaders フォルダにインクルードファイルをコピーし、無効になったら削除する、という挙動をさせればOKです。

この手法は以前のバージョンでは正常動作するのを確認していますが、現在も動作するかは不明です。そのうち試そうとは思います。

ただ、何度も書いていますがハック的な手法なのでいつ穴を塞がれるかわかりません。

それでもかなり面白い手法ではありますので、UE4で面白マテリアルを大量生産したい方は試してみてはどうでしょうか?