UE4.24で独自のShading Modelを追加する

# UE4.24でShading Model独自のShading Modelを追加してみた
表題の通りUE4はエンジン改造によって独自のShading Modelを追加することができます
今回はそちらを4.24.2をベースとしてやってみたという記事になります、誤っている点などありましたらご指摘頂けると幸いです
なお、コードの拡張部分にはその範囲を示すために以下のようなコメントを追加してあります(実際のプロジェクトでEngine改造をする際には、よりプロジェクトに適したコメントが望ましいと思います)

```
// begin
Custom code ...
// end

/* begin */ Custom code ... /* end */
```

## C++側
列挙型の定義
今回はCelShadingという名前のShading Modelを追加していきます

```EngineTypes.h
UENUM()
enum EMaterialShadingModel
{
	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"),
// begin
	MSM_CelShading				UMETA(DisplayName="CelShading"),
// end
	/** 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
};
```

列挙型の値から文字列を取得する部分

```MaterialShader.cpp
FString GetShadingModelString(EMaterialShadingModel ShadingModel)
{
	FString ShadingModelName;
	switch(ShadingModel)
	{
		case MSM_Unlit:				ShadingModelName = TEXT("MSM_Unlit"); break;
		case MSM_DefaultLit:		ShadingModelName = TEXT("MSM_DefaultLit"); break;
		case MSM_Subsurface:		ShadingModelName = TEXT("MSM_Subsurface"); break;
		case MSM_PreintegratedSkin:	ShadingModelName = TEXT("MSM_PreintegratedSkin"); break;
		case MSM_ClearCoat:			ShadingModelName = TEXT("MSM_ClearCoat"); break;
		case MSM_SubsurfaceProfile:	ShadingModelName = TEXT("MSM_SubsurfaceProfile"); break;
		case MSM_TwoSidedFoliage:	ShadingModelName = TEXT("MSM_TwoSidedFoliage"); break;
		case MSM_Cloth:				ShadingModelName = TEXT("MSM_Cloth"); break;
		case MSM_Eye:				ShadingModelName = TEXT("MSM_Eye"); break;
		case MSM_SingleLayerWater:	ShadingModelName = TEXT("MSM_SingleLayerWater"); break;
// begin
		case MSM_CelShading:		ShadingModelName = TEXT("MSM_CelShading"); break;
// end
		default: ShadingModelName = TEXT("Unknown"); break;
	}
	return ShadingModelName;
}
```

stat用の条件式に追加したShading Modelを追加します

```MaterialShader.cpp
	else if (ShadingModels.HasAnyShadingModel({ MSM_DefaultLit, MSM_Subsurface, MSM_PreintegratedSkin, MSM_ClearCoat, MSM_Cloth, MSM_SubsurfaceProfile, MSM_TwoSidedFoliage, MSM_SingleLayerWater, /* begin */ MSM_CelShading /* end */}))
	{
		INC_DWORD_STAT_BY(STAT_ShaderCompiling_NumLitMaterialShaders, 1);
	}
```

Shader側で使用する変数名を設定します

```HLSLMaterialTranslator.h
			if (ShadingModels.HasShadingModel(MSM_Eye))
			{
				OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_EYE"), TEXT("1"));
				NumSetMaterials++;
			}
			if (ShadingModels.HasShadingModel(MSM_SingleLayerWater))
			{
				OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_SINGLELAYERWATER"), TEXT("1"));
				NumSetMaterials++;
			}
// begin
			if (ShadingModels.HasShadingModel(MSM_CelShading))
			{
				OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_CELSHADING"), TEXT("1"));
				NumSetMaterials++;
			}
// end
```

ここまで書いてビルドをすれば、とりあえずMaterial EditorのShading ModelにCelShadingが追加されていることが確認できるはずです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158312/58a8e401-a46c-216b-fc34-f81af6a884f3.png)


## Shader側
ここからは実際にCelShadingというShading ModelがどのようにシェーディングされるのかをShader側で書いていきます
まずエンジンのShader(\*.ush/\*.usf)を編集する前にConsoleVariables.iniを編集してShader開発をしやすいようにします
*r.ShaderDevelopmentMode=1*の前のセミコロンを外すことでShader Compile時のエラー詳細と、その際にRecompileするかどうかのDialogが出るようになるのでShader開発がしやすくなるはずです
またエディタ起動時に`Ctrl + Shift + .`でShaderの再コンパイルも可能です

```ConsoleVariables.ini
; Uncomment to get detailed logs on shader compiles and the opportunity to retry on errors
r.ShaderDevelopmentMode=1
; Uncomment to dump shaders in the Saved folder
; Warning: leaving this on for a while will fill your hard drive with many small files and folders
;r.DumpShaderDebugInfo=1
; When this is enabled, SCW crashes will print out the list of jobs in the current worker
;r.DumpSCWQueuedJobs=1
; When this is enabled, when dumping shaders an additional file to use with ShaderCompilerWorker -direct mode will be generated
;r.DumpShaderDebugWorkerCommandLine=1
```

IDを設定します、コメントにあるように4bitのみShadingModelID用に確保されているので、ShadingModelは最大15までとなります

```ShadingCommon.ush
// SHADINGMODELID_* occupy the 4 low bits of an 8bit channel and SKIP_* occupy the 4 high bits
#define SHADINGMODELID_UNLIT				0
#define SHADINGMODELID_DEFAULT_LIT			1
#define SHADINGMODELID_SUBSURFACE			2
#define SHADINGMODELID_PREINTEGRATED_SKIN	3
#define SHADINGMODELID_CLEAR_COAT			4
#define SHADINGMODELID_SUBSURFACE_PROFILE	5
#define SHADINGMODELID_TWOSIDED_FOLIAGE		6
#define SHADINGMODELID_HAIR					7
#define SHADINGMODELID_CLOTH				8
#define SHADINGMODELID_EYE					9
#define SHADINGMODELID_SINGLELAYERWATER		10
// begin
#define SHADINGMODELID_CELSHADING			11
#define SHADINGMODELID_NUM					12
// end
#define SHADINGMODELID_MASK					0xF		// 4 bits reserved for ShadingModelID	
```

Debug表示用の色を設定
PS4用に別途ifでの分岐が設けられるようになってますね

```
// for debugging and to visualize
float3 GetShadingModelColor(uint ShadingModelID)
{
	// TODO: PS4 doesn't optimize out correctly the switch(), so it thinks it needs all the Samplers even if they get compiled out
	//	This will get fixed after launch per Sony...
#if PS4_PROFILE
		 if (ShadingModelID == SHADINGMODELID_UNLIT) return float3(0.1f, 0.1f, 0.2f); // Dark Blue
	else if (ShadingModelID == SHADINGMODELID_DEFAULT_LIT) return float3(0.1f, 1.0f, 0.1f); // Green
	else if (ShadingModelID == SHADINGMODELID_SUBSURFACE) return float3(1.0f, 0.1f, 0.1f); // Red
	else if (ShadingModelID == SHADINGMODELID_PREINTEGRATED_SKIN) return float3(0.6f, 0.4f, 0.1f); // Brown
	else if (ShadingModelID == SHADINGMODELID_CLEAR_COAT) return float3(0.1f, 0.4f, 0.4f); 
	else if (ShadingModelID == SHADINGMODELID_SUBSURFACE_PROFILE) return float3(0.2f, 0.6f, 0.5f); // Cyan
	else if (ShadingModelID == SHADINGMODELID_TWOSIDED_FOLIAGE) return float3(0.2f, 0.2f, 0.8f); // Blue
	else if (ShadingModelID == SHADINGMODELID_HAIR) return float3(0.6f, 0.1f, 0.5f);
	else if (ShadingModelID == SHADINGMODELID_CLOTH) return float3(0.7f, 1.0f, 1.0f); 
	else if (ShadingModelID == SHADINGMODELID_EYE) return float3(0.3f, 1.0f, 1.0f); 
	else if (ShadingModelID == SHADINGMODELID_SINGLELAYERWATER) return float3(0.5f, 0.5f, 1.0f);
// begin
	else if (ShadingModelID == SHADINGMODELID_CELSHADING) return float3(0.5f, 0.5f, 0.5f);
// end
	else return float3(1.0f, 1.0f, 1.0f); // White
#else
	switch(ShadingModelID)
	{
		case SHADINGMODELID_UNLIT: return float3(0.1f, 0.1f, 0.2f); // Dark Blue
		case SHADINGMODELID_DEFAULT_LIT: return float3(0.1f, 1.0f, 0.1f); // Green
		case SHADINGMODELID_SUBSURFACE: return float3(1.0f, 0.1f, 0.1f); // Red
		case SHADINGMODELID_PREINTEGRATED_SKIN: return float3(0.6f, 0.4f, 0.1f); // Brown
		case SHADINGMODELID_CLEAR_COAT: return float3(0.1f, 0.4f, 0.4f); // Brown
		case SHADINGMODELID_SUBSURFACE_PROFILE: return float3(0.2f, 0.6f, 0.5f); // Cyan
		case SHADINGMODELID_TWOSIDED_FOLIAGE: return float3(0.2f, 0.2f, 0.8f); // Cyan
		case SHADINGMODELID_HAIR: return float3(0.6f, 0.1f, 0.5f);
		case SHADINGMODELID_CLOTH: return float3(0.7f, 1.0f, 1.0f);
		case SHADINGMODELID_EYE: return float3(0.3f, 1.0f, 1.0f);
		case SHADINGMODELID_SINGLELAYERWATER: return float3(0.5f, 0.5f, 1.0f);
// begin
		case SHADINGMODELID_CELSHADING: return float3(0.5f, 0.5f, 0.5f);
// end
		default: return float3(1.0f, 1.0f, 1.0f); // White
	}
#endif
}
```

実際に拡張したShading Modelをどう描画するかの部分と呼び出し部分
今回はNoLを利用して段階的に色が分かれるようなShadingModelとしました、Stepの数を増やせば段階数も増えます

```ShadingModels.ush
// begin
FDirectLighting CelShadingBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, float NoL, FAreaLight AreaLight, FShadowTerms Shadow )
{
	float Step = 3.0f;
	FDirectLighting Lighting = DefaultLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
	Lighting.Diffuse = AreaLight.FalloffColor * Falloff * Diffuse_Lambert(GBuffer.DiffuseColor) * saturate(floor(NoL * Step) / Step);
	return Lighting;
}
// end

FDirectLighting IntegrateBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, float NoL, FAreaLight AreaLight, FShadowTerms Shadow )
{
	switch( GBuffer.ShadingModelID )
	{
		case SHADINGMODELID_DEFAULT_LIT:
		case SHADINGMODELID_SINGLELAYERWATER:
			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 );
// begin
		case SHADINGMODELID_CELSHADING:
			return CelShadingBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
// end
		default:
			return (FDirectLighting)0;
	}
}
```

ここまですると下の画像のようなものが出来上がります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158312/e0ae4f1e-6719-a0b5-bd9e-9a1b6555c76e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158312/854b71c3-55ab-5286-90fe-6a32e6d86f1f.png)

# Custom Dataについて
先程の状態だとShadingModels.ushのCelShadingBxDFのStepを変更しないと段階数が変更できず非常に不便です
なので更に拡張して、Material InputにあるCustom Data 0を使用できるようにした上で、そちらを利用してStepを変更できるようにしていこうと思います

## C++側

まずCelShadingでCustom Data 0の入力を有効化します

```Materials.cpp
static bool IsPropertyActive_Internal(EMaterialProperty InProperty,
	EMaterialDomain Domain,
	EBlendMode BlendMode,
	FMaterialShadingModelField ShadingModels,
	ETranslucencyLightingMode TranslucencyLightingMode,
	EDecalBlendMode DecalBlendMode,
	bool bBlendableOutputAlpha,
	bool bHasTessellation,
	bool bHasRefraction,
	bool bUsesShadingModelFromMaterialExpression)
{
/ *...* /
 	case MP_SubsurfaceColor:
		Active = ShadingModels.HasAnyShadingModel({ MSM_Subsurface, MSM_PreintegratedSkin, MSM_TwoSidedFoliage, MSM_Cloth });
		break;
// begin
	case MP_CustomData0:
		Active = ShadingModels.HasAnyShadingModel({ MSM_ClearCoat, MSM_Hair, MSM_Cloth, MSM_Eye, MSM_CelShading });
		break;
// end
```

次にCustom Data 0の名前をわかりやすいようにInv Step(Stepの逆数)へと変更します
逆数にしている理由としては、格納する対象のGBufferのフォーマットがHDRフォーマットではないので0~1までの範囲しか渡すことができないからです
ですので今回はStepの逆数を渡すようにしています

```MaterialGraph.cpp
FText UMaterialGraph::GetCustomDataPinName( uint32 Index ) const
{ 
	if( Index == 0 )
	{
		TArray<TKeyValuePair<EMaterialShadingModel, FString>> CustomPinNames(
			{{MSM_ClearCoat, "Clear Coat"},
			{MSM_Hair, "Backlit"},
			{MSM_Cloth, "Cloth"},
			{MSM_Eye, "Iris Mask"},
// begin
			{MSM_CelShading, "Inv Step"}
// end
			});
```

## Shader側

Custom DataをGBufferに書き込むための定義を追加します

```BasePassCommon.ush
#define WRITES_CUSTOMDATA_TO_GBUFFER		(USES_GBUFFER && (MATERIAL_SHADINGMODEL_SUBSURFACE || MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN || MATERIAL_SHADINGMODEL_SUBSURFACE_PROFILE || MATERIAL_SHADINGMODEL_CLEAR_COAT || MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE || MATERIAL_SHADINGMODEL_HAIR || MATERIAL_SHADINGMODEL_CLOTH || MATERIAL_SHADINGMODEL_EYE || /* begin */ MATERIAL_SHADINGMODEL_CELSHADING /* end */))
```

Custom Data 0を取得してGBuffer.CustomData.xへと代入します
ですのでここにStepの逆数であるInvStepが代入されることになります

```ShadingModelsMaterial.ush
	#endif
	}
#endif
// begin
#if MATERIAL_SHADINGMODEL_CELSHADING
	else if (ShadingModel == SHADINGMODELID_CELSHADING)
	{
		GBuffer.CustomData.x = saturate(GetMaterialCustomData0(MaterialParameters));
	}
#endif
// end
```

逆数を渡されるので、それに合わせてシェーディングの計算式も変更します

```ShadingModels.ush
// begin
FDirectLighting CelShadingBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, float NoL, FAreaLight AreaLight, FShadowTerms Shadow )
{
    // float Step = 3.0f;
	float InvStep = GBuffer.CustomData.x;
	FDirectLighting Lighting = DefaultLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
    // Lighting.Diffuse = AreaLight.FalloffColor * Falloff * Diffuse_Lambert(GBuffer.DiffuseColor) * saturate(floor(NoL * Step) / Step);
	Lighting.Diffuse = AreaLight.FalloffColor * Falloff * Diffuse_Lambert(GBuffer.DiffuseColor) * saturate(floor(NoL / InvStep) * InvStep);
    return Lighting;
}
// end
```

このような変更をしてきた上でMaterial Graphを下の画像のように組むことで、正しくInv Stepが反映されていること確認できます
お疲れさまでした

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158312/c8b5a5ab-a4ee-e58d-834f-bfe535171ed5.png)
![CustomData0.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158312/8bdfead3-a2b3-c096-9922-972393ab08aa.gif)

# 参考文献
下記文献には大変お世話になりました、この場を借りてお礼申し上げます
1. https://qiita.com/dgtanaka/items/41f96ef2090820035609
2. https://forums.unrealengine.com/development-discussion/rendering/1390821-material-customdata0-parameter-is-being-clamped-to-1