この記事は Unreal Engine (UE) Advent Calendar 2023 の記事です
はじめに
SceneViewExtensionでエフェクトを作ったら楽しかったので紹介します。
SceneViewExtensionとは
SceneViewExtensionとはエンジンを改造せずに独自のレンダリングパスを追加できる仕組みです。
独自のアセットの描画や複雑なポストプロセスを実現できます。
上の例では
- ライン判定時の複数のピクセルへの書き込み
- GBufferに対するラインの描画
- スクリーンカラーの縮小バッファの作成
- 畳み込みを使用した高速なブラー処理
- Summed Area Tableを使用したKuwaharaフィルター
など通常のマテリアルでは難しい処理を行っています。
SceneViewExtensionの作り方
SceneViewExtensionを含むプラグインの作成の流れを紹介します。
詳しいプラグインの作り方は公式のチュートリアル
完全なソースコードはエンジンに含まれるColor Correct Regions pluginのソースや上記のリポジトリなどを確認してください。
プラグインを作成しローディングフェーズを変更する
"LoadingPhase": "PostConfigInit"
シェダーのディレクトリを登録する
void FMySceneViewExtensionPluginModule::StartupModule()
{
FString PluginShaderDir = FPaths::Combine(IPluginManager::Get().FindPlugin(TEXT("MySceneViewExtensionPlugin"))->GetBaseDir(), TEXT("Shaders"));
AddShaderSourceDirectoryMapping(TEXT("/MySceneViewExtensionShaders"), PluginShaderDir);
}
エフェクトの設定を保持するアクターを作成する
UCLASS()
class MYSCENEVIEWEXTENSIONPLUGIN_API AMySceneViewExtensionSettings : public AActor
{
...
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "My Scene View Extension")
float SomeParameter = 0.0f;
};
void AMySceneViewExtensionSettings::BeginPlay()
{
Super::BeginPlay();
UMySceneViewExtensionWorldSubsystem* WorldSubsystem = World->GetSubsystem<UMySceneViewExtensionWorldSubsystem>();
WorldSubsystem->OnActorSpawned(this);
}
SceneViewExtensionを保持するWorldSubsystemを作成する
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を生成する
void UMySceneViewExtensionWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
MySceneViewExtension = FSceneViewExtensions::NewExtension<FMySceneViewExtension>(this);
}
設定を更新する
void UMySceneViewExtensionWorldSubsystem::Tick(float DeltaTime)
{
FMySceneViewExtensionSettingsRenderProxy TempSettings;
TempSettings.Parameter = SettingsActor->Parameter;
...
FScopeLock Lock(&Mutex);
Settings = TempSettings;
}
FSceneViewExtensionBaseを継承しレンダリングパスを追加する
class FMySceneViewExtension : public FSceneViewExtensionBase
{
...
FMySceneViewExtension(const FAutoRegister& AutoRegister, UMySceneViewExtensionWorldSubsystem* WorldSubsystem);
UMySceneViewExtensionWorldSubsystem* WorldSubsystem{};
}
// 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 にグローバル シェーダを追加するを参考にしてください。
エンジンの Engine\Source\Runtime\Renderer
に含まれるソースコードも非常に参考になります。
// シェーダーの宣言
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(...);
}
)
}
シェーダーを書く
シェーダー開発を参考にしてください。
r.ShaderDevelopmentMode
の設定を行ってください。
float SomeParameter;
void MyShaderPS(float4 SvPosition : SV_POSITION, out float4 OutSceneColor : SV_Target0)
{
OutSceneColor = float4(SomeParameter.xxx, 0);
}
プロジェクトをビルドする
.uprojectファイルを右クリックしGenerate Visual Studio project files
を実行してからプロジェクトをビルドしてください。
レベルにアクターを配置する
おまけ
エンジンを改造するとさらに好きなタイミングでレンダリングパスを追加できます。
とっても楽!
- 宣言を追加して
// ISceneViewExtension
virtual void SomewhereInRenderingPass_RenderThread(FRDGBuilder& GraphBuilder, FSceneView& InView, TRDGUniformBufferRef<FSceneTextureUniformParameters> SceneTextures) {}
- 好きなタイミングでSceneViewExtensionを呼び出すだけ!
// FDeferredShadingSceneRenderer::Render
for (const TSharedRef<ISceneViewExtension>& ViewExtension : ViewFamily.ViewExtensions)
{
for (FViewInfo& View : Views)
{
ViewExtension->SomewhereInRenderingPass_RenderThread(GraphBuilder, View, SceneTextures.UniformBuffer);
}
}