はじめに
UEのレンダリング周りについて調べていたところこのような記事にたどり着き、SceneViewExtensionというものがあるということを知りました。
それまでUEでレンダリングパスを拡張したい場合はエンジン改造しか方法がないイメージだったので「ハードルが高い...どうしたもんか...」と思っていたのですが、これなら取っ掛かりとしては触りやすかったので記事にしてみました。
できあがったもの
今回は取っ掛かりということで超絶シンプルに画面全体がモノクロになるようなものを作ってみました。
実装準備
・プラグインの作成
- 1. Edit->Pluginsを開いたら左上の「Add」を選択
-
2. テンプレートが色々ありますがひとまず「Blank」を選択して、名前を決めたら「Create Plugin」を押します。
※プラグイン名はシンプルに
Monocro
にします -
3. プラグインの作成が完了すると
Plugins/Monocro
フォルダ下にMonocro.uplugin
が作成されていると思うのでそれを開いてLoadingPhaseをPostConfigInit
にしておきます(シェーダーの読み込みがあるので)Monocro.uplugin//※他の箇所はデフォルトのままなので割愛 "Modules": [ { "Name": "Monocro", "Type": "Runtime", "LoadingPhase": "PostConfigInit" } ]
-
4.
Plugins/Monocro/Source/Monocro
フォルダ下にMonocro.Build.cs
があるので、後のコード実装に必要なパスを追加していきますMonocro.Build.cs// Copyright Epic Games, Inc. All Rights Reserved. using System.IO; using UnrealBuildTool; public class Monocro : ModuleRules { public Monocro(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; PublicIncludePaths.AddRange( new string[] { // ... add public include paths required here ... } ); PrivateIncludePaths.AddRange( new string[] { //追加分 Path.Combine(GetModuleDirectory("Renderer"), "Private"), } ); PublicDependencyModuleNames.AddRange( new string[] { "Core", // ... add other public dependencies that you statically link with here ... } ); PrivateDependencyModuleNames.AddRange( new string[] { "CoreUObject", "Engine", "Slate", "SlateCore", //追加分ここから "Projects" "RenderCore", "Renderer", "RHI", //追加分ここまで } ); DynamicallyLoadedModuleNames.AddRange( new string[] { // ... add any modules that your module loads dynamically here ... } ); } }
・usfファイルの作成
-
1.
Plugins/Monocro
フォルダ下にShaders/Private
フォルダを作成 -
2.
Private
フォルダ下にMonocro.usf
ファイルを作成※テキストファイル作成→拡張子を.usfに変更で大丈夫です
- 3.
Monocro.cpp
(プラグイン作成時最初に作られるファイル)のStartupModule
でシェーダーが参照できるようフォルダのパスを登録をします。Monocro.cppvoid FMonocroModule::StartupModule() { FString PluginShaderDir = FPaths::Combine(IPluginManager::Get().FindPlugin(TEXT("Monocro"))->GetBaseDir(), TEXT("Shaders")); AddShaderSourceDirectoryMapping(TEXT("/Plugin/Monocro"), PluginShaderDir); }
AddShaderSourceDirectoryMapping
でパスの登録を行っているのですが、それと同時に実際のフォルダパスをc++コード内で扱いやすいパス(今回は/Plugin/Monocro/
)に変換しています。後述するシェーダーの定義クラスで使用するのもこの変換後のフォルダパスです。
- 3.
コード実装
先述したこの記事を参考にコードを実装していきます。
この他にもSceneviewExtensionのコードはいくつかGithub等にアップされていましたが、大まかな処理の流れとしてはいずれもこんな感じだったのでそれに沿っていきます。
- パラメータをSceneViewExtensionに渡すためのSubsystemを作成
- パラメータを調整用のActorを作成→ActorからSubsystemにパラメータを渡す
- SceneViewExtensionでSubsystemからパラメータを取得して実際にシェーダーに渡す
・パラメータの構造体を作成
まずはパラメータをまとめた構造体(MonocroSettings
)を作ります。
といっても画面をモノクロにするだけなので有効/無効フラグとウェイトの2つくらいです。
#pragma once
#include "CoreMinimal.h"
#include "MonocroSettings.generated.h"
USTRUCT(BlueprintType)
struct FMonocroSettings
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool Enabled;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", ClampMax = "1"))
float Weight;
MONOCRO_API FMonocroSettings();
};
#include "MonocroSettings.h"
FMonocroSettings::FMonocroSettings()
{
//構造体のデフォルト値をカスタムしたいのでメモリをゼロクリアする
FMemory::Memzero(this, sizeof(FMonocroSettings));
Enabled = false;
Weight = 1.0f;
}
・Subsystemとパラメータ調整用のActor作成
次にUWorldSubsystem
辺りを継承してMonocroSubsystem
を作ります。
この段階で実装するのはパラメータのget/set関数とSubsystem自体へのアクセス関数のみです(後で処理を追加します)
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "MonocroSettings.h"
#include "MonocroSubsystem.generated.h"
UCLASS()
class MONOCRO_API UMonocroSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
static UMonocroSubsystem* Get(UWorld* World);
const FMonocroSettings& GetMonocroSettings() const;
void SetMonocroSettings(const FMonocroSettings& NewValue);
private:
FMonocroSettings MonocroSettings;
};
#include "MonocroSubsystem.h"
void UMonocroSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
}
void UMonocroSubsystem::Deinitialize()
{
Super::Deinitialize();
}
UMonocroSubsystem* UMonocroSubsystem::Get(UWorld* World)
{
if (World)
{
return World->GetSubsystem<UMonocroSubsystem>();
}
return nullptr;
}
const FMonocroSettings& UMonocroSubsystem::GetMonocroSettings() const
{
return MonocroSettings;
}
void UMonocroSubsystem::SetMonocroSettings(const FMonocroSettings& NewValue)
{
FMonocroSettings& Value = MonocroSettings;
Value.Enabled = NewValue.Enabled;
Value.Weight = NewValue.Weight;
}
Subsystemの実装が終わったら次にパラメータ調整用のMonocroControlActor
を実装します。
※といってもパラメータをUPROPERTY
で編集可能にしてBeginPlayやOnConstructionでSubsystemに渡すだけです。
#pragma once
#include "CoreMinimal.h"
#include "MonocroSettings.h"
#include "GameFramework/Actor.h"
#include "MonocroControlActor.generated.h"
UCLASS()
class MONOCRO_API AMonocroControlActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AMonocroControlActor();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
virtual void OnConstruction(const FTransform& Transform) override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
private:
void ApplySettings();
UPROPERTY(EditAnywhere, Category = "Monocro Settings")
FMonocroSettings MonocroSettings;
};
#include "MonocroControlActor.h"
#include "MonocroSubsystem.h"
// Sets default values
AMonocroControlActor::AMonocroControlActor()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
}
// Called when the game starts or when spawned
void AMonocroControlActor::BeginPlay()
{
Super::BeginPlay();
ApplySettings();
}
void AMonocroControlActor::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
ApplySettings();
}
// Called every frame
void AMonocroControlActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void AMonocroControlActor::ApplySettings()
{
if (UMonocroSubsystem* Subsystem = UMonocroSubsystem::Get(GetWorld()))
{
Subsystem->SetMonocroSettings(MonocroSettings);
}
}
・シェーダーを定義する
UEのグローバルシェーダー(.usfファイル)を使うにはまずシェーダーのパラメータ等を定義する必要があるので、定義用のクラスを作成します。
#pragma once
#include "GlobalShader.h"
#include "ShaderParameterStruct.h"
#include "SceneTexturesConfig.h"
#include "SceneView.h"
class FMonocroPS : public FGlobalShader
{
public:
DECLARE_GLOBAL_SHADER(FMonocroPS);
SHADER_USE_PARAMETER_STRUCT(FMonocroPS, FGlobalShader);
//シェーダー側に渡すパラメータを定義
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
SHADER_PARAMETER_STRUCT_REF(FViewUniformShaderParameters, View)
SHADER_PARAMETER_STRUCT_INCLUDE(FSceneTextureShaderParameters, SceneTextures)
SHADER_PARAMETER(float, Weight)
RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
// 対応するプラットフォームかどうかをここに書く(今回は全てのプラットフォームに対応)
return true;
}
};
#include "MonocroGlobalShader.h"
// 実際に使うシェーダーを登録
IMPLEMENT_GLOBAL_SHADER(FMonocroPS, "/Plugin/Monocro/Private/Monocro.usf", "MainPS", SF_Pixel);
BEGIN_SHADER_PARAMETER_STRUCT
とEND_SHADER_PARAMETER_STRUCT
で囲われた箇所がシェーダー内で使うパラメータになります。
FViewUniformShaderParameters
とFSceneTextureShaderParameters
は直前のレンダリング結果(SceneColor)をシェーダー内でサンプリングするためのものですね。
・SceneViewExtension作成
ここまで来たらいよいよSceneViewExtensionの実装です。
FSceneViewExtensionBase
を継承してMonocroViewExtension
を作っていくのですが、なぜか親クラスの一覧に出てこないのでNoneを選択してクラスを生成し手動でFSceneViewExtensionBase
を継承します。
#pragma once
#include "CoreMinimal.h"
#include "SceneViewExtension.h"
class MONOCRO_API FMonocroViewExtension : public FSceneViewExtensionBase
{
public:
FMonocroViewExtension(const FAutoRegister& AutoRegister);
//この3つは純粋仮想関数なので実装がないと怒られる
virtual void SetupViewFamily(FSceneViewFamily& InViewFamily) override {}
virtual void SetupView(FSceneViewFamily& InViewFamily, FSceneView& InView) override {}
virtual void BeginRenderViewFamily(FSceneViewFamily& InViewFamily) override {}
virtual void PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& InView, const FPostProcessingInputs& Inputs) override;
};
一番重要なのがPrePostProcessPass_RenderThread
で、ここにシェーダーへのパラメータの受け渡しや描画命令の実行を書いていきます。
ISceneViewExtension
を見た限りだとこの辺りの関数を使って各描画パスの前後に処理を挟んでいくようですが、今回は3D描画後にモノクロ処理が挟まれば良いのでPrePostProcessPass_RenderThread
にしています。
virtual void PreRenderViewFamily_RenderThread(FRDGBuilder& GraphBuilder, FSceneViewFamily& InViewFamily)
virtual void PreRenderView_RenderThread(FRDGBuilder& GraphBuilder, FSceneView& InView)
virtual void PreInitViews_RenderThread(FRDGBuilder& GraphBuilder)
virtual void PreRenderBasePass_RenderThread(FRDGBuilder& GraphBuilder, bool bDepthBufferIsPopulated)
virtual void PostRenderBasePassDeferred_RenderThread(FRDGBuilder& GraphBuilder, FSceneView& InView, const FRenderTargetBindingSlots& RenderTargets, TRDGUniformBufferRef<FSceneTextureUniformParameters> SceneTextures)
virtual void PostRenderBasePassMobile_RenderThread(FRHICommandList& RHICmdList, FSceneView& InView)
virtual void PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& InView, const FPostProcessingInputs& Inputs)
virtual void PostRenderViewFamily_RenderThread(FRDGBuilder& GraphBuilder, FSceneViewFamily& InViewFamily)
virtual void PostRenderView_RenderThread(FRDGBuilder& GraphBuilder, FSceneView& InView)
また、SubscribeToPostProcessingPass
という関数もあったのですが、これを使うと各ポストプロセス処理(モーションブラー・トーンマッピング等々)の前後に処理を挟む、ということもできそうでした。
virtual void SubscribeToPostProcessingPass(EPostProcessingPass Pass, const FSceneView& InView, FAfterPassCallbackDelegateArray& InOutPassCallbacks, bool bIsPassEnabled)
ここからPrePostProcessPass_RenderThread
の実装処理に入っていきます(長いので何回かに分けてます)。
まずはパラメータとSceneColorの取得から
void FMonocroViewExtension::PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessingInputs& Inputs)
{
Inputs.Validate();
FScene* Scene = View.Family->Scene->GetRenderScene();
const UMonocroSubsystem* Subsystem = UMonocroSubsystem::Get(Scene->World);
if (!IsValid(Subsystem))
{
return;
}
const FMonocroSettings& Settings = Subsystem->GetMonocroSettings();
if (!Settings.Enabled)
{
return;
}
const FIntRect PrimaryViewRect = static_cast<const FViewInfo&>(View).ViewRect;
// PrePostProcessPassなのでポストプロセス前の描画結果を取得
FScreenPassTexture SceneColor((*Inputs.SceneTextures)->SceneColorTexture, PrimaryViewRect);
次に描画命令に必要なパラメータとして入出力先のビューポートとVertexShader・PixelShaderを宣言します。
(RDG_EVENT_SCOPE
とRDG_GPU_STAT_SCOPE
はプロファイル用のマクロです)
RDG_EVENT_SCOPE(GraphBuilder, "Monocro");
RDG_GPU_STAT_SCOPE(GraphBuilder, Monocro);
const FScreenPassTextureViewport InputViewport(SceneColor);
const FScreenPassTextureViewport OutputViewport(InputViewport);
// FScreenVS=フルスクリーンシェーダーを作りたい場合の頂点シェーダーのプリセット
TShaderMapRef<FScreenVS> VertexShader(static_cast<const FViewInfo&>(View).ShaderMap);
TShaderMapRef<FMonocroPS> PixelShader(static_cast<const FViewInfo&>(View).ShaderMap);
描画結果格納用のテクスチャを作ります。
GraphBuilder.CreateTexture
で作ったテクスチャは自動的に破棄されるらしいので楽ちんですね。
// レンダリング結果を格納用のテクスチャを作成
FRDGTextureRef OutputTexture;
{
FRDGTextureDesc OutputTextureDesc = SceneColor.Texture->Desc;
OutputTextureDesc.Reset();
OutputTextureDesc.Flags |= TexCreate_RenderTargetable | TexCreate_ShaderResource;
// GraphBuilder.CreateTextureで作成したテクスチャは自動的に破棄してくれる
OutputTexture = GraphBuilder.CreateTexture(OutputTextureDesc, TEXT("Monocro_Output"));
}
シェーダーに使うパラメータの受け渡しと、レンダーターゲットの指定を行います。
FMonocroPS::FParameters* PassParameters = GraphBuilder.AllocParameters<FMonocroPS::FParameters>();
PassParameters->View = View.ViewUniformBuffer;
PassParameters->SceneTextures = GetSceneTextureShaderParameters(Inputs.SceneTextures);
PassParameters->Weight = Settings.Weight;
// 出力先のテクスチャを設定(ERenderTargetLoadAction::EClearにすることでバッファをクリアしてから書き込んでくれる)
PassParameters->RenderTargets[0] = FRenderTargetBinding(OutputTexture, ERenderTargetLoadAction::EClear);
この次にパス内で使わないテクスチャをダミーの黒テクスチャに置き換える、というのを行います(おまじない的な感じのものだそうです)
// このパス内で使わないテクスチャをダミーの黒テクスチャに置き換える
const FScreenPassTexture BlackDummy(GSystemTextures.GetBlackDummy(GraphBuilder));
GraphBuilder.RemoveUnusedTextureWarning(BlackDummy.Texture);
そしてここまで来たら描画命令を実行、スクリーンに反映となります(長かった~)
// 指定したレンダーターゲットに対して描画処理実行
AddDrawScreenPass(
GraphBuilder,
RDG_EVENT_NAME("Monocro"),
View,
OutputViewport,
InputViewport,
VertexShader,
PixelShader,
TStaticBlendState<>::GetRHI(),
TStaticDepthStencilState<false, CF_Always>::GetRHI(),
PassParameters);
// スクリーンに反映
AddCopyTexturePass(GraphBuilder, OutputTexture, SceneColor.Texture);
長々と書いてきましたがこれでC++側の作業は終了です。最終的なcppファイルの中身はこちら
MonocroViewExtension.cpp
#include "MonocroViewExtension.h"
#include "MonocroGlobalShader.h"
#include "MonocroSubsystem.h"
#include "RenderGraphEvent.h"
#include "ScenePrivate.h"
#include "ScreenPass.h"
#include "ScreenRendering.h"
#include "PostProcess/PostProcessInputs.h"
DECLARE_GPU_STAT(Monocro);
FMonocroViewExtension::FMonocroViewExtension(const FAutoRegister& AutoRegister)
: FSceneViewExtensionBase(AutoRegister)
{
}
void FMonocroViewExtension::PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessingInputs& Inputs)
{
Inputs.Validate();
FScene* Scene = View.Family->Scene->GetRenderScene();
const UMonocroSubsystem* Subsystem = UMonocroSubsystem::Get(Scene->World);
if (!IsValid(Subsystem))
{
return;
}
const FMonocroSettings& Settings = Subsystem->GetMonocroSettings();
if (!Settings.Enabled)
{
return;
}
const FIntRect PrimaryViewRect = static_cast<const FViewInfo&>(View).ViewRect;
// PrePostProcessPassなのでポストプロセス前の描画結果を取得
FScreenPassTexture SceneColor((*Inputs.SceneTextures)->SceneColorTexture, PrimaryViewRect);
RDG_EVENT_SCOPE(GraphBuilder, "Monocro");
RDG_GPU_STAT_SCOPE(GraphBuilder, Monocro);
const FScreenPassTextureViewport InputViewport(SceneColor);
const FScreenPassTextureViewport OutputViewport(InputViewport);
// シェーダー適用後のレンダリング結果を格納するテクスチャを作成
FRDGTextureRef OutputTexture;
{
FRDGTextureDesc OutputTextureDesc = SceneColor.Texture->Desc;
OutputTextureDesc.Reset();
OutputTextureDesc.Flags |= TexCreate_RenderTargetable | TexCreate_ShaderResource;
// GraphBuilder.CreateTextureで作成したテクスチャは自動的に破棄してくれる
OutputTexture = GraphBuilder.CreateTexture(OutputTextureDesc, TEXT("Monocro.Output"));
}
// FScreenVS=フルスクリーンシェーダーを作りたい場合の頂点シェーダーのプリセット
TShaderMapRef<FScreenVS> VertexShader(static_cast<const FViewInfo&>(View).ShaderMap);
TShaderMapRef<FMonocroPS> PixelShader(static_cast<const FViewInfo&>(View).ShaderMap);
FMonocroPS::FParameters* PassParameters = GraphBuilder.AllocParameters<FMonocroPS::FParameters>();
PassParameters->View = View.ViewUniformBuffer;
PassParameters->SceneTextures = GetSceneTextureShaderParameters(Inputs.SceneTextures);
PassParameters->Weight = Settings.Weight;
// 出力先のテクスチャを設定(ERenderTargetLoadAction::EClearにすることでバッファをクリアしてから書き込んでくれる)
PassParameters->RenderTargets[0] = FRenderTargetBinding(OutputTexture, ERenderTargetLoadAction::EClear);
// このパス内で使わないテクスチャをダミー黒テクスチャに置き換える
const FScreenPassTexture BlackDummy(GSystemTextures.GetBlackDummy(GraphBuilder));
GraphBuilder.RemoveUnusedTextureWarning(BlackDummy.Texture);
// 指定したレンダーターゲットに対して描画処理実行
AddDrawScreenPass(
GraphBuilder,
RDG_EVENT_NAME("Monocro"),
View,
OutputViewport,
InputViewport,
VertexShader,
PixelShader,
TStaticBlendState<>::GetRHI(),
TStaticDepthStencilState<false, CF_Always>::GetRHI(),
PassParameters);
// スクリーンに反映
AddCopyTexturePass(GraphBuilder, OutputTexture, SceneColor.Texture);
}
・シェーダーコード実装
最後に肝心のシェーダーコードの実装です。
といってもSceneColorをグレースケールにしてブレンドしてるだけなのであまり解説することはありません。
#include "/Engine/Private/Common.ush"
#include "/Engine/Private/SceneTexturesCommon.ush"
#include "/Engine/Private/DeferredShadingCommon.ush"
float Weight;
void MainPS(
FScreenVertexOutput Input,
out float4 OutColor : SV_Target0)
{
float2 BufferUV = Input.UV;
float2 ClampedUV = clamp(BufferUV, View.BufferBilinearUVMinMax.xy, View.BufferBilinearUVMinMax.zw);
float3 SceneColor = CalcSceneColor(ClampedUV);
float3 Monocro = SceneColor.r * 0.3f + SceneColor.g * 0.6f + SceneColor.b * 0.1f;
float3 Output = lerp(SceneColor, Monocro, Weight);
OutColor = float4(Output, 1.0);
}
・実際に動かしてみる
適当なLevelを作ってMonocroControlActor
をそこに配置します
あとはEnabledにチェックを入れれば画面にモノクロ処理が適用されるはずです
最後に
今回SceneViewExtensionを触ってみた感想ですが、個人的には直前までのレンダリング結果の取得と中間テクスチャの生成、この2つが簡単にできそうだな~という感じがしました。
なのでブラー処理あたりなんかは中間テクスチャを複数枚使って実装できると思うので、次回はそれに挑戦してみようと思います。