Unreal Engine 5.4へのShading Model追加の試みと、その改造箇所の意味を紐解く

## 概要

Unreal Engine 5.4へのShading Model追加をやってみました。
今回の作業にあたりインターネット上の複数の記事にお世話になりましたが、主にスパーククリエイティブ様の以下記事を参考にさせて頂きました。

https://cgworld.jp/regular/202311-ue5toon-04.html

Shading Modelの追加はエンジンの非常に多くの箇所を変更することになりますが、それぞれエンジンの何に対してどういった変更をしているのか調べてみました。

## マテリアルエディタからシェーディングモデルを選択できるようにする

まず、EMaterialShadingModelに自分のShading Modelを追加します。MSM_MyShadingModelとしました。
```EngineTypes.h
UENUM()
enum EMaterialShadingModel : int
{
	MSM_Unlit					UMETA(DisplayName="Unlit"),
	MSM_DefaultLit				UMETA(DisplayName="Default Lit"),
	MSM_Subsurface				UMETA(DisplayName="Subsurface"),
	MSM_PreintegratedSkin		UMETA(DisplayName="Preintegrated Skin"),
	MSM_ClearCoat				UMETA(DisplayName="Clear Coat"),
	MSM_SubsurfaceProfile		UMETA(DisplayName="Subsurface Profile"),
	MSM_TwoSidedFoliage			UMETA(DisplayName="Two Sided Foliage"),
	MSM_Hair					UMETA(DisplayName="Hair"),
	MSM_Cloth					UMETA(DisplayName="Cloth"),
	MSM_Eye						UMETA(DisplayName="Eye"),
	MSM_SingleLayerWater		UMETA(DisplayName="SingleLayerWater"),
	MSM_ThinTranslucent			UMETA(DisplayName="Thin Translucent"),
	MSM_Strata					UMETA(DisplayName="Substrate", Hidden),
	MSM_MyShadingModel			UMETA(DisplayName="MyShadingModel"),	// yorung
	/** Number of unique shading models. */
	MSM_NUM						UMETA(Hidden),
	/** Shading model will be determined by the Material Expression Graph,
		by utilizing the 'Shading Model' MaterialAttribute output pin. */
	MSM_FromMaterialExpression	UMETA(DisplayName="From Material Expression"),
	MSM_MAX
};
```

これだけの変更でマテリアルエディタのShading Modelに選択肢が追加されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66160/8d7d75e6-7fb6-f72b-cc77-6356b09e6499.png)

描画結果は真っ黒になります。これには２つ理由があります。まずシェーディングのコードがHLSLに追加されていないこと、もう一つはGBufferがバインドされていないことです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66160/c84d2083-5d20-8a99-6524-c20c477cf571.png)

理由の一方である、シェーディングのコードを先に追加しておくこととします。自身のシェーディングを書く前段階としてDefaultLitと同じ結果になるようにDefaultLitBxDFをコールするようにしておきます。
```C++:ShadingModels.ush
FDirectLighting IntegrateBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow )
{
	switch( GBuffer.ShadingModelID )
	{
		case SHADINGMODELID_DEFAULT_LIT:
		case SHADINGMODELID_SINGLELAYERWATER:
		case SHADINGMODELID_THIN_TRANSLUCENT:
			return DefaultLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
		case SHADINGMODELID_SUBSURFACE:
			return SubsurfaceBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
		case SHADINGMODELID_PREINTEGRATED_SKIN:
			return PreintegratedSkinBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
		case SHADINGMODELID_CLEAR_COAT:
			return ClearCoatBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
		case SHADINGMODELID_SUBSURFACE_PROFILE:
			return SubsurfaceProfileBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
		case SHADINGMODELID_TWOSIDED_FOLIAGE:
			return TwoSidedBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
		case SHADINGMODELID_HAIR:
			return HairBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
		case SHADINGMODELID_CLOTH:
			return ClothBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
		case SHADINGMODELID_EYE:
			return EyeBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
		// yorung begin
		case SHADINGMODELID_MYSHADINGMODEL:
			return DefaultLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
		// yorung end
		default:
			return (FDirectLighting)0;
	}
}
```
ただし、この状態でも真っ黒に描画されてしまう事にかわりありません。


## GBufferへ描き込まれない原因を調査

HLSLにシェーディングのコード及びBuffer Visualization => Shading Modelの表示色の記述を行いました。左の箱がMyShadingModel, 右の箱がUnlitです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66160/89c12f10-1aa3-e718-b175-1eb8103401ee.png)

Buffer Visualization => Overviewで見てみると、Shading ModelがどちらもUnlitになってしまっています。また、Base Color、World Normal等もUnlit同様に何も描き込まれていないことがわかります。

UnlitはShading Model 0番であり、GBufferをクリアした状態と同等です。状況から見るにMyShadingModelのマテリアルでShading ModelにUnlitが描き込まれたというようりは、Base ColorやWorld Normal同様に何も描き込まれなかったと考えるのが自然です。

箱に色がついていますが、これはマテリアルでEmissiveに色を差しています。これはEmissiveが描き込まれるScene Colorだけは描画ができていることを示しています。

「実際にシェーダーコードそのものは走っているが、Base Color等いくつかのGBufferへの書き込みが行われていない」という仮説を立てました。その検証はBasePassPixelShader.usfの最後の行でOut.MRT[]を操作することで行いました。

```HLSL:BasePassPixelShader.usf
void FPixelShaderInOut_MainPS(
	FVertexFactoryInterpolantsVSToPS Interpolants,
	FBasePassInterpolantsVSToPS BasePassInterpolants,
	in FPixelShaderIn In,
	inout FPixelShaderOut Out)
{
...
//	Out.MRT[0] = float4(0,0,0,0);	// このコードを実行すると、MyShadingModelを含む全マテリアルのエミッシブが消える
//	Out.MRT[3] = float4(1,1,1,1);	// このコードを実行すると、全てのBaseColorが塗りつぶされるが、MyShadingModelのBaseColorだけはは黒いまま
	// この実験結果から、MyShadingModelを含む全マテリアルがここを通る事がわかるが、MyShadingModelのBaseColorやShadingModelIDが実際にレンダーターゲットに描き込まれない
}
```
Out.MRT[0]はScene Colorを示していて、マテリアルのEmissiveが描き込まれる場所です。Out.MRT[3]はBase Colorを示しており、マテリアルのBase Colorが描き込まれる場所です。

上のコメントアウトを外し、Out.MRT[]に直接値を代入することでGBufferに起こる変化を見ます。結果は、Out.MRT[0]を0で埋めることによりマテリアルのEmissiveを全て無効に出来た一方で、Out.MRT[3]を1で埋めてもBase Colorに一切変更を加えることができませんでした。上の仮説とひとまず矛盾しないことは検証できました。

また、この仮説を裏付けるコードがPixelShaderOutputCommon.ushにあります。
```HLSL:PixelShaderOutputCommon.ush
#if PIXELSHADEROUTPUT_MRT0
	OutTarget0 = PixelShaderOut.MRT[0];
#endif

#if PIXELSHADEROUTPUT_MRT1
	OutTarget1 = PixelShaderOut.MRT[1];
#endif

#if PIXELSHADEROUTPUT_MRT2
	OutTarget2 = PixelShaderOut.MRT[2];
#endif

#if PIXELSHADEROUTPUT_MRT3
	OutTarget3 = PixelShaderOut.MRT[3];
#endif
```

PixelShaderOutputCommon.ushはBasePassPixelShader.usfからincludeされていますが、BasePassPixelShader.usfの実際のエントリーポイントであるMainPSはPixelShaderOutputCommon.ush側に記述されています。レンダーターゲットへの書き込みを実際に行うかどうかは PIXELSHADEROUTPUT_MRT1 などのマクロによって切り替えられていることがわかります。どうやらここはC++側がマクロ切り替えなどの細かい制御を行っている事が推測されます。

## C++側でGBufferへの書き込みを制御する

ここまでの推測が合っていれば、Shading Model追加が正しく行われるとPIXELSHADEROUTPUT_MRT1等のマクロに1が定義され、シェーダーからレンダーターゲットにアクセスできるようになるはずです。

結論を先に書くと、C++側のエンジン改造を行ったことによりShading Model, World Normal, Base Colorが描かれるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66160/913ec49d-2d02-3ad0-1dc9-a5a61e51d9f0.png)

PIXELSHADEROUTPUT_MRT1等が1になるためにはいくつかのステップを経ます。流れの発端はGetMaterialEnvironment関数です。
MyShadingModelを選択したとき、MATERIAL_SHADINGMODEL_MYSHADINGMODELマクロを定義するように指定します。

```C++:MaterialHLSLEmitter.cpp
static void GetMaterialEnvironment(EShaderPlatform InPlatform,
	const FMaterial& InMaterial,
	const UE::HLSLTree::FEmitContext& EmitContext,
	const FMaterialCompilationOutput& MaterialCompilationOutput,
	bool bUsesEmissiveColor,
	bool bUsesAnisotropy,
	bool bIsFullyRough,
	FShaderCompilerEnvironment& OutEnvironment)
{
    ...
	if (ShadingModels.IsLit())
	{
		...
		// yorung begin
		if (ShadingModels.HasShadingModel(MSM_MyShadingModel))
		{
			OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_MYSHADINGMODEL"), TEXT("1"));
			NumSetMaterials++;
		}
		// yorung end
		...
	}
	else
	{
		// Unlit shading model can only exist by itself
		OutEnvironment.SetDefine(TEXT("MATERIAL_SINGLE_SHADINGMODEL"), TEXT("1"));
		OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_UNLIT"), TEXT("1"));
	}
```

定義したMATERIAL_SHADINGMODEL_MYSHADINGMODELはHLSLから参照されるものでもありますが、C++自身でも使います。
以下、それぞれのマクロの定義有無が一通りFShaderMaterialPropertyDefines構造体に収集されていきます。

```C++:ShaderGenerationUtil.cpp
template<typename EnvironmentType>
void ApplyFetchEnvironmentInternal(FShaderMaterialPropertyDefines& SrcDefines, const EnvironmentType& Environment)
{
    ...
	FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_UNLIT);

	FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_DEFAULT_LIT);
	FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_SUBSURFACE);
	FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN);
	FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_SUBSURFACE_PROFILE);
	FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_CLEAR_COAT);
	FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE);
	FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_HAIR);
	FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_CLOTH);
	FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_EYE);
	FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_SINGLELAYERWATER);
	FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_THIN_TRANSLUCENT);
	FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_MYSHADINGMODEL);	// yorung
```

FShaderMaterialPropertyDefines構造体に収集された情報を元に、どのGBufferのスロットが使われるか、使われるのであれば読み込みなのか書き込みなのかがSlotsに収集されていきます。

```C++:ShaderGenerationUtil.cpp
static void DetermineUsedMaterialSlots(
	EGBufferSlotUsage Slots[GBS_Num],
	const FShaderMaterialDerivedDefines& Dst,
	const FShaderMaterialPropertyDefines& Mat,
	const FShaderLightmapPropertyDefines& Lightmap,
	const FShaderGlobalDefines& SrcGlobal,
	const FShaderCompilerDefines& Compiler,
	ERHIFeatureLevel::Type FEATURE_LEVEL)
{
    ...
	// yorung begin
	if (Mat.MATERIAL_SHADINGMODEL_MYSHADINGMODEL)
	{
		SetStandardGBufferSlots(Slots, bWriteEmissive, bHasTangent, bHasVelocity, bWritesVelocity, bHasStaticLighting, bIsSubstrateMaterial);
		Slots[GBS_CustomData] = GetGBufferSlotUsage(bUseCustomData);
	}
	// yorung end

```

最終的に、使われるスロットに紐づくGBufferが特定され、"PIXELSHADEROUTPUT_MRT%d"の行で該当のマクロが定義されるようになっていました。

```C++:ShaderGenerationUtil.cpp
void FShaderCompileUtilities::ApplyDerivedDefines(FShaderCompilerEnvironment& OutEnvironment, FShaderCompilerEnvironment * SharedEnvironment, const EShaderPlatform Platform)
{
    ...
			DetermineUsedMaterialSlots(Slots, DerivedDefines, MaterialDefines, LightmapDefines, GlobalDefines, CompilerDefines, FeatureLevel);
    ...


	// Decide which pixel shader outputs are enabled based on which targets are written and what the first substrate
	// target is based on which target slots are in use
	int32 SubstrateFirstMRT = 0;
	for (int32 Iter = 0; Iter < FGBufferInfo::MaxTargets; Iter++)
	{
		if (TargetUsage[Iter] >= EGBufferSlotUsage::Written)
		{
			FString TargetName = FString::Printf(TEXT("PIXELSHADEROUTPUT_MRT%d"), Iter);
			OutEnvironment.SetDefine(TargetName.GetCharArray().GetData(), TEXT("1"));
		}

		if (TargetUsage[Iter] >= EGBufferSlotUsage::Used)
		{
			SubstrateFirstMRT = Iter + 1;
		}
	}
```

## まとめ

Shading Model追加のためエンジンの多くの箇所を修正する主な理由の１つは、書き込み対象のGBufferを特定し、必要なGBufferのみバインドする仕組みのためとわかりました。