[UE4]メッシュを描画した後に別のマテリアルを使って追加描画する

# 20230126 追記
UE5.1で透過オーバーレイ マテリアルという機能が追加されましたね。
https://docs.unrealengine.com/5.1/ja/unreal-engine-5.1-release-notes/
これで賄えるようであればこれを使えば良さそうですね。
1つしか設定できないのが残念。配列で複数重ねることもできそうなものなんだけど。

# 20220601 追記
MultiDrawプラグイン
https://www.unrealengine.com/marketplace/ja/product/multidraw
の方が良い実装になっていました。
参考にするならこちらのプラグインを買って中を見るのが良いです。

# 概要

![MAR_StaticMesh.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/844fd52f-aafb-a866-dfe2-92c6fb23dfb0.gif)

- UE4.24
- エンジン改造

LIGHT11さんの記事 [【Unity】【シェーダ】オブジェクトを描画した後に別のシェーダを使って追加描画する](https://light11.hatenadiary.com/entry/2020/02/24/214923) を見て、UE4でも同じことができないかと思いやってみました。
メッシュのコンポネントに追加マテリアルを指定すると、そのマテリアルで追加描画します。
メッシュ押し出し法のマテリアルでアウトラインつけたり、ステータス異常の演出を別マテリアルで作って上書きしたい場合に便利かなと思います。
しかし、残念ながらエンジンの改造が必要になってしまいました。
なのでよっぽど必要にならないと自分でも採用しないかなと思いますが、備忘も兼ねて記事にしておきます。

# 改造の方針

StaticMeshComponentとSkeletalMeshComponentを対象とします。
各コンポネントに追加マテリアルを指定できるようにして、その追加マテリアルの分だけMeshBatchを生成・登録するように改造します。
また、現状はLODやメッシュのセクションを考慮していません。

# 具体的なソースコード改造

修正箇所は // EDIT BEGIN と // EDIT END というコメントで囲んであります。

## StaticMeshComponentについての改造

UStaticMeshComponentそのものと、FMeshBatchを作成しているFStaticMeshSceneProxyの二つのクラスを改造します。

### UStaticMeshComponentの改造

追加マテリアルリストAdditionalMaterialsを持たせます。

#### Engine/Source/Runtime/Engine/Classes/Components/StaticMeshComponent.h

UStaticMeshComponentのクラス定義の末尾にAdditionalMaterialsを追加します。

```
class ENGINE_API UStaticMeshComponent : public UMeshComponent
{

	--- 略 ---

	friend class FStaticMeshComponentRecreateRenderStateContext;

	// EDIT BEGIN
public:
	/** 追加マテリアル */
	UPROPERTY(EditAnywhere, Category = Rendering)
	TArray<UMaterialInterface*> AdditionalMaterials;
	// EDIT END
};
```

#### Engine/Source/Runtime/Engine/Private/Components/StaticMeshComponent.cpp

関数GetNumMaterials()にAdditionalMaterialsを考慮させます。
元々はスタティックメッシュが持っているマテリアルの数を返していたので、追加マテリアルの数を足すようにします。

```
int32 UStaticMeshComponent::GetNumMaterials() const
{
	// @note : you don't have to consider Materials.Num()
	// that only counts if overridden and it can't be more than GetStaticMesh()->Materials. 
	if(GetStaticMesh())
	{
		// EDIT BEGIN
		// ORG>return GetStaticMesh()->StaticMaterials.Num();
		return GetStaticMesh()->StaticMaterials.Num() + AdditionalMaterials.Num();
		// EDIT END
	}
	else
	{
		return 0;
	}
}
```

関数GetMaterial()にAdditionalMaterialsを考慮させます。
スタティックメッシュのマテリアル数までのインデックスは従来通りの処理で、それを超えた分については追加マテリアルを返すようにします。

```
UMaterialInterface* UStaticMeshComponent::GetMaterial(int32 MaterialIndex) const
{
	// EDIT BEGIN
	if (GetStaticMesh() == nullptr)
	{
		return nullptr;
	}
	if (MaterialIndex < GetStaticMesh()->StaticMaterials.Num())
	{
	// EDIT END

	--- 略 ---

	// EDIT BEGIN
	}

	int32 AdditionalMaterialIndex = MaterialIndex - GetStaticMesh()->StaticMaterials.Num();
	if (AdditionalMaterialIndex < AdditionalMaterials.Num())
	{
		return AdditionalMaterials[AdditionalMaterialIndex];
	}

	return nullptr;
	// EDIT END
}
```

関数GetUsedMaterials()にAdditionalMaterialsを考慮させます。
追加マテリアルを使用マテリアルリストに追加します。

```
void UStaticMeshComponent::GetUsedMaterials(TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials) const
{
	--- 略 ---
				OutMaterials[MaterialIndex++] = Kvp.Value;
			}
		}
		// EDIT BEGIN
		OutMaterials.Append(AdditionalMaterials);
		// EDIT END
	}
}
```

### FStaticMeshSceneProxyの改造

元のソースコードに複数のMeshBatchを生成する構想がある形跡が見られます。
ただしバッチ数取得関数が常に1しか返さないので現状意味は無いようです。
後のエンジンアップデートに対して弱くなるかもしれませんが、ひとまずこの複数のバッチを扱おうとしている設計に乗っかって対応します。

#### Engine/Source/Runtime/Engine/Public/StaticMeshResources.h

FStaticMeshSceneProxyのバッチ数取得関数GetNumMeshBatches()が1を返しているので、それに追加マテリアルの数を加えます。
またクラス定義末尾に追加マテリアルのリストAdditionalMaterialsを追加します。
さらにLOD、セクション、バッチを考慮してマテリアルを返す関数GetMaterial()を宣言します。

```
class ENGINE_API FStaticMeshSceneProxy : public FPrimitiveSceneProxy
{
	--- 略 ---

	/** Gets the number of mesh batches required to represent the proxy, aside from section needs. */
	virtual int32 GetNumMeshBatches() const
	{
		// EDIT BEGIN
		// ORG>return 1;
		return 1 + AdditionalMaterials.Num();
		// EDIT END
	}
	
	--- 略 ---

	void RemoveSpeedTreeWind();

	// EDIT BEGIN
	/** LOD, セクション、バッチに応じたマテリアルを取得 */
	UMaterialInterface* GetMaterial(const int32 LODIndex, const int32 SectionIndex, const int32 BatchIndex) const;

	/** 追加マテリアル */
	TArray<UMaterialInterface*> AdditionalMaterials;
	// EDIT END
};
```

#### Engine/Source/Runtime/Engine/Private/StaticMeshRender.cpp

FStaticMeshSceneProxyのコンストラクタでAdditionalMaterialsを初期化します。

```
FStaticMeshSceneProxy::FStaticMeshSceneProxy(UStaticMeshComponent* InComponent, bool bForceLODsShareStaticLighting)
	
	--- 略 ---
	
	, bDrawMeshCollisionIfSimple(InComponent->bDrawMeshCollisionIfSimple)
#endif
	// EDIT BEGIN
	, AdditionalMaterials(InComponent->AdditionalMaterials)
	// EDIT END
{
	check(RenderData);
```

関数GetMaterial()を適当な場所に実装します。
BatchIndexが0なら引数のLODとセクションに対応するマテリアルを返し、BatchIndexが1以上なら追加マテリアルからマテリアルを返します。
"1"が完全にマジックナンバーなのが良くないんですがひとまず。

```
// EDIT BEGIN
UMaterialInterface * FStaticMeshSceneProxy::GetMaterial(const int32 LODIndex, const int32 SectionIndex, const int32 BatchIndex) const
{
	if (BatchIndex < 1)
	{
		const FLODInfo& ProxyLODInfo = LODs[LODIndex];
		return ProxyLODInfo.Sections[SectionIndex].Material;
	}

	int32 AdditionalMaterialIndex = BatchIndex - 1;
	if (AdditionalMaterialIndex < AdditionalMaterials.Num())
	{
		return AdditionalMaterials[AdditionalMaterialIndex];
	}

	return nullptr;
}
// EDIT END
```

関数GetMeshElement()でマテリアルを取得している部分を作成した関数GetMaterial()に差し替えます。
マテリアルのnullチェックもしておきます。

```
bool FStaticMeshSceneProxy::GetMeshElement(
	int32 LODIndex, 
	int32 BatchIndex, 
	int32 SectionIndex, 
	uint8 InDepthPriorityGroup, 
	bool bUseSelectionOutline,
	bool bAllowPreCulledIndices, 
	FMeshBatch& OutMeshBatch) const
{
	const ERHIFeatureLevel::Type FeatureLevel = GetScene().GetFeatureLevel();
	const FStaticMeshLODResources& LOD = RenderData->LODResources[LODIndex];
	const FStaticMeshVertexFactories& VFs = RenderData->LODVertexFactories[LODIndex];
	const FStaticMeshSection& Section = LOD.Sections[SectionIndex];
	const FLODInfo& ProxyLODInfo = LODs[LODIndex];

	// EDIT BEGIN
	// ORG>UMaterialInterface* MaterialInterface = ProxyLODInfo.Sections[SectionIndex].Material;
	UMaterialInterface* MaterialInterface = GetMaterial(LODIndex, SectionIndex, BatchIndex);
	if (MaterialInterface == nullptr)
	{
		return false;
	}
	// EDIT END
	FMaterialRenderProxy* MaterialRenderProxy = MaterialInterface->GetRenderProxy();
```

### 挙動確認

挙動確認用にいくつかマテリアルを作成します。

[LIGHT11さんの記事](https://light11.hatenadiary.com/entry/2020/02/24/214923)でやっていた疑似リムライト
![2020-03-02_14h29_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/6740cb38-e50c-ca2d-a152-05e981e676e6.png)

メッシュ押し出し法のアウトライン
![2020-03-02_14h29_54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/7daa3335-21f4-df6c-9b76-d592f725a426.png)

なんか縞模様がスクロールするマテリアル
![2020-03-02_14h30_16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/e04bb5d4-412a-39e1-07ef-59f487434fb7.png)

以上のマテリアルを使用して確認します。
StaticMeshComponentのRenderingカテゴリにAdditionalMaterialsが追加されているので、そこに上記マテリアルを追加していきます。

まずは素の状態。
![2020-03-02_14h34_37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/4dacfdf3-a514-f00f-86c0-a55e382ebcbd.png)

そこに疑似リムライトマテリアルを追加。
![2020-03-02_14h34_48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/79810621-a054-5a62-9163-a188dd288ca2.png)

アウトラインマテリアルも追加。
![2020-03-02_14h35_06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/1deea0f5-8874-6aa1-c8e4-ccd7cd35e99e.png)

さらに縞模様マテリアルを追加。
![2020-03-02_14h35_15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/172818cb-5bf1-5971-41a2-cc85ddcf3a1f.png)

StaticMeshComponentに設定した追加マテリアルで追加描画ができることが確認できました。

## SkeletalMeshComponentについての改造

StaticMeshComponentと同様に、USkeletalMeshComponentとFSkeletalMeshSceneProxyのソースコードを改変します。
対応内容も同様ですが、こちらの方が改変箇所が多くなってしまいました。

### USkeletalMeshComponentの改造

#### Engine/Source/Runtime/Engine/Classes/Components/SkeletalMeshComponent.h

USkeletalMeshComponentのクラス定義末尾に追加マテリアルリストAdditionalMaterialsを追加します。
また、使用するマテリアルとして追加マテリアルを考慮させたいので、GetNumMaterials()、GetUsedMaterials()、GetMaterial()をオーバーライドする関数宣言を追加します。

```
class ENGINE_API USkeletalMeshComponent : public USkinnedMeshComponent, public IInterface_CollisionDataProvider
{
	--- 略 ---

	bool IsPostEvaluatingAnimation() const { return bPostEvaluatingAnimation; }

	// EDIT BEGIN
public:
	//~ Begin UPrimitiveComponent Interface.
	virtual int32 GetNumMaterials() const override;
	virtual void GetUsedMaterials(TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials = false) const override;
	virtual UMaterialInterface* GetMaterial(int32 MaterialIndex) const override;
	//~ End UPrimitiveComponent Interface.

	/** 追加マテリアル */
	UPROPERTY(EditAnywhere, Category = Rendering)
	TArray<UMaterialInterface*> AdditionalMaterials;
	// EDIT END
};
```

#### Engine/Source/Runtime/Engine/Private/Components/SkeletalMeshComponent.cpp

適当な場所にGetNumMaterials()、GetUsedMaterials()、GetMaterial()の実装を追加します。
GetNumMaterials()はSuperクラスが返す数に対してAdditionalMaterialsの数を加えます。
GetUsedMaterials()はSuperクラスが返す使用マテリアルリストにさらにAdditionalMaterialsを追加します。
GetMaterial()はMaterialIndexに応じてSuperクラスに任せたり、AdditionalMaterialsから返したりします。

```
// EDIT BEGIN
int32 USkeletalMeshComponent::GetNumMaterials() const
{
	return Super::GetNumMaterials() + AdditionalMaterials.Num();
}

void USkeletalMeshComponent::GetUsedMaterials(TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials) const
{
	Super::GetUsedMaterials(OutMaterials, bGetDebugMaterials);
	OutMaterials.Append(AdditionalMaterials);
}

UMaterialInterface* USkeletalMeshComponent::GetMaterial(int32 MaterialIndex) const
{
	int32 SuperNumMaterials = Super::GetNumMaterials();
	if (MaterialIndex < SuperNumMaterials)
	{
		return Super::GetMaterial(MaterialIndex);
	}

	int32 AdditionalMaterialIndex = MaterialIndex - SuperNumMaterials;
	if (AdditionalMaterialIndex < AdditionalMaterials.Num())
	{
		return AdditionalMaterials[AdditionalMaterialIndex];
	}

	return nullptr;
}
// EDIT END
```

### FSkeletalMeshSceneProxyの改造

FStaticMeshSceneProxyとは違い、複数のMeshBatchへの対応が全くありません。
なので独自にバッチ数の考慮を追加していきます。

#### Engine/Source/Runtime/Engine/Public/SkeletalMeshTypes.h

FStaticMeshSceneProxyと同じような形で対応するため、バッチ数取得関数GetNumMeshBatches()、マテリアル取得関数GetMaterial()、追加マテリアルリストAdditionalMaterialsを追加します。
また、内部でマテリアルを設定してメッシュバッチを作成する関数CreateBaseMeshBatch()の引数にBatchIndexを追加します。
さらにその関数CreateBaseMeshBatch()を呼び出している関数GetDynamicElementsSection()の引数にもBatchIndexを追加します。

```
class ENGINE_API FSkeletalMeshSceneProxy : public FPrimitiveSceneProxy
{
	--- 略 ---

	float StreamingDistanceMultiplier;
#endif

	// EDIT BEGIN
	// ORG>void GetDynamicElementsSection(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap,
	// ORG>								const FSkeletalMeshLODRenderData& LODData, const int32 LODIndex, const int32 SectionIndex, bool bSectionSelected,
	// ORG>								const FSectionElementInfo& SectionElementInfo, bool bInSelectable, FMeshElementCollector& Collector) const;
	void GetDynamicElementsSection(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap,
		const FSkeletalMeshLODRenderData& LODData, const int32 LODIndex, const int32 SectionIndex, bool bSectionSelected,
		const FSectionElementInfo& SectionElementInfo, const int32 BatchIndex, bool bInSelectable, FMeshElementCollector& Collector) const;
	// EDIT END

	void GetMeshElementsConditionallySelectable(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, bool bInSelectable, uint32 VisibilityMap, FMeshElementCollector& Collector) const;

	/** Only call on render thread timeline */
	uint8 GetCurrentFirstLODIdx_Internal() const;
private:
	// EDIT BEGIN
	// ORG>void CreateBaseMeshBatch(const FSceneView* View, const FSkeletalMeshLODRenderData& LODData, const int32 LODIndex, const int32 SectionIndex, const FSectionElementInfo& SectionElementInfo, FMeshBatch& Mesh) const;
	void CreateBaseMeshBatch(const FSceneView* View, const FSkeletalMeshLODRenderData& LODData, const int32 LODIndex, const int32 SectionIndex, const FSectionElementInfo& SectionElementInfo, const int32 BatchIndex, FMeshBatch& Mesh) const;
	// EDIT END

	// EDIT BEGIN
public:
	// バッチ数取得
	virtual int32 GetNumMeshBatches() const
	{
		return 1 + AdditionalMaterials.Num();
	}

	/** LOD, セクション、バッチに応じたマテリアルを取得 */
	UMaterialInterface* GetMaterial(const int32 LODIndex, const int32 SectionIndex, const FSectionElementInfo& SectionElementInfo, const int32 BatchIndex) const;

	TArray<UMaterialInterface*> AdditionalMaterials;
	// EDIT END
};
```

#### Engine/Source/Runtime/Engine/Private/SkeletalMesh.cpp

FSkeletalMeshSceneProxyのコンストラクタ内でAdditionalMaterialsの初期化を行いますが、引数がUSkinnedMeshComponentなのでキャストする必要があります。
またSkeletalMesh向けのUsageフラグをチェックしておきます。

```
FSkeletalMeshSceneProxy::FSkeletalMeshSceneProxy(const USkinnedMeshComponent* Component, FSkeletalMeshRenderData* InSkelMeshRenderData)
	--- 略 ---
{
	check(MeshObject);
	check(SkeletalMeshRenderData);
	check(SkeletalMeshForDebug);

	// EDIT BEGIN
	const USkeletalMeshComponent* SkeletalMeshComponent = Cast<const USkeletalMeshComponent>(Component);
	if (SkeletalMeshComponent)
	{
		AdditionalMaterials = SkeletalMeshComponent->AdditionalMaterials;
	}
	for (UMaterialInterface* AdditionalMaterial : AdditionalMaterials)
	{
		if (AdditionalMaterial)
		{
			AdditionalMaterial->CheckMaterialUsage_Concurrent(MATUSAGE_SkeletalMesh);
		}
	}
	// EDIT END

	bIsCPUSkinned = MeshObject->IsCPUSkinned();
	
	--- 略 ---
```

メッシュバッチを作成する関数CreateBaseMeshBatch()の引数にBatchIndexを追加し、マテリアルを取得している箇所を独自に追加した関数GetMaterial()に置き換えます。

```
// EDIT BEGIN
// ORG>void FSkeletalMeshSceneProxy::CreateBaseMeshBatch(const FSceneView* View, const FSkeletalMeshLODRenderData& LODData, const int32 LODIndex, const int32 SectionIndex, const FSectionElementInfo& SectionElementInfo, FMeshBatch& Mesh) const
void FSkeletalMeshSceneProxy::CreateBaseMeshBatch(const FSceneView* View, const FSkeletalMeshLODRenderData& LODData, const int32 LODIndex, const int32 SectionIndex, const FSectionElementInfo& SectionElementInfo, const int32 BatchIndex, FMeshBatch& Mesh) const
// EDIT END
{
	Mesh.VertexFactory = MeshObject->GetSkinVertexFactory(View, LODIndex, SectionIndex);
	// EDIT BEGIN
	// ORG>Mesh.MaterialRenderProxy = SectionElementInfo.Material->GetRenderProxy();
	UMaterialInterface* Material = GetMaterial(LODIndex, SectionIndex, SectionElementInfo, BatchIndex);
	Mesh.MaterialRenderProxy = Material ? Material->GetRenderProxy() : nullptr;
	// EDIT END

	--- 略 ---
```

関数GetMeshElementsConditionallySelectable()内で、LODセクションの分ループを回して関数GetDynamicElementsSection()を呼び出している箇所があるので、そこをバッチ数の分もループするようにします。

```
void FSkeletalMeshSceneProxy::GetMeshElementsConditionallySelectable(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, bool bInSelectable, uint32 VisibilityMap, FMeshElementCollector& Collector) const
{
	--- 略 ---

	if( LODSections.Num() > 0 && LODIndex >= SkeletalMeshRenderData->CurrentFirstLODIdx )
	{
		const FLODSectionElements& LODSection = LODSections[LODIndex];

		check(LODSection.SectionElements.Num() == LODData.RenderSections.Num());

		// EDIT BEGIN
		const int32 NumBatches = GetNumMeshBatches();
		for (int32 BatchIndex = 0; BatchIndex < NumBatches; ++BatchIndex)
		{
		// EDIT END
		for (FSkeletalMeshSectionIter Iter(LODIndex, *MeshObject, LODData, LODSection); Iter; ++Iter)
		{
			--- ちょっと略 ---

			// If hidden skip the draw
			if (MeshObject->IsMaterialHidden(LODIndex, SectionElementInfo.UseMaterialIndex) || Section.bDisabled)
			{
				continue;
			}

			// EDIT BEGIN
			// ORG>GetDynamicElementsSection(Views, ViewFamily, VisibilityMap, LODData, LODIndex, SectionIndex, bSectionSelected, SectionElementInfo, bInSelectable, Collector);
			GetDynamicElementsSection(Views, ViewFamily, VisibilityMap, LODData, LODIndex, SectionIndex, bSectionSelected, SectionElementInfo, BatchIndex, bInSelectable, Collector);
			// EDIT END
		}
		// EDIT BEGIN
		}
		// EDIT END
	}

	--- 略 ---
```

関数GetDynamicElementsSection()の引数にBatchIndexを追加し、関数CreateBaseMeshBatch()の呼び出しにBatchIndexを指定するようにします。
マテリアルが存在しなかった場合に備えてnullチェックもしています。

```
// EDIT BEGIN
// ORG>void FSkeletalMeshSceneProxy::GetDynamicElementsSection(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, 
// ORG>	const FSkeletalMeshLODRenderData& LODData, const int32 LODIndex, const int32 SectionIndex, bool bSectionSelected,
// ORG>	const FSectionElementInfo& SectionElementInfo, bool bInSelectable, FMeshElementCollector& Collector ) const
void FSkeletalMeshSceneProxy::GetDynamicElementsSection(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap,
	const FSkeletalMeshLODRenderData& LODData, const int32 LODIndex, const int32 SectionIndex, bool bSectionSelected,
	const FSectionElementInfo& SectionElementInfo, const int32 BatchIndex, bool bInSelectable, FMeshElementCollector& Collector) const
// EDIT END
{
	--- 略 ---

	for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
	{
		if (VisibilityMap & (1 << ViewIndex))
		{
			const FSceneView* View = Views[ViewIndex];

			FMeshBatch& Mesh = Collector.AllocateMesh();

			// EDIT BEGIN
			// ORG>CreateBaseMeshBatch(View, LODData, LODIndex, SectionIndex, SectionElementInfo, Mesh);
			CreateBaseMeshBatch(View, LODData, LODIndex, SectionIndex, SectionElementInfo, BatchIndex, Mesh);
			if (Mesh.MaterialRenderProxy == nullptr)
			{
				continue;
			}
			// EDIT END
			
			if(!Mesh.VertexFactory)

	--- 略 ---
```

関数GetDynamicRayTracingInstances()にLODセクション数の分ループを回して関数CreateBaseMeshBatch()を呼び出している箇所があるので、バッチ数の分もループを回し、関数CreateBaseMeshBatch()の呼び出しにBatchIndexを追加します。

```
void FSkeletalMeshSceneProxy::GetDynamicRayTracingInstances(FRayTracingMaterialGatheringContext & Context, TArray<struct FRayTracingInstance>& OutRayTracingInstances)
{
	if (MeshObject->GetRayTracingGeometry())
	{
		// #dxr: the only case where RayTracingGeometryRHI is invalid is the very first frame - if that's not the case we have a bug somewhere else
		if (MeshObject->GetRayTracingGeometry()->RayTracingGeometryRHI.IsValid())
		{
			check(MeshObject->GetRayTracingGeometry()->Initializer.IndexBuffer.IsValid());
			
			FRayTracingInstance RayTracingInstance;
			RayTracingInstance.Geometry = MeshObject->GetRayTracingGeometry();

			{
				--- 略 ---

			#if WITH_EDITORONLY_DATA
				int32 SectionIndexPreview = MeshObject->SectionIndexPreview;
				int32 MaterialIndexPreview = MeshObject->MaterialIndexPreview;
				MeshObject->SectionIndexPreview = INDEX_NONE;
				MeshObject->MaterialIndexPreview = INDEX_NONE;
			#endif
				// EDIT BEGIN
				const int32 NumBatches = GetNumMeshBatches();
				for (int32 BatchIndex = 0; BatchIndex < NumBatches; ++BatchIndex)
				{
				// EDIT END
				for (FSkeletalMeshSectionIter Iter(LODIndex, *MeshObject, LODData, LODSection); Iter; ++Iter)
				{
					const FSkelMeshRenderSection& Section = Iter.GetSection();
					const int32 SectionIndex = Iter.GetSectionElementIndex();
					const FSectionElementInfo& SectionElementInfo = Iter.GetSectionElementInfo();

					FMeshBatch MeshBatch;
					// EDIT BEGIN
					// ORG>CreateBaseMeshBatch(Context.ReferenceView, LODData, LODIndex, SectionIndex, SectionElementInfo, MeshBatch);
					CreateBaseMeshBatch(Context.ReferenceView, LODData, LODIndex, SectionIndex, SectionElementInfo, BatchIndex, MeshBatch);
					if (MeshBatch.MaterialRenderProxy == nullptr)
					{
						continue;
					}
					// EDIT END

					RayTracingInstance.Materials.Add(MeshBatch);
				}
				// EDIT BEGIN
				}
				// EDIT END
			#if WITH_EDITORONLY_DATA

				--- 略 ---
```

関数GetMaterial()を適当な場所に実装します。
FStaticMeshSceneProxyの時と同じです。

```
// EDIT BEGIN
UMaterialInterface* FSkeletalMeshSceneProxy::GetMaterial(const int32 LODIndex, const int32 SectionIndex, const FSectionElementInfo& SectionElementInfo, const int32 BatchIndex) const
{
	if (BatchIndex < 1)
	{
		return SectionElementInfo.Material;
	}

	int32 AdditionalMaterialIndex = BatchIndex - 1;
	if (AdditionalMaterialIndex < AdditionalMaterials.Num())
	{
		return AdditionalMaterials[AdditionalMaterialIndex];
	}

	return nullptr;
}
// EDIT END
```

### 挙動確認

StaticMeshComponentで使ったマテリアルをそのまま使って確認をします。
SkeletalMeshComponentのRenderingカテゴリにAdditionalMaterialsがあるので、そこに追加していきます。
FSkeletalMeshSceneProxyのコンストラクタでMATUSAGE_SkeletalMeshのUsageフラグをチェックしているため、自動的に各マテリアルのUsed with Skeletal Meshフラグがtrueになります。

まずは素の状態。
![2020-03-02_14h40_39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/049dfe19-3656-92b0-fdf3-c5323707fd9d.png)

そこに疑似リムライトマテリアルを追加。
![2020-03-02_14h41_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/0a180883-46e3-7b22-16bd-de9ad05d3492.png)

アウトラインマテリアルも追加。
![2020-03-02_14h41_27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/6f8ac5a8-88f3-13bf-11ae-c52353afe4ab.png)

さらに縞模様マテリアルを追加。
![2020-03-02_14h41_52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/060b23c3-4311-2e13-e1e2-a5d10d7605f7.png)

SkeletalMeshComponentに設定した追加マテリアルで追加描画ができることが確認できました。

# 注意事項

今回はFSkeletalMeshSceneProxyのコンストラクタでMATUSAGE_SkeletalMeshのチェックはしましたが、メッシュによってMATUSAGE_ClothingやMATUSAGE_MorphTargetsのチェックもしなければならないと思われます。
追加マテリアルのUsed with Clothingにチェックを入れずにクロスシミュレーションのあるメッシュに適用してしまうと、エディタが落ちると思われます。

また冒頭にも記載しましたがLODやメッシュセクションの考慮をしていません。
しかしマテリアル取得関数にはLODIndexやSectionIndexが引数で渡ってくるようにしてあるので、必要であれば対応することは難しくないと思います。
僕が想定しているのはアウトラインを付けたりステータス異常の演出を上書きしたりなので、今のところLODやセクションの考慮が要りませんでした。

# まとめ
StaticMeshComponentとSkeletalMeshComponentに追加マテリアルを指定できるようにし、そのマテリアルで各メッシュが追加描画されるようにエンジンを改造しました。
エンジン改造はリスキーなので、もし役に立ちそうでも採用は慎重になる必要があるかと思います。

ここまで目を通して頂きありがとうございます。何かの助けになったら幸いです。
こういった記事の書くのは初めてなので、至らぬ点がありましたらすみません。
本当はUE4のgithubからフォークして対応を追加したリポジトリを公開したらもっとわかりやすいのかなと思うのですが、そういうことをやっていいのかよくわからないのでとりあえず止めておきました。
よかったらコメントをよろしくお願いいたします。