【UE5】マテリアルCustomノードの使い方

プログラマー尾関です。

今回は HLSLを直接記述できる「Customノード」の基本的な使い方について紹介します。

Customノードとは

Customノードとは、マテリアルノードを使わずにシェーダー (HLSL) を直接記述できるノードで、メリットとしては以下のとおりです。

  • for文やif文による条件分岐など、既存のマテリアルノードでは難しい処理が可能
  • より複雑なシェーダーを記述しやすくなる

このメリットにより、例えば Shadertoy などからコードの流用が容易となります。

Customノードを使うデメリット

ただいくつかのデメリットがあるため「どうしてもマテリアルノードで実装できない処理のみ Customノードを使う」といった用途が良さそうです。

  • マテリアルノードの「視覚的なわかりやすさ」という利点が失われる
  • 実装と保守のコストが上がる可能性がある
  • 最適化を手動で行う必要がある
  • HLSLコードの記述方法により、プラットフォーム依存の問題が発生する可能性がある

Customノードの作成方法

Customノードは、Customで検索して作成します。

ノードの設定項目

ノードの設定項目には以下のものがあります。

カテゴリ項目説明
マテリアル
エクスプレッション
カスタム
Codeシェーダー (HLSL) コード
Output Type出力データの型CMOT Float 1~4: float1~4CMOT Material Attributes: マテリアルのすべての属性
Descriptionノードの名前
Inputs入力パラメータ
Addtional Outputs追加の出力パラメータ
Addtional Defines定数の定義
Include File Pathsアンリアル シェーダー ヘッダ (.ush)、アンリアル シェーダー フォーマット (.USF) のパスを指定するとインクルードできます(※1)
コードを表示コードをノードに表示するかどうか
マテリアル
エクスプレッション
Descコメント

典型的な .ushファイルの構造としては以下のものがあります。

#pragma once
// インクルード
#include "Common.ush"
// 定数定義
#define MY_CONSTANT 1.0
// 関数宣言
float3 MyFunction(float2 UV);
// マクロ定義
#define MY_MACRO(x) ((x) * 2)
// 関数定義
float3 MyFunction(float2 UV)
{
    // 関数の実装
    return float3(UV.x, UV.y, 0);
}

シェーダーコードの記述方法

シェーダーコードはCustomノード、または詳細タブの “Code” から入力します。

ただ入力補完などの補助機能は期待できないため、別のシェーダーエディタを使用してコードを記述し、それをペーストするのが良さそうです。

生成されるHLSLコードの確認方法

メニューから「ウィンドウ > シェーダーコード > HLSLコード」を選びます。

CustomExpression” で検索すると対象のコードが見つかります。もしくは「Addtional Defines」で定数を設定すると、検索しやすくなります。

// 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の値でグラデーションする例です。

また引数を増やしたい場合は「+」ボタンで増やせます。

戻り値の設定 (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” とすると、以下のコードが記述できます。

RetEmissive = EMISSIVE_POWER;

コンパイル

[CTRL+S]で保存すると、自動でコンパイルされます。

エラー対応

コンパイルしたときに以下のエラーが出ることがあります。

エラーログは「統計」タブに表示されます。

なお統計タブはマテリアルエディタ上部にある「統計」の項目をクリックして有効にすると表示されます。

そしてエラーコードを見ても原因がわからない場合は、メニューから「ウィンドウ > シェーダーコード > HLSLコード」を選んでHLSLコードを表示することができます。

このウィンドウから、生成されるHLSLコードを確認できます。

  • 注意点:コードの修正はすぐに反映されないようなので、少し待ってから[CTRL+S]で保存するとうまくいく場合があります。

UVとテクスチャを引数に渡す方法

Customノードを選択して、”Inputs” に “UV” と “Tex” を追加します。

そして TexCoord ノードと Texture Object ノードを接続します。

HLSLコードを例えば以下のように記述することで、UVに合ったピクセルの値をベースカラーに設定できます。

return Texture2DSample(Tex, TexSampler, UV).rgb;

組み込みマテリアルノードの使用

HLSL内では、エンジンが提供している “SphereMask” などの組み込みマテリアルノードは使用できません。

そのため、組み込みマテリアルの機能を使いたい場合は、別途自作する必要があります。

例えば SphereMaskのHLSLコードは以下のようになります。

// 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 が含まれている…はずなのですが、

[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 を使っていない場合は最適化のため自動で無効化される」とのこと。

そこで意図的に “TexCoord” を使用するように修正…。

以下の Parameters.TexCoordsを使用した HLSL コードが正常に動作するようになりました。

// 中心座標.
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」を使用します。

// 中心座標 (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文でマスクを合成するようにしました。

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;

実行して複数のマスクを動かせるようになりました。

なお、シェーダーコードを色々試したときに GPUハングが発生してしまい、プロジェクトが開けなくなることがありました (プロジェクトを起動するとGPUハングする)。

HLSLを直接書くと、そういった問題が起きる可能性があるので、バックアップを取っておくのは大切ですね。

Follow me!