UE5のGIプラグインを調べてみた、ニセGIも作ってみた。

# はじめに
　この記事は[Unreal Engine (UE) Advent Calendar 2024](https://qiita.com/advent-calendar/2024/ue)シリーズ１、18日目の記事です。

　UE5のGIにPluginという項目があるので、GIプラグインを作ってみようという趣向ですが、作ってみるだけで実際にGIを実装するわけではありません。方法は示すので頑張ればオレオレGIをエンジン改造無しに組み込めるかもしれません。知らんけど。
　また、それだけでは物足りないという方むけに、独自描画パス内でPostProcessMaterialを使用した描画をやってみたいと思います。
 
# GIプラグイン
　ポストプロセスボリュームのグローバルイルミネーションの項目にメソッドというのがあります。ここでLumenとか、Screen SpaceとかGIを選択できるのですが、選択項目に*Plugin*というのがあります。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/c36d9006-0455-212a-c0ac-947808e02d8d.png)
 　現状選択してもNoneと同じでGIが無効になるだけなのですが、PluginでGIを実装する仕組みがあるっぽいので調べてみました。
　ソースコードを検索すると以下のようなDelegateが定義されています。

```DeferredShadingRenderer.cpp
static FGlobalIlluminationPluginDelegates::FAnyRayTracingPassEnabled GIPluginAnyRaytracingPassEnabledDelegate;
FGlobalIlluminationPluginDelegates::FAnyRayTracingPassEnabled& FGlobalIlluminationPluginDelegates::AnyRayTracingPassEnabled()
{
	return GIPluginAnyRaytracingPassEnabledDelegate;
}

static FGlobalIlluminationPluginDelegates::FPrepareRayTracing GIPluginPrepareRayTracingDelegate;
FGlobalIlluminationPluginDelegates::FPrepareRayTracing& FGlobalIlluminationPluginDelegates::PrepareRayTracing()
{
	return GIPluginPrepareRayTracingDelegate;
}

static FGlobalIlluminationPluginDelegates::FRenderDiffuseIndirectLight GIPluginRenderDiffuseIndirectLightDelegate;
FGlobalIlluminationPluginDelegates::FRenderDiffuseIndirectLight& FGlobalIlluminationPluginDelegates::RenderDiffuseIndirectLight()
{
	return GIPluginRenderDiffuseIndirectLightDelegate;
}

#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
static FGlobalIlluminationPluginDelegates::FRenderDiffuseIndirectVisualizations GIPluginRenderDiffuseIndirectVisualizationsDelegate;
FGlobalIlluminationPluginDelegates::FRenderDiffuseIndirectVisualizations& FGlobalIlluminationPluginDelegates::RenderDiffuseIndirectVisualizations()
{
	return GIPluginRenderDiffuseIndirectVisualizationsDelegate;
}
#endif //!(UE_BUILD_SHIPPING || UE_BUILD_TEST)
```
　Delegateを呼び出しているのはここ
```IndirectLightRendering.cpp
		else if (ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::Plugin)
		{
			// Get the resources and call the GI plugin's rendering function delegate
			FGlobalIlluminationPluginResources GIPluginResources;
			GIPluginResources.GBufferA = SceneTextures.GBufferA;
			GIPluginResources.GBufferB = SceneTextures.GBufferB;
			GIPluginResources.GBufferC = SceneTextures.GBufferC;
			GIPluginResources.LightingChannelsTexture = LightingChannelsTexture;
			GIPluginResources.SceneDepthZ = SceneTextures.Depth.Target;
			GIPluginResources.SceneColor = SceneTextures.Color.Target;

			FGlobalIlluminationPluginDelegates::FRenderDiffuseIndirectLight& Delegate = FGlobalIlluminationPluginDelegates::RenderDiffuseIndirectLight();
			Delegate.Broadcast(*Scene, View, GraphBuilder, GIPluginResources);
		}
```

:::note info
UEのソースから関数名や変数名なんかを検索するにはRiderが超便利です。
Shift+Ctrl+FでGrep検索でソースコード解析が捗ります
:::

　Delegateを登録しておけば、リソース情報を添えて呼び出してくれるみたいです。簡単ですね。
　Delegateに渡される情報はソースコードを見ればわかりますが
 - GBufferA : World Normal
 - GBufferB : Metallic, Specular, Roughness
 - GBufferC : BaseColor
 - LightingChannelsTexture : ライティングチャネルマスク
 - SceneDepthZ : 深度情報
 - SceneColor : ライティング直前のレンダリング結果

　といった感じです。
　これだけの情報でGIを実装するのは厳しそうにみえますが、その他のバッファなどもエンジンから直接取得するなど手段はあります。

 ## 実際の描画の流れ
 DumpGPUで見てみると、RenderDeferredLightingパスでライティングに入った直後のDiffuseIndirectAndAOパスで呼び出されていることがわかります。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/e79cf99f-f5a8-61dd-6139-df09f5eefc7d.png)
　DiffuseIndirectつまり間接光とAO、アンビエントオクルージョンの処理を行うパスです。
　M_PP_SSGIがこの記事用に作ったポストプロセスマテリアルが実行されています。その次のDrawTextureでポストプロセスマテリアルの結果をSceneColorに書き込んでいます。次のDiffuseIndirectCompositeはエンジンのパスで間接光が描画されたSceneColorにAOなどを合成しています。

:::note info
DumpGpu超便利です。RenderDocの方が詳しく見られるけどプラグイン入れたり設定したりが必要だし、アイドル状態のエディタの描画プロセスが取れなかったり。DumpGpuは手軽に実行できるし、パスの構成が非常に見やすいので処理を追いやすい。
[【UE5】DumpGPUコマンドで描画をデバッグする(ヒストリア)](https://historia.co.jp/archives/26483/)
:::

# プラグインを作ってみよう
　というわけで本編です。GIプラグインを作成してみました。まずは結果から。
 GIなし。暗いですね。
![HighresScreenshot00000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/2b82838e-ecc6-2474-1b15-e90aa5648152.png)

今回のプラグインによるGI風マテリアルの結果。GIなんかやってませんが、なんかそれっぽいですね。あとで解説します。 
 ![HighresScreenshot00003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/6f83387d-05de-4882-7c54-bc4a057f43bb.png)

 みんな大好きLumenによるGI
 ![HighresScreenshot00001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/f7eabd34-c4a2-594a-aa7c-287fe7b3891e.png)

　Lumenキレイですね。
　でも、ニセGIもええ感じやん？けっこう使えるんじゃ？騙されてはいけません。いい感じに調整しているだけです。そんなに上手くいくなら誰も苦労しません。

## ニセモノGIマテリアル
　それでは、今回即席で作ったGI風マテリアルをちょっと解説します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/21812249-8c8c-1fec-211d-905fbd862df3.png)
　カメラからシーンを法線でライティングしたものに、フレネルを足して色をつけているだけです。フレネルを足している理由はそれっぽい陰影を手っ取り早く足したかっただけで特に理論とか無い思いつきです。

 :::note info
 SceneTextureに入力するUVにTextureCoordinateを使っていないのは、エディタ内のViewで描画する際にエディタのサイズを変更した場合にTextureCoordinateが0-1にならずに描画がずれる現象の対策です。描画画面のサイズが変わった場合、特に縮小方向に変更された場合、バッファのサイズはそのままで描画領域が縮小されるためにズレが生じます。
:::

## ニセモノじゃないGIは作れる？
　頑張ればニセモノじゃない、それなりのGIも実装可能だと思います。今回そこまで紹介しませんが、個人的にはチャレンジしてみようかと思っています。Screen Space Reflectionsの要領で画面内で反射先の色を拾うとか、配置しておいたプローブから情報を取得するとか。ちゃんとやろうとするとエンジン改造が必要になるかもしれませんが。
 
## 実際のプラグイン
　ソースコードを丸ごと掲載しておきます。
 
```SSGI_Plugin.uplugin
{
	"FileVersion": 3,
	"Version": 1,
	"VersionName": "1.0",
	"FriendlyName": "SSGI_Plugin",
	"Description": "UE Advent Calendar 2024",
	"Category": "Other",
	"CreatedBy": "dgtanaka",
	"CreatedByURL": "",
	"DocsURL": "",
	"MarketplaceURL": "",
	"SupportURL": "",
	"CanContainContent": true,
	"IsBetaVersion": false,
	"IsExperimentalVersion": false,
	"Installed": false,
	"Modules": [
		{
			"Name": "SSGI_Plugin",
			"Type": "Runtime",
			"LoadingPhase": "Default"
		}
	]
}
```

```Plugins\SSGI_Plugin\Source\SSGI_Plugin\SSGI_Plugin.Build.cs
// Copyright Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class SSGI_Plugin : ModuleRules
{
	public SSGI_Plugin(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
		
		
		PrivateIncludePaths.AddRange(
			new string[] {
				System.IO.Path.Combine(GetModuleDirectory("Renderer"), "Private"),
			}
		);
		PublicIncludePaths.AddRange(
			new string[] {
				// ... add public include paths required here ...
			}
			);
				
		
		PrivateIncludePaths.AddRange(
			new string[] {
				// ... add other private include paths required here ...
			}
			);
			
		
		PublicDependencyModuleNames.AddRange(
			new string[]
			{
				"Core",
				"Renderer",
				// ... add other public dependencies that you statically link with here ...
			}
			);
			
		
		PrivateDependencyModuleNames.AddRange(
			new string[]
			{
				"CoreUObject",
				"Engine",
				"Slate",
				"SlateCore",
				// ... add private dependencies that you statically link with here ...	
			}
			);
		
		
		DynamicallyLoadedModuleNames.AddRange(
			new string[]
			{
				// ... add any modules that your module loads dynamically here ...
			}
			);
	}
}

```

ポイントはここ
```
		PrivateIncludePaths.AddRange(
			new string[] {
				System.IO.Path.Combine(GetModuleDirectory("Renderer"), "Private"),
			}
		);
```
RendererモジュールのPrivaeフォルダのヘッダーを参照するので、この記述が必要になります。

```Plugins\SSGI_Plugin\Source\SSGI_Plugin\Public\SSGI_Module.h
// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"

class FSSGIModule : public IModuleInterface
{
public:

	/** IModuleInterface implementation */
	virtual void StartupModule() override;
	virtual void ShutdownModule() override;

private:
	class FSSGIRenderer* SSGIRenderer;
};

```

```
// Copyright Epic Games, Inc. All Rights Reserved.

#include "SSGI_Module.h"
#include "SSGI_Renderer.h"

#define LOCTEXT_NAMESPACE "FSSGIModule"

void FSSGIModule::StartupModule()
{
	SSGIRenderer = new FSSGIRenderer;
	SSGIRenderer->Initialize();
}

void FSSGIModule::ShutdownModule()
{
	if (SSGIRenderer)
	{
		delete SSGIRenderer;
		SSGIRenderer = nullptr;
	}
}

#undef LOCTEXT_NAMESPACE
	
IMPLEMENT_MODULE(FSSGIModule, SSGI)
```

　ここまでは実際の描画を行うFSSBGIRendererモジュールを作成して初期化しているだけです。

 ここからが本体です。

 ```Plugins\SSGI_Plugin\Source\SSGI_Plugin\Private\SSGI_Renderer.cpp
 // Copyright Tatsuhiro Tanaka All Rights Reserved.

#include "SSGI_Renderer.h"
#include "ScenePrivate.h"
#include "DeferredShadingRenderer.h"
#include "PostProcess/PostProcessMaterialInputs.h"

#define LOCTEXT_NAMESPACE "FSSGIRENDERER"

DEFINE_LOG_CATEGORY( LogSSGIRENDERER );

static TObjectPtr<UMaterialInterface> PPMaterial;

FSSGIRenderer::~FSSGIRenderer()
{
	Deinitialize();
}

// Initialize
void FSSGIRenderer::Initialize()
{
    // GIプラグイン呼び出しのDelegateを登録
	Handle_FGlobalIlluminationPluginDelegates = FGlobalIlluminationPluginDelegates::RenderDiffuseIndirectLight().AddStatic(FSSGIRenderer::RenderDiffuseIndirectLight);
    // ポストプロセスマテリアルをロードする
	PPMaterial = LoadObject<UMaterialInterface>(nullptr, TEXT("/SSGI_Plugin/M_PP_SSGI_Inst.M_PP_SSGI_Inst"), nullptr, LOAD_None, nullptr);
	if (PPMaterial)
	{
		UE_LOG(LogSSGIRENDERER, Log, TEXT("Material loaded"));
	}
	else
	{
		UE_LOG(LogSSGIRENDERER, Error, TEXT("Material not loaded"));
	}
}

// Deinitialize
void FSSGIRenderer::Deinitialize()
{
	FGlobalIlluminationPluginDelegates::RenderDiffuseIndirectLight().Remove(Handle_FGlobalIlluminationPluginDelegates);
	PPMaterial = nullptr;
}

// Render diffuse indirect light 
void FSSGIRenderer::RenderDiffuseIndirectLight(const FScene& Scene, const FViewInfo& View, FRDGBuilder& GraphBuilder, FGlobalIlluminationPluginResources& Resources)
{
    if (PPMaterial)
    {
        // ポストプロセスマテリアルの準備
        FPostProcessMaterialInputs MaterialInputs = FPostProcessMaterialInputs();
        MaterialInputs.SetInput(GraphBuilder, EPostProcessMaterialInput::SceneColor, FScreenPassTexture(Resources.SceneColor));
        MaterialInputs.SceneTextures = GetSceneTextureShaderParameters(View);
        // ポストプロセスマテリアルを描画 
        FScreenPassTexture SSGI_Result = AddPostProcessMaterialPass(
            GraphBuilder,
            View,
            MaterialInputs,
            PPMaterial.Get());
        // ポストプロセスマテリアルの結果をSceneColorに描画
        AddDrawTexturePass(GraphBuilder, View, SSGI_Result, FScreenPassRenderTarget(Resources.SceneColor, ERenderTargetLoadAction::ENoAction));
	}
}

#undef LOCTEXT_NAMESPACE
 ```

 　ソースコードはこれだけです。実際のコード量はとても少ないですね。
 　Delegateに描画処理を登録して、ポストプロセスマテリアルをロードしておきます。描画処理内ではポストプロセスマテリアルを実行して、結果をSceneColorに書き戻します。
　シンプルです。シェーダーコードは一行も書いてません。
　C++での実装だとシェーダーもシェーダーコードで作ることが多いのですが、こんなふうにポストプロセスマテリアルで描画することもできます。
　GI用に用意されたリソースは限定されていますが、実はGetSceneTextureShaderParametersでポストプロセスからアクセスできるバッファを一通り取得することができます。

# 考察
　ということでPluginを作成してGIを切り替えることが可能であることはわかりました。とはいえ自作でGIを実装するのはなかなかハードルが高いなとは思います。将来的にEpicが複数のGIをリリースしたり、DLSSやFSRのようにサードパーティがGIをプラグイン提供できるように用意している仕組みなのかなと思います。
　この機能を使ってライティングパスの前に独自の描画パスを挿入できるのは面白い応用法が色々ありそうです。とはいえ既存のGIと共存はできないのでモバイルやVRなど、軽量な描画で結果を出したい場合に使うというのはありかもしれません。
　例えばアニメ系の画作りでガッツリGI不要でふんわり暗いところが暗くなれば良いみたいなケースだと可能性があるのと、ライティング前に描画できるので、ライティング前にGBufferを加工するとか。
 　通常のポストプロセスマテリアルでアウトラインを描画するとフォグの後にアウトラインが描画されてしまうの地味に困ったりするのでそういう場合に使えるかもしれません。

# まとめ
　UEのバージョンが上がるたびにユーザーが描画に介入できる手段が少しずつ増えてきたのは良いですよね。SceneViewExtentionと組み合わせるとエンジン改造なしでさらに複雑な描画を実装できそうです。安易にエンジン改造する前に改造しなくてもできる手段が無いか吟味するのが大事だと思います。
　それではみなさん良いお年を