【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を直接書くと、そういった問題が起きる可能性があるので、バックアップを取っておくのは大切ですね。