3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【UE5】 エンジンを改造してSubstrateにカスタムシェーディングを追加する

Last updated at Posted at 2025-12-16

【UE5】 エンジンを改造してSubstrateにカスタムシェーディングを追加する

Cover.jpg
モデル 沙花叉クロヱ © 2016 cover corp. / マップ Goddess Temple

はじめに

Unreal Engine 5.7がリリースされSubstrateも正式版になりました。
いずれSubstrateに一本化するという話もあるようです。
Substrateでもトゥーンがしたい!ということでエンジンとシェーダーをカスタマイズしてトゥーンシェーダーを追加しようと思います。
エンジンはバージョンは5.7.1を使用しました。

前提知識

エンジンの改造(C++)

マテリアルグラフ上で独自のパラメーターを渡せるようにSubstrateEyeBSDFノードを改造します。
エンジンのソースを開きSubstrateEyeBSDFを検索すると以下のファイルで使用されていることがわかります。
これらを修正していきます。

  • \Engine\Source\Runtime\Engine\Public\Materials\MaterialExpressionSubstrate.h
  • \Engine\Source\Runtime\Engine\Public\MaterialCompiler.h
  • \Engine\Source\Runtime\Engine\Private\Materials\MaterialExpressionSubstrate.cpp

Node_02.jpg
改造されたBSDFノード

MaterialExpressionSubstrate.h

UMaterialExpressionSubstrateEyeBSDFクラスでSubstrate Eye BSDFノードが定義されています。
必要なパラメーターを設定するためのピンをクラスに定義します。

\Engine\Source\Runtime\Engine\Public\Materials\MaterialExpressionSubstrate.h
UCLASS(MinimalAPI, collapsecategories, hidecategories = Object, DisplayName = "Substrate Toon BSDF")
class UMaterialExpressionSubstrateEyeBSDF : public UMaterialExpressionSubstrateBSDF
{
	GENERATED_UCLASS_BODY()
		
	UPROPERTY()
	FExpressionInput DiffuseColor;
		
	UPROPERTY()
	FExpressionInput Roughness;

	UPROPERTY()
	FExpressionInput Normal;

	UPROPERTY()
	FExpressionInput Tangent;

	UPROPERTY()
	FExpressionInput EmissiveColor;

	// ... (以下略)
}

MaterialExpressionSubstrate.cpp

ここでは、マテリアルノードのピンの型定義や、コンパイラへのデータ受け渡しの処理を記述します。

UMaterialExpressionSubstrateEyeBSDF::Compile

パラメーターの整合性の確認やデフォルト値の設定を行います。
NormalやTangentがSharedLocalBasisで使われていることに注意して書き換えます。

\Engine\Source\Runtime\Engine\Private\Materials\MaterialExpressionSubstrate.cpp
int32 UMaterialExpressionSubstrateEyeBSDF::Compile(class FMaterialCompiler* Compiler, int32 OutputIndex)
{
	int32 NormalCodeChunk = CompileWithDefaultNormalWS(Compiler, Normal);
	int32 TangentCodeChunk = CompileWithDefaultTangentWS(Compiler, Tangent);
	const FSubstrateRegisteredSharedLocalBasis NewRegisteredSharedLocalBasis = SubstrateCompilationInfoCreateSharedLocalBasis(Compiler, NormalCodeChunk, TangentCodeChunk);

	FSubstrateOperator& SubstrateOperator = Compiler->SubstrateCompilationGetOperator(Compiler->SubstrateTreeStackGetPathUniqueId());
	SubstrateOperator.BSDFRegisteredSharedLocalBasis = NewRegisteredSharedLocalBasis;

	if (SubstrateOperator.bUseParameterBlending)
	{
		return Compiler->Errorf(TEXT("Substrate Eye BSDF node cannot be used with parameter blending."));
	}
	else if (SubstrateOperator.bRootOfParameterBlendingSubTree)
	{
		return Compiler->Errorf(TEXT("Substrate Eye BSDF node cannot be the root of a parameter blending sub tree."));
	}

	int32 OutputCodeChunk = Compiler->SubstrateEyeBSDF(
		CompileWithDefaultFloat3(Compiler, DiffuseColor, 0.0f, 0.0f, 0.0f),
		CompileWithDefaultFloat1(Compiler, Roughness, 0.5f),
		NormalCodeChunk,
		TangentCodeChunk,
		CompileWithDefaultFloat3(Compiler, EmissiveColor, 0.0f, 0.0f, 0.0f),
		Compiler->GetSubstrateSharedLocalBasisIndexMacro(NewRegisteredSharedLocalBasis),
		&SubstrateOperator);

	return OutputCodeChunk;
}

UMaterialExpressionSubstrateEyeBSDF::SubstrateGenerateMaterialTopologyTree

SubstrateOperatorを設定します。
BSDFFeaturesFSubstrateCompilationContext::SubstrateGenerateDerivedMaterialOperatorDataでピクセルあたりに必要なバイト数を計算するのに使われます。
SubstrateEyeBSDFは固定長ですが、ピンが接続状況に応じて追加のバッファを使用するか決定したい場合は、ここでフラグを設定します。

\Engine\Source\Runtime\Engine\Private\Materials\MaterialExpressionSubstrate.cpp
FSubstrateOperator* UMaterialExpressionSubstrateEyeBSDF::SubstrateGenerateMaterialTopologyTree(class FMaterialCompiler* Compiler, class UMaterialExpression* Parent, int32 OutputIndex)
{
	FSubstrateOperator& SubstrateOperator = Compiler->SubstrateCompilationRegisterOperator(SUBSTRATE_OPERATOR_BSDF, Compiler->SubstrateTreeStackGetPathUniqueId(), this->MaterialExpressionGuid, Parent, Compiler->SubstrateTreeStackGetParentPathUniqueId());
	SubstrateOperator.BSDFType = SUBSTRATE_BSDF_TYPE_EYE;
	SubstrateOperator.BSDFFeatures = ESubstrateBsdfFeature::Eye;

	SubstrateOperator.ThicknessIndex = Compiler->SubstrateThicknessStackGetThicknessIndex();
	SubstrateOperator.bBSDFWritesEmissive = EmissiveColor.IsConnected();
	return &SubstrateOperator;
}

使用するGBufferのサイズを変更する場合:

\Engine\Source\Runtime\Engine\Private\Materials\HLSLMaterialTranslator.cpp
case SUBSTRATE_BSDF_TYPE_EYE:
{
	// Custom encoding
	SubstrateMaterialRequestedSizeByte += UintByteSize;
	SubstrateMaterialRequestedSizeByte += UintByteSize;

	// ESubstrateBsdfFeatureの値によって可変長にしてもよい
	if (It.Has(ESubstrateBsdfFeature::ExCustomParameter))
	{
		SubstrateMaterialRequestedSizeByte += UintByteSize;
	}
	break;
}

MaterialCompiler.h / HLSLTranslator.h

パラメーターに対応した引数に変更します。
HLSLTranslator.h も同様に変更しておきます。

\Engine\Source\Runtime\Engine\Public\MaterialCompiler.h
virtual int32 SubstrateEyeBSDF(
	int32 DiffuseAlbedo, int32 Roughness, int32 Normal, int32 Tangent,
	int32 EmissiveColor, const FString& SharedLocalBasisIndexMacro, FSubstrateOperator* PromoteToOperator) = 0;

HLSLTranslator.cpp

GetSubstrateEyeBSDF関数を呼び出すシェーダーコードを生成します。

\Engine\Source\Runtime\Engine\Private\Materials\HLSLMaterialTranslator.cpp
int32 FHLSLMaterialTranslator::SubstrateEyeBSDF(
		int32 DiffuseAlbedo, int32 Roughness, int32 EmissiveColor, 
		int32 Normal, int32 Tangent, const FString& SharedLocalBasisIndexMacro, 
		FSubstrateOperator* PromoteToOperator)
{
	return AddCodeChunk(
		MCT_Substrate, TEXT("Parameters.%s.PromoteParameterBlendedBSDFToOperator(GetSubstrateEyeBSDF(\n\
								/*DiffuseAlbedo*/				%s,\n\
								/*Roughness*/					%s,\n\
								/*EmissiveColor*/				%s,\n\
								/*SharedLocalBasisIndexMacro*/	%s),\n\
								%u, %u, %u, %u) /* Normal = %s ; Tangent = %s */"),
		*GetParametersSubstrateTreeName(CurrentSubstrateCompilationContext),
		*SubstrateGetCastParameterCode(DiffuseAlbedo, MCT_Float3),
		*SubstrateGetCastParameterCode(Roughness, MCT_Float),
		*SubstrateGetCastParameterCode(EmissiveColor, MCT_Float),
		*SharedLocalBasisIndexMacro,
		PromoteToOperator->Index,
		PromoteToOperator->BSDFIndex,
		PromoteToOperator->LayerDepth,
		PromoteToOperator->bIsBottom ? 1 : 0,
		*GetParameterCode(Normal),
		*GetParameterCode(Tangent)
	);
}

パラメーターの読み書き(HLSL)

Substrateで独自のパラメーターを追加する場合、以下の3箇所の処理を書き換える必要があります。

  • マテリアルノードからBSDFコンテキストへの変換
  • BSDFコンテキストからGBufferへのパッキング
  • GBufferからBSDFコンテキストへのアンパッキング

GetSubstrateEyeBSDF

マテリアルノードから渡されたパラメーターをFSubstrateBSDF構造体に設定する関数です。
FSubstrateBSDFは以下のようにフラグとパラメーターを持っています。

\Engine\Shaders\Private\Substrate\Substrate.ush
struct FSubstrateBSDF
{
	uint   State;
	float4 VGPRs[5];
	//  ... (以下略)
}

パラメーターの格納順は自分で定義し、マクロを使用してアクセスします。

\Engine\Shaders\Private\Substrate\Substrate.ush
#define EYE_DIFFUSEALBEDO(X)			X.VGPRs[0].xyz
#define EYE_ROUGHNESS(X)				X.VGPRs[0].w

// 今回の例では使いませんが、様々なパラメーターを持つことができます。
#define EYE_F0(X)						X.VGPRs[1].xyz
#define EYE_F90(X)						X.VGPRs[2].xyz
#define EYE_SSSMFP(X)					X.VGPRs[3].xyz
#define EYE_SSSPHASEANISOTROPY(X)		X.VGPRs[3].w
#define EYE_HASSPECULAR(X)				false

// 一部のシェーダーで呼ばれるので定義だけしておく
#define EYE_IRISDISTANCE(X)				0.f
#define EYE_IRISMASK(X)					0.f
#define EYE_IRISNORMAL(X)				float3(0.f, 0.f, 0.f)
#define EYE_IRISPLANENORMAL(X)			float3(0.f, 0.f, 0.f)

GetSubstrateEyeBSDF関数内でフラグやパラメーターを設定します。

\Engine\Shaders\Private\Substrate\Substrate.ush
FSubstrateData GetSubstrateEyeBSDF(float3 DiffuseAlbedo, float Roughness, float3 Emissive, uint SharedLocalBasisIndex)
{
	FSubstrateData SubstrateData = GetInitialisedSubstrateData();
	
	BSDF_SETTYPE					(SubstrateData.InlinedBSDF, SUBSTRATE_BSDF_TYPE_EYE);
	BSDF_SETEMISSIVE				(SubstrateData.InlinedBSDF, Emissive);
	EYE_DIFFUSEALBEDO				(SubstrateData.InlinedBSDF) = DiffuseAlbedo;
	EYE_ROUGHNESS					(SubstrateData.InlinedBSDF) = Roughness;

	// 必要に応じてフラグの設定を行います
	// const bool bHasAnisotropy = Anisotropy != 0.f;
	// BSDF_SETHASANISOTROPY	(SubstrateData.InlinedBSDF,		bHasAnisotropy);

	// const bool bHasSSS = any(SSSMFP > 0.f);
	// BSDF_SETSSSTYPE(SubstrateData.InlinedBSDF, bHasSSS ? SSS_TYPE_DIFFUSION : SSS_TYPE_NONE);

#if SUBSTRATE_INLINE_SHADING
	SubstrateData.InlinedBSDF.Coverage = 1.0f;
#endif

#if USE_DEVELOPMENT_SHADERS
	SubstrateData.PreviewColor			= DiffuseAlbedo;
#endif

	return SubstrateData;
}

PackSubstrateOut

マテリアルのステートやパラメーターをGBufferにパッキングします。
プロジェクトの設定でAdaptive GBufferを指定した場合GBufferの構成は以下のようになります。

  • PixelHeader
    • UINT32型テクスチャ
    • 8ビットのフラグ
    • 8ビットの追加のフラグ(Slabの場合)、あるいはAO値
    • 16ビットの自由に使える領域
  • TopLayerData
    • UINT32型テクスチャ
    • 2ビットのフラグ (Invalid/Valid/SingleLayeredWater)
    • 22ビットのワールド法線
    • 8ビットのラフネス値
  • SubsrfaceData
    • UINT32型テクスチャ
    • 3ビットのサブサーフェスの種類を表すフラグ
    • 29ビットのサブサーフェスデータ(SubsurfaceProfileId/SSSMFP)
  • マテリアルのパラメーターに応じた追加のバッファ
    • BSDF_TYPE_EYEの場合は UINT32型テクスチャ x2
    • FSubstrateCompilationContext::SubstrateGenerateDerivedMaterialOperatorDataSubstrateMaterialRequestedSizeByteで決定されます
Engine\Shaders\Private\Substrate\SubstrateExport.ush
case SUBSTRATE_BSDF_TYPE_EYE:
{
	bIsOnlySlab = false;
	bIsSimpleMaterial = false;
	bIsSingleMaterial = false;

	TopLayerTotalWeight += BSDF.TopLayerDataWeight;
	TopLayerData.Roughness += BSDF.TopLayerDataWeight * EYE_ROUGHNESS(BSDF);
	TopLayerData.UnlitViewBaseColor += BSDF.TopLayerDataWeight * SubstrateGetBSDFBaseColor(BSDF);
	TopLayerData.Material = TOP_LAYER_MATERIAL_VALID;

	const bool bHasSSS = BSDF_GETSSSTYPE(BSDF) != SSS_TYPE_NONE;
	SubstratePixelHeader.SetHasSubsurface(bHasSSS);
	SubstratePixelHeader.SetMaterialMode(HEADER_MATERIALMODE_EYE);
	SubstratePixelHeader.SetCastContactShadow(false); // Eye shading disables contact shadow. See CastContactShadow(...)
	
	bSubstrateSubsurfaceEnable = bHasSSS;

	break;
}

PixelHeaderを追加のデータとともに書き出します。

Engine\Shaders\Private\Substrate\SubstrateExport.ush
else if (SubstratePixelHeader.IsEye())
{
	// Data0 (Header_State|Header_AO|Data)
	uint Out = 0;
	HEADER_SETCOMMONSTATES(Out, SubstratePixelHeader.State);
	Out = Out & HEADER_EYEENCODING_MASK;
	SUBSTRATE_STORE_UINT1(Out);

	#if (HEADER_EYEENCODING_BIT_COUNT) > 32
	#error Substrate eye path header is > 32bits
	#endif
}

独自のパラメーターを決定したバッファサイズに合うように書き出します。
サブサーフェスが有効な場合、設定しておきます。

Engine\Shaders\Private\Substrate\SubstrateExport.ush
case SUBSTRATE_BSDF_TYPE_EYE:
{
	const bool bHasSSS = BSDF_GETSSSTYPE(BSDF) != SSS_TYPE_NONE;

	float3 DiffuseAlbedo = EYE_DIFFUSEALBEDO(BSDF);
	float Alpha = 0.f;
	uint EyeData1 = PackColorLinearToGamma2AlphaLinear(float4(DiffuseAlbedo, Alpha));
	SUBSTRATE_STORE_UINT1(EyeData1);

	const uint EyeData2 = 0;
	SUBSTRATE_STORE_UINT1(EyeData2);
	
	// const bool bHasSSS = BSDF_GETSSSTYPE(BSDF) != SSS_TYPE_NONE;
	// if (bHasSSS)
	// {
	// 	SSSDataType = BSDF_GETSSSTYPE(BSDF);
	// 	SSSDataAniso = EYE_SSSPHASEANISOTROPY(BSDF);
	// 	SSSDataMFP = EYE_SSSMFP(BSDF);
	// 	SSSDataBaseColor = EYE_DIFFUSEALBEDO(BSDF);
	// }
}
break;

UnpackSubstrateBSDFIn

GBufferからデータを読み取りFSubstrateBSDFを設定して返します。

\Engine\Shaders\Private\Substrate\Substrate.ush
else if (SubstrateHeader.IsEye())
{
	OutBSDF.State = Data0 & HEADER_EYEENCODING_MASK;
	OutBSDF.LuminanceWeightV = 1.0f;

	BSDF_SETTYPE(OutBSDF, SUBSTRATE_BSDF_TYPE_EYE);
	BSDF_SETSHAREDLOCALBASISID(OutBSDF, 0);
	BSDF_SETHASANISOTROPY(OutBSDF, 0);
	BSDF_SETISTOPLAYER(OutBSDF, 1);
	BSDF_SETSSSTYPE(OutBSDF, SSS_TYPE_NONE);

	const float4 EyeData1 = UnpackRGBA8(Data1);
	EYE_DIFFUSEALBEDO(OutBSDF) = SubstrateSrgbToLinear(EyeData1.xyz);
	EYE_ROUGHNESS(OutBSDF) = SubstrateUnpackTopLayerData(SubstrateHeader.PackedTopLayerData).Roughness;

	// Data2や追加のバッファのデータを読み取ることもできます
	// EYE_CUSTOMDATA(OutBSDF) = UnpackR11G11B10F(Data2);
	// EYE_EXDATA(OutBSDF) = UnpackRGBA8(SubstrateLoadUint1(SubstrateBuffer, SubstrateAddressing));
}

ライティング(HLSL)

最後に、ライティング計算を行う箇所を修正します。

SubstrateEvaluateBSDFCommon

直接光の計算パスで呼び出されます。
ここで独自のトゥーン調のシェーディングを実装します。
Sample.IntegratedDiffuseValueSample.IntegratedSpecularValueに計算結果を代入します。

\Engine\Shaders\Private\Substrate\SubstrateEvaluation.ush
case SUBSTRATE_BSDF_TYPE_EYE:
{
	const float3 DiffuseColor	= EYE_DIFFUSEALBEDO(BSDFContext.BSDF);

	// ビルボード風のライティング
	const float LightingIntensity =  saturate(BSDFContext.Context.VoL);

	Sample.DiffuseColor		= DiffuseColor;
	Sample.DiffusePathValue = Diffuse_Lambert(DiffuseColor);
	Sample.IntegratedDiffuseValue += (LightingIntensity * AreaLight.Falloff) * AreaLight.FalloffColor * Sample.DiffusePathValue;
	Sample.IntegratedSpecularValue = 0.f;
	Sample.bPostProcessSubsurface = false;
	break;
}

SubstrateImportanceSampleBSDF

反射の計算パスで呼ばれます。
反射を使用しない場合は処理をスキップしても構いません。

\Engine\Shaders\Private\Substrate\SubstrateEvaluation.ush
case SUBSTRATE_BSDF_TYPE_EYE:
{	
	if (Out.Term & SHADING_TERM_SPECULAR && EYE_HASSPECULAR(BSDFContext.BSDF))
	{
		float SpecularRoughness = MakeRoughnessSafe(EYE_ROUGHNESS(BSDFContext.BSDF));
		float2 Alpha = Pow2(SpecularRoughness);

		float4 GGXSample	= ImportanceSampleVisibleGGX(E, Alpha, BSDFContext.TangentV);
		const float HPDF	= GGXSample.w;
		const float3 H		= mul(GGXSample.xyz, BSDFContext.TangentBasis);
		const float VoH		= saturate(dot(BSDFContext.V, H));

		Out.L				= 2 * dot(BSDFContext.V, H) * H - BSDFContext.V;
		Out.PDF				= RayPDFToReflectionRayPDF(VoH, HPDF);
		Out.Term = SHADING_TERM_SPECULAR;
		Out.Weight = 1.f;
	}
	break;
}

SubstrateEvaluateForEnvLight

間接光の計算パスや反射光の合成パスで呼ばれます。

\Engine\Shaders\Private\Substrate\SubstrateEvaluation.ush
case SUBSTRATE_BSDF_TYPE_EYE:
{
	float3 DiffuseColor		= EYE_DIFFUSEALBEDO(BSDFContext.BSDF);
	SubstrateEnvLightResult.DiffuseNormal = -View.ViewForward;
	SubstrateEnvLightResult.DiffuseColor = DiffuseColor;
	SubstrateEnvLightResult.DiffuseWeight = DiffuseColor;

	if (bEnableSpecular && EYE_HASSPECULAR(BSDFContext.BSDF))
	{
		float3 F0 				= EYE_F0(BSDFContext.BSDF);
		float3 F90 				= EYE_F90(BSDFContext.BSDF);
		F90 *= F0RGBToMicroOcclusion(F0);

		float SafeRoughness = MakeRoughnessSafe(EYE_ROUGHNESS(BSDFContext.BSDF));
		FBxDFEnergyTermsRGB SpecularEnergyTerms 		= ComputeGGXSpecEnergyTermsRGB_Env(SafeRoughness, BSDFContext.Context.NoV, F0, F90);
		FBxDFEnergyTermsRGB DiffuseEnergyTerms  		= ComputeDiffuseEnergyTermsRGB(SafeRoughness, BSDFContext.Context.NoV);
		float3 EvalEnvBRDF								= SpecularEnergyTerms.E;
		float DirectionalAlbedo_SpecularTransmission	= ComputeEnergyPreservation(SpecularEnergyTerms);

		SubstrateEnvLightResult.DiffuseWeight			*= DiffuseEnergyTerms.E;
		SubstrateEnvLightResult.SpecularDirection = SubstrateGetOffSpecularPeakReflectionDir(BSDFContext.N, BSDFContext.R, SafeRoughness);
		SubstrateEnvLightResult.SpecularWeight = EvalEnvBRDF;
		SubstrateEnvLightResult.SpecularSafeRoughness = SafeRoughness;
		SubstrateEnvLightResult.SpecularColor = F0;
		SubstrateEnvLightResult.DiffuseWeight *= DirectionalAlbedo_SpecularTransmission;
	}

	break;
}

SubstrateDeferredLighting

デフォルトだとシャドウマップで影だと判定されるとライティングの計算がスキップされます。
このままだと一部が不自然に真っ暗になるので強制的にライティングを行うようにします。

\Engine\Shaders\Private\Substrate\SubstrateDeferredLighting.ush
const bool bNeedLighting = (BSDFShadowTerms.SurfaceShadow + BSDFShadowTerms.TransmissionShadow) > 0 || BSDF_GETTYPE(BSDF) == SUBSTRATE_BSDF_TYPE_EYE;

if (bNeedLighting)
{
	// ...

InternalReadMaterialData_Substrate_Regular

よりフラットなライティングを行うためFLumenMaterialDataを生成する部分も変更します。

\Engine\Shaders\Private\Lumen\LumenMaterial.ush
Out.WorldNormal						= Out.TangentBasis[2];
if (BSDFType == SUBSTRATE_BSDF_TYPE_EYE)
{
	Out.WorldNormal = -View.ViewForward;
}
Out.WorldNormalForPositionBias		= Out.TangentBasis[2];

確認

エンジンのコンパイル後、ConsoleVariables.iniを編集してシェーダーのデバッグを有効にしてエンジンを起動し、プロジェクト設定でSubstrate GBuffer FormatAdaptive GBufferに変更して描画を確認します。

\Engine\Config\ConsoleVariables.ini
r.ShaderDevelopmentMode=1

Test_Direct.jpg
直接光の確認

Test_Indirect.jpg
Lumen(Emissiveマテリアルからのライティング)の確認

結び

お疲れ様でした。
Substrateの改造やレンダリングパスのイメージを掴むヒントになれば幸いです。

おまけ

Slab BSDFのフラグを確認するとSingle LayoutComplex Layoutの場合使われていないビットがあることがわかります。
カスタムシェーダーフラグを立てて各ライティングパスに介入する方針にするとマテリアルレイヤーを使えたりFuzzやGlintなど移植が大変な機能が使えるかもしれません。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?