UEのシェーダーを勉強中のプログラマーの尾関です。
今回はHLSLを直接記述できる「Customノード」の基本的な使い方について紹介します。
Customノードとは
Customノードとは、マテリアルノードを使わずにシェーダー (HLSL) を直接記述できるノードで、メリットとしては以下のとおりです。
- for文やif文による条件分岐など、既存のマテリアルノードでは難しい処理が可能
- より複雑なシェーダーを記述しやすくなる
このメリットにより、例えば Shadertoy などからコードの流用が容易となります。
Customノードを使うデメリット
Customノードを使うデメリットは以下のとおりです。
- マテリアルノードの「視覚的なわかりやすさ」という利点が失われる
- 実装と保守のコストが上がる可能性がある
- 最適化を手動で行う必要がある
- HLSLコードの記述方法により、プラットフォーム依存の問題が発生する可能性がある
こういったデメリットがあるため「どうしてもマテリアルノードで実装できない処理のみ Customノードを使う」といった用途が良さそうです。
Customノードの基本的な使い方
Customノードの作成
Customノードは、Customで検索して作成します。
シェーダーコードの記述方法
シェーダーコードはCustomノード、または詳細タブの “Code” から入力します。
ただ入力補完などの補助機能は期待できないため、別のシェーダーエディタを使用してコードを記述し、それをペーストするのが良さそうです。
生成されるHLSLコードの確認方法
メニューから「ウィンドウ > シェーダーコード > HLSLコード」を選びます。
“CustomExpression” で検索すると対象のコードが見つかります。もしくは「Addtional Defines」で定数を設定すると、検索しやすくなります。
1 2 3 4 5 6 7 8 |
// Uniform material expressions. #ifndef EMISSIVE_POWER #define EMISSIVE_POWER 100 #endif//EMISSIVE_POWER MaterialFloat3 CustomExpression0(FMaterialPixelParameters Parameters, inout MaterialFloat RetEmissive) { return RetEmissive = EMISSIVE_POWER;; } |
引数の設定 (Inputs)
引数は “Inputs” から設定します。Input Name に名前を指定し、データ型は接続されたノードから自動判定されます。
ここでは “UV” という引数を追加してみました。
Customノードの引数UVに TexCoordノードを渡すと、UV値が flaot2 として代入されます。
また引数を増やしたい場合は、Inputsのところにある「+」ボタンで増やせます。
戻り値の設定 (Output Type)
戻り値は “Output Type” から設定します。
Additional Outputsの使い方
出力(戻り値)を増やすには “Additional Outputs” の「+」をクリックで項目が増えます。
- Output Name: 出力名
- Output Type; 出力データ型
例えば Output Name に “RetEmissive“、Output Type に “CMOT Float 1” を指定して、エミッシブの値を出力できます。
Additional Defines: 定数
「+」で項目を追加して、”Define Name” に定数名、”Define Value” に値を設定します。
例えば定数名を “EMISSIVE_POWER” が “100” とすると、以下のようにコードに “EMISSIVE_POWER” を含めることができます。
1 |
RetEmissive = EMISSIVE_POWER; |
コンパイル
[CTRL+S]で保存すると、自動でコンパイルされます。ただコードの修正はすぐに反映されないようなので、少し待ってから[CTRL+S]で保存するとうまくいく場合があります。
エラー対応
コンパイルしたときに以下のエラーが出ることがあります。
エラーログは「統計」タブに出力されます。
統計タブが表示されていない場合、マテリアルエディタ上部にある「統計」の項目をクリックして有効にすると、統計タブが表示されます。
そしてエラーコードを見ても原因がわからない場合は、メニューから「ウィンドウ > シェーダーコード > HLSLコード」を選んでHLSLコードを表示することができます。
UVとテクスチャを引数に渡す方法
Customノードを選択して、”Inputs” に “UV” と “Tex” を追加します。
そして TexCoord ノードと Texture Object ノードを接続します。
HLSLコードを例えば以下のように記述することで、UVに合ったピクセルの値をベースカラーに設定できます。
1 |
return Texture2DSample(Tex, TexSampler, UV).rgb; |
組み込みマテリアルノードは使用できない
HLSL内では、エンジンが提供しているマテリアルノードは使用できません。
そのため、組み込みのマテリアルノードの機能を使いたい場合は、同等のものを別途自作する必要があります。
例えば SphereMaskのHLSLコードの実装例は以下のようになります。
1 2 3 4 5 |
// SphereMask の実装 float dist = distance(Center, Position); float result = 1 - saturate((dist - Radius) / (1 - Hardness)); return result; |
このCustomノードの引数 (Inputs) には以下を定義します。
- Center [VectorParameter]: 中心座標
- Position [TexCoord]: UV座標
- Radius [ScalarParameter]: 半径
- Hardness [ScalarParameter]: 境界のシャープ値 (0.0~1.0)
マテリアルグラフは以下のように実装しました。
オパシティマスクのマテリアルパラメータを表示するには Blend Mode を Masked にします。
メッシュを丸でくり抜くことができました。
Parameters.TexCoordsを使う方法
HLSLコードでは “Parameters” という定義が使えます。そしてParametersには TexCoords が含まれている…はずなのですが、
1 2 3 |
[SM6] /Engine/Generated/Material.ush:3342:30: error: no member named 'TexCoords' in 'FMaterialPixelParameters' float2 position = Parameters.TexCoords[0].xy; ~~~~~~~~~~ ^ |
実際に使ってみると、未定義のメンバーというエラーになりました。
生成されるHLSLコードを読んでみたところ、「#define NUM_TEX_COORD_INTERPOLATORS 0」という定義が見つかり、Parameters の TexCoordsが無効化されていました。
調べてみたところ「そのマテリアル内で UV を使っていない場合は TexCoordsの定義は最適化のため自動で無効化される」とのこと。
そこで意図的に “TexCoord” を使用するように修正…。
以下の Parameters.TexCoordsを使用した HLSL コードが正常に動作するようになりました。
1 2 3 4 5 6 7 8 9 10 |
// 中心座標. float cx = 0.5; float cy = 0.5; float2 center = float2(cx, cy); // UV座標. float2 position = Parameters.TexCoords[0].xy; float dist = distance(center, position); float result = 1 - saturate((dist - Radius) / (1 - Hardness)); return result; |
Custom Primitive Data (CPD) に HLSL からアクセスする方法
Unreal Engineには Custom Primitive Data (CPD) という、プリミティブデータにシェーダーパラメータを含める方法が用意されています。
ここに保存したパラメータを Customノードからアクセスする方法を紹介します。
まずは VectorParameter ノードの作成。
そしてノードの詳細タブから「Use Custom Primitive Data」にチェックを入れると、Custom Primitive Data を使うことができます。
HLSL から Custom Primitive Data にアクセスするには「GetPrimitiveData(Parameters.PrimitiveId).CustomPrimitiveData」を使用します。
1 2 3 4 5 6 7 8 9 |
// 中心座標 (Custom Primitive Dataにアクセス). float2 center = GetPrimitiveData(Parameters.PrimitiveId).CustomPrimitiveData[0].xy; // UV座標. float2 position = Parameters.TexCoords[0].xy; // SphereMaskの計算. float dist = distance(center, position); float result = 1 - saturate((dist - Radius) / (1 - Hardness)); return result; |
そしてマテリアルをレベルに配置した任意のメッシュに割り当て、詳細タブの Custom Primitive Data からパラメータを「+」で追加します。
[0] に 0.5、[1] に 0.5 を割り当てると、中心に穴を開けることができました。
さらに、for文のコードをテストするため、Custom Primitive Data を「8つ」作ってみます。
- v0 [0]
- v1 [4]
- v2 [8]
- v3 [12]
- v4 [16]
- v5 [20]
- v6 [24]
- v7 [28]
[]の中は数字はアドレス (Primitive Data Index)の値となります。
シェーダーコードは以下のように for文でマスクを合成するようにしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
float mask = 0; for(int i = 0; i < 8; i++) { // 中心座標 (Custom Primitive Dataにアクセス). float2 center = GetPrimitiveData(Parameters.PrimitiveId).CustomPrimitiveData[i].xy; // UV座標. float2 position = Parameters.TexCoords[0].xy; float dist = distance(center, position); float tmp = saturate((dist - Radius) / (1 - Hardness)); float sphereMask = (1.0 - tmp * tmp); // 合成. mask = max(mask, sphereMask); } return mask; |
for文で中心座標を別々に処理することで、複数のマスクを動かせるようになりました。
おしまい
今回Customノードでシェーダーコードを色々試したときに GPUハングが発生してしまい、プロジェクトが開けなくなることがありました (ハングするマテリアルを貼り付けたアクターがレベルにあるため、プロジェクトを起動するとGPUハングする)。
HLSLを直接書くと、そういった問題が起きる可能性があるので、バックアップを取っておくのは大切ですね。
以上、Customノードの使い方の紹介でした。