플러그인을 사용한 포스트 프로세스 효과

참고
플러그인의 이름을 'LearnShader'라 가정하고 진행한다.

플러그인 설정

LearnShader.uplugin 파일에서 LoadingPhase 설정을 "PostConfigInit"으로 해준다.

{
	"FileVersion": 3,
	"Version": 1,
	"VersionName": "1.0",
	"FriendlyName": "LearnShader",
	"Description": "",
	"Category": "Other",
	"CreatedBy": "",
	"CreatedByURL": "",
	"DocsURL": "",
	"MarketplaceURL": "",
	"SupportURL": "",
	"CanContainContent": true,
	"IsBetaVersion": false,
	"IsExperimentalVersion": false,
	"Installed": false,
	"Modules": [
		{
			"Name": "LearnShader",
			"Type": "Runtime",
			"LoadingPhase": "PostConfigInit"
		}
	]
}

LearnShader.Build.cs 파일에서 모듈과 IncludePath를 추가해 준다.

...

PrivateIncludePaths.AddRange(
	new string[] {
        Path.Combine(GetModuleDirectory("Renderer"), "Private")
    }
	);

...

PrivateDependencyModuleNames.AddRange(
	new string[]
	{
		"CoreUObject",
		"Engine",
		"Slate",
		"SlateCore",
		"Projects",
		"RHI",
		"Renderer",
		"RenderCore"
	}
	);

구현

void FLearnShaderModule::StartupModule()
{
	// 셰이더 소스 폴더 매핑
	FString BaseDir = IPluginManager::Get().FindPlugin(TEXT("LearnShader"))->GetBaseDir();
	FString PluginShaderDir = FPaths::Combine(BaseDir, TEXT("Shaders"));
	if (!AllShaderSourceDirectoryMappings().Contains(PluginShaderDir))
	{
		AddShaderSourceDirectoryMapping("/LearnShader", PluginShaderDir);
	}
}

셰이더 소스가 들어있는 폴더를 "/LearnShader"라는 가상의 경로로 매핑하는 과정이다. 즉
"엔진 경로/플러그인 경로/Shaders" 라는 폴더를 "/LearnShader"라는 경로로 매핑 하는 것이다.

SceneViewExtensionBase

FSceneViewExtensionBase를 상속한 FMySceneViewExtension 클래스를 만들어 준다. 실질적으로 렌더링 코드가 작성될 클래스다. 오버라이드된 함수의 이름마다 호출되는 타이밍이 다르다는 것을 알 수 있다.

#pragma once

#include "SceneViewExtension.h"

class LEARNSHADER_API FMySceneViewExtension : public FSceneViewExtensionBase
{
public:
	FMySceneViewExtension(const FAutoRegister& AutoRegister);
	~FMySceneViewExtension();

	virtual void SetupViewFamily(FSceneViewFamily& InViewFamily) override;
	virtual void SetupView(FSceneViewFamily& InViewFamily, FSceneView& InView) override;
	virtual void BeginRenderViewFamily(FSceneViewFamily& InViewFamily) override;

	virtual void PreRenderView_RenderThread(FRDGBuilder& GraphBuilder, FSceneView& InView) override;
	virtual void PreRenderViewFamily_RenderThread(FRDGBuilder& GraphBuilder, FSceneViewFamily& InViewFamily) override;
	virtual void PostRenderViewFamily_RenderThread(FRDGBuilder& GraphBuilder, FSceneViewFamily& InViewFamily) override;	
	virtual void PostRenderBasePassDeferred_RenderThread(
		FRDGBuilder& GraphBuilder,
		FSceneView& InView,
		const FRenderTargetBindingSlots& RenderTargets,
		TRDGUniformBufferRef<FSceneTextureUniformParameters> SceneTextures
	) override;
	virtual void PrePostProcessPass_RenderThread(
		FRDGBuilder& GraphBuilder,
		const FSceneView& InView,
		const FPostProcessingInputs& Inputs
	) override;
};

소스코드 부분은 일단 전체를 확인하고, 부분부분 내용을 확인해 보자.

#include "Render/MySceneViewExtension.h"
#include "Runtime/Renderer/Private/SceneRendering.h"
#include "PostProcess/PostProcessInputs.h"
#include "PixelShaderUtils.h"

BEGIN_SHADER_PARAMETER_STRUCT(FColorExtractParams, )
	SHADER_PARAMETER(FVector3f, TargetColor)
	SHADER_PARAMETER_RDG_TEXTURE(Texture2D, SceneColorTexture)
	SHADER_PARAMETER_STRUCT_INCLUDE(FSceneTextureShaderParameters, SceneTextures)

	RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()

class FMyTestShaderPS : public FGlobalShader
{
public:
	DECLARE_EXPORTED_SHADER_TYPE(FMyTestShaderPS, Global, )
	using FParameters = FColorExtractParams;
	SHADER_USE_PARAMETER_STRUCT(FMyTestShaderPS, FGlobalShader);

	static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
	{
		return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5);
	}

	static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
	{
		FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);

		// SET_SHADER_DEFINE(OutEnvironment, YOUR_SHADER_MACRO, 0);
	}
};
IMPLEMENT_SHADER_TYPE(, FMyTestShaderPS, TEXT("/LearnShader/MyTestShader.usf"), TEXT("MainPS"), SF_Pixel);

DECLARE_GPU_DRAWCALL_STAT(ColorMix);

FMySceneViewExtension::FMySceneViewExtension(const FAutoRegister& AutoRegister):
	FSceneViewExtensionBase(AutoRegister)
{
}

FMySceneViewExtension::~FMySceneViewExtension()
{
}

void FMySceneViewExtension::SetupViewFamily(FSceneViewFamily& InViewFamily)
{
}

void FMySceneViewExtension::SetupView(FSceneViewFamily& InViewFamily, FSceneView& InView)
{
}

void FMySceneViewExtension::BeginRenderViewFamily(FSceneViewFamily& InViewFamily)
{
}

void FMySceneViewExtension::PreRenderView_RenderThread(FRDGBuilder& GraphBuilder, FSceneView& InView)
{
}

void FMySceneViewExtension::PreRenderViewFamily_RenderThread(FRDGBuilder& GraphBuilder, FSceneViewFamily& InViewFamily)
{
}

void FMySceneViewExtension::PostRenderViewFamily_RenderThread(FRDGBuilder& GraphBuilder, FSceneViewFamily& InViewFamily)
{
}

void FMySceneViewExtension::PostRenderBasePassDeferred_RenderThread(
	FRDGBuilder& GraphBuilder,
	FSceneView& InView,
	const FRenderTargetBindingSlots& RenderTargets,
	TRDGUniformBufferRef<FSceneTextureUniformParameters> SceneTextures
)
{
}

void FMySceneViewExtension::PrePostProcessPass_RenderThread(
	FRDGBuilder& GraphBuilder,
	const FSceneView& InView,
	const FPostProcessingInputs& Inputs
)
{
	FSceneViewExtensionBase::PrePostProcessPass_RenderThread(GraphBuilder, InView, Inputs);

	// Unreal Insights
	RDG_GPU_STAT_SCOPE(GraphBuilder, ColorMix);
	// Render Doc
	RDG_EVENT_SCOPE(GraphBuilder, "ColorMix");

	// Grab the Scene Texture
	const FSceneTextureShaderParameters SceneTextures = CreateSceneTextureShaderParameters(
		GraphBuilder,
		InView,
		ESceneTextureSetupMode::SceneColor | ESceneTextureSetupMode::GBuffers
	);
	// This is the color that actually has the shadow and the shade
	check(InView.bIsViewInfo);
	const FIntRect Viewport = static_cast<const FViewInfo&>(InView).ViewRect;
	const FScreenPassTexture SceneColorTexture((*Inputs.SceneTextures)->SceneColorTexture, Viewport);

	// Set global shader data, allocate memory
	FMyTestShaderPS::FParameters* Parameters = GraphBuilder.AllocParameters<FMyTestShaderPS::FParameters>();
	Parameters->SceneColorTexture = SceneColorTexture.Texture;
	Parameters->SceneTextures = SceneTextures;
	Parameters->TargetColor = FVector3f(1.0f, 0.0f, 1.0f);

	// Set RenderTarget and Return Texture
	Parameters->RenderTargets[0] = FRenderTargetBinding(
		(*Inputs.SceneTextures)->SceneColorTexture,
		ERenderTargetLoadAction::ELoad
	);

	const FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel);
	TShaderMapRef<FMyTestShaderPS> PixelShader(GlobalShaderMap);
	FPixelShaderUtils::AddFullscreenPass(
		GraphBuilder,
		GlobalShaderMap,
		FRDGEventName(TEXT("Color Mix Pass")),
		PixelShader,
		Parameters,
		Viewport
	);
}

// https://illu.tistory.com/1500

먼저 셰이더 파라미터를 선언하는 부분이다. 상수 버퍼를 포함한 셰이더에 필요한 파라미터를 설정하는 부분이다.

BEGIN_SHADER_PARAMETER_STRUCT(FColorExtractParams, )
	SHADER_PARAMETER(FVector3f, TargetColor)
	SHADER_PARAMETER_RDG_TEXTURE(Texture2D, SceneColorTexture)
	SHADER_PARAMETER_STRUCT_INCLUDE(FSceneTextureShaderParameters, SceneTextures)

	RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()

FMyTestShaderPS 클래스를 통해 셰이더 내용을 작성한다.

class FMyTestShaderPS : public FGlobalShader
{
public:
	DECLARE_EXPORTED_SHADER_TYPE(FMyTestShaderPS, Global, )
	using FParameters = FColorExtractParams;
	SHADER_USE_PARAMETER_STRUCT(FMyTestShaderPS, FGlobalShader);

	static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
	{
		return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5);
	}

	static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
	{
		FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);

		// SET_SHADER_DEFINE(OutEnvironment, YOUR_SHADER_MACRO, 0);
	}
};
IMPLEMENT_SHADER_TYPE(, FMyTestShaderPS, TEXT("/LearnShader/MyTestShader.usf"), TEXT("MainPS"), SF_Pixel);

성능 추적을 위한 함수들을 정의하는 매크로다.

DECLARE_GPU_DRAWCALL_STAT(ColorMix);

아래에서 사용될
RDG_GPU_STAT_SCOPE(GraphBuilder, ColorMix);
매크로 사용을 위해 필요하다.

FMySceneViewExtension::PrePostProcessPass_RenderThread 함수는 차례대로 살펴본다.

FSceneViewExtensionBase::PrePostProcessPass_RenderThread(GraphBuilder, InView, Inputs);

부모의 함수를 호출한다. 실질적으로 실행되는 것은 아무것도 없는 함수다. 생략해도 무관하지만, 이후 언리얼 엔진의 버전이 달라짐에 따라 어떻게 될지 모른다.

성능 체크를 위한 매크로를 사용한다.

// Unreal Insights
RDG_GPU_STAT_SCOPE(GraphBuilder, ColorMix);
// Render Doc
RDG_EVENT_SCOPE(GraphBuilder, "ColorMix");

주석에 나온 것 처럼, RDG_GPU_STAT_SCOPE 는 Unreal Insights를 위한 매크로 이고 RDG_EVENT_SCOPE 는 Render Doc을 위한 매크로다. RDG_GPU_STAT_SCOPE 매크로는 위에 선언한 DECLARE_GPU_DRAWCALL_STAT가 없을 경우 컴파일이 안되므로 주의

셰이더 파라미터를 설정하는 부분이다.

// Grab the Scene Texture
const FSceneTextureShaderParameters SceneTextures = CreateSceneTextureShaderParameters(
	GraphBuilder,
	InView,
	ESceneTextureSetupMode::SceneColor | ESceneTextureSetupMode::GBuffers
);
// This is the color that actually has the shadow and the shade
check(InView.bIsViewInfo);
const FIntRect Viewport = static_cast<const FViewInfo&>(InView).ViewRect;
const FScreenPassTexture SceneColorTexture((*Inputs.SceneTextures)->SceneColorTexture, Viewport);

	// Set global shader data, allocate memory
FMyTestShaderPS::FParameters* Parameters = GraphBuilder.AllocParameters<FMyTestShaderPS::FParameters>();
Parameters->SceneColorTexture = SceneColorTexture.Texture;
Parameters->SceneTextures = SceneTextures;
Parameters->TargetColor = FVector3f(1.0f, 0.0f, 1.0f);

// Set RenderTarget and Return Texture
Parameters->RenderTargets[0] = FRenderTargetBinding(
	(*Inputs.SceneTextures)->SceneColorTexture,
	ERenderTargetLoadAction::ELoad
);

다른 부분은 읽으면 알 수 있는 부분이라 생각하고,

check(InView.bIsViewInfo);
const FIntRect Viewport = static_cast<const FViewInfo&>(InView).ViewRect;

이부분의 경우 InView의 타입인 FSceneView가 FViewInfo의 부모 클래스 이기 때문에 위험한 변환이지만,
check(InView.bIsViewInfo); 를 통해 타입을 체크 후 하는 변환이므로 사실상 안전하게 변환한다고 볼 수 있다.

마지막으로 셰이더를 호출하고, 드로우를 하는 부분이다.

const FGlobalShaderMap* GlobalShaderMap = GetGlobalShaderMap(GMaxRHIFeatureLevel);
TShaderMapRef<FMyTestShaderPS> PixelShader(GlobalShaderMap);
FPixelShaderUtils::AddFullscreenPass(
	GraphBuilder,
	GlobalShaderMap,
	FRDGEventName(TEXT("Color Mix Pass")),
	PixelShader,
	Parameters,
	Viewport
);

이전에 설정한 값들을 그대로 보내주는데, FRDGEventName(TEXT("Color Mix Pass"))의 경우는 RenderDoc에서 해당 드로우 이벤트를 "Color Mix Pass"로 표시하게 될 것 이다.

Shader Source

간단한 셰이더 소스를 만든다.

#include "/Engine/Public/Platform.ush"

float3 TargetColor;
Texture2D<float4> SceneColorTexture;

float4 MainPS(float4 SvPosition : SV_Position) : SV_Target
{
    const float4 SceneColor = SceneColorTexture.Load(int3(SvPosition.xy, 0.0f));
    const float3 MainColor = SceneColor.rgb * TargetColor;

    return float4(MainColor, 1.0f);
}
#include "/Engine/Public/Platform.ush"

위 헤더파일을 포함해야 컴파일이 가능하다. Common.ush에 해당 헤더가 포함되어 있으니 이것을 포함시키는 것도 방법.

float3 TargetColor;
Texture2D<float4> SceneColorTexture;

FColorExtractParams클래스에서 선언한 파라미터에 대응되는 값이다.

float4 MainPS(float4 SvPosition : SV_Position) : SV_Target
{
    const float4 SceneColor = SceneColorTexture.Load(int3(SvPosition.xy, 0.0f));
    const float3 MainColor = SceneColor.rgb * TargetColor;

    return float4(MainColor, 1.0f);
}

메인 함수다. 별 내용 없이 단순한 함수다.

EngineSubSystem

UEngineSubsystem을 상속하는 UMyEngineSubsystem라는 클래스를 만들어 준다. 서브시스템은 언리얼 엔진의 싱글톤 클래스다. 이 클래스에서 FMySceneViewExtension의 인스턴스를 만들어 줄 것 이다.

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Subsystems/EngineSubsystem.h"
#include "MyEngineSubsystem.generated.h"

class FSceneViewExtensionBase;

UCLASS()
class LEARNSHADER_API UMyEngineSubsystem : public UEngineSubsystem
{
	GENERATED_BODY()
	
public:
	virtual void Initialize(FSubsystemCollectionBase& Collection) override;
	virtual void Deinitialize() override;

private:
	TSharedPtr<FSceneViewExtensionBase, ESPMode::ThreadSafe> CustomSceneViewExtension;
};
#include "SubSystems/MyEngineSubsystem.h"
#include "Render/MySceneViewExtension.h"

void UMyEngineSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	Super::Initialize(Collection);
	
	CustomSceneViewExtension = FSceneViewExtensions::NewExtension<FMySceneViewExtension>();
}

void UMyEngineSubsystem::Deinitialize()
{
	CustomSceneViewExtension.Reset();
	CustomSceneViewExtension = nullptr;
	Super::Deinitialize();
}

특별한 내용 없고 FMySceneViewExtension를 만들고, 소멸시키는 내용이다.

프로젝트를 실행해서 확인하면 다음과 같은 화면을 볼 수 있다.
4. Archive/Unreal Lab/Attachments/Pasted image 20250604101453.png

Unreal Insights에서도 Color Mix 항목을 찾을 수 있다.
4. Archive/Unreal Lab/Attachments/Pasted image 20250604102035.png