1. サイトトップ
  2. ブログ
  3. Unreal Engine
  4. 【UE4】マテリアルノード作ってみた

【UE4】マテリアルノード作ってみた

はじめに

こんにちは。
UnrealEngine5が気になるプログラマーの村田です。
残念ながらUE5のお話ではなく、今回もUE4のお話になりますが、最後までご覧頂けると幸いです。

タイトルの通り、マテリアルノードを作ってみたお話になります。
UE4の標準でシェーダの組み込み関数に対応する各ノードが実装されておりますが、中にはノード化されていないものもあります。
その中でも今回は「Smoothstep」関数のノードを作成してみました!
Smoothstep関数についてはこちらをご覧ください。

※使用したバージョンはUE4.25.0になります。
※エディタの言語環境は日本語になります。

実装

まず、Smoothstepノード用のクラスを追加しましょう。
UE4.25.0/Engine/Source/Runtime/Engine/Classes/Materials階層にMaterialExpressionSmoothstep.hファイルを追加し、クラスの定義を行います。

UCLASS(MinimalAPI)
class UMaterialExpressionSmoothstep : public UMaterialExpression
{
	GENERATED_UCLASS_BODY()

	UPROPERTY()
	FExpressionInput Input;

	UPROPERTY()
	FExpressionInput Min;

	UPROPERTY()
	FExpressionInput Max;

	//~ Begin UMaterialExpression Interface
#if WITH_EDITOR
	virtual int32 Compile(class FMaterialCompiler* Compiler, int32 OutputIndex) override;
	virtual void GetCaption(TArray<FString>& OutCaptions) const override;
#endif
	//~ End UMaterialExpression Interface
};

実装はMaterialExpression.cppに記述します。
※エンジン側のソースコードを編集する場合は、UE4のコーディングルールに則りコメントを必ず記述しましょう!
詳しくは こちらをご覧ください!

// @Logicalbeat Murata - BEGIN Smoothstepノード追加
#include "Materials/MaterialExpressionSmoothstep.h"
// @Logicalbeat Murata - END
︙(略)
︙
// @Logicalbeat Murata - BEGIN Smoothstepノード追加
//
//	UMaterialExpressionSmoothstep
//
UMaterialExpressionSmoothstep::UMaterialExpressionSmoothstep(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	// Structure to hold one-time initialization
	struct FConstructorStatics
	{
		FText NAME_LB;
		FConstructorStatics()
			: NAME_LB(LOCTEXT( "Logicalbeat", "Logicalbeat" ))	// カテゴリー名を設定。指定しない場合は「その他」に追加される。
		{
		}
	};
	static FConstructorStatics ConstructorStatics;

#if WITH_EDITORONLY_DATA
	MenuCategories.Add(ConstructorStatics.NAME_LB);		// ↑で指定したカテゴリーを反映。
#endif
}

#if WITH_EDITOR
// Shaderコードへ変換
int32 UMaterialExpressionSmoothstep::Compile(class FMaterialCompiler* Compiler, int32 OutputIndex)
{
	if(!Input.GetTracedInput().Expression || !Min.GetTracedInput().Expression || !Max.GetTracedInput().Expression)
	{
		return Compiler->Error(TEXT("Missing Smoothstep Input"));	// Inputが繋がっていない時
	}

	return Compiler->Smoothstep(Min.Compile(Compiler), Max.Compile(Compiler), Input.Compile(Compiler));
}

// マテリアルノードの見出し部分の表示文言
void UMaterialExpressionSmoothstep::GetCaption(TArray<FString>& OutCaptions) const
{
	FString	Caption = TEXT( "Smoothstep" );
	OutCaptions.Add(Caption);
}

#endif // WITH_EDITOR
// @Logicalbeat Murata - END

続いて上記コードの38行目のShaderコードに変換するためのSmoothstep関数を実装していきます。
FMaterialCompilerクラスは抽象クラスで派生クラスに実装を任せています。
なので、FMaterialCompilerクラスには純粋仮想関数を追加しておきます。
同ファイルにFMaterialCompilerを継承したFProxyMaterialCompilerが存在するので先程の関数をオーバーライドしておきます。
今回はこのクラスは特に関係ないので中身は気にしなくてもOKです。

class FMaterialCompiler
{
public:
	︙(略)
	︙
	// @Logicalbeat Murata - BEGIN Smoothstepコンパイル純粋仮想関数追加
	virtual int32 Smoothstep(int32 A, int32 B, int32 X) = 0;
	// @Logicalbeat Murata - END
};

/** 
 * A proxy for the material compiler interface which by default passes all function calls unmodified. 
 * Note: Any functions of FMaterialCompiler that change the internal compiler state must be routed!
 */
class FProxyMaterialCompiler : public FMaterialCompiler
{
public:
	︙(略)
	︙
	// @Logicalbeat Murata - BEGIN Smoothstepのコンパイル関数をオーバーライド
	virtual int32 Smoothstep(int32 A, int32 B, int32 X) override
	{
		return Compiler->Smoothstep(A, B, X);
	}
	// @Logicalbeat Murata - END
protected:
	FMaterialCompiler* Compiler;
};

続いてHLSLMaterialTranslator.hと.cppにもFMaterialCompilerを継承したFHLSLMaterialTranslatorクラスが存在するのでオーバライドしていきます。
今回大事なのはこっちのクラスになります。
(ヘッダーファイルの方は宣言だけなので割愛します。)

// @Logicalbeat Murata - BEGIN Smoothsteのコンパイル関数を追加
int32 FHLSLMaterialTranslator::Smoothstep(int32 A, int32 B, int32 X)
{
	if(A == INDEX_NONE || B == INDEX_NONE || X == INDEX_NONE)
	{
		return INDEX_NONE;
	}

	FMaterialUniformExpression* ExpressionA = GetParameterUniformExpression(A);	// Min
	FMaterialUniformExpression* ExpressionB = GetParameterUniformExpression(B);	// Max
	FMaterialUniformExpression* ExpressionX = GetParameterUniformExpression(X);	// X
	EMaterialValueType ResultType = GetParameterType(X);										// 戻り値の型

	if(ExpressionA && ExpressionB && ExpressionX)
	{
		return AddUniformExpression(new FMaterialUniformExpressionSmoothstep(ExpressionA, ExpressionB, ExpressionX), ResultType, TEXT("smoothstep(%s,%s,%s)"), *CoerceParameter(A, ResultType), *CoerceParameter(B, ResultType), *GetParameterCode(X));
	}
	else
	{
		return AddCodeChunk(ResultType, TEXT("smoothstep(%s,%s,%s)"), *CoerceParameter(A, ResultType), *CoerceParameter(B, ResultType), *GetParameterCode(X));
	}
}
// @Logicalbeat Murata - END

コードの編集は次で最後です。
MaterialUniformExpression.hにSmoothstep用のクラスを追加と.cppにStaticTypeを定義するマクロを追加すれば完成です!

// @Logicalbeat Murata - BEGIN Smoothstep用MaterialUniformExpressionクラスを追加
/**
 */
class FMaterialUniformExpressionSmoothstep : public FMaterialUniformExpression
{
	DECLARE_MATERIALUNIFORMEXPRESSION_TYPE(FMaterialUniformExpressionSmoothstep);
public:
	FMaterialUniformExpressionSmoothstep(){}
	FMaterialUniformExpressionSmoothstep(FMaterialUniformExpression* InMin, FMaterialUniformExpression* InMax, FMaterialUniformExpression* InInput)
		: Min(InMin)
		, Max(InMax)
		, Input(InInput)
	{}

	// FMaterialUniformExpression interface.
	virtual bool IsConstant() const
	{
		return Input->IsConstant() && Min->IsConstant() && Max->IsConstant();
	}
	virtual bool IsIdentical(const FMaterialUniformExpression* OtherExpression) const
	{
		if (GetType() != OtherExpression->GetType())
		{
			return false;
		}
		FMaterialUniformExpressionSmoothstep* OtherSmoothstep = (FMaterialUniformExpressionSmoothstep*)OtherExpression;
		return Input->IsIdentical(OtherSmoothstep->Input) && Min->IsIdentical(OtherSmoothstep->Min) && Max->IsIdentical(OtherSmoothstep->Max);
	}
private:
	TRefCountPtr<FMaterialUniformExpression> Min;
	TRefCountPtr<FMaterialUniformExpression> Max;
	TRefCountPtr<FMaterialUniformExpression> Input;
};
// @Logicalbeat Murata - END
// @Logicalbeat Murata - BEGIN FMaterialUniformExpressionSmoothstep追加
IMPLEMENT_MATERIALUNIFORMEXPRESSION_TYPE(FMaterialUniformExpressionSmoothstep);
// @Logicalbeat Murata - END

確認

実装が完了したので、実際に動作を確認してみます。

値をグラフ化して確認してみましたが問題なさそうですね!
念の為に変換されたShaderコードも確認してみます。

float  Local0 = smoothstep(Material_ScalarExpressions[0].y,Material_ScalarExpressions[0].x,Parameters.TexCoords[0].xy.r);
float  Local1 = (1.00000000 - Parameters.TexCoords[0].xy.g);
float  Local2 = (Local0 - Local1);
float  Local3 = abs(Local2);
float  Local4 = ((abs(Local3 - Material_ScalarExpressions[0].z) > 0.00001000) ? (Local3 >= Material_ScalarExpressions[0].z ? 0.00000000 : 1.00000000) : 1.00000000);
float3  Local5 = lerp( float3 (0.00000000,0.00000000,0.00000000),Material_VectorExpressions[3].rgb, float (Local4));
float3  Local6 = lerp(Local5,Material_VectorExpressions[4].rgb, float (Material_ScalarExpressions[0].w));

PixelMaterialInputs.EmissiveColor = Local6;

ちゃんとSmoothstep関数が呼ばれてますね!

最後に

やってみると案外簡単にノードを追加できました。
UE4はソースが公開されているので、こういう拡張がしやすくて助かりますね。
今回紹介した方法の他にもCustomノードを使えば、ノード化されていない関数を呼び出すことが可能です。
Customノードだと関数の呼び出しが増えるので、今回の手法より少しだけ負荷が高くなる気がします。
(ちゃんと計測したわけではないです。。)

今回の内容が少しでも皆様の開発の参考になれば幸いです。
最後までご覧いただき、ありがとうございました。


【免責事項】

本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。