2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SceneViewExtensionを触ってみる

Last updated at Posted at 2025-09-02

はじめに

UEのレンダリング周りについて調べていたところこのような記事にたどり着き、SceneViewExtensionというものがあるということを知りました。

それまでUEでレンダリングパスを拡張したい場合はエンジン改造しか方法がないイメージだったので「ハードルが高い...どうしたもんか...」と思っていたのですが、これなら取っ掛かりとしては触りやすかったので記事にしてみました。

できあがったもの

今回は取っ掛かりということで超絶シンプルに画面全体がモノクロになるようなものを作ってみました。
Monocro.gif

実装準備

・プラグインの作成

1. Edit->Pluginsを開いたら左上の「Add」を選択

image.png

2. テンプレートが色々ありますがひとまず「Blank」を選択して、名前を決めたら「Create Plugin」を押します。

※プラグイン名はシンプルにMonocroにします

image.png

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フォルダを作成

image.png

image.png

2. Privateフォルダ下にMonocro.usfファイルを作成

※テキストファイル作成→拡張子を.usfに変更で大丈夫です

image.png

3. Monocro.cpp(プラグイン作成時最初に作られるファイル)のStartupModuleでシェーダーが参照できるようフォルダのパスを登録をします。
Monocro.cpp
void FMonocroModule::StartupModule()
{
	FString PluginShaderDir = FPaths::Combine(IPluginManager::Get().FindPlugin(TEXT("Monocro"))->GetBaseDir(), TEXT("Shaders"));
	AddShaderSourceDirectoryMapping(TEXT("/Plugin/Monocro"), PluginShaderDir);
}

AddShaderSourceDirectoryMappingでパスの登録を行っているのですが、それと同時に実際のフォルダパスをc++コード内で扱いやすいパス(今回は/Plugin/Monocro/)に変換しています。

後述するシェーダーの定義クラスで使用するのもこの変換後のフォルダパスです。

コード実装

先述したこの記事を参考にコードを実装していきます。

この他にもSceneviewExtensionのコードはいくつかGithub等にアップされていましたが、大まかな処理の流れとしてはいずれもこんな感じだったのでそれに沿っていきます。

  1. パラメータをSceneViewExtensionに渡すためのSubsystemを作成
  2. パラメータを調整用のActorを作成→ActorからSubsystemにパラメータを渡す
  3. SceneViewExtensionでSubsystemからパラメータを取得して実際にシェーダーに渡す

・パラメータの構造体を作成

まずはパラメータをまとめた構造体(MonocroSettings)を作ります。
といっても画面をモノクロにするだけなので有効/無効フラグとウェイトの2つくらいです。

MonocroSettings.h
#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();
};

MonocroSettings.cpp
#include "MonocroSettings.h"

FMonocroSettings::FMonocroSettings()
{
	//構造体のデフォルト値をカスタムしたいのでメモリをゼロクリアする
	FMemory::Memzero(this, sizeof(FMonocroSettings));

	Enabled = false;
	Weight = 1.0f;
}

・Subsystemとパラメータ調整用のActor作成

次にUWorldSubsystem辺りを継承してMonocroSubsystemを作ります。
この段階で実装するのはパラメータのget/set関数とSubsystem自体へのアクセス関数のみです(後で処理を追加します)

MonocroSubsystem.h
#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;
};
MonocroSubsystem.cpp
#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に渡すだけです。

MonocroControlActor.h
#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;
};
MonocroControlActor.cpp
#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ファイル)を使うにはまずシェーダーのパラメータ等を定義する必要があるので、定義用のクラスを作成します。

MonocroGlobalShader.h
#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;
	}
};
MonocroGlobalShader.cpp
#include "MonocroGlobalShader.h"

// 実際に使うシェーダーを登録
IMPLEMENT_GLOBAL_SHADER(FMonocroPS, "/Plugin/Monocro/Private/Monocro.usf", "MainPS", SF_Pixel);

BEGIN_SHADER_PARAMETER_STRUCTEND_SHADER_PARAMETER_STRUCTで囲われた箇所がシェーダー内で使うパラメータになります。
FViewUniformShaderParametersFSceneTextureShaderParametersは直前のレンダリング結果(SceneColor)をシェーダー内でサンプリングするためのものですね。

・SceneViewExtension作成

ここまで来たらいよいよSceneViewExtensionの実装です。

FSceneViewExtensionBaseを継承してMonocroViewExtensionを作っていくのですが、なぜか親クラスの一覧に出てこないのでNoneを選択してクラスを生成し手動でFSceneViewExtensionBaseを継承します。

MonocroViewExtension.h
#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_SCOPERDG_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をグレースケールにしてブレンドしてるだけなのであまり解説することはありません。

Monocro.usf
#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をそこに配置します
image.png

あとはEnabledにチェックを入れれば画面にモノクロ処理が適用されるはずです
Monocro.gif

最後に

今回SceneViewExtensionを触ってみた感想ですが、個人的には直前までのレンダリング結果の取得と中間テクスチャの生成、この2つが簡単にできそうだな~という感じがしました。

なのでブラー処理あたりなんかは中間テクスチャを複数枚使って実装できると思うので、次回はそれに挑戦してみようと思います。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?