はじめに
こんにちは、クライアントエンジニアの下野です。
ブログ書くのにちょうどいい感じのネタが残ってたので割と早めに2回目です。
前回は最適化をしましたが、今回はエディター拡張とマテリアルエディターに独自のショートカットキーを用意したりします。
前提
エンジンのバージョンは5.6です。
バージョンによってはiniファイルの場所が違ったりする可能性もあるため完全にコードを一致させても動かない場合があります。
ショートカットキー色々
UEのエディターには、Ctrl+Sで全保存みたいなショートカットの他に、BPやマテリアルエディターで使えるノード生成のショートカットキーがあります。
具体的には、マテリアルエディターだとキーボードのAを押しながらマウスを左クリックするとAddノードが出たり、数字キーの1を押しながらマウスを左クリックするとConstantScalarが出たりする物です。

既存のショートカットキーの確認は、「編集->エディタの環境設定->キーボードショートカット->マテリアルエディタ - ノードをスポーン」から見れます。
LerpとかOneMinusとかよく使うノードのショートカットキーはデフォルトで入っていて、既に定義されている画像内のものに関しては別のショートカットを割り当てることも可能です。

結構な数ありますが、未だにStepが無かったり、ReflectionVectorショートカットにするほどかとか若干気になります。
それに、このウィンドウからキー割り当て変えれるのは便利なのですが、新規でショートカットを作成することはできません。
UEのドキュメントにも明記されてなかったので、ショートカットキー追加の手順を記載していきます。
ショートカットキーの追加方法
最初にUnrealEngineをインストールしたフォルダを開きます。(デフォルトだとC:\Program Files\Epic Games\にあります。)
UE_(UEのバージョン、今回は5.6)/Engine/Config/BaseEditorPerProjectUserSettings.iniファイルを開きます。(テキストファイルとか、個人的にはVSCodeが見やすいと思いました)

この中にノードのショートカットキーとかエディターのユーザー設定が色々詰まっています。
メモ帳とかならCtrl+Fで文字列検索を開き、「MaterialEditorSpawnNodes」で検索すると以下のような文字列がヒットします。
これがマテリアルのノード生成ショートカットです。
ここに文を追加すればショートカットキー増やせますし、元の文を変えれば生成するノードの置き換えも出来ます。
[MaterialEditorSpawnNodes] +Node=(Class=/Script/Engine.MaterialExpressionAdd Key=A Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionBumpOffset Key=B Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionDivide Key=D Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionPower Key=E Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionMaterialFunctionCall Key=F Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionIf Key=I Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionLinearInterpolate Key=L Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionMultiply Key=M Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionNormalize Key=N Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionOneMinus Key=O Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionPanner Key=P Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionReflectionVectorWS Key=R Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionScalarParameter Key=S Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionTextureSample Key=T Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionTextureCoordinate Key=U Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionVectorParameter Key=V Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionConstant Key=One Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionConstant2Vector Key=Two Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionConstant3Vector Key=Three Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionConstant4Vector Key=Four Shift=false Ctrl=false Alt=false) +Node=(Class=/Script/Engine.MaterialExpressionReroute Key=R Shift=true Ctrl=false Alt=false)
Class=〇〇はエンジン内のノードのファイル名で、5.6では
/Engine/Source/Runtime/Engine/Public/Merials/の中にまとまって入っています。
分からない場合はエクスプローラーとかで検索をかけてファイルを探して下さい。

Key=〇〇はキーボードのショートカットでAならAボタンを押してマウスをクリックした際にノードが生成されるという感じです。Oneとかは数字キーになります。
ShiftやCtrl,AltはKeyの値だけでなく、他のキーの組み合わせを使用するかの選択です。trueで有効、falseで無効になります。
実際にショートカットキーを作成する
今回は試しにKキーを押したらStepノードが生成されるようにしてみます。
上記のMaterialEditorSpawnNodesの末尾に以下を追加します。
+Node=(Class=/Script/Engine.MaterialExpressionStep Key=K Shift=false Ctrl=false Alt=false)
Shift,Ctrl,Altは一切押さずに、Cキーとマウスクリックを押した際にStepノードを追加する感じです。
これでiniファイルを保存してUnrealEngineのプロジェクトを開くと追加されるのが確認できます。
iniファイルの読み込みはエンジン起動時なので、プロジェクトが開きっぱなしの場合は一回閉じて開きなおすと確認出来ます。
確認
追加が成功していた場合、Sキーを押しながらマウス左クリックでStepノードが呼び出せます。
マウスクリックが必須なのは、ノード生成時にエディターの座標が必要だからなのだと思います。

終わり
これでショートカット作成は終わりです。
これでブログ終わりでも良かったのですが、ちょっとブログの内容としては味気ないのでおまけでノードの作成もしてみます。
ここから下は当然のようにエンジン改造必須になります。
ノードの新規作成
コードはGitHubのリンクを貼り付けています。(Epicの規則的に)
URLをクリックした際に下画像が出る場合は、UnrealEngineのEULAの同意をしないといけないため
リポジトリ取得の手順を踏んでからお試しください。

作るノードの紹介
今回はSMinというノードを作ってみます。
Minの結果がSmoothStepされる感じで、よくSDFの合成とかに使ったりするらしいです。
既存のノードの繋合わせでも作成できるためなのか、UEのノードには存在しません。
詳しくは参考にしたものがShadertoyにあるのでそちらをご覧ください。
https://www.shadertoy.com/view/4dtXRnwww.shadertoy.com
https://www.shadertoy.com/view/XlVSztwww.shadertoy.com
クラス作成
最初にUMaterialExpressionを継承したクラスを作成します。
エンジンに元々あるMaterialExpressionAddとかをコピペして名前変えるのが楽です。

UMaterialExpressionはマテリアルのノードの基底クラスです。
入出力ピンとかHLSL変換とか色々がまとまっています。
/Engine/Source/Runtime/Engine/Public/Materials/MaterialExpressionSMin.h
FExpressionInputが入力ピンの変数です。
Const〇〇はピンの接続が無いときに出ている定数です。

クラス実装
関数の実装は個別のC++ではなく、MaterialExpression.cppとMaterialExpressionsIR.cpp内に集約されているため、いい感じの位置に置きます。
今回はSMinなのでMinの下に置きました。
/Engine/Source/Runtime/Engine/Private/Materials/MaterialExpressions.cpp
/Engine/Source/Runtime/Engine/Private/Materials/MaterialExpressionsIR.cpp
Compile関数はマテリアルがコンパイルされた際に入力ピンを確認して接続があればその値、なければConst〇〇の値を使ってHLSLコードを作ります。
この時点だとSMinの処理を作ってないのでエラーが出ます。
GetCaptionはノード上端の緑枠の文字列で、関数名と入力ピンが無い場合定数なので値の表示をします。
5.4など以前だとConstの値をノード名に追加する処理がありましたが、気づいたら無くなってました。
欲しいと思ったらここで追加可能です。
/Engine/Source/Runtime/Engine/Private/Materials/MaterialExpressions.cpp

BuildはマテリアルIRの構築です。
マテリアルIRというのはこのノードが使われているかとか、前のノードとのリンクとかノードグラフの構築や解析に必要な色々が詰まっているものです。
上記とは別で、必要に応じてDescriptionやToolTipの関数をオーバーライドしてあげるとそれっぽくなりますが、今回は本題から逸れてるので行いません。
マテリアルIRの構築
IRはグラフ内のノードの繋がりを担保したりするのに必要らしいので処理を書いていきます。
最初にEnumに処理タイプを宣言していきます。
SMinは引数3つあるためTernaryに置いておきます。
/Engine/Source/Runtime/Engine/Public/Materials/MaterialIR.h
IRから処理を受けとり実行するエミッターにSMin用の関数を定義しておきます。
/Engine/Source/Runtime/Engine/Public/Materials/MaterialIREmitter.h
cpp内でSwitch文を使って処理を行うため、実装を書いていきます。
FoldScalarOperator関数内のSwitchは、静的な処理なら関数を畳み込むための処理用らしく、SMinに入ってきたら処理を行ってあげます。
/Engine/Source/Runtime/Engine/Private/Materials/MaterialIREmitter.cpp
DifferentiateOperator関数内のSwitchは、ddxやddyなど周囲を解析する際に使う関数で、使うことないと思ったので、Step等と同じく0を返しています。
/Engine/Source/Runtime/Engine/Private/Materials/MaterialIREmitter.cpp
何かログ出したそうな雰囲気あったのでつなげてあげます。
/Engine/Source/Runtime/Engine/Private/Materials/MaterialIR.cpp
IR側でHLSLコード展開書くことがあるっぽいので書いておきます。
/Engine/Source/Runtime/Engine/Private/Materials/MaterialIRToHLSLTranslator.cpp
最後に、UMaterialExpressionSMin::Buildで入力ピンなどをエミッターに送ってあげれば完了です。
/Engine/Source/Runtime/Engine/Private/Materials/MaterialExpressionsIR.cpp
シェーダー対応
既存のMinやMax,AddなどはHLSL展開処理まで書いてあるので簡単にCompilerから呼び出せますが、新しい処理のHLSL展開はまだ書いてないので対応が必要になります。
最初に、UMaterialExpressionSMin::Compile関数でSMinの処理を呼べるように処理を追加します。
FMaterialCompilerが基底クラスで、これを継承して様々なシェーダーコードに変換するのかなと思います。
/Engine/Source/Runtime/Engine/Public/MaterialCompiler.h
すぐ下にFProxyMaterialCompilerという中継くんが居るので、こっちにも実装を忘れないでください。
/Engine/Source/Runtime/Engine/Public/MaterialCompiler.h
次に、HLSLTranslatorにもoveerrideを書きます。
/Engine/Source/Runtime/Engine/Private/Materials/HLSLMaterialTranslator.h
実装は受け取った引数から入力ピンを探して、すべてが定数値ならCPU計算した値を返して、どれか一つでも入力ピンつながっていたら動的に値が変わる可能性があるため、シェーダコードにこの後書く予定のsmoothminという関数を追加する感じです。
/Engine/Source/Runtime/Engine/Private/Materials/HLSLMaterialTranslator.cpp
最後にC++で呼び出しているsmoothmin関数をCommon.ushに記述してあげれば終わりです。
関数の名前をsminにしてないのは、ACESUtilities.ushでsminを定義していて競合起きたからです。
float ~ float4まで入力のバリエーション豊富なため、忘れないようにしましょう。
/Engine/Shaders/Private/Common.ush
確認
ここまでの変更が正しく行えていればノードの追加自体は完了です。
確認してみます。
右クリックしてSMinで検索すれば出てきます。

動作確認もしたいので適当にマテリアルを作ります。
プレビューで即結果見たいのでマテリアルドメインはUserInterfaceにします。

SMoothMinはSDF合成でよく使うのでそんな感じの処理を作ります。
片方は中心に円がある感じの見た目にします。
UVは0~1で、-0.5してLengthを取ると中心は値が0になり、外に行くほど値が高い円になります。

もう片方は常に左上と右下を行き来する感じの見た目にします。


これをMinで合成するとこのような見た目になります。最小値なのでぶつ切り感があります。

これをSMinで合成すると以下の見た目になります。スムーズな結合なので見た目だいぶ変わりますね。

これで機能自体は完成ですが最後に、Minなどのノードは演算やユーティリティのカテゴリーが付いているのに、新規で作成したノードだけカテゴリーが無いというのも寂しいのでそこだけ対応をします。

カテゴリー付け
本当にすぐ終わります。
/Engine/Config/BaseMaterialExpressions.iniファイルを開きます。
このファイルは[]内のMaterialExpressionにカテゴリーを追加出来ます。
MinやMaxと同じくMathとUtilityを宣言しておきます。
[/Script/Engine.MaterialExpressionSMin] +MenuCategories=NSLOCTEXT("MaterialExpression", "Math", "Math") +MenuCategories=NSLOCTEXT("MaterialExpression", "Utility", "Utility")
ついでに、記事の一番最初で行ったショートカットキーも作ってあげれば完成です。


本当に終わり
いかがでしたか?
おまけで書いたはずのノード作成の方がボリューミーになってしまいました。
ショートカットキーは便利なので覚えておいてもいいと思います。
ノードの自作は正味CustomNodeという便利なのがあるので、わざわざ時間かけて新規ノード作ること無い気もしました。
しかし、CustomNodeと違って書いたコードをミスって消す心配が無かったり、今回は行いませんでしたがAddInlinedCodeChunkを使って、SMinの処理を全部C++側に書けばシェーダーコードをインライン展開出来るので、関数呼び出しの分処理速くなったりするので覚えておいて損はないのかなという感じでした。