［UE5］SceneViewExtensionが楽しい

この記事は Unreal Engine (UE) Advent Calendar 2023 の記事です

https://qiita.com/advent-calendar/2023/ue

## はじめに

SceneViewExtensionでエフェクトを作ったら楽しかったので紹介します。

エフェクトの作例
![ScreenShot0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608589/ddf2862c-ea97-a266-aff0-574c4bcbb972.png)

https://github.com/Pachira762/Animepoy

## SceneViewExtensionとは

SceneViewExtensionとはエンジンを改造せずに独自のレンダリングパスを追加できる仕組みです。  
独自のアセットの描画や複雑なポストプロセスを実現できます。  
上の例では
* ライン判定時の複数のピクセルへの書き込み
* GBufferに対するラインの描画
* スクリーンカラーの縮小バッファの作成
* 畳み込みを使用した高速なブラー処理
* Summed Area Tableを使用したKuwaharaフィルター

など通常のマテリアルでは難しい処理を行っています。

## SceneViewExtensionの作り方

SceneViewExtensionを含むプラグインの作成の流れを紹介します。  
詳しいプラグインの作り方は[公式のチュートリアル](https://docs.unrealengine.com/5.0/ja/plugins-in-unreal-engine/)  
完全なソースコードはエンジンに含まれる[Color Correct Regions plugin](https://docs.unrealengine.com/4.26/en-US/WorkingWithMedia/Colorcorrectregions/)のソースや上記のリポジトリなどを確認してください。

### プラグインを作成しローディングフェーズを変更する

```MySceneViewExtensionPlugin.uplugin
"LoadingPhase": "PostConfigInit"
```

### シェダーのディレクトリを登録する

```cpp:MySceneViewExtensionPlugin.cpp
void FMySceneViewExtensionPluginModule::StartupModule()
{
    FString PluginShaderDir = FPaths::Combine(IPluginManager::Get().FindPlugin(TEXT("MySceneViewExtensionPlugin"))->GetBaseDir(), TEXT("Shaders"));
    AddShaderSourceDirectoryMapping(TEXT("/MySceneViewExtensionShaders"), PluginShaderDir);
}
```

### エフェクトの設定を保持するアクターを作成する

```cpp:MySceneViewExtensionSettings.h
UCLASS()
class MYSCENEVIEWEXTENSIONPLUGIN_API AMySceneViewExtensionSettings : public AActor
{
    ...
    
	UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "My Scene View Extension")
	float SomeParameter = 0.0f;
};
```

```cpp:MySceneViewExtensionSettings.cpp
void AMySceneViewExtensionSettings::BeginPlay()
{
	Super::BeginPlay();

    UMySceneViewExtensionWorldSubsystem* WorldSubsystem = World->GetSubsystem<UMySceneViewExtensionWorldSubsystem>();
    WorldSubsystem->OnActorSpawned(this);
}
```

### SceneViewExtensionを保持するWorldSubsystemを作成する

```cpp:MySceneViewExtensionWorldSubsystem
struct FMySceneViewExtensionSettingsRenderProxy 
{
    float SomeParameter;
    ...
}

UCLASS()
class MYSCENEVIEWEXTENSIONPLUGIN_API UMySceneViewExtensionWorldSubsystem : public UTickableWorldSubsystem
{
    ...

	TSharedPtr<class FMySceneViewExtension, ESPMode::ThreadSafe> MySceneViewExtension;
	AMySceneViewExtensionSettings* SettingsActor;

    // GameスレッドとRenderスレッドの両方からアクセスするのでミューテックスが必要です
	mutable FCriticalSection Mutex;
	FMySceneViewExtensionSettingsRenderProxy Settings{};
}
```

### SceneViewExtensionを生成する

```cpp:MySceneViewExtensionWorldSubsystem.cpp
void UMySceneViewExtensionWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	Super::Initialize(Collection);
	MySceneViewExtension = FSceneViewExtensions::NewExtension<FMySceneViewExtension>(this);
}
```

### 設定を更新する

```cpp:MySceneViewExtensionWorldSubsystem.cpp
void UMySceneViewExtensionWorldSubsystem::Tick(float DeltaTime)
{
    FMySceneViewExtensionSettingsRenderProxy TempSettings;
    TempSettings.Parameter = SettingsActor->Parameter;
    ...

    FScopeLock Lock(&Mutex);
    Settings = TempSettings;
}
```

### FSceneViewExtensionBaseを継承しレンダリングパスを追加する

```cpp:MySceneViewExtension.h
class FMySceneViewExtension : public FSceneViewExtensionBase
{
    ... 

	FMySceneViewExtension(const FAutoRegister& AutoRegister, UMySceneViewExtensionWorldSubsystem* WorldSubsystem);
    
	UMySceneViewExtensionWorldSubsystem* WorldSubsystem{};
}
```

```cpp:MySceneViewExtension.cpp
// SceneTexturesではなくRenderTargetsを使用します
void FMySceneViewExtension::PostRenderBasePassDeferred_RenderThread(FRDGBuilder& GraphBuilder, FSceneView& InView, const FRenderTargetBindingSlots& RenderTargets, TRDGUniformBufferRef<FSceneTextureUniformParameters> SceneTextures)
{
    FMyRenderingPassInputs PassInputs;
    PassInputs.Parameter = WorldSubsystem->GetSettings().SomeParameter;
    ...

    AddMyRenderingPass(GraphBuilder, (const FViewInfo&)InView, PassInputs);
}

// ライティングやフォグの描画の後に呼ばれます
void FMySceneViewExtension::PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& InView, const FPostProcessingInputs& Inputs)
{
    ...

    AddSomeRenderingPass(...);
}

void FMySceneViewExtension::SubscribeToPostProcessingPass(EPostProcessingPass Pass, FAfterPassCallbackDelegateArray& InOutPassCallbacks, bool bIsPassEnabled)
{
	if (Pass == EPostProcessingPass::Tonemap)
	{
		InOutPassCallbacks.Add(FAfterPassCallbackDelegate::CreateRaw(this, &FMySceneViewExtension::AfterTonemapPass));
	}
}

FScreenPassTexture FMySceneViewExtension::AfterTonemapPass(FRDGBuilder& GraphBuilder, const FSceneView& InView, const FPostProcessMaterialInputs& Inputs)
{
    ...

	return AddSomePostProcessPass(...);
}
```

### レンダリングパスを実装する

詳しくは[Unreal Engine にグローバル シェーダを追加する](https://docs.unrealengine.com/5.0/ja/adding-global-shaders-to-unreal-engine/)を参考にしてください。  
エンジンの `Engine\Source\Runtime\Renderer` に含まれるソースコードも非常に参考になります。

```cpp:MyRenderingPass.cpp
// シェーダーの宣言
class FMyShaderPS : public FGlobalShader
{
public:
    DECLARE_GLOBAL_SHADER(FMyShaderPS);
    SHADER_USE_PARAMETER_STRUCT(FMyShaderPS, FGlobalShader);

    BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
        SHADER_PARAMETER(float, SomeParameter)
		RENDER_TARGET_BINDING_SLOTS()
    END_SHADER_PARAMETER_STRUCT()
};

IMPLEMENT_GLOBAL_SHADER(FMyShaderPS, "/MySceneViewExtensionShaders/Private/MyShader.usf", "MyShaderPS", SF_Pixel);

void AddMyRenderingPass(GraphBuilder& GraphBuilder, FViewInfo& View, FMyRenderingPassInputs& Inputs)
{
    // バッファを使用したり
	FRDGTextureRef Texture = GraphBuilder.CreateTexture(...);

    // コンピュートシェーダーだったり
    FComputeShaderUtils::AddPass(...);

    // ポストプロセスだったり
    FPixelShaderUtils::AddFullscreenPass(...);

    // 独自のアセットのレンダリングだったり
    GraphBuilder.AddPass(
        [...](FRHICommandList& RHICmdList)
        {
            SetGraphicsPipelineState(...);
			RHICmdList.DrawIndexedPrimitive(...);
        }
    )
}
```

### シェーダーを書く

[シェーダー開発](https://docs.unrealengine.com/4.27/ja/ProgrammingAndScripting/Rendering/ShaderDevelopment/)を参考にしてください。  
`r.ShaderDevelopmentMode`の設定を行ってください。


```hlsl:/Shaders/Private/MyShader.usf
float SomeParameter;

void MyShaderPS(float4 SvPosition : SV_POSITION, out float4 OutSceneColor : SV_Target0)
{
    OutSceneColor = float4(SomeParameter.xxx, 0);
}
```

### プロジェクトをビルドする
.uprojectファイルを右クリックし`Generate Visual Studio project files`を実行してからプロジェクトをビルドしてください。

### レベルにアクターを配置する
![ScreenShot3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608589/76306a36-ccd5-ca73-ba37-0f8ba4594cf2.png)

### おまけ

エンジンを改造するとさらに好きなタイミングでレンダリングパスを追加できます。  
とっても楽！

- 宣言を追加して
```cpp:Engine/Source/Runtime/Engine/Public/SceneViewExtension.h
// ISceneViewExtension
virtual void SomewhereInRenderingPass_RenderThread(FRDGBuilder& GraphBuilder, FSceneView& InView, TRDGUniformBufferRef<FSceneTextureUniformParameters> SceneTextures) {}
```

- 好きなタイミングでSceneViewExtensionを呼び出すだけ！
```cpp:Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.cpp
// FDeferredShadingSceneRenderer::Render
for (const TSharedRef<ISceneViewExtension>& ViewExtension : ViewFamily.ViewExtensions)
{
    for (FViewInfo& View : Views)
    {
        ViewExtension->SomewhereInRenderingPass_RenderThread(GraphBuilder, View, SceneTextures.UniformBuffer);
    }
}
		
```

## 参考

[Using SceneViewExtension to Extend the Rendering System](https://forums.unrealengine.com/t/using-sceneviewextension-to-extend-the-rendering-system/600098)

[UnrealEngine レンダリングシステムとその拡張](https://www.docswell.com/s/strvert/K38NEM-rendering-system-and-extensions-in-unrealengine)